#308 - Supported mime types for media dashboard

This commit is contained in:
Elio Struyf
2022-04-04 17:24:55 +02:00
parent f059b89fa5
commit e6e5c5c881
15 changed files with 232 additions and 8536 deletions
+42 -8453
View File
File diff suppressed because it is too large Load Diff
+33 -8
View File
@@ -568,7 +568,10 @@
"markdownDescription": "Specify the command you want to use to start your static site generator or framework. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.framework.startcommand)"
},
"frontMatter.global.activeMode": {
"type": ["string", "null"],
"type": [
"string",
"null"
],
"default": "",
"markdownDescription": "Specify the activated mode of Front Matter. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.global.activemode)",
"scope": "Global"
@@ -592,14 +595,14 @@
"items": {
"type": "string",
"enum": [
"panel.globalSettings",
"panel.globalSettings",
"panel.seo",
"panel.actions",
"panel.metadata",
"panel.recentlyModified",
"panel.actions",
"panel.metadata",
"panel.recentlyModified",
"panel.otherActions",
"dashboard.snippets.view",
"dashboard.snippets.manage",
"dashboard.snippets.manage",
"dashboard.data.view"
]
}
@@ -608,7 +611,7 @@
"required": [
"id",
"features"
]
]
},
"scope": "Global"
},
@@ -630,6 +633,19 @@
"markdownDescription": "Specifies the notifications you want to see. By default, all notifications types will be shown. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.global.notifications)",
"scope": "Templates"
},
"frontMatter.media.supportedMimeTypes": {
"type": "array",
"default": [
"image/*",
"video/*",
"audio/*"
],
"markdownDescription": "Specify the mime types to support for the media files. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.media.supportedMimeTypes)",
"items": {
"type": "string"
},
"scope": "Media"
},
"frontMatter.media.defaultSorting": {
"type": "string",
"default": "",
@@ -748,7 +764,14 @@
"description": "Title to show in the UI"
},
"default": {
"type": ["string", "number", "boolean", "array", "object", "null"],
"type": [
"string",
"number",
"boolean",
"array",
"object",
"null"
],
"description": "Default value"
},
"choices": {
@@ -1736,6 +1759,7 @@
"@types/lodash.omit": "^4.5.6",
"@types/lodash.uniqby": "4.7.6",
"@types/lodash.xor": "^4.5.6",
"@types/mime-types": "^2.1.1",
"@types/mocha": "^5.2.6",
"@types/mustache": "^4.1.2",
"@types/node": "10.17.48",
@@ -1766,6 +1790,7 @@
"lodash.uniqby": "4.7.0",
"lodash.xor": "^4.5.0",
"mdast-util-from-markdown": "1.0.0",
"mime-types": "^2.1.35",
"mustache": "^4.2.0",
"node-json-db": "^1.3.0",
"npm-run-all": "^4.1.5",
+2
View File
@@ -58,6 +58,8 @@ export const SETTING_MEDIA_SORTING_DEFAULT = "content.defaultSorting";
export const SETTING_CONTENT_DEFAULT_FILETYPE = "content.defaultFileType";
export const SETTING_CONTENT_SUPPORTED_FILETYPES = "content.supportedFileTypes";
export const SETTING_MEDIA_SUPPORTED_MIMETYPES = "media.supportedMimeTypes";
export const SETTING_DASHBOARD_OPENONSTART = "dashboard.openOnStart";
export const SETTING_DASHBOARD_MEDIA_SNIPPET = "dashboard.mediaSnippet";
export const SETTING_DASHBOARD_CONTENT_TAGS = "dashboard.contentTags";
+1
View File
@@ -3,6 +3,7 @@ export enum DashboardMessage {
reload = 'reload',
setPageViewType = 'setPageViewType',
getMode = 'getMode',
showWarning = 'showWarning',
// Content dashboard
getData = 'getData',
@@ -1,18 +1,13 @@
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { MediaTotalSelector, PageAtom, SearchAtom, SelectedMediaFolderSelector } from '../../state';
import { Messenger } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
import { RefreshIcon } from '@heroicons/react/outline';
import { useRecoilValue } from 'recoil';
import { MediaTotalSelector, PageAtom } from '../../state';
import { LIMIT } from '../../hooks/useMedia';
export interface IPaginationStatusProps {}
export const PaginationStatus: React.FunctionComponent<IPaginationStatusProps> = (props: React.PropsWithChildren<IPaginationStatusProps>) => {
const totalMedia = useRecoilValue(MediaTotalSelector);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const [ page, setPage ] = useRecoilState(PageAtom);
const [ , setSearch ] = useRecoilState(SearchAtom);
const page = useRecoilValue(PageAtom);
const getTotalPage = () => {
const mediaItems = ((page + 1) * LIMIT);
@@ -22,21 +17,8 @@ export const PaginationStatus: React.FunctionComponent<IPaginationStatusProps> =
return mediaItems;
};
const refresh = () => {
setPage(0);
setSearch('');
Messenger.send(DashboardMessage.refreshMedia, { folder: selectedFolder });
}
return (
<div className="hidden sm:flex">
<button className={`mr-2 text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500`}
title="Refresh media"
onClick={refresh}>
<RefreshIcon className={`h-5 w-5`} />
<span className="sr-only">Refresh media</span>
</button>
<div className="hidden sm:flex">
<p className="text-sm text-gray-500 dark:text-whisper-900">
Showing <span className="font-medium">{(page * LIMIT) + 1}</span> to <span className="font-medium">{getTotalPage()}</span> of{' '}
<span className="font-medium">{totalMedia}</span> results
@@ -0,0 +1,57 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { RefreshIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useCallback } from 'react';
import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil';
import { DashboardMessage } from '../../DashboardMessage';
import { NavigationType } from '../../models';
import { CategoryAtom, DashboardViewAtom, FolderAtom, LoadingAtom, PageAtom, SearchAtom, SelectedMediaFolderSelector, SortingAtom, TagAtom } from '../../state';
export interface IRefreshDashboardDataProps {}
export const RefreshDashboardData: React.FunctionComponent<IRefreshDashboardDataProps> = (props: React.PropsWithChildren<IRefreshDashboardDataProps>) => {
const view = useRecoilValue(DashboardViewAtom);
const [, setLoading] = useRecoilState(LoadingAtom);
const resetSearch = useResetRecoilState(SearchAtom);
const resetSorting = useResetRecoilState(SortingAtom);
const resetFolder = useResetRecoilState(FolderAtom);
const resetTag = useResetRecoilState(TagAtom);
const resetCategory = useResetRecoilState(CategoryAtom);
// Media
const resetPage = useResetRecoilState(PageAtom);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const refreshPages = () => {
setLoading(true);
resetSearch();
resetSorting();
resetFolder();
resetTag();
resetCategory();
Messenger.send(DashboardMessage.refreshPages);
};
const refreshMedia = () => {
setLoading(true);
resetPage();
resetSearch();
Messenger.send(DashboardMessage.refreshMedia, { folder: selectedFolder });
};
const refresh = useCallback(() => {
if (view === NavigationType.Contents) {
refreshPages();
} else if (view === NavigationType.Media) {
refreshMedia();
}
}, [view]);
return (
<button className={`mr-2 text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500`}
title="Refresh dashboard"
onClick={refresh}>
<RefreshIcon className={`h-5 w-5`} />
<span className="sr-only">Refresh dashboard</span>
</button>
);
};
@@ -1,36 +0,0 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { RefreshIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useRecoilState, useResetRecoilState } from 'recoil';
import { DashboardMessage } from '../../DashboardMessage';
import { CategoryAtom, FolderAtom, LoadingAtom, SearchAtom, SortingAtom, TagAtom } from '../../state';
export interface IRefreshPagesProps {}
export const RefreshPages: React.FunctionComponent<IRefreshPagesProps> = (props: React.PropsWithChildren<IRefreshPagesProps>) => {
const [, setLoading] = useRecoilState(LoadingAtom);
const resetSearch = useResetRecoilState(SearchAtom);
const resetSorting = useResetRecoilState(SortingAtom);
const resetFolder = useResetRecoilState(FolderAtom);
const resetTag = useResetRecoilState(TagAtom);
const resetCategory = useResetRecoilState(CategoryAtom);
const refresh = () => {
setLoading(true);
resetSearch();
resetSorting();
resetFolder();
resetTag();
resetCategory();
Messenger.send(DashboardMessage.refreshPages);
}
return (
<button className={`mr-2 text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500`}
title="Refresh dashboard"
onClick={refresh}>
<RefreshIcon className={`h-5 w-5`} />
<span className="sr-only">Refresh dashboard</span>
</button>
);
};
@@ -3,7 +3,7 @@ import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useDebounce } from '../../../hooks/useDebounce';
import { SearchAtom, SearchReadyAtom } from '../../state';
import { RefreshPages } from './RefreshPages';
import { RefreshDashboardData } from './RefreshDashboardData';
export interface ISearchboxProps {
placeholder?: string;
@@ -63,7 +63,7 @@ export const Searchbox: React.FunctionComponent<ISearchboxProps> = ({placeholder
</div>
</div>
<RefreshPages />
<RefreshDashboardData />
</div>
);
};
+41 -7
View File
@@ -1,9 +1,9 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { Menu } from '@headlessui/react';
import { ClipboardIcon, CodeIcon, EyeIcon, PencilIcon, PhotographIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline';
import { ClipboardIcon, CodeIcon, EyeIcon, MusicNoteIcon, PencilIcon, PhotographIcon, PlusIcon, TrashIcon, VideoCameraIcon } from '@heroicons/react/outline';
import { basename, dirname } from 'path';
import * as React from 'react';
import { useEffect } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { CustomScript } from '../../../helpers/CustomScript';
import { parseWinPath } from '../../../helpers/parseWinPath';
@@ -166,9 +166,11 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
setShowDetails(true);
};
const openLightbox = () => {
setLightbox(media.vsPath || "");
};
const openLightbox = useCallback(() => {
if (!isVideoFile() && !isAudioFile()) {
setLightbox(media.vsPath || "");
}
}, [media.vsPath]);
const updateMetadata = () => {
setShowForm(true);
@@ -184,6 +186,28 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
))
}
const isVideoFile = useCallback(() => {
if (media.mimeType?.startsWith("video/")) {
return true;
}
return false;
}, [media]);
const isAudioFile = useCallback(() => {
if (media.mimeType?.startsWith("audio/")) {
return true;
}
return false;
}, [media]);
const renderMedia = useMemo(() => {
if (isVideoFile() || isAudioFile()) {
return null;
}
return <img src={media.vsPath} alt={basename(media.fsPath)} className="mx-auto object-cover" />;
}, [media]);
useEffect(() => {
if (media.alt !== alt) {
setAlt(media.alt);
@@ -208,10 +232,20 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
<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-200 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`} />
{
isVideoFile() ? (
<VideoCameraIcon className={`h-1/2 text-gray-300 dark:text-vulcan-200`} />
) : (
isAudioFile() ? (
<MusicNoteIcon className={`h-1/2 text-gray-300 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" />
{ renderMedia }
</div>
</button>
<div className={`relative py-4 pl-4 pr-12`}>
@@ -73,7 +73,10 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
const {getRootProps, isDragActive} = useDropzone({
onDrop,
accept: 'image/*'
accept: settings?.dashboardState.media.mimeTypes || ['image/*', 'video/*', 'audio/*'],
onDropRejected: () => {
Messenger.send(DashboardMessage.showWarning, 'Unsupported file type');
}
});
return (
+1
View File
@@ -46,4 +46,5 @@ export interface ContentsViewState {
export interface MediaViewState extends ContentsViewState {
selectedFolder: string | null | undefined;
mimeTypes: string[] | null | undefined;
}
+3 -2
View File
@@ -2,7 +2,7 @@ import { basename, join } from "path";
import { workspace } from "vscode";
import { Folders } from "../commands/Folders";
import { Template } from "../commands/Template";
import { CONTEXT, ExtensionState, SETTING_CONTENT_DRAFT_FIELD, SETTING_CONTENT_SORTING, SETTING_CONTENT_SORTING_DEFAULT, SETTING_CONTENT_STATIC_FOLDER, SETTING_DASHBOARD_MEDIA_SNIPPET, SETTING_DASHBOARD_OPENONSTART, SETTING_DATA_FILES, SETTING_DATA_FOLDERS, SETTING_DATA_TYPES, SETTING_FRAMEWORK_ID, SETTING_MEDIA_SORTING_DEFAULT, SETTING_CUSTOM_SCRIPTS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_CONTENT_SNIPPETS, SETTING_DATE_FORMAT, SETTING_DASHBOARD_CONTENT_TAGS } from "../constants";
import { CONTEXT, ExtensionState, SETTING_CONTENT_DRAFT_FIELD, SETTING_CONTENT_SORTING, SETTING_CONTENT_SORTING_DEFAULT, SETTING_CONTENT_STATIC_FOLDER, SETTING_DASHBOARD_MEDIA_SNIPPET, SETTING_DASHBOARD_OPENONSTART, SETTING_DATA_FILES, SETTING_DATA_FOLDERS, SETTING_DATA_TYPES, SETTING_FRAMEWORK_ID, SETTING_MEDIA_SORTING_DEFAULT, SETTING_CUSTOM_SCRIPTS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_CONTENT_SNIPPETS, SETTING_DATE_FORMAT, SETTING_DASHBOARD_CONTENT_TAGS, SETTING_MEDIA_SUPPORTED_MIMETYPES } from "../constants";
import { DashboardViewType, SortingOption, Settings as ISettings } from "../dashboardWebView/models";
import { CustomScript, DraftField, ScriptType, Snippets, SortingSetting, TaxonomyType } from "../models";
import { DataFile } from "../models/DataFile";
@@ -51,7 +51,8 @@ export class DashboardSettings {
media: {
sorting: await ext.getState<SortingOption | undefined>(ExtensionState.Dashboard.Media.Sorting, "workspace"),
defaultSorting: Settings.get<string>(SETTING_MEDIA_SORTING_DEFAULT),
selectedFolder: await ext.getState<string | undefined>(ExtensionState.SelectedFolder, "workspace")
selectedFolder: await ext.getState<string | undefined>(ExtensionState.SelectedFolder, "workspace"),
mimeTypes: Settings.get<string[]>(SETTING_MEDIA_SUPPORTED_MIMETYPES)
}
},
dataFiles: await this.getDataFiles(),
+37 -4
View File
@@ -1,7 +1,7 @@
import { decodeBase64Image, Extension, MediaLibrary, Notifications, parseWinPath, Settings, Sorting } from ".";
import { Dashboard } from "../commands/Dashboard";
import { Folders } from "../commands/Folders";
import { DEFAULT_CONTENT_TYPE, ExtensionState, HOME_PAGE_NAVIGATION_ID, SETTING_CONTENT_STATIC_FOLDER } from "../constants";
import { DEFAULT_CONTENT_TYPE, ExtensionState, HOME_PAGE_NAVIGATION_ID, SETTING_CONTENT_STATIC_FOLDER, SETTING_MEDIA_SUPPORTED_MIMETYPES } from "../constants";
import { SortingOption } from "../dashboardWebView/models";
import { MediaInfo, MediaPaths, SortOrder, SortType } from "../models";
import { basename, extname, join, parse, dirname, relative } from "path";
@@ -12,6 +12,7 @@ import { EditorHelper } from "@estruyf/vscode";
import { SortOption } from "../dashboardWebView/constants/SortOption";
import { DataListener, MediaListener } from "../listeners/panel";
import { ArticleHelper } from "./ArticleHelper";
import { lookup } from "mime-types";
export class MediaHelpers {
@@ -109,10 +110,12 @@ export class MediaHelpers {
files = files.map((file) => {
try {
const metadata = MediaLibrary.getInstance().get(file.fsPath);
const mimeType = lookup(file.fsPath);
return {
...file,
dimensions: imageSize(file.fsPath),
dimensions: mimeType && mimeType.startsWith('image/') ? imageSize(file.fsPath) : undefined,
mimeType: lookup(file.fsPath) || '',
...metadata
};
} catch (e) {
@@ -356,9 +359,16 @@ export class MediaHelpers {
* Filter the media files
*/
private static filterMedia(files: Uri[]) {
let mimeTypes = Settings.get<string[]>(SETTING_MEDIA_SUPPORTED_MIMETYPES) || ["image/*", "video/*", "audio/*"];
mimeTypes = mimeTypes.map(type => type.toLowerCase());
return files.filter(file => {
const ext = extname(file.fsPath);
return ['.jpg', '.jpeg', '.png', '.gif', '.svg'].includes(ext.toLowerCase());
const type = lookup(file.fsPath);
if (type) {
const isValid = this.acceptMimeType(type, mimeTypes);
return isValid;
}
return false;
}).map((file) => ({
filename: basename(file.fsPath),
fsPath: file.fsPath,
@@ -379,4 +389,27 @@ export class MediaHelpers {
return Object.assign([], files);
}
/**
* Validate mimetype of the selected file
* @param crntType
* @param supportedTypes
* @returns
*/
private static acceptMimeType(crntType: string, supportedTypes: string[]) {
if (crntType && supportedTypes) {
const mimeType = crntType.toLowerCase();
const baseMimeType = mimeType.replace(/\/.*$/, '')
return supportedTypes.some(type => {
const validType = type.trim().toLowerCase()
if (validType.endsWith('/*')) {
// This is something like a image/* mime type
return baseMimeType === validType.replace(/\/.*$/, '')
}
return mimeType === validType
})
}
return true
}
}
+4 -1
View File
@@ -2,7 +2,7 @@ import { Dashboard } from "../../commands/Dashboard";
import { ExtensionState } from "../../constants";
import { DashboardCommand } from "../../dashboardWebView/DashboardCommand";
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
import { Extension } from "../../helpers";
import { Extension, Notifications } from "../../helpers";
import { BaseListener } from "./BaseListener";
@@ -27,6 +27,9 @@ export class DashboardListener extends BaseListener {
case DashboardMessage.setPageViewType:
Extension.getInstance().setState(ExtensionState.PagesView, msg.data, "workspace");
break;
case DashboardMessage.showWarning:
Notifications.warning(msg.data);
break;
}
}
}
+1
View File
@@ -14,6 +14,7 @@ export interface MediaInfo {
dimensions?: ISizeCalculationResult | undefined;
caption?: string | undefined;
alt?: string | undefined;
mimeType?: string | undefined;
mtime?: Date;
ctime?: Date;
size?: number;