mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-07-06 09:51:29 +02:00
#199 - Search media files
This commit is contained in:
@@ -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<IBreadcrumbProps> = (props: React.PropsWithChildren<IBreadcrumbProps>) => {
|
||||
const [ selectedFolder, setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom);
|
||||
const settings = useRecoilValue(SettingsAtom);
|
||||
const [ , setSearchValue ] = useRecoilState(SearchAtom);
|
||||
const [ folders, setFolders ] = React.useState<string[]>([]);
|
||||
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<IBreadcrumbProps> = (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<IBreadcrumbProps> = (props: Rea
|
||||
|
||||
setFolders(allFolders);
|
||||
}
|
||||
}, [selectedFolder]);
|
||||
|
||||
}, [selectedFolder, settings]);
|
||||
|
||||
return (
|
||||
<nav className="w-full bg-gray-200 text-vulcan-300 dark:bg-vulcan-400 dark:text-whisper-600 border-b border-gray-300 dark:border-vulcan-100 flex justify-between py-2" aria-label="Breadcrumb">
|
||||
<ol role="list" className="flex space-x-4 px-5">
|
||||
<li className="flex">
|
||||
<ol role="list" className="flex space-x-4 px-5 flex-1">
|
||||
<li className="flex">
|
||||
<div className="flex items-center">
|
||||
<button onClick={() => setSelectedFolder(HOME_PAGE_NAVIGATION_ID)} className="text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500">
|
||||
<CollectionIcon className="flex-shrink-0 h-5 w-5" aria-hidden="true" />
|
||||
<span className="sr-only">Home</span>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{folders.map((folder) => (
|
||||
<li key={folder} className="flex">
|
||||
<div className="flex items-center">
|
||||
<button onClick={() => setSelectedFolder(HOME_PAGE_NAVIGATION_ID)} className="text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500">
|
||||
<CollectionIcon className="flex-shrink-0 h-5 w-5" aria-hidden="true" />
|
||||
<span className="sr-only">Home</span>
|
||||
<svg
|
||||
className="flex-shrink-0 h-5 w-5 text-gray-300 dark:text-whisper-900"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M5.555 17.776l8-16 .894.448-8 16-.894-.448z" />
|
||||
</svg>
|
||||
<button
|
||||
onClick={() => updateFolder(folder)}
|
||||
className="ml-4 text-sm font-medium text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500"
|
||||
>
|
||||
{basename(folder)}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
{folders.map((folder) => (
|
||||
<li key={folder} className="flex">
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="flex-shrink-0 h-5 w-5 text-gray-300 dark:text-whisper-900"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M5.555 17.776l8-16 .894.448-8 16-.894-.448z" />
|
||||
</svg>
|
||||
<button
|
||||
onClick={() => setSelectedFolder(folder)}
|
||||
className="ml-4 text-sm font-medium text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500"
|
||||
>
|
||||
{basename(folder)}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
<div className={`flex px-5`}>
|
||||
<Sorting view={NavigationType.Media} disableCustomSorting />
|
||||
</div>
|
||||
</nav>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
};
|
||||
@@ -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<IHeaderProps> = ({totalPages, folde
|
||||
{
|
||||
view === NavigationType.Media && (
|
||||
<>
|
||||
<Pagination />
|
||||
<Breadcrumb />
|
||||
<MediaHeaderTop />
|
||||
|
||||
<MediaHeaderBottom />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<IPaginationProps> = (props: React.PropsWithChildren<IPaginationProps>) => {
|
||||
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 (
|
||||
<div className="flex justify-between items-center sm:justify-end space-x-2 text-sm">
|
||||
<PaginationButton
|
||||
title="First"
|
||||
disabled={page === 0}
|
||||
onClick={() => {
|
||||
if (page > 0) {
|
||||
setPage(0)
|
||||
}
|
||||
}} />
|
||||
|
||||
<PaginationButton
|
||||
title="Previous"
|
||||
disabled={page === 0}
|
||||
onClick={() => {
|
||||
if (page > 0) {
|
||||
setPage(page - 1)
|
||||
}
|
||||
}} />
|
||||
|
||||
{getButtons().map((button) => (
|
||||
<button
|
||||
key={button}
|
||||
disabled={button === page}
|
||||
onClick={() => {
|
||||
setPage(button)
|
||||
}
|
||||
}
|
||||
className={`${page === button ? 'bg-gray-200 px-2 text-vulcan-500' : 'text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500'} max-h-8`}
|
||||
>{button + 1}</button>
|
||||
))}
|
||||
|
||||
<PaginationButton
|
||||
title="Next"
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => setPage(page + 1)} />
|
||||
|
||||
<PaginationButton
|
||||
title="Last"
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => setPage(totalPages)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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<IPaginationStatusProps> = (props: React.PropsWithChildren<IPaginationStatusProps>) => {
|
||||
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 (
|
||||
<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>
|
||||
|
||||
<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
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -8,25 +8,32 @@ export interface ISearchboxProps {}
|
||||
|
||||
export const Searchbox: React.FunctionComponent<ISearchboxProps> = ({}: React.PropsWithChildren<ISearchboxProps>) => {
|
||||
const [ value, setValue ] = React.useState('');
|
||||
const [ , setDebounceValue ] = useRecoilState(SearchAtom);
|
||||
const [ debounceSearchValue, setDebounceValue ] = useRecoilState(SearchAtom);
|
||||
const debounceSearch = useDebounce<string>(value, 500);
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(event.target.value);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!debounceSearchValue && value) {
|
||||
setValue('');
|
||||
}
|
||||
} , [debounceSearchValue]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setDebounceValue(debounceSearch);
|
||||
}, [debounceSearch]);
|
||||
|
||||
return (
|
||||
<div className="flex space-x-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex space-x-4 flex-1">
|
||||
<div className="min-w-0">
|
||||
<label htmlFor="search" className="sr-only">Search</label>
|
||||
<div className="relative flex justify-center">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<SearchIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="search"
|
||||
name="search"
|
||||
|
||||
@@ -26,24 +26,28 @@ export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (pr
|
||||
const scripts = (settings?.scripts || []).filter(script => script.type === ScriptType.MediaFolder);
|
||||
if (scripts.length > 0) {
|
||||
return (
|
||||
<ChoiceButton
|
||||
title={`Create new folder`}
|
||||
choices={scripts.map(s => ({
|
||||
title: s.title,
|
||||
onClick: () => runCustomScript(s)
|
||||
}))}
|
||||
onClick={onFolderCreation}
|
||||
disabled={!settings?.initialized} />
|
||||
<div className="flex flex-1 justify-end">
|
||||
<ChoiceButton
|
||||
title={`Create new folder`}
|
||||
choices={scripts.map(s => ({
|
||||
title: s.title,
|
||||
onClick: () => runCustomScript(s)
|
||||
}))}
|
||||
onClick={onFolderCreation}
|
||||
disabled={!settings?.initialized} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`inline-flex items-center px-3 py-1 border border-transparent text-xs leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500`}
|
||||
title={`Create new folder`}
|
||||
onClick={onFolderCreation}>
|
||||
<FolderAddIcon className={`mr-2 h-6 w-6`} />
|
||||
<span className={``}>Create new folder</span>
|
||||
</button>
|
||||
<div className="flex flex-1 justify-end">
|
||||
<button
|
||||
className={`inline-flex items-center px-3 py-1 border border-transparent text-xs leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500`}
|
||||
title={`Create new folder`}
|
||||
onClick={onFolderCreation}>
|
||||
<FolderAddIcon className={`mr-2 h-6 w-6`} />
|
||||
<span className={``}>Create new folder</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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<IMediaProps> = (props: React.PropsWithChildren<IMediaProps>) => {
|
||||
const { media } = useMedia();
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const [ selectedFolder, setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom);
|
||||
const [ media, setMedia ] = React.useState<MediaInfo[]>([]);
|
||||
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<IMediaProps> = (props: React.PropsWi
|
||||
onDrop,
|
||||
accept: 'image/*'
|
||||
});
|
||||
|
||||
const messageListener = (message: MessageEvent<EventData<MediaPaths | { key: string, value: any }>>) => {
|
||||
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<MediaPaths>(messageListener);
|
||||
|
||||
return () => {
|
||||
Messenger.unlisten(messageListener);
|
||||
}
|
||||
}, ['']);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-auto">
|
||||
|
||||
@@ -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<IMediaHeaderBottomProps> = (props: React.PropsWithChildren<IMediaHeaderBottomProps>) => {
|
||||
const settings = useRecoilValue(SettingsAtom);
|
||||
|
||||
if (!settings?.wsFolder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="w-full bg-gray-200 text-vulcan-300 dark:bg-vulcan-400 dark:text-whisper-600 border-b border-gray-300 dark:border-vulcan-100 flex justify-between py-2" aria-label="Breadcrumb">
|
||||
|
||||
<Breadcrumb />
|
||||
|
||||
<Pagination />
|
||||
|
||||
<div className={`flex px-5 flex-1 justify-end`}>
|
||||
<Sorting view={NavigationType.Media} disableCustomSorting />
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
@@ -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<IMediaHeaderTopProps> = ({}: React.PropsWithChildren<IMediaHeaderTopProps>) => {
|
||||
const [ lastUpdated, setLastUpdated ] = React.useState<string | null>(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<string | null>(lastUpdated, 200);
|
||||
const prevSelectedFolder = usePrevious<string | null>(selectedFolder);
|
||||
|
||||
const mediaUpdate = (message: MessageEvent<EventData<{ key: string, value: any }>>) => {
|
||||
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 (
|
||||
<nav
|
||||
className="py-4 px-5 flex items-center justify-between border-b border-gray-300 dark:border-vulcan-100"
|
||||
aria-label="Pagination"
|
||||
>
|
||||
<Searchbox />
|
||||
|
||||
<PaginationStatus />
|
||||
|
||||
<FolderCreation />
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
@@ -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<IPaginationProps> = ({}: React.PropsWithChildren<IPaginationProps>) => {
|
||||
const [ lastUpdated, setLastUpdated ] = React.useState<string | null>(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<string | null>(lastUpdated, 200);
|
||||
const prevSelectedFolder = usePrevious<string | null>(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<EventData<{ key: string, value: any }>>) => {
|
||||
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 (
|
||||
<nav
|
||||
className="py-4 px-5 flex items-center justify-between bg-gray-200 border-b border-gray-300 dark:bg-vulcan-400 dark:border-vulcan-100"
|
||||
aria-label="Pagination"
|
||||
>
|
||||
<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>
|
||||
|
||||
<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
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FolderCreation />
|
||||
|
||||
<div className="flex justify-between sm:justify-end space-x-2 text-sm">
|
||||
<PaginationButton
|
||||
title="First"
|
||||
disabled={page === 0}
|
||||
onClick={() => {
|
||||
if (page > 0) {
|
||||
setPage(0)
|
||||
}
|
||||
}} />
|
||||
|
||||
<PaginationButton
|
||||
title="Previous"
|
||||
disabled={page === 0}
|
||||
onClick={() => {
|
||||
if (page > 0) {
|
||||
setPage(page - 1)
|
||||
}
|
||||
}} />
|
||||
|
||||
{getButtons().map((button) => (
|
||||
<button
|
||||
key={button}
|
||||
disabled={button === page}
|
||||
onClick={() => {
|
||||
setPage(button)
|
||||
}
|
||||
}
|
||||
className={`${page === button ? 'bg-gray-200 px-2 text-vulcan-500' : 'text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500'}`}
|
||||
>{button + 1}</button>
|
||||
))}
|
||||
|
||||
<PaginationButton
|
||||
title="Next"
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => setPage(page + 1)} />
|
||||
|
||||
<PaginationButton
|
||||
title="Last"
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => setPage(totalPages)} />
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
@@ -21,7 +21,7 @@ export const Startup: React.FunctionComponent<IStartupProps> = ({settings}: Reac
|
||||
}, [settings?.openOnStart]);
|
||||
|
||||
return (
|
||||
<div className={`relative flex items-start`}>
|
||||
<div className={`relative flex items-start ml-4`}>
|
||||
<div className="flex items-center h-5">
|
||||
<input
|
||||
id="startup"
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { EventData } from '@estruyf/vscode/dist/models';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { MediaInfo, MediaPaths } from '../../models';
|
||||
import { DashboardCommand } from '../DashboardCommand';
|
||||
import { LoadingAtom, MediaFoldersAtom, MediaTotalAtom, PageAtom, SearchAtom, SearchSelector, SelectedMediaFolderAtom } from '../state';
|
||||
import Fuse from 'fuse.js';
|
||||
|
||||
const fuseOptions: Fuse.IFuseOptions<MediaInfo> = {
|
||||
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<MediaInfo[]>([]);
|
||||
const [ page, setPage ] = useRecoilState(PageAtom);
|
||||
const [ searchedMedia, setSearchedMedia ] = useState<MediaInfo[]>([]);
|
||||
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<EventData<MediaPaths | { key: string, value: any }>>) => {
|
||||
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<MediaPaths>(messageListener);
|
||||
|
||||
return () => {
|
||||
Messenger.unlisten(messageListener);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
media: getMedia()
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface MediaPaths {
|
||||
}
|
||||
|
||||
export interface MediaInfo {
|
||||
filename: string;
|
||||
fsPath: string;
|
||||
vsPath: string | undefined;
|
||||
dimensions?: ISizeCalculationResult | undefined;
|
||||
|
||||
Reference in New Issue
Block a user