diff --git a/frontend/src/components/toolbar/face-recognition-files-toolbar.js b/frontend/src/components/toolbar/face-recognition-files-toolbar.js new file mode 100644 index 0000000000..8afb4200da --- /dev/null +++ b/frontend/src/components/toolbar/face-recognition-files-toolbar.js @@ -0,0 +1,125 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import ItemDropdownMenu from '../dropdown-menu/item-dropdown-menu'; +import { gettext } from '../../utils/constants'; +import { EVENT_BUS_TYPE, GALLERY_OPERATION_KEYS } from '../../metadata/constants'; +import { useFileOperations } from '../../hooks/file-operations'; + +const FaceRecognitionFilesToolbar = () => { + const [selectedRecordIds, setSelectedRecordIds] = useState([]); + const [selectedRecords, setSelectedRecords] = useState([]); + const [isSomeone, setIsSomeone] = useState(false); + const menuRef = useRef(null); + const { handleDownload: handleDownloadAPI } = useFileOperations(); + const eventBus = window.sfMetadataContext && window.sfMetadataContext.eventBus; + + const checkCanDeleteRow = window.sfMetadataContext.checkCanDeleteRow(); + const canRemovePhotoFromPeople = window.sfMetadataContext.canRemovePhotoFromPeople(); + const canAddPhotoToPeople = window.sfMetadataContext.canAddPhotoToPeople(); + const canSetPeoplePhoto = window.sfMetadataContext.canSetPeoplePhoto(); + + useEffect(() => { + const unsubscribeSelectedFileIds = eventBus && eventBus.subscribe(EVENT_BUS_TYPE.SELECT_RECORDS, (ids, metadata, selectedRecords, isSomeone) => { + setSelectedRecordIds(ids); + setSelectedRecords(selectedRecords); + setIsSomeone(isSomeone); + }); + + return () => { + unsubscribeSelectedFileIds && unsubscribeSelectedFileIds(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const unSelect = useCallback(() => { + setSelectedRecordIds([]); + eventBus && eventBus.dispatch(EVENT_BUS_TYPE.UPDATE_SELECTED_RECORD_IDS, []); + eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE); + }, [eventBus]); + + const handleDownload = useCallback(() => { + const list = selectedRecords.map(record => { + const { parentDir, name: fileName } = record || {}; + const name = parentDir === '/' ? fileName : `${parentDir}/${fileName}`; + return { name }; + }); + handleDownloadAPI('/', list); + }, [selectedRecords, handleDownloadAPI]); + + const deleteRecords = useCallback(() => { + eventBus && eventBus.dispatch(EVENT_BUS_TYPE.DELETE_FACE_RECOGNITION_RECORDS, selectedRecords, { + success_callback: () => { + eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE); + } + }); + }, [eventBus, selectedRecords]); + + const opList = useMemo(() => { + const list = []; + if (isSomeone && canRemovePhotoFromPeople) { + list.push({ + key: GALLERY_OPERATION_KEYS.REMOVE_PHOTO_FROM_CURRENT_SET, + value: gettext('Remove from this group') + }); + } + if (!isSomeone && canAddPhotoToPeople) { + list.push({ + key: GALLERY_OPERATION_KEYS.ADD_PHOTO_TO_GROUPS, + value: gettext('Add to groups') + }); + } + if (canSetPeoplePhoto && selectedRecordIds.length == 1) { + list.push({ + key: GALLERY_OPERATION_KEYS.SET_PHOTO_AS_COVER, + value: gettext('Set as cover photo') + }); + } + return list; + }, [isSomeone, selectedRecordIds, canRemovePhotoFromPeople, canAddPhotoToPeople, canSetPeoplePhoto]); + + const getMenuList = useCallback(() => { + return opList; + }, [opList]); + + const onMenuItemClick = useCallback((operation) => { + switch (operation) { + case GALLERY_OPERATION_KEYS.REMOVE_PHOTO_FROM_CURRENT_SET: + eventBus && eventBus.dispatch(EVENT_BUS_TYPE.REMOVE_PHOTOS_FROM_CURRENT_SET, selectedRecords); + break; + case GALLERY_OPERATION_KEYS.ADD_PHOTO_TO_GROUPS: + eventBus && eventBus.dispatch(EVENT_BUS_TYPE.ADD_PHOTO_TO_GROUPS); + break; + case GALLERY_OPERATION_KEYS.SET_PHOTO_AS_COVER: + eventBus && eventBus.dispatch(EVENT_BUS_TYPE.SET_PHOTO_AS_COVER, selectedRecords[0]); + break; + default: + return; + } + }, [eventBus, selectedRecords]); + + const length = selectedRecordIds.length; + return ( +
+ + + {length}{' '}{gettext('selected')} + + + + + {checkCanDeleteRow && + + + + } + +
+ ); +}; + +export default FaceRecognitionFilesToolbar; diff --git a/frontend/src/components/toolbar/metadata-path-toolbar.js b/frontend/src/components/toolbar/metadata-path-toolbar.js index dcb42cb485..4af2c8406b 100644 --- a/frontend/src/components/toolbar/metadata-path-toolbar.js +++ b/frontend/src/components/toolbar/metadata-path-toolbar.js @@ -8,6 +8,7 @@ import AllTagsToolbar from './all-tags-toolbar'; import TagFilesToolbar from './tag-files-toolbar'; import TableFilesToolbar from './table-files-toolbar'; import GalleryFilesToolbar from './gallery-files-toolbar'; +import FaceRecognitionFilesToolbar from './face-recognition-files-toolbar'; const MetadataPathToolbar = ({ repoID, repoInfo, mode, path, viewId }) => { const { idViewMap } = useMetadata(); @@ -20,6 +21,10 @@ const MetadataPathToolbar = ({ repoID, repoInfo, mode, path, viewId }) => { ); } + if (type === VIEW_TYPE.FACE_RECOGNITION) { + return ; + } + if (type === VIEW_TYPE.TABLE) { return ( diff --git a/frontend/src/metadata/constants/event-bus-type.js b/frontend/src/metadata/constants/event-bus-type.js index 66c46004c9..a3d85b5a4b 100644 --- a/frontend/src/metadata/constants/event-bus-type.js +++ b/frontend/src/metadata/constants/event-bus-type.js @@ -85,6 +85,12 @@ export const EVENT_BUS_TYPE = { MODIFY_GALLERY_ZOOM_GEAR: 'modify_gallery_zoom_gear', SWITCH_GALLERY_GROUP_BY: 'switch_gallery_group_by', + // face recognition + DELETE_FACE_RECOGNITION_RECORDS: 'delete_face_recognition_records', + REMOVE_PHOTOS_FROM_CURRENT_SET: 'remove_photos_from_current_set', + SET_PHOTO_AS_COVER: 'set_photo_as_cover', + ADD_PHOTO_TO_GROUPS: 'add_photo_to_groups', + // kanban TOGGLE_KANBAN_SETTINGS: 'toggle_kanban_settings', OPEN_KANBAN_SETTINGS: 'open_kanban_settings', diff --git a/frontend/src/metadata/constants/view/gallery.js b/frontend/src/metadata/constants/view/gallery.js index 966b0a6c5b..387262d43c 100644 --- a/frontend/src/metadata/constants/view/gallery.js +++ b/frontend/src/metadata/constants/view/gallery.js @@ -22,8 +22,8 @@ export const STORAGE_GALLERY_ZOOM_GEAR_KEY = 'gallery_zoom_gear'; export const GALLERY_OPERATION_KEYS = { DOWNLOAD: 'download', DELETE: 'delete', - DUPLICATE: 'duplicate', - REMOVE: 'remove', - SET_PEOPLE_PHOTO: 'set_people_photo', + COPY: 'copy', + REMOVE_PHOTO_FROM_CURRENT_SET: 'remove_photo_from_current_set', + SET_PHOTO_AS_COVER: 'set_photo_as_cover', ADD_PHOTO_TO_GROUPS: 'add_photo_to_groups' }; diff --git a/frontend/src/metadata/hooks/metadata-view.js b/frontend/src/metadata/hooks/metadata-view.js index 5d205a8c80..a1ef9c7a0d 100644 --- a/frontend/src/metadata/hooks/metadata-view.js +++ b/frontend/src/metadata/hooks/metadata-view.js @@ -296,10 +296,14 @@ export const MetadataViewProvider = ({ storeRef.current.updateFileTags(data); }, [storeRef, modifyLocalFileTags]); - const updateSelectedRecordIds = useCallback((ids) => { + const updateSelectedRecordIds = useCallback((ids, records, isSomeone) => { toggleShowDirentToolbar(ids.length > 0); setTimeout(() => { - window.sfMetadataContext && window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_RECORDS, ids, metadata); + if (records != undefined) { + window.sfMetadataContext && window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_RECORDS, ids, metadata, records, isSomeone); + } else { + window.sfMetadataContext && window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_RECORDS, ids, metadata); + } }, 0); }, [metadata, toggleShowDirentToolbar]); diff --git a/frontend/src/metadata/views/face-recognition/person-photos/index.js b/frontend/src/metadata/views/face-recognition/person-photos/index.js index 24a9077451..ac875d8260 100644 --- a/frontend/src/metadata/views/face-recognition/person-photos/index.js +++ b/frontend/src/metadata/views/face-recognition/person-photos/index.js @@ -237,12 +237,14 @@ const PeoplePhotos = ({ view, people, onClose, onDeletePeoplePhotos, onAddPeople const eventBus = window?.sfMetadataContext?.eventBus; if (!eventBus) return; const unsubscribeViewChange = eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_SERVER_VIEW, onViewChange); + const unsubscribeDeleteRecords = eventBus.subscribe(EVENT_BUS_TYPE.DELETE_FACE_RECOGNITION_RECORDS, handleDelete); const localRecordChangedSubscribe = eventBus.subscribe(EVENT_BUS_TYPE.LOCAL_RECORD_CHANGED, onRecordChange); return () => { unsubscribeViewChange && unsubscribeViewChange(); + unsubscribeDeleteRecords && unsubscribeDeleteRecords(); localRecordChangedSubscribe && localRecordChangedSubscribe(); }; - }, [onViewChange, onRecordChange]); + }, [onViewChange, handleDelete, onRecordChange]); if (isLoading) return (); @@ -256,6 +258,7 @@ const PeoplePhotos = ({ view, people, onClose, onDeletePeoplePhotos, onAddPeople onRemoveImage={people._is_someone ? handleRemove : null} onAddImage={!people._is_someone ? handleAdd : null} onSetPeoplePhoto={handleSetPeoplePhoto} + isSomeone={people._is_someone} /> ); diff --git a/frontend/src/metadata/views/gallery/context-menu/index.js b/frontend/src/metadata/views/gallery/context-menu/index.js index 861baba96c..c3586fe5c9 100644 --- a/frontend/src/metadata/views/gallery/context-menu/index.js +++ b/frontend/src/metadata/views/gallery/context-menu/index.js @@ -1,16 +1,12 @@ -import React, { useMemo, useCallback, useState } from 'react'; +import React, { useMemo, useCallback } from 'react'; import PropTypes from 'prop-types'; import ContextMenu from '../../../components/context-menu'; -import ModalPortal from '../../../../components/modal-portal'; -import PeoplesDialog from '../../../components/dialog/peoples-dialog'; import { gettext } from '../../../../utils/constants'; import { Dirent } from '../../../../models'; import { useFileOperations } from '../../../../hooks/file-operations'; import { GALLERY_OPERATION_KEYS } from '../../../constants'; const GalleryContextMenu = ({ selectedImages, onDelete, onDuplicate, onRemoveImage, onAddImage, onSetPeoplePhoto }) => { - const [isPeoplesDialogShow, setPeoplesDialogShow] = useState(false); - const { handleDownload: handleDownloadAPI, handleCopy: handleCopyAPI } = useFileOperations(); const checkCanDeleteRow = window.sfMetadataContext.checkCanDeleteRow(); @@ -22,22 +18,39 @@ const GalleryContextMenu = ({ selectedImages, onDelete, onDuplicate, onRemoveIma const options = useMemo(() => { let validOptions = [{ value: GALLERY_OPERATION_KEYS.DOWNLOAD, label: gettext('Download') }]; if (onDelete && checkCanDeleteRow) { - validOptions.push({ value: GALLERY_OPERATION_KEYS.DELETE, label: gettext('Delete') }); + validOptions.push({ + value: GALLERY_OPERATION_KEYS.DELETE, + label: gettext('Delete') + }); } if (onDuplicate && canDuplicateRow && selectedImages.length === 1) { - validOptions.push({ value: GALLERY_OPERATION_KEYS.DUPLICATE, label: gettext('Copy') }); + validOptions.push({ + value: GALLERY_OPERATION_KEYS.COPY, + label: gettext('Copy') + }); } if (onRemoveImage && canRemovePhotoFromPeople) { - validOptions.push({ value: GALLERY_OPERATION_KEYS.REMOVE, label: gettext('Remove from this group') }); + validOptions.push({ + value: GALLERY_OPERATION_KEYS.REMOVE_PHOTO_FROM_CURRENT_SET, + label: gettext('Remove from this group') + }); } if (onAddImage && canAddPhotoToPeople) { - validOptions.push({ value: GALLERY_OPERATION_KEYS.ADD_PHOTO_TO_GROUPS, label: gettext('Add to groups') }); + validOptions.push({ + value: GALLERY_OPERATION_KEYS.ADD_PHOTO_TO_GROUPS, + label: gettext('Add to groups') + }); } - if (onSetPeoplePhoto && canSetPeoplePhoto) { - validOptions.push({ value: GALLERY_OPERATION_KEYS.SET_PEOPLE_PHOTO, label: gettext('Set as cover photo') }); + if (onSetPeoplePhoto && canSetPeoplePhoto && selectedImages.length === 1) { + validOptions.push({ + value: GALLERY_OPERATION_KEYS.SET_PHOTO_AS_COVER, + label: gettext('Set as cover photo') + }); } return validOptions; - }, [checkCanDeleteRow, canDuplicateRow, canRemovePhotoFromPeople, canAddPhotoToPeople, selectedImages, onDuplicate, onDelete, onRemoveImage, onAddImage, canSetPeoplePhoto, onSetPeoplePhoto]); + }, [checkCanDeleteRow, canDuplicateRow, canRemovePhotoFromPeople, + canAddPhotoToPeople, selectedImages, onDuplicate, onDelete, + onRemoveImage, onAddImage, canSetPeoplePhoto, onSetPeoplePhoto]); const handleDuplicate = useCallback((destRepo, dirent, destPath, nodeParentPath, isByDialog) => { const selectedImage = selectedImages[0]; @@ -68,44 +81,29 @@ const GalleryContextMenu = ({ selectedImages, onDelete, onDuplicate, onRemoveIma case GALLERY_OPERATION_KEYS.DELETE: onDelete(selectedImages); break; - case GALLERY_OPERATION_KEYS.DUPLICATE: + case GALLERY_OPERATION_KEYS.COPY: handleCopy(); break; - case GALLERY_OPERATION_KEYS.REMOVE: + case GALLERY_OPERATION_KEYS.REMOVE_PHOTO_FROM_CURRENT_SET: onRemoveImage(selectedImages); break; case GALLERY_OPERATION_KEYS.ADD_PHOTO_TO_GROUPS: - setPeoplesDialogShow(true); + onAddImage(); break; - case GALLERY_OPERATION_KEYS.SET_PEOPLE_PHOTO: + case GALLERY_OPERATION_KEYS.SET_PHOTO_AS_COVER: onSetPeoplePhoto(selectedImages[0]); break; default: break; } - }, [handleDownload, onDelete, selectedImages, handleCopy, onRemoveImage, onSetPeoplePhoto]); - - const closePeoplesDialog = useCallback(() => { - setPeoplesDialogShow(false); - }, []); - - const addPeople = useCallback((peopleIds, addedImages, callback) => { - onAddImage(peopleIds, addedImages, callback); - }, [onAddImage]); + }, [handleDownload, onDelete, selectedImages, handleCopy, onRemoveImage, onAddImage, onSetPeoplePhoto]); return ( - <> - - {isPeoplesDialogShow && ( - - - - )} - + ); }; diff --git a/frontend/src/metadata/views/gallery/main.js b/frontend/src/metadata/views/gallery/main.js index 9423db1c11..2ad16cdb5a 100644 --- a/frontend/src/metadata/views/gallery/main.js +++ b/frontend/src/metadata/views/gallery/main.js @@ -15,12 +15,13 @@ import { getEventClassName } from '../../../utils/dom'; import { getColumns, getImageSize, getRowHeight } from './utils'; import ObjectUtils from '../../../utils/object'; import { openFile } from '../../utils/file'; +import PeoplesDialog from '../../components/dialog/peoples-dialog'; import './index.css'; const OVER_SCAN_ROWS = 20; -const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, onRemoveImage, onAddImage, onSetPeoplePhoto }) => { +const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, onRemoveImage, onAddImage, onSetPeoplePhoto, isSomeone }) => { const [isFirstLoading, setFirstLoading] = useState(true); const [zoomGear, setZoomGear] = useState(0); const [containerWidth, setContainerWidth] = useState(0); @@ -30,6 +31,7 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, const [imageIndex, setImageIndex] = useState(0); const [selectedImages, setSelectedImages] = useState([]); const [lastSelectedImage, setLastSelectedImage] = useState(null); + const [isPeoplesDialogShow, setPeoplesDialogShow] = useState(false); const containerRef = useRef(null); const scrollContainer = useRef(null); @@ -193,8 +195,12 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, const updateSelectedImages = useCallback((selectedImages) => { const ids = selectedImages.map(item => item.id); - updateSelectedRecordIds(ids); - }, [updateSelectedRecordIds]); + if (isSomeone != undefined) { // 'face recognition' + updateSelectedRecordIds(ids, selectedImages, isSomeone); + } else { + updateSelectedRecordIds(ids); + } + }, [isSomeone, updateSelectedRecordIds]); const handleClick = useCallback((event, image) => { if (event.metaKey || event.ctrlKey) { @@ -301,7 +307,22 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, }); }, [onRemoveImage, updateCurrentDirent, updateSelectedImages]); - const handleMakeSelectedAsCoverPhoto = useCallback((selectedImage) => { + const handleAddPhotoToGroup = useCallback((selectedImages) => { + setPeoplesDialogShow(true); + }, []); + + const closePeoplesDialog = useCallback(() => { + setPeoplesDialogShow(false); + }, []); + + const addPhotoToGroup = useCallback((peopleIds, addedImages, callback) => { + onAddImage(peopleIds, addedImages, callback); + updateCurrentDirent(); + setSelectedImages([]); + updateSelectedImages([]); + }, [onAddImage, updateCurrentDirent, updateSelectedImages]); + + const setSelectedImageAsCover = useCallback((selectedImage) => { onSetPeoplePhoto(selectedImage, { success_callback: () => { updateCurrentDirent(); @@ -409,11 +430,17 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, }); const unsubscribeSelectNone = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.SELECT_NONE, selectNone); + const unsubscribeRemovePhotosFromCurrentSet = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.REMOVE_PHOTOS_FROM_CURRENT_SET, handleRemoveSelectedImages); + const unsubscribeSetPhotoAsCover = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.SET_PHOTO_AS_COVER, setSelectedImageAsCover); + const unsubscribeAddPhotoToGroups = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.ADD_PHOTO_TO_GROUPS, handleAddPhotoToGroup); return () => { container && resizeObserver.unobserve(container); modifyGalleryZoomGearSubscribe(); unsubscribeSelectNone(); + unsubscribeRemovePhotosFromCurrentSet(); + unsubscribeSetPhotoAsCover(); + unsubscribeAddPhotoToGroups(); switchGalleryModeSubscribe(); }; // eslint-disable-next-line react-hooks/exhaustive-deps @@ -481,8 +508,8 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, onDelete={handleDeleteSelectedImages} onDuplicate={duplicateRecord} onRemoveImage={onRemoveImage ? handleRemoveSelectedImages : null} - onAddImage={onAddImage} - onSetPeoplePhoto={handleMakeSelectedAsCoverPhoto} + onAddImage={onAddImage ? handleAddPhotoToGroup : null} + onSetPeoplePhoto={setSelectedImageAsCover} /> {isImagePopupOpen && ( @@ -498,6 +525,11 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, /> )} + {isPeoplesDialogShow && ( + + + + )} ); }; diff --git a/frontend/src/pages/lib-content-view/lib-content-view.js b/frontend/src/pages/lib-content-view/lib-content-view.js index 823cfd0630..7a5b580b3c 100644 --- a/frontend/src/pages/lib-content-view/lib-content-view.js +++ b/frontend/src/pages/lib-content-view/lib-content-view.js @@ -2473,7 +2473,13 @@ class LibContentView extends React.Component { })}> {isDirentSelected ? ( currentMode === TAGS_MODE || currentMode === METADATA_MODE ? ( - + ) : (