Skip to content

Commit

Permalink
face_cluster (#6470)
Browse files Browse the repository at this point in the history
* face_cluster

* update

* update

* feat: update ui

* feat: optimize code

* feat: update code

* feat: optimize ui

* feat: optimize view name

---------

Co-authored-by: zheng.shen <[email protected]>
Co-authored-by: 杨国璇 <[email protected]>
  • Loading branch information
3 people authored Oct 16, 2024
1 parent 24405a6 commit 065f158
Show file tree
Hide file tree
Showing 24 changed files with 872 additions and 31 deletions.
1 change: 1 addition & 0 deletions frontend/src/components/dir-view-mode/constants.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const LIST_MODE = 'list';
export const GRID_MODE = 'grid';
export const METADATA_MODE = 'metadata';
export const FACE_RECOGNITION_MODE = 'person_image';
6 changes: 5 additions & 1 deletion frontend/src/components/dir-view-mode/dir-column-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import ResizeBar from '../resize-bar';
import { DRAG_HANDLER_HEIGHT, MAX_SIDE_PANEL_RATE, MIN_SIDE_PANEL_RATE } from '../resize-bar/constants';
import { SeafileMetadata } from '../../metadata';
import { mediaUrl } from '../../utils/constants';
import { GRID_MODE, LIST_MODE, METADATA_MODE } from './constants';
import { GRID_MODE, LIST_MODE, METADATA_MODE, FACE_RECOGNITION_MODE } from './constants';
import FaceRecognition from '../../metadata/views/face-recognition';

const propTypes = {
isSidePanelFolded: PropTypes.bool,
Expand Down Expand Up @@ -203,6 +204,9 @@ class DirColumnView extends React.Component {
renameFileCallback={this.props.renameFileCallback}
/>
}
{currentMode === FACE_RECOGNITION_MODE &&
<FaceRecognition repoID={this.props.repoID}/>
}
{currentMode === LIST_MODE &&
<DirListView
path={this.props.path}
Expand Down
43 changes: 36 additions & 7 deletions frontend/src/components/dir-view-mode/dir-views.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useCallback, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { gettext } from '../../utils/constants';
import TreeSection from '../tree-section';
import { MetadataStatusManagementDialog, MetadataTreeView, useMetadata } from '../../metadata';
import { MetadataStatusManagementDialog, MetadataFaceRecognitionDialog, MetadataTreeView, useMetadata } from '../../metadata';
import ExtensionPrompts from './extension-prompts';

const DirViews = ({ userPerm, repoID, currentPath, currentRepoInfo }) => {
Expand All @@ -12,25 +12,46 @@ const DirViews = ({ userPerm, repoID, currentPath, currentRepoInfo }) => {
}, [window.app.pageOptions.enableMetadataManagement]);

const [showMetadataStatusManagementDialog, setShowMetadataStatusManagementDialog] = useState(false);
const { enableMetadata, updateEnableMetadata, navigation } = useMetadata();
const [showMetadataFaceRecognitionDialog, setShowMetadataFaceRecognitionDialog] = useState(false);
const { enableMetadata, updateEnableMetadata, enableFaceRecognition, updateEnableFaceRecognition, navigation } = useMetadata();
const moreOperations = useMemo(() => {
if (!enableMetadataManagement || !currentRepoInfo.is_admin) return [];
return [
let operations = [
{ key: 'extended-properties', value: gettext('Extended properties') }
];
}, [enableMetadataManagement, currentRepoInfo]);
if (enableMetadata) {
operations.push({ key: 'face-recognition', value: gettext('Face recognition') });
}
return operations;
}, [enableMetadataManagement, enableMetadata, currentRepoInfo]);

const moreOperationClick = useCallback((operationKey) => {
if (operationKey === 'extended-properties') {
setShowMetadataStatusManagementDialog(true);
return;
switch (operationKey) {
case 'extended-properties': {
setShowMetadataStatusManagementDialog(true);
break;
}
case 'face-recognition': {
setShowMetadataFaceRecognitionDialog(true);
break;
}
default:
break;
}
}, []);

const closeMetadataManagementDialog = useCallback(() => {
setShowMetadataStatusManagementDialog(false);
}, []);

const closeMetadataFaceRecognitionDialog = useCallback(() => {
setShowMetadataFaceRecognitionDialog(false);
}, []);

const openMetadataFaceRecognition = useCallback(() => {
updateEnableFaceRecognition(true);
}, [updateEnableFaceRecognition]);

const toggleMetadataStatus = useCallback((value) => {
updateEnableMetadata(value);
}, [updateEnableMetadata]);
Expand Down Expand Up @@ -63,6 +84,14 @@ const DirViews = ({ userPerm, repoID, currentPath, currentRepoInfo }) => {
submit={toggleMetadataStatus}
/>
)}
{showMetadataFaceRecognitionDialog && (
<MetadataFaceRecognitionDialog
value={enableFaceRecognition}
repoID={repoID}
toggle={closeMetadataFaceRecognitionDialog}
submit={openMetadataFaceRecognition}
/>
)}
</>
);
};
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/dropdown-menu/item-dropdown-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ class ItemDropdownMenu extends React.Component {

UNSAFE_componentWillReceiveProps(nextProps) { // for toolbar item operation
let { item } = nextProps;
if (item.name !== this.props.item.name) {
let menuList = this.props.getMenuList(item);
this.setState({ menuList: menuList });
const nextMenuList = nextProps.getMenuList(item);
if (item.name !== this.props.item.name || this.state.menuList !== nextMenuList) {
this.setState({ menuList: nextMenuList });
}
}

Expand Down
3 changes: 2 additions & 1 deletion frontend/src/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import KeyCodes from './keyCodes';
export const DIALOG_MAX_HEIGHT = window.innerHeight - 56; // Dialog margin is 3.5rem (56px)

export const PRIVATE_FILE_TYPE = {
FILE_EXTENDED_PROPERTIES: '__file_extended_properties'
FILE_EXTENDED_PROPERTIES: '__file_extended_properties',
FACE_RECOGNITION: '__face_recognition',
};

const TAG_COLORS = ['#FBD44A', '#EAA775', '#F4667C', '#DC82D2', '#9860E5', '#9F8CF1', '#59CB74', '#ADDF84',
Expand Down
26 changes: 26 additions & 0 deletions frontend/src/metadata/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,32 @@ class MetadataManagerAPI {
};
return this.req.delete(url, { data });
}

// face recognition
getFaceRecognitionStatus(repoID) {
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/face-recognition/';
return this.req.get(url);
}

openFaceRecognition = (repoID) => {
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/face-recognition/';
return this.req.post(url);
};

getFaceData = (repoID, start = 0, limit = 1000) => {
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/face-records/?start=' + start + '&limit=' + limit;
return this.req.get(url);
};

updateFaceName = (repoID, recordID, name) => {
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/face-record/';
const params = {
record_id: recordID,
name: name,
};
return this.req.put(url, params);
};

}

const metadataAPI = new MetadataManagerAPI();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, { useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
import { gettext } from '../../../../utils/constants';
import metadataAPI from '../../../api';
import toaster from '../../../../components/toast';
import { Utils } from '../../../../utils/utils';

const MetadataFaceRecognitionDialog = ({ value, repoID, toggle, submit }) => {
const [submitting, setSubmitting] = useState(false);

const onToggle = useCallback(() => {
toggle();
}, [toggle]);

const onSubmit = useCallback(() => {
setSubmitting(true);
metadataAPI.openFaceRecognition(repoID).then(res => {
submit(true);
toggle();
}).catch(error => {
const errorMsg = Utils.getErrorMsg(error);
toaster.danger(errorMsg);
setSubmitting(false);
});
}, [repoID, submit, toggle]);

return (
<Modal className="metadata-face-recognition-dialog" isOpen={true} toggle={onToggle}>
<ModalHeader toggle={onToggle}>{gettext('Face recognition')}</ModalHeader>
<ModalBody>
{value ? gettext('Face recognition enabled.') : gettext('Whether to enable face recognition?')}
</ModalBody>
{!value && (
<ModalFooter>
<Button color="secondary" onClick={onToggle}>{gettext('Cancel')}</Button>
<Button color="primary" disabled={submitting} onClick={onSubmit}>{gettext('Submit')}</Button>
</ModalFooter>
)}
</Modal>
);
};

MetadataFaceRecognitionDialog.propTypes = {
value: PropTypes.bool.isRequired,
repoID: PropTypes.string.isRequired,
toggle: PropTypes.func.isRequired,
submit: PropTypes.func.isRequired,
};

export default MetadataFaceRecognitionDialog;
1 change: 1 addition & 0 deletions frontend/src/metadata/constants/column/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const NOT_DISPLAY_COLUMN_KEYS = [
PRIVATE_COLUMN_KEY.FILE_DETAILS,
PRIVATE_COLUMN_KEY.LOCATION,
PRIVATE_COLUMN_KEY.IS_DIR,
PRIVATE_COLUMN_KEY.FACE_LINKS,
];

export const VIEW_NOT_DISPLAY_COLUMN_KEYS = [
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/metadata/constants/column/private.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const PRIVATE_COLUMN_KEY = {
CAPTURE_TIME: '_capture_time',
FILE_REVIEWER: '_reviewer',
OWNER: '_owner',
FACE_LINKS: '_face_links',
};

export const PRIVATE_COLUMN_KEYS = [
Expand Down Expand Up @@ -59,6 +60,7 @@ export const PRIVATE_COLUMN_KEYS = [
PRIVATE_COLUMN_KEY.CAPTURE_TIME,
PRIVATE_COLUMN_KEY.FILE_REVIEWER,
PRIVATE_COLUMN_KEY.OWNER,
PRIVATE_COLUMN_KEY.FACE_LINKS,
];

export const EDITABLE_PRIVATE_COLUMN_KEYS = [
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/metadata/constants/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { SORT_COLUMN_OPTIONS, GALLERY_SORT_COLUMN_OPTIONS, GALLERY_FIRST_SORT_CO

export const VIEW_TYPE = {
TABLE: 'table',
GALLERY: 'gallery'
GALLERY: 'gallery',
};

export const VIEW_TYPE_ICON = {
Expand Down
48 changes: 45 additions & 3 deletions frontend/src/metadata/hooks/metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
}, [window.app.pageOptions.enableMetadataManagement]);

const [enableMetadata, setEnableExtendedProperties] = useState(false);
const [enableFaceRecognition, setEnableFaceRecognition] = useState(false);
const [showFirstView, setShowFirstView] = useState(false);
const [navigation, setNavigation] = useState([]);
const [staticView, setStaticView] = useState([]);
const [, setCount] = useState(0);
const viewsMap = useRef({});

Expand Down Expand Up @@ -55,12 +57,21 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
if (!newValue) {
hideMetadataView && hideMetadataView();
cancelURLView();
setEnableFaceRecognition(false);
} else {
setShowFirstView(true);
}
setEnableExtendedProperties(newValue);
}, [enableMetadata, hideMetadataView, cancelURLView]);

const updateEnableFaceRecognition = useCallback((newValue) => {
if (newValue === enableFaceRecognition) return;
setEnableFaceRecognition(newValue);
if (newValue) {
toaster.success(gettext('Recognizing portraits. Please refresh the page later.'));
}
}, [enableFaceRecognition]);

// views
useEffect(() => {
if (enableMetadata) {
Expand All @@ -71,6 +82,11 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
viewsMap.current[view._id] = view;
});
}
viewsMap.current['_face_recognition'] = {
_id: '_face_recognition',
name: gettext('Photos - classfied by people'),
type: PRIVATE_FILE_TYPE.FACE_RECOGNITION,
};
setNavigation(navigation);
}).catch(error => {
const errorMsg = Utils.getErrorMsg(error);
Expand All @@ -84,8 +100,31 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [repoID, enableMetadata]);

useEffect(() => {
if (!enableMetadata) {
setStaticView([]);
setEnableFaceRecognition(false);
return;
}
metadataAPI.getFaceRecognitionStatus(repoID).then(res => {
setEnableFaceRecognition(res.data.enabled);
}).catch(error => {
const errorMsg = Utils.getErrorMsg(error);
toaster.danger(errorMsg);
});
}, [repoID, enableMetadata]);

useEffect(() => {
if (!enableFaceRecognition) {
setStaticView([]);
return;
}
setStaticView([{ _id: '_face_recognition', type: 'view' }]);
}, [enableFaceRecognition]);

const selectView = useCallback((view, isSelected) => {
if (isSelected) return;
const isFaceRecognitionView = view.type === PRIVATE_FILE_TYPE.FACE_RECOGNITION;
const node = {
children: [],
path: '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + view._id,
Expand All @@ -94,9 +133,9 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
isPreload: true,
object: {
file_tags: [],
id: PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES,
name: gettext('File extended properties'),
type: PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES,
id: isFaceRecognitionView ? PRIVATE_FILE_TYPE.FACE_RECOGNITION : PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES,
name: isFaceRecognitionView ? gettext('Photos - classfied by people') : gettext('File extended properties'),
type: isFaceRecognitionView ? PRIVATE_FILE_TYPE.FACE_RECOGNITION : PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES,
isDir: () => false,
},
parentNode: {},
Expand Down Expand Up @@ -177,9 +216,12 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
<MetadataContext.Provider value={{
enableMetadata,
updateEnableMetadata,
enableFaceRecognition,
updateEnableFaceRecognition,
showFirstView,
setShowFirstView,
navigation,
staticView,
viewsMap: viewsMap.current,
selectView,
addView,
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/metadata/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import SeafileMetadata, { Context as MetadataContext } from './metadata-view';
import MetadataStatusManagementDialog from './components/dialog/metadata-status-manage-dialog';
import MetadataFaceRecognitionDialog from './components/dialog/metadata-face-recognition-dialog';
import MetadataDetails from './components/metadata-details';
import MetadataTreeView from './metadata-tree-view';
import metadataAPI from './api';
Expand All @@ -11,6 +12,7 @@ export {
MetadataContext,
SeafileMetadata,
MetadataStatusManagementDialog,
MetadataFaceRecognitionDialog,
MetadataTreeView,
MetadataDetails,
};
16 changes: 16 additions & 0 deletions frontend/src/metadata/metadata-tree-view/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
}, [userPerm]);
const [, setState] = useState(0);
const {
enableFaceRecognition,
showFirstView,
navigation,
staticView,
viewsMap,
selectView,
addView,
Expand Down Expand Up @@ -196,6 +198,20 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
/>
</div>
)}
{enableFaceRecognition && staticView.map((item) => {
const view = viewsMap[item._id];
const viewPath = '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + view._id;
const isSelected = currentPath === viewPath;
return (
<ViewItem
key={view._id}
userPerm="r"
view={view}
isSelected={isSelected}
onClick={(view) => selectView(view, isSelected)}
/>
);
})}
{canAdd && (
<div id="sf-metadata-view-popover">
<CustomizeAddTool
Expand Down
Loading

0 comments on commit 065f158

Please sign in to comment.