From ab3686b3b5cbf32dc4b20da3f87af2534043532d Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Mon, 27 Dec 2021 15:56:12 +0100 Subject: [PATCH] #199 - Search media files --- CHANGELOG.md | 1 + .../components/Header/Breadcrumb.tsx | 87 +++++---- .../components/Header/Header.tsx | 9 +- .../components/Header/Pagination.tsx | 72 ++++++++ .../{Media => Header}/PaginationButton.tsx | 0 .../components/Header/PaginationStatus.tsx | 46 +++++ .../components/Header/Searchbox.tsx | 13 +- .../components/Media/FolderCreation.tsx | 34 ++-- .../components/Media/Media.tsx | 31 +--- .../components/Media/MediaHeaderBottom.tsx | 30 ++++ .../components/Media/MediaHeaderTop.tsx | 81 +++++++++ .../components/Media/Pagination.tsx | 169 ------------------ src/dashboardWebView/components/Startup.tsx | 2 +- src/dashboardWebView/hooks/useMedia.tsx | 75 ++++++++ src/helpers/MediaHelpers.ts | 2 +- src/models/MediaPaths.ts | 1 + tsconfig.json | 3 + 17 files changed, 393 insertions(+), 263 deletions(-) create mode 100644 src/dashboardWebView/components/Header/Pagination.tsx rename src/dashboardWebView/components/{Media => Header}/PaginationButton.tsx (100%) create mode 100644 src/dashboardWebView/components/Header/PaginationStatus.tsx create mode 100644 src/dashboardWebView/components/Media/MediaHeaderBottom.tsx create mode 100644 src/dashboardWebView/components/Media/MediaHeaderTop.tsx delete mode 100644 src/dashboardWebView/components/Media/Pagination.tsx create mode 100644 src/dashboardWebView/hooks/useMedia.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 80b61165..4deaf71c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### 🎨 Enhancements +- [#199](https://github.com/estruyf/vscode-front-matter/issues/199): Search media files in the currently selected folder - [#211](https://github.com/estruyf/vscode-front-matter/issues/211): Replace text selection on media inserts - [#212](https://github.com/estruyf/vscode-front-matter/issues/212): Create folder watchers for content folders. When new content gets created, the dashboard updates. - [#213](https://github.com/estruyf/vscode-front-matter/issues/213): New media folder overview design diff --git a/src/dashboardWebView/components/Header/Breadcrumb.tsx b/src/dashboardWebView/components/Header/Breadcrumb.tsx index 3b7866a2..b7ddc697 100644 --- a/src/dashboardWebView/components/Header/Breadcrumb.tsx +++ b/src/dashboardWebView/components/Header/Breadcrumb.tsx @@ -1,28 +1,32 @@ -import {CollectionIcon} from '@heroicons/react/outline'; +import { CollectionIcon } from '@heroicons/react/outline'; import { basename, join } from 'path'; import * as React from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; -import { Sorting } from '.'; import { HOME_PAGE_NAVIGATION_ID } from '../../../constants'; import { parseWinPath } from '../../../helpers/parseWinPath'; -import { NavigationType } from '../../models'; -import { SelectedMediaFolderAtom, SettingsAtom } from '../../state'; +import { SearchAtom, SelectedMediaFolderAtom, SettingsAtom } from '../../state'; export interface IBreadcrumbProps {} export const Breadcrumb: React.FunctionComponent = (props: React.PropsWithChildren) => { const [ selectedFolder, setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom); - const settings = useRecoilValue(SettingsAtom); + const [ , setSearchValue ] = useRecoilState(SearchAtom); const [ folders, setFolders ] = React.useState([]); + const settings = useRecoilValue(SettingsAtom); - if (!settings?.wsFolder) { - return null; + const updateFolder = (folder: string) => { + setSearchValue(''); + setSelectedFolder(folder); } React.useEffect(() => { + if (!settings) { + return; + } + const { wsFolder, staticFolder, contentFolders } = settings; - const isValid = (folderPath: string) => { + const isValid = (folderPath: string) => { if (staticFolder) { const staticPath = parseWinPath(join(wsFolder, staticFolder)) as string; const relPath = folderPath.replace(staticPath, '') as string; @@ -33,7 +37,7 @@ export const Breadcrumb: React.FunctionComponent = (props: Rea return false; } } -1 + for (let i = 0; i < contentFolders.length; i++) { const folder = contentFolders[i]; const contentFolder = parseWinPath(folder.path) as string; @@ -62,45 +66,40 @@ export const Breadcrumb: React.FunctionComponent = (props: Rea setFolders(allFolders); } - }, [selectedFolder]); - + }, [selectedFolder, settings]); + return ( - + ))} + ); }; \ No newline at end of file diff --git a/src/dashboardWebView/components/Header/Header.tsx b/src/dashboardWebView/components/Header/Header.tsx index a3818485..c480b25f 100644 --- a/src/dashboardWebView/components/Header/Header.tsx +++ b/src/dashboardWebView/components/Header/Header.tsx @@ -15,9 +15,9 @@ import { Messenger } from '@estruyf/vscode/dist/client'; import { ClearFilters } from './ClearFilters'; import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon'; import {PhotographIcon} from '@heroicons/react/outline'; -import { Pagination } from '../Media/Pagination'; +import { MediaHeaderTop } from '../Media/MediaHeaderTop'; import { ChoiceButton } from '../ChoiceButton'; -import { Breadcrumb } from './Breadcrumb'; +import { MediaHeaderBottom } from '../Media/MediaHeaderBottom'; export interface IHeaderProps { settings: Settings | null; @@ -125,8 +125,9 @@ export const Header: React.FunctionComponent = ({totalPages, folde { view === NavigationType.Media && ( <> - - + + + ) } diff --git a/src/dashboardWebView/components/Header/Pagination.tsx b/src/dashboardWebView/components/Header/Pagination.tsx new file mode 100644 index 00000000..85dbdda1 --- /dev/null +++ b/src/dashboardWebView/components/Header/Pagination.tsx @@ -0,0 +1,72 @@ +import * as React from 'react'; +import { useRecoilState, useRecoilValue } from 'recoil'; +import { LIMIT } from '../../hooks/useMedia'; +import { MediaTotalSelector, PageAtom } from '../../state'; +import { PaginationButton } from './PaginationButton'; + +export interface IPaginationProps {} + +export const Pagination: React.FunctionComponent = (props: React.PropsWithChildren) => { + const [ page, setPage ] = useRecoilState(PageAtom); + const totalMedia = useRecoilValue(MediaTotalSelector); + + const totalPages = Math.ceil(totalMedia / LIMIT) - 1; + + const getButtons = (): number[] => { + const maxButtons = 5; + const buttons: number[] = []; + const start = page - maxButtons; + const end = page + maxButtons; + + for (let i = start; i <= end; i++) { + if (i >= 0 && i <= totalPages) { + buttons.push(i); + } + } + return buttons; + }; + + return ( +
+ { + if (page > 0) { + setPage(0) + } + }} /> + + { + if (page > 0) { + setPage(page - 1) + } + }} /> + + {getButtons().map((button) => ( + + ))} + + = totalPages} + onClick={() => setPage(page + 1)} /> + + = totalPages} + onClick={() => setPage(totalPages)} /> +
+ ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/components/Media/PaginationButton.tsx b/src/dashboardWebView/components/Header/PaginationButton.tsx similarity index 100% rename from src/dashboardWebView/components/Media/PaginationButton.tsx rename to src/dashboardWebView/components/Header/PaginationButton.tsx diff --git a/src/dashboardWebView/components/Header/PaginationStatus.tsx b/src/dashboardWebView/components/Header/PaginationStatus.tsx new file mode 100644 index 00000000..49d6abf0 --- /dev/null +++ b/src/dashboardWebView/components/Header/PaginationStatus.tsx @@ -0,0 +1,46 @@ +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 { LIMIT } from '../../hooks/useMedia'; + +export interface IPaginationStatusProps {} + +export const PaginationStatus: React.FunctionComponent = (props: React.PropsWithChildren) => { + const totalMedia = useRecoilValue(MediaTotalSelector); + const selectedFolder = useRecoilValue(SelectedMediaFolderSelector); + const [ page, setPage ] = useRecoilState(PageAtom); + const [ , setSearch ] = useRecoilState(SearchAtom); + + const getTotalPage = () => { + const mediaItems = ((page + 1) * LIMIT); + if (totalMedia < mediaItems) { + return totalMedia; + } + return mediaItems; + }; + + const refresh = () => { + setPage(0); + setSearch(''); + Messenger.send(DashboardMessage.refreshMedia, { folder: selectedFolder }); + } + + return ( +
+ + +

+ Showing {(page * LIMIT) + 1} to {getTotalPage()} of{' '} + {totalMedia} results +

+
+ ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/components/Header/Searchbox.tsx b/src/dashboardWebView/components/Header/Searchbox.tsx index 80e9dc63..a9840a80 100644 --- a/src/dashboardWebView/components/Header/Searchbox.tsx +++ b/src/dashboardWebView/components/Header/Searchbox.tsx @@ -8,25 +8,32 @@ export interface ISearchboxProps {} export const Searchbox: React.FunctionComponent = ({}: React.PropsWithChildren) => { const [ value, setValue ] = React.useState(''); - const [ , setDebounceValue ] = useRecoilState(SearchAtom); + const [ debounceSearchValue, setDebounceValue ] = useRecoilState(SearchAtom); const debounceSearch = useDebounce(value, 500); const handleChange = (event: React.ChangeEvent) => { setValue(event.target.value); }; + React.useEffect(() => { + if (!debounceSearchValue && value) { + setValue(''); + } + } , [debounceSearchValue]); + React.useEffect(() => { setDebounceValue(debounceSearch); }, [debounceSearch]); return ( -
-
+
+
+ = (pr const scripts = (settings?.scripts || []).filter(script => script.type === ScriptType.MediaFolder); if (scripts.length > 0) { return ( - ({ - title: s.title, - onClick: () => runCustomScript(s) - }))} - onClick={onFolderCreation} - disabled={!settings?.initialized} /> +
+ ({ + title: s.title, + onClick: () => runCustomScript(s) + }))} + onClick={onFolderCreation} + disabled={!settings?.initialized} /> +
) } return ( - +
+ +
); }; \ No newline at end of file diff --git a/src/dashboardWebView/components/Media/Media.tsx b/src/dashboardWebView/components/Media/Media.tsx index 83fc1dd7..78062ca1 100644 --- a/src/dashboardWebView/components/Media/Media.tsx +++ b/src/dashboardWebView/components/Media/Media.tsx @@ -17,19 +17,17 @@ import { useCallback } from 'react'; import { DashboardMessage } from '../../DashboardMessage'; import { FrontMatterIcon } from '../../../panelWebView/components/Icons/FrontMatterIcon'; import { FolderItem } from './FolderItem'; +import useMedia from '../../hooks/useMedia'; export interface IMediaProps {} -export const LIMIT = 16; - export const Media: React.FunctionComponent = (props: React.PropsWithChildren) => { + const { media } = useMedia(); const settings = useRecoilValue(SettingsSelector); - const [ selectedFolder, setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom); - const [ media, setMedia ] = React.useState([]); - const [ , setTotal ] = useRecoilState(MediaTotalAtom); - const [ folders, setFolders ] = useRecoilState(MediaFoldersAtom); - const [ loading, setLoading ] = useRecoilState(LoadingAtom); const viewData = useRecoilValue(ViewDataSelector); + const selectedFolder = useRecoilValue(SelectedMediaFolderAtom); + const folders = useRecoilValue(MediaFoldersAtom); + const loading = useRecoilValue(LoadingAtom); const onDrop = useCallback((acceptedFiles: File[]) => { acceptedFiles.forEach((file) => { @@ -52,25 +50,6 @@ export const Media: React.FunctionComponent = (props: React.PropsWi onDrop, accept: 'image/*' }); - - const messageListener = (message: MessageEvent>) => { - if (message.data.command === DashboardCommand.media) { - const data: MediaPaths = message.data.data as MediaPaths; - setLoading(false); - setMedia(data.media); - setTotal(data.total); - setFolders(data.folders); - setSelectedFolder(data.selectedFolder); - } - }; - - React.useEffect(() => { - Messenger.listen(messageListener); - - return () => { - Messenger.unlisten(messageListener); - } - }, ['']); return (
diff --git a/src/dashboardWebView/components/Media/MediaHeaderBottom.tsx b/src/dashboardWebView/components/Media/MediaHeaderBottom.tsx new file mode 100644 index 00000000..6aefa5b2 --- /dev/null +++ b/src/dashboardWebView/components/Media/MediaHeaderBottom.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { useRecoilValue } from 'recoil'; +import { NavigationType } from '../../models/NavigationType'; +import { SettingsAtom } from '../../state'; +import { Sorting } from '../Header'; +import { Breadcrumb } from '../Header/Breadcrumb'; +import { Pagination } from '../Header/Pagination'; + +export interface IMediaHeaderBottomProps {} + +export const MediaHeaderBottom: React.FunctionComponent = (props: React.PropsWithChildren) => { + const settings = useRecoilValue(SettingsAtom); + + if (!settings?.wsFolder) { + return null; + } + + return ( + + ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/components/Media/MediaHeaderTop.tsx b/src/dashboardWebView/components/Media/MediaHeaderTop.tsx new file mode 100644 index 00000000..75ca8a1b --- /dev/null +++ b/src/dashboardWebView/components/Media/MediaHeaderTop.tsx @@ -0,0 +1,81 @@ +import { EventData } from '@estruyf/vscode'; +import { Messenger } from '@estruyf/vscode/dist/client'; +import * as React from 'react'; +import { useRecoilState, useRecoilValue } from 'recoil'; +import { useDebounce } from '../../../hooks/useDebounce'; +import { usePrevious } from '../../../panelWebView/hooks/usePrevious'; +import { DashboardCommand } from '../../DashboardCommand'; +import { DashboardMessage } from '../../DashboardMessage'; +import { LoadingAtom, PageAtom, SelectedMediaFolderSelector, SettingsSelector, SortingSelector } from '../../state'; +import { Searchbox } from '../Header'; +import { PaginationStatus } from '../Header/PaginationStatus'; +import { FolderCreation } from './FolderCreation'; + +export interface IMediaHeaderTopProps {} + +export const MediaHeaderTop: React.FunctionComponent = ({}: React.PropsWithChildren) => { + const [ lastUpdated, setLastUpdated ] = React.useState(null); + const selectedFolder = useRecoilValue(SelectedMediaFolderSelector); + const crntSorting = useRecoilValue(SortingSelector); + const [ , setLoading ] = useRecoilState(LoadingAtom); + const [ page, setPage ] = useRecoilState(PageAtom); + const settings = useRecoilValue(SettingsSelector); + const debounceGetMedia = useDebounce(lastUpdated, 200); + const prevSelectedFolder = usePrevious(selectedFolder); + + const mediaUpdate = (message: MessageEvent>) => { + if (message.data.command === DashboardCommand.mediaUpdate) { + setLoading(true); + Messenger.send(DashboardMessage.getMedia, { + page, + folder: selectedFolder || '', + sorting: crntSorting + }); + } + } + + React.useEffect(() => { + if (prevSelectedFolder !== null || settings?.dashboardState?.media.selectedFolder !== selectedFolder) { + setLoading(true); + setPage(0); + setLastUpdated(new Date().getTime().toString()); + } + }, [selectedFolder]); + + React.useEffect(() => { + setLastUpdated(new Date().getTime().toString()); + }, [crntSorting]); + + React.useEffect(() => { + if (debounceGetMedia) { + setLoading(true); + + Messenger.send(DashboardMessage.getMedia, { + page, + folder: selectedFolder || '', + sorting: crntSorting + }); + } + }, [debounceGetMedia]); + + React.useEffect(() => { + Messenger.listen(mediaUpdate); + + return () => { + Messenger.unlisten(mediaUpdate); + } + }, []); + + return ( + + ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/components/Media/Pagination.tsx b/src/dashboardWebView/components/Media/Pagination.tsx deleted file mode 100644 index cd8a62fb..00000000 --- a/src/dashboardWebView/components/Media/Pagination.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import { EventData } from '@estruyf/vscode'; -import { Messenger } from '@estruyf/vscode/dist/client'; -import {RefreshIcon} from '@heroicons/react/outline'; -import * as React from 'react'; -import { useRecoilState, useRecoilValue } from 'recoil'; -import { useDebounce } from '../../../hooks/useDebounce'; -import { usePrevious } from '../../../panelWebView/hooks/usePrevious'; -import { DashboardCommand } from '../../DashboardCommand'; -import { DashboardMessage } from '../../DashboardMessage'; -import { LoadingAtom, MediaTotalSelector, PageAtom, SelectedMediaFolderSelector, SettingsSelector, SortingSelector } from '../../state'; -import { FolderCreation } from './FolderCreation'; -import { LIMIT } from './Media'; -import { PaginationButton } from './PaginationButton'; - -export interface IPaginationProps {} - -export const Pagination: React.FunctionComponent = ({}: React.PropsWithChildren) => { - const [ lastUpdated, setLastUpdated ] = React.useState(null); - const selectedFolder = useRecoilValue(SelectedMediaFolderSelector); - const crntSorting = useRecoilValue(SortingSelector); - const totalMedia = useRecoilValue(MediaTotalSelector); - const [ , setLoading ] = useRecoilState(LoadingAtom); - const [ page, setPage ] = useRecoilState(PageAtom); - const settings = useRecoilValue(SettingsSelector); - const debounceGetMedia = useDebounce(lastUpdated, 200); - const prevSelectedFolder = usePrevious(selectedFolder); - - const totalPages = Math.ceil(totalMedia / LIMIT) - 1; - - const getTotalPage = () => { - const mediaItems = ((page + 1) * LIMIT); - if (totalMedia < mediaItems) { - return totalMedia; - } - return mediaItems; - }; - - // Write me function to retrieve buttons before and after current page - const getButtons = (): number[] => { - const maxButtons = 5; - const buttons: number[] = []; - const start = page - maxButtons; - const end = page + maxButtons; - - for (let i = start; i <= end; i++) { - if (i >= 0 && i <= totalPages) { - buttons.push(i); - } - } - return buttons; - }; - - const refresh = () => { - setPage(0); - Messenger.send(DashboardMessage.refreshMedia, { folder: selectedFolder }); - } - - const mediaUpdate = (message: MessageEvent>) => { - if (message.data.command === DashboardCommand.mediaUpdate) { - setLoading(true); - Messenger.send(DashboardMessage.getMedia, { - page, - folder: selectedFolder || '', - sorting: crntSorting - }); - } - } - - React.useEffect(() => { - setLastUpdated(new Date().getTime().toString()); - }, [page]); - - React.useEffect(() => { - if (prevSelectedFolder !== null || settings?.dashboardState?.media.selectedFolder !== selectedFolder) { - setLoading(true); - setPage(0); - setLastUpdated(new Date().getTime().toString()); - } - }, [selectedFolder]); - - React.useEffect(() => { - setLastUpdated(new Date().getTime().toString()); - }, [crntSorting]); - - React.useEffect(() => { - if (debounceGetMedia) { - setLoading(true); - - Messenger.send(DashboardMessage.getMedia, { - page, - folder: selectedFolder || '', - sorting: crntSorting - }); - } - }, [debounceGetMedia]); - - React.useEffect(() => { - Messenger.listen(mediaUpdate); - - return () => { - Messenger.unlisten(mediaUpdate); - } - }, []); - - return ( - - ); -}; \ No newline at end of file diff --git a/src/dashboardWebView/components/Startup.tsx b/src/dashboardWebView/components/Startup.tsx index 79e25e66..07156d94 100644 --- a/src/dashboardWebView/components/Startup.tsx +++ b/src/dashboardWebView/components/Startup.tsx @@ -21,7 +21,7 @@ export const Startup: React.FunctionComponent = ({settings}: Reac }, [settings?.openOnStart]); return ( -
+
= { + keys: [ + { name: 'filename', weight: 0.8 }, + { name: 'fsPath', weight: 0.5 }, + { name: 'caption', weight: 0.5 }, + { name: 'alt', weight: 0.5 } + ], + threshold: 0.2, + includeScore: true +}; + +export const LIMIT = 16; + +export default function useMedia() { + const [ media, setMedia ] = useState([]); + const [ page, setPage ] = useRecoilState(PageAtom); + const [ searchedMedia, setSearchedMedia ] = useState([]); + const [ , setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom); + const [ , setTotal ] = useRecoilState(MediaTotalAtom); + const [ , setFolders ] = useRecoilState(MediaFoldersAtom); + const [ , setLoading ] = useRecoilState(LoadingAtom); + const search = useRecoilValue(SearchAtom); + + const getMedia = useCallback(() => { + return searchedMedia.slice(page * LIMIT, ((page + 1) * LIMIT)); + }, [searchedMedia, page]); + + const messageListener = (message: MessageEvent>) => { + if (message.data.command === DashboardCommand.media) { + const data: MediaPaths = message.data.data as MediaPaths; + setLoading(false); + setMedia(data.media); + setTotal(data.total); + setFolders(data.folders); + setSelectedFolder(data.selectedFolder); + setSearchedMedia(data.media); + } + }; + + useEffect(() => { + if (search) { + const fuse = new Fuse(media, fuseOptions); + const results = fuse.search(search); + const newSearchedMedia = results.map(page => page.item); + + setSearchedMedia(newSearchedMedia); + setTotal(results.length); + + return; + } + + setSearchedMedia(media); + }, [search]); + + useEffect(() => { + Messenger.listen(messageListener); + + return () => { + Messenger.unlisten(messageListener); + } + }, []); + + return { + media: getMedia() + }; +} \ No newline at end of file diff --git a/src/helpers/MediaHelpers.ts b/src/helpers/MediaHelpers.ts index 05b88b85..a1d6815b 100644 --- a/src/helpers/MediaHelpers.ts +++ b/src/helpers/MediaHelpers.ts @@ -111,7 +111,6 @@ export class MediaHelpers { const total = files.length; // Get media set - files = files.slice(page * 16, ((page + 1) * 16)); files = files.map((file) => { try { const metadata = MediaLibrary.getInstance().get(file.fsPath); @@ -323,6 +322,7 @@ export class MediaHelpers { const ext = extname(file.fsPath); return ['.jpg', '.jpeg', '.png', '.gif', '.svg'].includes(ext.toLowerCase()); }).map((file) => ({ + filename: basename(file.fsPath), fsPath: file.fsPath, vsPath: Dashboard.getWebview()?.asWebviewUri(file).toString(), stats: undefined diff --git a/src/models/MediaPaths.ts b/src/models/MediaPaths.ts index e40a2b22..bf51df50 100644 --- a/src/models/MediaPaths.ts +++ b/src/models/MediaPaths.ts @@ -8,6 +8,7 @@ export interface MediaPaths { } export interface MediaInfo { + filename: string; fsPath: string; vsPath: string | undefined; dimensions?: ISizeCalculationResult | undefined; diff --git a/tsconfig.json b/tsconfig.json index 493a911b..c16f7952 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,9 @@ "es6", "DOM" ], + "typeRoots": [ + "node_modules/@types" + ], "sourceMap": true, "rootDir": "src", "strict": true,