#785 - Media actions

This commit is contained in:
Elio Struyf
2024-03-29 17:34:28 +01:00
parent 34b331b0ee
commit c4267a69fa
8 changed files with 222 additions and 74 deletions
@@ -19,11 +19,12 @@ export const FooterActions: React.FunctionComponent<IFooterActionsProps> = ({
const [, setSelectedItemAction] = useRecoilState(SelectedItemActionAtom);
return (
<div className={`py-2 w-full flex items-center justify-evenly border-t border-t-[var(--frontmatter-border)]`}>
<div className={`py-2 w-full flex items-center justify-evenly border-t border-t-[var(--frontmatter-border)] bg-[var(--frontmatter-sideBar-background)] group-hover:bg-[var(--vscode-list-hoverBackground)]`}>
<QuickAction
title={l10n.t(LocalizationKey.dashboardContentsContentActionsMenuItemView)}
className={`text-[var(--frontmatter-secondary-text)]`}
onClick={() => openFile(filePath)}>
<span className={`sr-only`}>{l10n.t(LocalizationKey.dashboardContentsContentActionsMenuItemView)}</span>
<EyeIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
@@ -33,6 +34,7 @@ export const FooterActions: React.FunctionComponent<IFooterActionsProps> = ({
title={l10n.t(LocalizationKey.commonOpenOnWebsite)}
className={`text-[var(--frontmatter-secondary-text)]`}
onClick={() => openOnWebsite(websiteUrl, filePath)}>
<span className={`sr-only`}>{l10n.t(LocalizationKey.commonOpenOnWebsite)}</span>
<GlobeEuropeAfricaIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
)
@@ -42,6 +44,7 @@ export const FooterActions: React.FunctionComponent<IFooterActionsProps> = ({
title={l10n.t(LocalizationKey.commonDelete)}
className={`text-[var(--frontmatter-secondary-text)] hover:text-[var(--vscode-statusBarItem-errorBackground)]`}
onClick={() => setSelectedItemAction({ path: filePath, action: 'delete' })}>
<span className={`sr-only`}>{l10n.t(LocalizationKey.commonDelete)}</span>
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
</div>
@@ -0,0 +1,98 @@
import * as React from 'react';
import * as l10n from '@vscode/l10n';
import { QuickAction } from '../Menu';
import { LocalizationKey } from '../../../localization';
import { ClipboardIcon, CodeBracketIcon, EyeIcon, PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/outline';
import { useRecoilState } from 'recoil';
import { SelectedItemActionAtom } from '../../state';
import { MediaInfo, Snippet, ViewData } from '../../../models';
import { copyToClipboard } from '../../utils';
import { parseWinPath } from '../../../helpers/parseWinPath';
export interface IFooterActionsProps {
media: MediaInfo;
snippets: Snippet[];
relPath?: string;
viewData?: ViewData | undefined
insertIntoArticle: () => void;
insertSnippet: () => void;
onDelete: () => void;
}
export const FooterActions: React.FunctionComponent<IFooterActionsProps> = ({
relPath,
media,
snippets,
viewData,
insertIntoArticle,
insertSnippet,
onDelete,
}: React.PropsWithChildren<IFooterActionsProps>) => {
const [, setSelectedItemAction] = useRecoilState(SelectedItemActionAtom);
return (
<div className={`py-2 w-full flex items-center justify-evenly border-t border-t-[var(--frontmatter-border)] bg-[var(--frontmatter-sideBar-background)] group-hover:bg-[var(--vscode-list-hoverBackground)]`}>
<QuickAction
title={l10n.t(LocalizationKey.dashboardMediaItemMenuItemView)}
onClick={() => setSelectedItemAction({
path: media.fsPath,
action: 'view'
})}>
<EyeIcon className={`w-4 h-4`} aria-hidden="true" />
<span className='sr-only'>{l10n.t(LocalizationKey.dashboardMediaItemMenuItemView)}</span>
</QuickAction>
<QuickAction
title={l10n.t(LocalizationKey.dashboardMediaItemMenuItemEditMetadata)}
onClick={() => setSelectedItemAction({
path: media.fsPath,
action: 'edit'
})}>
<PencilIcon className={`w-4 h-4`} aria-hidden="true" />
<span className='sr-only'>{l10n.t(LocalizationKey.dashboardMediaItemMenuItemEditMetadata)}</span>
</QuickAction>
{viewData?.filePath ? (
<>
<QuickAction
title={
viewData.metadataInsert && viewData.fieldName
? l10n.t(LocalizationKey.dashboardMediaItemQuickActionInsertField, viewData.fieldName)
: l10n.t(LocalizationKey.dashboardMediaItemQuickActionInsertMarkdown)
}
onClick={insertIntoArticle}
>
<PlusIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
{viewData?.position && snippets.length > 0 && (
<QuickAction
title={l10n.t(LocalizationKey.commonInsertSnippet)}
onClick={insertSnippet}>
<CodeBracketIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
)}
</>
) : (
<>
{
relPath && (
<QuickAction
title={l10n.t(LocalizationKey.dashboardMediaItemQuickActionCopyPath)}
onClick={() => copyToClipboard(parseWinPath(relPath) || '')}>
<ClipboardIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
)
}
</>
)}
<QuickAction
title={l10n.t(LocalizationKey.dashboardMediaItemQuickActionDelete)}
className={`hover:text-[var(--vscode-statusBarItem-errorBackground)]`}
onClick={onDelete}>
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
</div>
);
};
+21 -12
View File
@@ -32,6 +32,7 @@ import { getRelPath } from '../../utils';
import { Snippet } from '../../../models';
import useMediaInfo from '../../hooks/useMediaInfo';
import { ItemSelection } from '../Common/ItemSelection';
import { FooterActions } from './FooterActions';
export interface IItemProps {
media: MediaInfo;
@@ -185,13 +186,6 @@ export const Item: React.FunctionComponent<IItemProps> = ({
}
}, [media.vsPath]);
const updateMetadata = useCallback(() => {
setSelectedItemAction({
path: media.fsPath,
action: 'edit'
});
}, [media]);
const renderMediaIcon = useMemo(() => {
const path = media.fsPath;
const extension = path.split('.').pop();
@@ -265,7 +259,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({
return (
<>
<li className={`group relative shadow-md hover:shadow-xl dark:shadow-none border rounded bg-[var(--vscode-sideBar-background)] hover:bg-[var(--vscode-list-hoverBackground)] text-[var(--vscode-sideBarTitle-foreground)] border-[var(--frontmatter-border)]`}>
<li className={`group flex flex-col relative shadow-md hover:shadow-xl dark:shadow-none border rounded bg-[var(--vscode-sideBar-background)] hover:bg-[var(--vscode-list-hoverBackground)] text-[var(--vscode-sideBarTitle-foreground)] border-[var(--frontmatter-border)]`}>
<button
className={`group/button relative block w-full aspect-w-10 aspect-h-7 overflow-hidden h-48 ${isImage ? 'cursor-pointer' : 'cursor-default'} border-b border-[var(--frontmatter-border)]`}
onClick={hasViewData ? undefined : openLightbox}
@@ -318,7 +312,8 @@ export const Item: React.FunctionComponent<IItemProps> = ({
</div>
)}
</button>
<div className={`relative py-4 pl-4 pr-12`}>
<div className={`relative py-4 pl-4 pr-12 grow`}>
<ItemMenu
media={media}
relPath={relPath}
@@ -327,9 +322,14 @@ export const Item: React.FunctionComponent<IItemProps> = ({
snippets={mediaSnippets}
scripts={settings?.scripts}
insertIntoArticle={insertIntoArticle}
insertSnippet={insertSnippet}
showUpdateMedia={updateMetadata}
showMediaDetails={() => setSelectedItemAction({ path: media.fsPath, action: 'view' })}
showUpdateMedia={() => setSelectedItemAction({
path: media.fsPath,
action: 'edit'
})}
showMediaDetails={() => setSelectedItemAction({
path: media.fsPath,
action: 'view'
})}
processSnippet={processSnippet}
onDelete={() => setShowAlert(true)} />
@@ -371,6 +371,15 @@ export const Item: React.FunctionComponent<IItemProps> = ({
</p>
)}
</div>
<FooterActions
media={media}
relPath={relPath}
snippets={mediaSnippets}
viewData={viewData?.data}
insertIntoArticle={insertIntoArticle}
insertSnippet={insertSnippet}
onDelete={() => setShowAlert(true)} />
</li>
{showSnippetSelection && (
@@ -1,13 +1,13 @@
import * as React from 'react';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { QuickAction } from '../Menu';
import { ClipboardIcon, CodeBracketIcon, CommandLineIcon, EllipsisVerticalIcon, EyeIcon, PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/outline';
import { ClipboardIcon, CodeBracketIcon, CommandLineIcon, EllipsisHorizontalIcon, EyeIcon, PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/outline';
import { CustomScript, MediaInfo, ScriptType, Snippet, ViewData } from '../../../models';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../../../components/shadcn/Dropdown';
import { messageHandler } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
import { parseWinPath } from '../../../helpers/parseWinPath';
import { copyToClipboard } from '../../utils';
export interface IItemMenuProps {
media: MediaInfo;
@@ -17,7 +17,6 @@ export interface IItemMenuProps {
snippets: Snippet[];
scripts?: CustomScript[];
insertIntoArticle: () => void;
insertSnippet: () => void;
showUpdateMedia: () => void;
showMediaDetails: () => void;
processSnippet: (snippet: Snippet) => void;
@@ -32,17 +31,14 @@ export const ItemMenu: React.FunctionComponent<IItemMenuProps> = ({
snippets,
scripts,
insertIntoArticle,
insertSnippet,
showUpdateMedia,
showMediaDetails,
processSnippet,
onDelete,
}: React.PropsWithChildren<IItemMenuProps>) => {
const copyToClipboard = React.useCallback(() => {
if (relPath) {
messageHandler.send(DashboardMessage.copyToClipboard, parseWinPath(relPath) || '');
}
const onCopyToClipboard = React.useCallback(() => {
copyToClipboard(parseWinPath(relPath) || '');
}, [relPath]);
const runCustomScript = React.useCallback((script: CustomScript) => {
@@ -75,65 +71,20 @@ export const ItemMenu: React.FunctionComponent<IItemMenuProps> = ({
return (
<div className={`group/actions absolute top-4 right-4 flex flex-col space-y-4`}>
<div className={`flex items-center border border-transparent rounded-full p-2 -mr-2 -mt-2 group-hover/actions:bg-[var(--vscode-sideBar-background)] group-hover/actions:border-[var(--frontmatter-border)]`}>
<div className={`flex items-center border border-transparent rounded-full p-1 -mr-2 -mt-1 group-hover/actions:bg-[var(--vscode-sideBar-background)] group-hover/actions:border-[var(--frontmatter-border)]`}>
<div className="relative z-10 flex text-left">
<div className="hidden group-hover/actions:flex">
<QuickAction title={l10n.t(LocalizationKey.dashboardMediaItemMenuItemView)} onClick={showMediaDetails}>
<EyeIcon className={`w-4 h-4`} aria-hidden="true" />
<span className='sr-only'>{l10n.t(LocalizationKey.dashboardMediaItemMenuItemView)}</span>
</QuickAction>
<QuickAction title={l10n.t(LocalizationKey.dashboardMediaItemMenuItemEditMetadata)} onClick={showUpdateMedia}>
<PencilIcon className={`w-4 h-4`} aria-hidden="true" />
<span className='sr-only'>{l10n.t(LocalizationKey.dashboardMediaItemMenuItemEditMetadata)}</span>
</QuickAction>
{viewData?.filePath ? (
<>
<QuickAction
title={
viewData.metadataInsert && viewData.fieldName
? l10n.t(LocalizationKey.dashboardMediaItemQuickActionInsertField, viewData.fieldName)
: l10n.t(LocalizationKey.dashboardMediaItemQuickActionInsertMarkdown)
}
onClick={insertIntoArticle}
>
<PlusIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
{viewData?.position && snippets.length > 0 && (
<QuickAction title={l10n.t(LocalizationKey.commonInsertSnippet)} onClick={insertSnippet}>
<CodeBracketIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
)}
</>
) : (
<>
{
relPath && (
<QuickAction title={l10n.t(LocalizationKey.dashboardMediaItemQuickActionCopyPath)} onClick={copyToClipboard}>
<ClipboardIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
)
}
</>
)}
<QuickAction
title={l10n.t(LocalizationKey.dashboardMediaItemQuickActionDelete)}
className={`hover:text-[var(--vscode-statusBarItem-errorBackground)]`}
onClick={onDelete}>
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
</QuickAction>
</div>
<DropdownMenu>
<DropdownMenuTrigger className='text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-tab-activeForeground)]'>
<span className="sr-only">{l10n.t(LocalizationKey.commonMenu)}</span>
<EllipsisVerticalIcon className="w-4 h-4" aria-hidden="true" />
<EllipsisHorizontalIcon className="w-4 h-4" aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={showMediaDetails}>
<EyeIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<span>{l10n.t(LocalizationKey.commonView)}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={showUpdateMedia}>
<PencilIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<span>{l10n.t(LocalizationKey.dashboardMediaItemMenuItemEditMetadata)}</span>
@@ -165,7 +116,7 @@ export const ItemMenu: React.FunctionComponent<IItemMenuProps> = ({
</>
) : (
<>
<DropdownMenuItem onClick={copyToClipboard}>
<DropdownMenuItem onClick={onCopyToClipboard}>
<ClipboardIcon className="mr-2 h-4 w-4" aria-hidden={true} />
<span>{l10n.t(LocalizationKey.dashboardMediaItemQuickActionCopyPath)}</span>
</DropdownMenuItem>
@@ -28,3 +28,11 @@ export const openOnWebsite = (websiteUrl?: string, filePath?: string) => {
filePath
});
};
export const copyToClipboard = (value: string) => {
if (!value) {
return;
}
messageHandler.send(DashboardMessage.copyToClipboard, value);
};
+70
View File
@@ -0,0 +1,70 @@
export const darkenColor = (color: string | undefined, percentage: number) => {
if (!color) {
return color;
}
// Remove any whitespace and convert to lowercase
color = color.trim().toLowerCase();
// Check if the color is in hex format
if (color.startsWith('#')) {
// Remove the "#" symbol
color = color.slice(1);
// Convert the color to rgb format
const hexToRgb = (hex: string) => {
const bigint = parseInt(hex, 16);
const r = (bigint >> 16) & 255;
const g = (bigint >> 8) & 255;
const b = bigint & 255;
return [r, g, b];
};
const [r, g, b] = hexToRgb(color);
// Calculate the darkened color
const darkenValue = Math.round(255 * (percentage / 100));
const darkenedR = Math.max(r - darkenValue, 0);
const darkenedG = Math.max(g - darkenValue, 0);
const darkenedB = Math.max(b - darkenValue, 0);
// Convert the darkened color back to hex format
const rgbToHex = (r: number, g: number, b: number) => {
const toHex = (c: number) => {
const hex = c.toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
};
return rgbToHex(darkenedR, darkenedG, darkenedB);
}
// Check if the color is in rgb or rgba format
if (color.startsWith('rgb')) {
// Extract the RGB values
const rgbValues = color.match(/\d+/g);
if (rgbValues) {
const [r, g, b] = rgbValues.map(Number);
// Calculate the darkened color
const darkenValue = Math.round(255 * (percentage / 100));
const darkenedR = Math.max(r - darkenValue, 0);
const darkenedG = Math.max(g - darkenValue, 0);
const darkenedB = Math.max(b - darkenValue, 0);
// Check if the color is in rgba format
if (color.startsWith('rgba')) {
// Extract the alpha value
const alpha = Number(color.match(/[\d\.]+$/));
return `rgba(${darkenedR}, ${darkenedG}, ${darkenedB}, ${alpha})`;
}
return `rgb(${darkenedR}, ${darkenedG}, ${darkenedB})`;
}
}
return color;
};
+1
View File
@@ -1,4 +1,5 @@
export * from './MessageHandlers';
export * from './darkenColor';
export * from './getRelPath';
export * from './preserveColor';
export * from './updateCssVariables';
@@ -1,3 +1,4 @@
import { darkenColor } from './darkenColor';
import { preserveColor } from './preserveColor';
export const updateCssVariables = () => {
@@ -75,4 +76,11 @@ export const updateCssVariables = () => {
preserveColor(buttonHoverBackground) || 'var(--vscode-button-hoverBackground)'
);
}
// Darken the background of a color
const sideBarBg = styles.getPropertyValue('--vscode-sideBar-background');
document.documentElement.style.setProperty(
'--frontmatter-sideBar-background',
darkenColor(sideBarBg, 2) || 'var(--vscode-sideBar-background)'
);
};