mirror of
https://github.com/haiwen/seahub.git
synced 2025-07-12 14:38:58 +00:00
refactor: face ui (#6981)
* refactor: face ui * feat: people pgotos * feat: people pgotos * feat: optimize ui * feat: optimize ui * feat: delete file * optimize code * feat: replace icon * update * feat: replace icon * feat: optimize back btn --------- Co-authored-by: 杨国璇 <ygx@Hello-word.local> Co-authored-by: 杨国璇 <ygx@192.168.1.3> Co-authored-by: ‘JoinTyang’ <yangtong1009@163.com>
This commit is contained in:
parent
8f04a770f7
commit
2cb758e302
15
frontend/src/assets/icons/face-recognition-view.svg
Normal file
15
frontend/src/assets/icons/face-recognition-view.svg
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#999999;}
|
||||||
|
</style>
|
||||||
|
<title>photos-classfied-by-people</title>
|
||||||
|
<g id="photos-classfied-by-people">
|
||||||
|
<path id="形状" class="st0" d="M13.7,17.5c1.4,0.8,3.1,0.8,4.5,0s2.3-2.3,2.3-3.9c0-2.5-2-4.5-4.5-4.5s-4.5,2-4.5,4.5
|
||||||
|
C11.5,15.2,12.3,16.7,13.7,17.5z M26,25.8c0,1.6-4.5,3.9-9.3,4C11.5,29.8,6,27.5,6,25.8c0-3.3,4.5-5.3,10-5.3S26,22.5,26,25.8z"/>
|
||||||
|
<path id="形状结合" class="st0" d="M16,0c8.8,0,16,7.2,16,16s-7.2,16-16,16S0,24.8,0,16S7.2,0,16,0z M16,3C8.8,3,3,8.8,3,16
|
||||||
|
s5.8,13,13,13s13-5.8,13-13S23.2,3,16,3z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 907 B |
@ -11,7 +11,6 @@ import ReposSortMenu from '../../components/repos-sort-menu';
|
|||||||
import MetadataViewToolBar from '../../metadata/components/view-toolbar';
|
import MetadataViewToolBar from '../../metadata/components/view-toolbar';
|
||||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||||
import { DIRENT_DETAIL_MODE } from '../dir-view-mode/constants';
|
import { DIRENT_DETAIL_MODE } from '../dir-view-mode/constants';
|
||||||
import { FACE_RECOGNITION_VIEW_ID } from '../../metadata/constants';
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
repoID: PropTypes.string.isRequired,
|
repoID: PropTypes.string.isRequired,
|
||||||
@ -118,7 +117,6 @@ class DirTool extends React.Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isFileExtended) {
|
if (isFileExtended) {
|
||||||
if (viewId === FACE_RECOGNITION_VIEW_ID) return null;
|
|
||||||
return (
|
return (
|
||||||
<div className="dir-tool">
|
<div className="dir-tool">
|
||||||
<MetadataViewToolBar viewId={viewId} isCustomPermission={isCustomPermission} showDetail={this.showDirentDetail} />
|
<MetadataViewToolBar viewId={viewId} isCustomPermission={isCustomPermission} showDetail={this.showDirentDetail} />
|
||||||
|
@ -2,4 +2,3 @@ export const LIST_MODE = 'list';
|
|||||||
export const GRID_MODE = 'grid';
|
export const GRID_MODE = 'grid';
|
||||||
export const DIRENT_DETAIL_MODE = 'detail';
|
export const DIRENT_DETAIL_MODE = 'detail';
|
||||||
export const METADATA_MODE = 'metadata';
|
export const METADATA_MODE = 'metadata';
|
||||||
export const FACE_RECOGNITION_MODE = 'person_image';
|
|
||||||
|
@ -9,8 +9,7 @@ import ResizeBar from '../resize-bar';
|
|||||||
import { DRAG_HANDLER_HEIGHT, MAX_SIDE_PANEL_RATE, MIN_SIDE_PANEL_RATE } from '../resize-bar/constants';
|
import { DRAG_HANDLER_HEIGHT, MAX_SIDE_PANEL_RATE, MIN_SIDE_PANEL_RATE } from '../resize-bar/constants';
|
||||||
import { SeafileMetadata } from '../../metadata';
|
import { SeafileMetadata } from '../../metadata';
|
||||||
import { mediaUrl } from '../../utils/constants';
|
import { mediaUrl } from '../../utils/constants';
|
||||||
import { GRID_MODE, LIST_MODE, METADATA_MODE, FACE_RECOGNITION_MODE } from './constants';
|
import { GRID_MODE, LIST_MODE, METADATA_MODE } from './constants';
|
||||||
import FaceRecognition from '../../metadata/views/face-recognition';
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
isSidePanelFolded: PropTypes.bool,
|
isSidePanelFolded: PropTypes.bool,
|
||||||
@ -81,6 +80,7 @@ const propTypes = {
|
|||||||
onItemsScroll: PropTypes.func.isRequired,
|
onItemsScroll: PropTypes.func.isRequired,
|
||||||
eventBus: PropTypes.object,
|
eventBus: PropTypes.object,
|
||||||
updateCurrentDirent: PropTypes.func.isRequired,
|
updateCurrentDirent: PropTypes.func.isRequired,
|
||||||
|
closeDirentDetail: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
class DirColumnView extends React.Component {
|
class DirColumnView extends React.Component {
|
||||||
@ -195,7 +195,7 @@ class DirColumnView extends React.Component {
|
|||||||
onScroll={this.props.isViewFile ? () => {} : this.props.onItemsScroll}
|
onScroll={this.props.isViewFile ? () => {} : this.props.onItemsScroll}
|
||||||
ref={this.dirContentMain}
|
ref={this.dirContentMain}
|
||||||
>
|
>
|
||||||
{currentMode === METADATA_MODE &&
|
{currentMode === METADATA_MODE && (
|
||||||
<SeafileMetadata
|
<SeafileMetadata
|
||||||
mediaUrl={mediaUrl}
|
mediaUrl={mediaUrl}
|
||||||
repoID={this.props.repoID}
|
repoID={this.props.repoID}
|
||||||
@ -204,11 +204,9 @@ class DirColumnView extends React.Component {
|
|||||||
deleteFilesCallback={this.props.deleteFilesCallback}
|
deleteFilesCallback={this.props.deleteFilesCallback}
|
||||||
renameFileCallback={this.props.renameFileCallback}
|
renameFileCallback={this.props.renameFileCallback}
|
||||||
updateCurrentDirent={this.props.updateCurrentDirent}
|
updateCurrentDirent={this.props.updateCurrentDirent}
|
||||||
|
closeDirentDetail={this.props.closeDirentDetail}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
{currentMode === FACE_RECOGNITION_MODE &&
|
|
||||||
<FaceRecognition repoID={this.props.repoID}/>
|
|
||||||
}
|
|
||||||
{currentMode === LIST_MODE &&
|
{currentMode === LIST_MODE &&
|
||||||
<DirListView
|
<DirListView
|
||||||
path={this.props.path}
|
path={this.props.path}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
line-height: 2.5rem;
|
line-height: 2.5rem;
|
||||||
border-bottom: 1px solid #e8e8e8;
|
border-bottom: 1px solid #eee;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
}
|
}
|
||||||
|
@ -272,15 +272,20 @@ class MetadataManagerAPI {
|
|||||||
return this.req.get(url);
|
return this.req.get(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
updateFaceName = (repoID, recordID, name) => {
|
renamePeople = (repoID, recordId, name) => {
|
||||||
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/face-record/';
|
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/face-record/';
|
||||||
const params = {
|
const params = {
|
||||||
record_id: recordID,
|
record_id: recordId,
|
||||||
name: name,
|
name: name,
|
||||||
};
|
};
|
||||||
return this.req.put(url, params);
|
return this.req.put(url, params);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getPeoplePhotos = (repoID, peopleId, start = 0, limit = 1000) => {
|
||||||
|
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/people-photos/' + peopleId + '/?start=' + start + '&limit=' + limit;
|
||||||
|
return this.req.get(url);
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const metadataAPI = new MetadataManagerAPI();
|
const metadataAPI = new MetadataManagerAPI();
|
||||||
|
@ -16,6 +16,7 @@ const ViewDetails = ({ viewId, onClose }) => {
|
|||||||
const type = view.type;
|
const type = view.type;
|
||||||
if (type === VIEW_TYPE.GALLERY) return `${mediaUrl}favicons/gallery.png`;
|
if (type === VIEW_TYPE.GALLERY) return `${mediaUrl}favicons/gallery.png`;
|
||||||
if (type === VIEW_TYPE.TABLE) return `${mediaUrl}favicons/table.png`;
|
if (type === VIEW_TYPE.TABLE) return `${mediaUrl}favicons/table.png`;
|
||||||
|
if (type === VIEW_TYPE.FACE_RECOGNITION) return `${mediaUrl}favicons/face-recognition-view.png`;
|
||||||
return `${mediaUrl}img/file/256/file.png`;
|
return `${mediaUrl}img/file/256/file.png`;
|
||||||
}, [view]);
|
}, [view]);
|
||||||
|
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { GalleryGroupBySetter, GallerySliderSetter } from '../../data-process-setter';
|
||||||
|
import { gettext } from '../../../../utils/constants';
|
||||||
|
import { EVENT_BUS_TYPE } from '../../../constants';
|
||||||
|
|
||||||
|
const FaceRecognitionViewToolbar = ({ isCustomPermission, view, showDetail }) => {
|
||||||
|
const [isShow, setShow] = useState(false);
|
||||||
|
|
||||||
|
const onToggle = useCallback((isShow) => {
|
||||||
|
setShow(isShow);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribeToggle = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.TOGGLE_VIEW_TOOLBAR, onToggle);
|
||||||
|
return () => {
|
||||||
|
unsubscribeToggle && unsubscribeToggle();
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!isShow) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="sf-metadata-tool-left-operations">
|
||||||
|
<GalleryGroupBySetter view={view} />
|
||||||
|
<GallerySliderSetter view={view} />
|
||||||
|
{!isCustomPermission && (
|
||||||
|
<div className="cur-view-path-btn ml-2" onClick={showDetail}>
|
||||||
|
<span className="sf3-font sf3-font-info" aria-label={gettext('Properties')} title={gettext('Properties')}></span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="sf-metadata-tool-right-operations"></div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
FaceRecognitionViewToolbar.propTypes = {
|
||||||
|
isCustomPermission: PropTypes.bool,
|
||||||
|
view: PropTypes.object.isRequired,
|
||||||
|
showDetail: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FaceRecognitionViewToolbar;
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import { EVENT_BUS_TYPE, VIEW_TYPE } from '../../constants';
|
import { EVENT_BUS_TYPE, VIEW_TYPE } from '../../constants';
|
||||||
import TableViewToolbar from './table-view-toolbar';
|
import TableViewToolbar from './table-view-toolbar';
|
||||||
import GalleryViewToolbar from './gallery-view-toolbar';
|
import GalleryViewToolbar from './gallery-view-toolbar';
|
||||||
|
import FaceRecognitionViewToolbar from './face-recognition';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
@ -89,6 +90,13 @@ const ViewToolBar = ({ viewId, isCustomPermission, showDetail }) => {
|
|||||||
showDetail={showDetail}
|
showDetail={showDetail}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{viewType === VIEW_TYPE.FACE_RECOGNITION && (
|
||||||
|
<FaceRecognitionViewToolbar
|
||||||
|
isCustomPermission={isCustomPermission}
|
||||||
|
view={view}
|
||||||
|
showDetail={showDetail}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -53,4 +53,7 @@ export const EVENT_BUS_TYPE = {
|
|||||||
// gallery
|
// gallery
|
||||||
MODIFY_GALLERY_ZOOM_GEAR: 'modify_gallery_zoom_gear',
|
MODIFY_GALLERY_ZOOM_GEAR: 'modify_gallery_zoom_gear',
|
||||||
SWITCH_GALLERY_GROUP_BY: 'switch_gallery_group_by',
|
SWITCH_GALLERY_GROUP_BY: 'switch_gallery_group_by',
|
||||||
|
|
||||||
|
// face recognition
|
||||||
|
TOGGLE_VIEW_TOOLBAR: 'toggle_view_toolbar',
|
||||||
};
|
};
|
||||||
|
@ -7,6 +7,7 @@ import { SORT_COLUMN_OPTIONS, GALLERY_SORT_COLUMN_OPTIONS, GALLERY_FIRST_SORT_CO
|
|||||||
export const VIEW_TYPE = {
|
export const VIEW_TYPE = {
|
||||||
TABLE: 'table',
|
TABLE: 'table',
|
||||||
GALLERY: 'gallery',
|
GALLERY: 'gallery',
|
||||||
|
FACE_RECOGNITION: 'face_recognition',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FACE_RECOGNITION_VIEW_ID = '_face_recognition';
|
export const FACE_RECOGNITION_VIEW_ID = '_face_recognition';
|
||||||
@ -14,6 +15,7 @@ export const FACE_RECOGNITION_VIEW_ID = '_face_recognition';
|
|||||||
export const VIEW_TYPE_ICON = {
|
export const VIEW_TYPE_ICON = {
|
||||||
[VIEW_TYPE.TABLE]: 'table',
|
[VIEW_TYPE.TABLE]: 'table',
|
||||||
[VIEW_TYPE.GALLERY]: 'image',
|
[VIEW_TYPE.GALLERY]: 'image',
|
||||||
|
[VIEW_TYPE.FACE_RECOGNITION]: 'face-recognition-view',
|
||||||
'image': 'image'
|
'image': 'image'
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -36,6 +38,7 @@ export const VIEW_TYPE_DEFAULT_BASIC_FILTER = {
|
|||||||
filter_term: 'picture'
|
filter_term: 'picture'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
[VIEW_TYPE.FACE_RECOGNITION]: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VIEW_TYPE_DEFAULT_SORTS = {
|
export const VIEW_TYPE_DEFAULT_SORTS = {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import metadataAPI from './api';
|
import metadataAPI from './api';
|
||||||
import {
|
import {
|
||||||
PRIVATE_COLUMN_KEYS, EDITABLE_DATA_PRIVATE_COLUMN_KEYS, EDITABLE_PRIVATE_COLUMN_KEYS, DELETABLE_PRIVATE_COLUMN_KEY,
|
PRIVATE_COLUMN_KEYS, EDITABLE_DATA_PRIVATE_COLUMN_KEYS, EDITABLE_PRIVATE_COLUMN_KEYS, DELETABLE_PRIVATE_COLUMN_KEY,
|
||||||
|
FACE_RECOGNITION_VIEW_ID,
|
||||||
|
VIEW_TYPE,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import LocalStorage from './utils/local-storage';
|
import LocalStorage from './utils/local-storage';
|
||||||
import EventBus from '../components/common/event-bus';
|
import EventBus from '../components/common/event-bus';
|
||||||
@ -71,8 +73,14 @@ class Context {
|
|||||||
|
|
||||||
// metadata
|
// metadata
|
||||||
getMetadata = (params) => {
|
getMetadata = (params) => {
|
||||||
|
if (!this.metadataAPI) return null;
|
||||||
const repoID = this.settings['repoID'];
|
const repoID = this.settings['repoID'];
|
||||||
return this.metadataAPI ? this.metadataAPI.getMetadata(repoID, params) : null;
|
const { view_id, start, limit } = params;
|
||||||
|
if (view_id === FACE_RECOGNITION_VIEW_ID) {
|
||||||
|
return this.metadataAPI.getFaceData(repoID, start, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.metadataAPI.getMetadata(repoID, params);
|
||||||
};
|
};
|
||||||
|
|
||||||
getRecord = (parentDir, fileName) => {
|
getRecord = (parentDir, fileName) => {
|
||||||
@ -86,6 +94,17 @@ class Context {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getView = (viewId) => {
|
getView = (viewId) => {
|
||||||
|
if (viewId === FACE_RECOGNITION_VIEW_ID) {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
view: {
|
||||||
|
_id: FACE_RECOGNITION_VIEW_ID,
|
||||||
|
type: VIEW_TYPE.FACE_RECOGNITION,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const repoID = this.settings['repoID'];
|
const repoID = this.settings['repoID'];
|
||||||
return this.metadataAPI.getView(repoID, viewId);
|
return this.metadataAPI.getView(repoID, viewId);
|
||||||
};
|
};
|
||||||
@ -223,6 +242,18 @@ class Context {
|
|||||||
const repoID = this.settings['repoID'];
|
const repoID = this.settings['repoID'];
|
||||||
return this.metadataAPI.extractFileDetails(repoID, objIds);
|
return this.metadataAPI.extractFileDetails(repoID, objIds);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// face api
|
||||||
|
renamePeople = (recordId, name) => {
|
||||||
|
const repoID = this.settings['repoID'];
|
||||||
|
return this.metadataAPI.renamePeople(repoID, recordId, name);
|
||||||
|
};
|
||||||
|
|
||||||
|
getPeoplePhotos = (recordId, start, limit) => {
|
||||||
|
const repoID = this.settings['repoID'];
|
||||||
|
return this.metadataAPI.getPeoplePhotos(repoID, recordId, start, limit);
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Context;
|
export default Context;
|
||||||
|
@ -127,6 +127,7 @@ export const MetadataViewProvider = ({
|
|||||||
deleteFilesCallback: params.deleteFilesCallback,
|
deleteFilesCallback: params.deleteFilesCallback,
|
||||||
renameFileCallback: params.renameFileCallback,
|
renameFileCallback: params.renameFileCallback,
|
||||||
updateCurrentDirent: params.updateCurrentDirent,
|
updateCurrentDirent: params.updateCurrentDirent,
|
||||||
|
closeDirentDetail: params.closeDirentDetail,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -4,7 +4,7 @@ import { Utils } from '../../utils/utils';
|
|||||||
import toaster from '../../components/toast';
|
import toaster from '../../components/toast';
|
||||||
import { gettext } from '../../utils/constants';
|
import { gettext } from '../../utils/constants';
|
||||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||||
import { FACE_RECOGNITION_VIEW_ID } from '../constants';
|
import { FACE_RECOGNITION_VIEW_ID, VIEW_TYPE } from '../constants';
|
||||||
|
|
||||||
// This hook provides content related to seahub interaction, such as whether to enable extended attributes, views data, etc.
|
// This hook provides content related to seahub interaction, such as whether to enable extended attributes, views data, etc.
|
||||||
const MetadataContext = React.createContext(null);
|
const MetadataContext = React.createContext(null);
|
||||||
@ -87,7 +87,7 @@ export const MetadataProvider = ({ repoID, currentRepoInfo, hideMetadataView, se
|
|||||||
viewsMap.current[FACE_RECOGNITION_VIEW_ID] = {
|
viewsMap.current[FACE_RECOGNITION_VIEW_ID] = {
|
||||||
_id: FACE_RECOGNITION_VIEW_ID,
|
_id: FACE_RECOGNITION_VIEW_ID,
|
||||||
name: gettext('Photos - classfied by people'),
|
name: gettext('Photos - classfied by people'),
|
||||||
type: PRIVATE_FILE_TYPE.FACE_RECOGNITION,
|
type: VIEW_TYPE.FACE_RECOGNITION,
|
||||||
};
|
};
|
||||||
setNavigation(navigation);
|
setNavigation(navigation);
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
@ -15,16 +15,20 @@ import { isEnter } from '../utils/hotkey';
|
|||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
const updateFavicon = (iconName) => {
|
const updateFavicon = (type) => {
|
||||||
const favicon = document.getElementById('favicon');
|
const favicon = document.getElementById('favicon');
|
||||||
if (favicon) {
|
if (favicon) {
|
||||||
switch (iconName) {
|
switch (type) {
|
||||||
|
case VIEW_TYPE.GALLERY:
|
||||||
case 'image':
|
case 'image':
|
||||||
favicon.href = `${mediaUrl}favicons/gallery.png`;
|
favicon.href = `${mediaUrl}favicons/gallery.png`;
|
||||||
break;
|
break;
|
||||||
case VIEW_TYPE.TABLE:
|
case VIEW_TYPE.TABLE:
|
||||||
favicon.href = `${mediaUrl}favicons/table.png`;
|
favicon.href = `${mediaUrl}favicons/table.png`;
|
||||||
break;
|
break;
|
||||||
|
case VIEW_TYPE.FACE_RECOGNITION:
|
||||||
|
favicon.href = `${mediaUrl}favicons/face-recognition-view.png`;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
favicon.href = `${mediaUrl}favicons/favicon.png`;
|
favicon.href = `${mediaUrl}favicons/favicon.png`;
|
||||||
}
|
}
|
||||||
@ -70,7 +74,7 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
|
|||||||
if (lastOpenedView) {
|
if (lastOpenedView) {
|
||||||
selectView(lastOpenedView);
|
selectView(lastOpenedView);
|
||||||
document.title = `${lastOpenedView.name} - Seafile`;
|
document.title = `${lastOpenedView.name} - Seafile`;
|
||||||
updateFavicon(VIEW_TYPE_ICON[lastOpenedView.type] || VIEW_TYPE.TABLE);
|
updateFavicon(lastOpenedView.type);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const url = `${origin}${pathname}`;
|
const url = `${origin}${pathname}`;
|
||||||
@ -82,7 +86,7 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
|
|||||||
if (showFirstView && firstView) {
|
if (showFirstView && firstView) {
|
||||||
selectView(firstView);
|
selectView(firstView);
|
||||||
document.title = `${firstView.name} - Seafile`;
|
document.title = `${firstView.name} - Seafile`;
|
||||||
updateFavicon(VIEW_TYPE_ICON[firstView.type] || VIEW_TYPE.TABLE);
|
updateFavicon(firstView.type);
|
||||||
} else {
|
} else {
|
||||||
document.title = originalTitle;
|
document.title = originalTitle;
|
||||||
updateFavicon('default');
|
updateFavicon('default');
|
||||||
@ -95,7 +99,7 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
|
|||||||
const currentView = viewsMap[currentViewId];
|
const currentView = viewsMap[currentViewId];
|
||||||
if (currentView) {
|
if (currentView) {
|
||||||
document.title = `${currentView.name} - Seafile`;
|
document.title = `${currentView.name} - Seafile`;
|
||||||
updateFavicon(VIEW_TYPE_ICON[currentView.type] || VIEW_TYPE.TABLE);
|
updateFavicon(currentView.type);
|
||||||
} else {
|
} else {
|
||||||
document.title = originalTitle;
|
document.title = originalTitle;
|
||||||
updateFavicon('default');
|
updateFavicon('default');
|
||||||
|
@ -2,6 +2,7 @@ import React, { useCallback } from 'react';
|
|||||||
import { CenteredLoading } from '@seafile/sf-metadata-ui-component';
|
import { CenteredLoading } from '@seafile/sf-metadata-ui-component';
|
||||||
import Table from '../views/table';
|
import Table from '../views/table';
|
||||||
import Gallery from '../views/gallery';
|
import Gallery from '../views/gallery';
|
||||||
|
import FaceRecognition from '../views/face-recognition';
|
||||||
import { useMetadataView } from '../hooks/metadata-view';
|
import { useMetadataView } from '../hooks/metadata-view';
|
||||||
import { gettext } from '../../utils/constants';
|
import { gettext } from '../../utils/constants';
|
||||||
import { VIEW_TYPE } from '../constants';
|
import { VIEW_TYPE } from '../constants';
|
||||||
@ -10,7 +11,7 @@ const View = () => {
|
|||||||
const { isLoading, metadata, errorMsg } = useMetadataView();
|
const { isLoading, metadata, errorMsg } = useMetadataView();
|
||||||
|
|
||||||
const renderView = useCallback((metadata) => {
|
const renderView = useCallback((metadata) => {
|
||||||
if (!metadata) return false;
|
if (!metadata) return null;
|
||||||
const viewType = metadata.view.type;
|
const viewType = metadata.view.type;
|
||||||
switch (viewType) {
|
switch (viewType) {
|
||||||
case VIEW_TYPE.GALLERY: {
|
case VIEW_TYPE.GALLERY: {
|
||||||
@ -19,6 +20,9 @@ const View = () => {
|
|||||||
case VIEW_TYPE.TABLE: {
|
case VIEW_TYPE.TABLE: {
|
||||||
return <Table />;
|
return <Table />;
|
||||||
}
|
}
|
||||||
|
case VIEW_TYPE.FACE_RECOGNITION: {
|
||||||
|
return (<FaceRecognition />);
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -528,6 +528,22 @@ class Store {
|
|||||||
return this.data.rows.some((row) => newPath === Utils.joinPath(row._parent_dir, row._name));
|
return this.data.rows.some((row) => newPath === Utils.joinPath(row._parent_dir, row._name));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
renamePeopleName = (peopleId, newName, oldName) => {
|
||||||
|
const type = OPERATION_TYPE.RENAME_PEOPLE_NAME;
|
||||||
|
const operation = this.createOperation({
|
||||||
|
type, repo_id: this.repoId, people_id: peopleId, new_name: newName, old_name: oldName
|
||||||
|
});
|
||||||
|
this.applyOperation(operation);
|
||||||
|
};
|
||||||
|
|
||||||
|
deletePeoplePhotos = (peopleId, deletedPhotos) => {
|
||||||
|
const type = OPERATION_TYPE.DELETE_PEOPLE_PHOTOS;
|
||||||
|
const operation = this.createOperation({
|
||||||
|
type, repo_id: this.repoId, people_id: peopleId, deleted_photos: deletedPhotos
|
||||||
|
});
|
||||||
|
this.applyOperation(operation);
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Store;
|
export default Store;
|
||||||
|
@ -184,6 +184,44 @@ export default function apply(data, operation) {
|
|||||||
data.view = new View({ ...data.view, columns_keys: new_columns_keys }, data.columns);
|
data.view = new View({ ...data.view, columns_keys: new_columns_keys }, data.columns);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
// face table op
|
||||||
|
case OPERATION_TYPE.RENAME_PEOPLE_NAME: {
|
||||||
|
const { people_id, new_name } = operation;
|
||||||
|
const { rows } = data;
|
||||||
|
let updatedRows = [...rows];
|
||||||
|
rows.forEach((row, index) => {
|
||||||
|
const { _id: rowId } = row;
|
||||||
|
if (rowId === people_id) {
|
||||||
|
const updatedRow = Object.assign({}, row, { _name: new_name });
|
||||||
|
updatedRows[index] = updatedRow;
|
||||||
|
data.id_row_map[rowId] = updatedRow;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
data.rows = updatedRows;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
case OPERATION_TYPE.DELETE_PEOPLE_PHOTOS: {
|
||||||
|
const { people_id, deleted_photos } = operation;
|
||||||
|
const { rows } = data;
|
||||||
|
const idNeedDeletedMap = deleted_photos.reduce((currIdNeedDeletedMap, rowId) => ({ ...currIdNeedDeletedMap, [rowId]: true }), {});
|
||||||
|
let updatedRows = [...rows];
|
||||||
|
rows.forEach((row, index) => {
|
||||||
|
const { _id: rowId, _photo_links: photoLinks } = row;
|
||||||
|
if (rowId === people_id) {
|
||||||
|
const updatedRow = Object.assign({}, row, { _photo_links: photoLinks.filter(p => !idNeedDeletedMap[p.row_id]) });
|
||||||
|
if (updatedRow._photo_links.length === 0) {
|
||||||
|
updatedRows.splice(index, 1);
|
||||||
|
delete data.id_row_map[rowId];
|
||||||
|
} else {
|
||||||
|
updatedRows[index] = updatedRow;
|
||||||
|
data.id_row_map[rowId] = updatedRow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
data.rows = updatedRows;
|
||||||
|
data.recordsCount = updatedRows.length;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,10 @@ export const OPERATION_TYPE = {
|
|||||||
MODIFY_COLUMN_DATA: 'modify_column_data',
|
MODIFY_COLUMN_DATA: 'modify_column_data',
|
||||||
MODIFY_COLUMN_WIDTH: 'modify_column_width',
|
MODIFY_COLUMN_WIDTH: 'modify_column_width',
|
||||||
MODIFY_COLUMN_ORDER: 'modify_column_order',
|
MODIFY_COLUMN_ORDER: 'modify_column_order',
|
||||||
|
|
||||||
|
// face table
|
||||||
|
RENAME_PEOPLE_NAME: 'rename_people_name',
|
||||||
|
DELETE_PEOPLE_PHOTOS: 'delete_people_photos',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const COLUMN_DATA_OPERATION_TYPE = {
|
export const COLUMN_DATA_OPERATION_TYPE = {
|
||||||
@ -45,6 +49,8 @@ export const OPERATION_ATTRIBUTES = {
|
|||||||
[OPERATION_TYPE.DELETE_COLUMN]: ['repo_id', 'column_key', 'column'],
|
[OPERATION_TYPE.DELETE_COLUMN]: ['repo_id', 'column_key', 'column'],
|
||||||
[OPERATION_TYPE.MODIFY_COLUMN_WIDTH]: ['column_key', 'new_width', 'old_width'],
|
[OPERATION_TYPE.MODIFY_COLUMN_WIDTH]: ['column_key', 'new_width', 'old_width'],
|
||||||
[OPERATION_TYPE.MODIFY_COLUMN_ORDER]: ['repo_id', 'view_id', 'new_columns_keys', 'old_columns_keys'],
|
[OPERATION_TYPE.MODIFY_COLUMN_ORDER]: ['repo_id', 'view_id', 'new_columns_keys', 'old_columns_keys'],
|
||||||
|
[OPERATION_TYPE.RENAME_PEOPLE_NAME]: ['repo_id', 'people_id', 'new_name', 'old_name'],
|
||||||
|
[OPERATION_TYPE.DELETE_PEOPLE_PHOTOS]: ['repo_id', 'people_id', 'deleted_photos'],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UNDO_OPERATION_TYPE = [
|
export const UNDO_OPERATION_TYPE = [
|
||||||
|
@ -174,6 +174,17 @@ class ServerOperator {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// face table op
|
||||||
|
case OPERATION_TYPE.RENAME_PEOPLE_NAME: {
|
||||||
|
const { people_id, new_name } = operation;
|
||||||
|
window.sfMetadataContext.renamePeople(people_id, new_name).then(res => {
|
||||||
|
callback({ operation });
|
||||||
|
}).catch(error => {
|
||||||
|
callback({ error: gettext('Failed to modify people name') });
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1,113 +0,0 @@
|
|||||||
import React, { useCallback, useRef, useState } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
||||||
import { Input } from 'reactstrap';
|
|
||||||
import toaster from '../../../components/toast';
|
|
||||||
import { Utils } from '../../../utils/utils';
|
|
||||||
import { gettext, siteRoot } from '../../../utils/constants';
|
|
||||||
import metadataAPI from '../../api';
|
|
||||||
import isHotkey from 'is-hotkey';
|
|
||||||
import { isEnter } from '../../utils/hotkey';
|
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
|
||||||
|
|
||||||
const theadData = [
|
|
||||||
{ width: '5%', text: '' },
|
|
||||||
{ width: '39%', text: gettext('Name') },
|
|
||||||
{ width: '34%', text: gettext('Original path') },
|
|
||||||
{ width: '11%', text: gettext('Size') },
|
|
||||||
{ width: '11%', text: gettext('Last Update') },
|
|
||||||
];
|
|
||||||
|
|
||||||
const FaceGroup = ({ repoID, group, onPhotoClick }) => {
|
|
||||||
const [name, setName] = useState(group.name);
|
|
||||||
const [isRenaming, setRenaming] = useState(false);
|
|
||||||
const serverName = useRef(group.name);
|
|
||||||
|
|
||||||
const showPhoto = useCallback((event, photo) => {
|
|
||||||
event.preventDefault();
|
|
||||||
onPhotoClick(photo);
|
|
||||||
}, [onPhotoClick]);
|
|
||||||
|
|
||||||
const changeName = useCallback((event) => {
|
|
||||||
const value = event.target.value;
|
|
||||||
if (name === value) return;
|
|
||||||
setName(value);
|
|
||||||
}, [name]);
|
|
||||||
|
|
||||||
const renameName = useCallback(() => {
|
|
||||||
setRenaming(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const updateName = useCallback(() => {
|
|
||||||
if (name === serverName.current) {
|
|
||||||
setRenaming(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
metadataAPI.updateFaceName(repoID, group.record_id, name).then(res => {
|
|
||||||
serverName.current = name;
|
|
||||||
setRenaming(false);
|
|
||||||
}).catch(err => {
|
|
||||||
const errorMsg = Utils.getErrorMsg(err);
|
|
||||||
toaster.danger(errorMsg);
|
|
||||||
setName(serverName.current);
|
|
||||||
setRenaming(false);
|
|
||||||
});
|
|
||||||
}, [repoID, group, name]);
|
|
||||||
|
|
||||||
const onRenameKeyDown = useCallback((event) => {
|
|
||||||
if (isEnter(event)) {
|
|
||||||
updateName();
|
|
||||||
} else if (isHotkey('esc', event)) {
|
|
||||||
setName(serverName.current);
|
|
||||||
setRenaming(false);
|
|
||||||
}
|
|
||||||
}, [updateName]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={group.record_id} className="sf-metadata-face-recognition-item">
|
|
||||||
{isRenaming ?
|
|
||||||
(<Input
|
|
||||||
autoFocus
|
|
||||||
value={name}
|
|
||||||
onChange={changeName}
|
|
||||||
onBlur={updateName}
|
|
||||||
onKeyDown={onRenameKeyDown}
|
|
||||||
/>)
|
|
||||||
:
|
|
||||||
(<div className="sf-metadata-face-recognition-name form-control" onClick={renameName}>{name}</div>)
|
|
||||||
}
|
|
||||||
<table className="table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
{theadData.map((item, index) => {
|
|
||||||
return <th key={index} width={item.width}>{item.text}</th>;
|
|
||||||
})}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{group.photos.map((photo, index) => {
|
|
||||||
return (
|
|
||||||
<tr key={index} onClick={(event) => showPhoto(event, photo)}>
|
|
||||||
<td className="text-center"><img src={photo.src} alt="" className="thumbnail cursor-pointer" /></td>
|
|
||||||
<td><a href={`${siteRoot}lib/${repoID}/file${photo.path}`} onClick={(event) => showPhoto(event, photo)}>{photo.file_name}</a></td>
|
|
||||||
<td>{photo.parent_dir}</td>
|
|
||||||
<td>{Utils.bytesToSize(photo.size)}</td>
|
|
||||||
<td title={dayjs(photo.mtime).fromNow()}>{dayjs(photo.mtime).fromNow()}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
FaceGroup.propTypes = {
|
|
||||||
repoID: PropTypes.string,
|
|
||||||
group: PropTypes.object.isRequired,
|
|
||||||
onPhotoClick: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FaceGroup;
|
|
@ -1,29 +1,7 @@
|
|||||||
.sf-metadata-face-recognition {
|
.sf-metadata-face-recognition-container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sf-metadata-face-recognition .sf-metadata-face-recognition-item {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-face-recognition .sf-metadata-face-recognition-item:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-face-recognition .sf-metadata-face-recognition-name {
|
|
||||||
border-color: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-face-recognition .sf-metadata-face-recognition-loading-more {
|
|
||||||
height: 30px;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
@ -1,161 +1,48 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { useMetadataView } from '../../hooks/metadata-view';
|
||||||
import { CenteredLoading } from '@seafile/sf-metadata-ui-component';
|
import Peoples from './peoples';
|
||||||
import toaster from '../../../components/toast';
|
import PeoplePhotos from './person-photos';
|
||||||
import { Utils } from '../../../utils/utils';
|
|
||||||
import metadataAPI from '../../api';
|
|
||||||
import FaceGroup from './face-group';
|
|
||||||
import ImageDialog from '../../../components/dialog/image-dialog';
|
|
||||||
import ModalPortal from '../../../components/modal-portal';
|
|
||||||
import { siteRoot, gettext, thumbnailSizeForOriginal, thumbnailDefaultSize } from '../../../utils/constants';
|
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
const LIMIT = 1000;
|
const FaceRecognition = () => {
|
||||||
|
const [showPeopleFaces, setShowPeopleFaces] = useState(false);
|
||||||
|
const peopleRef = useRef(null);
|
||||||
|
|
||||||
const FaceRecognition = ({ repoID }) => {
|
const { metadata, store } = useMetadataView();
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [faceOriginData, setFaceOriginData] = useState([]);
|
|
||||||
const [isLoadingMore, setLoadingMore] = useState(false);
|
|
||||||
const [isImagePopupOpen, setIsImagePopupOpen] = useState(false);
|
|
||||||
const [imageIndex, setImageIndex] = useState(-1);
|
|
||||||
const containerRef = useRef(null);
|
|
||||||
const hasMore = useRef(true);
|
|
||||||
|
|
||||||
const faceData = useMemo(() => {
|
const peoples = useMemo(() => {
|
||||||
if (!Array.isArray(faceOriginData) || faceOriginData.length === 0) return [];
|
if (!Array.isArray(metadata.rows) || metadata.rows.length === 0) return [];
|
||||||
const data = faceOriginData.map(dataItem => {
|
return metadata.rows;
|
||||||
const { record_id, link_photos } = dataItem;
|
}, [metadata]);
|
||||||
const linkPhotos = link_photos || [];
|
|
||||||
const name = dataItem.name || gettext('Person Image');
|
|
||||||
return {
|
|
||||||
record_id: record_id,
|
|
||||||
name: name || gettext('Person Image'),
|
|
||||||
photos: linkPhotos.map(photo => {
|
|
||||||
const { path } = photo;
|
|
||||||
return {
|
|
||||||
...photo,
|
|
||||||
name: photo.file_name,
|
|
||||||
url: `${siteRoot}lib/${repoID}/file${path}`,
|
|
||||||
default_url: `${siteRoot}thumbnail/${repoID}/${thumbnailDefaultSize}${path}`,
|
|
||||||
src: `${siteRoot}thumbnail/${repoID}/${thumbnailDefaultSize}${path}`,
|
|
||||||
thumbnail: `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForOriginal}${path}`,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}, [repoID, faceOriginData]);
|
|
||||||
|
|
||||||
const imageItems = useMemo(() => {
|
const onDeletePeoplePhotos = useCallback((peopleId, peoplePhotos) => {
|
||||||
return faceData.map(group => group.photos).flat();
|
store.deletePeoplePhotos(peopleId, peoplePhotos);
|
||||||
}, [faceData]);
|
}, [store]);
|
||||||
|
|
||||||
useEffect(() => {
|
const openPeople = useCallback((people) => {
|
||||||
setLoading(true);
|
peopleRef.current = people;
|
||||||
metadataAPI.getFaceData(repoID, 0, LIMIT).then(res => {
|
setShowPeopleFaces(true);
|
||||||
const faceOriginData = res.data.results || [];
|
|
||||||
if (faceOriginData.length < LIMIT) {
|
|
||||||
hasMore.current = false;
|
|
||||||
}
|
|
||||||
setFaceOriginData(faceOriginData);
|
|
||||||
setLoading(false);
|
|
||||||
}).catch(error => {
|
|
||||||
const errorMsg = Utils.getErrorMsg(error);
|
|
||||||
toaster.danger(errorMsg);
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadMore = useCallback(() => {
|
const closePeople = useCallback(() => {
|
||||||
if (!hasMore.current) return;
|
peopleRef.current = null;
|
||||||
setLoadingMore(true);
|
setShowPeopleFaces(false);
|
||||||
metadataAPI.getFaceData(repoID, faceOriginData.length, LIMIT).then(res => {
|
|
||||||
const newFaceData = res.data.results || [];
|
|
||||||
if (newFaceData.length < LIMIT) {
|
|
||||||
hasMore.current = false;
|
|
||||||
}
|
|
||||||
setFaceOriginData([...faceOriginData, ...newFaceData]);
|
|
||||||
setLoadingMore(false);
|
|
||||||
}).catch(error => {
|
|
||||||
const errorMsg = Utils.getErrorMsg(error);
|
|
||||||
toaster.danger(errorMsg);
|
|
||||||
setLoadingMore(false);
|
|
||||||
});
|
|
||||||
}, [repoID, faceOriginData]);
|
|
||||||
|
|
||||||
const handleScroll = useCallback(() => {
|
|
||||||
if (!containerRef.current) return;
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
|
|
||||||
if (scrollTop + clientHeight >= scrollHeight - 10) {
|
|
||||||
loadMore();
|
|
||||||
}
|
|
||||||
}, [loadMore]);
|
|
||||||
|
|
||||||
const onPhotoClick = useCallback((photo) => {
|
|
||||||
let imageIndex = imageItems.findIndex(item => item.url === photo.url);
|
|
||||||
if (imageIndex < 0) imageIndex = 0;
|
|
||||||
setImageIndex(imageIndex);
|
|
||||||
setIsImagePopupOpen(true);
|
|
||||||
}, [imageItems]);
|
|
||||||
|
|
||||||
const closeImagePopup = useCallback(() => {
|
|
||||||
setIsImagePopupOpen(false);
|
|
||||||
setImageIndex(-1);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const moveToPrevImage = useCallback(() => {
|
const onRename = useCallback((id, newName, oldName) => {
|
||||||
let prevImageIndex = imageIndex - 1;
|
store.renamePeopleName(id, newName, oldName);
|
||||||
if (prevImageIndex < 0) prevImageIndex = imageItems.length - 1;
|
}, [store]);
|
||||||
setImageIndex(prevImageIndex);
|
|
||||||
}, [imageIndex, imageItems]);
|
|
||||||
|
|
||||||
const moveToNextImage = useCallback(() => {
|
|
||||||
let nextImageIndex = imageIndex + 1;
|
|
||||||
if (nextImageIndex > imageItems.length - 1) nextImageIndex = 0;
|
|
||||||
setImageIndex(nextImageIndex);
|
|
||||||
}, [imageIndex, imageItems]);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (<CenteredLoading />);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="sf-metadata-container">
|
||||||
<div className="sf-metadata-wrapper">
|
{showPeopleFaces ? (
|
||||||
<div className="sf-metadata-main">
|
<PeoplePhotos people={peopleRef.current} onClose={closePeople} onDeletePeoplePhotos={onDeletePeoplePhotos} />
|
||||||
<div className="sf-metadata-container">
|
) : (
|
||||||
<div className="sf-metadata-face-recognition" ref={containerRef} onScroll={handleScroll}>
|
<Peoples peoples={peoples} onRename={onRename} onOpenPeople={openPeople} />
|
||||||
{faceData.length > 0 && faceData.map((face) => {
|
|
||||||
return (<FaceGroup key={face.record_id} group={face} repoID={repoID} onPhotoClick={onPhotoClick} />);
|
|
||||||
})}
|
|
||||||
{isLoadingMore && (
|
|
||||||
<div className="sf-metadata-face-recognition-loading-more">
|
|
||||||
<CenteredLoading />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{isImagePopupOpen && (
|
|
||||||
<ModalPortal>
|
|
||||||
<ImageDialog
|
|
||||||
imageItems={imageItems}
|
|
||||||
imageIndex={imageIndex}
|
|
||||||
closeImagePopup={closeImagePopup}
|
|
||||||
moveToPrevImage={moveToPrevImage}
|
|
||||||
moveToNextImage={moveToNextImage}
|
|
||||||
/>
|
|
||||||
</ModalPortal>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
FaceRecognition.propTypes = {
|
|
||||||
repoID: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FaceRecognition;
|
export default FaceRecognition;
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
/* loading more */
|
||||||
|
.sf-metadata-face-recognition-container .sf-metadata-face-recognition-loading-more {
|
||||||
|
height: 30px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-peoples-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: fit-content !important;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
103
frontend/src/metadata/views/face-recognition/peoples/index.js
Normal file
103
frontend/src/metadata/views/face-recognition/peoples/index.js
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { CenteredLoading } from '@seafile/sf-metadata-ui-component';
|
||||||
|
import EmptyTip from '../../../../components/empty-tip';
|
||||||
|
import { gettext } from '../../../../utils/constants';
|
||||||
|
import { useMetadataView } from '../../../hooks/metadata-view';
|
||||||
|
import { PER_LOAD_NUMBER } from '../../../constants';
|
||||||
|
import toaster from '../../../../components/toast';
|
||||||
|
import { Utils } from '../../../../utils/utils';
|
||||||
|
import People from './people';
|
||||||
|
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const Peoples = ({ peoples, onOpenPeople, onRename }) => {
|
||||||
|
const [isLoadingMore, setLoadingMore] = useState(false);
|
||||||
|
const [haveFreezed, setHaveFreezed] = useState(false);
|
||||||
|
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
|
||||||
|
const { metadata, store, closeDirentDetail } = useMetadataView();
|
||||||
|
|
||||||
|
const loadMore = useCallback(async () => {
|
||||||
|
if (isLoadingMore) return;
|
||||||
|
if (!metadata.hasMore) return;
|
||||||
|
setLoadingMore(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await store.loadMore(PER_LOAD_NUMBER);
|
||||||
|
setLoadingMore(false);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errorMsg);
|
||||||
|
setLoadingMore(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [isLoadingMore, metadata, store]);
|
||||||
|
|
||||||
|
const handleScroll = useCallback(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
|
||||||
|
if (scrollTop + clientHeight >= scrollHeight - 10) {
|
||||||
|
loadMore();
|
||||||
|
}
|
||||||
|
window.sfMetadataContext.localStorage.setItem('scroll_top', scrollTop);
|
||||||
|
}, [loadMore]);
|
||||||
|
|
||||||
|
const onFreezed = useCallback(() => {
|
||||||
|
setHaveFreezed(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onUnFreezed = useCallback(() => {
|
||||||
|
setHaveFreezed(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const _localStorage = window.sfMetadataContext.localStorage;
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
const scrollTop = _localStorage.getItem('scroll_top') || 0;
|
||||||
|
if (scrollTop) {
|
||||||
|
containerRef.current.scrollTop = Number(scrollTop);
|
||||||
|
}
|
||||||
|
return () => {};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
closeDirentDetail();
|
||||||
|
return () => {};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!Array.isArray(peoples) || peoples.length === 0) return (<EmptyTip text={gettext('No faces')} />);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sf-metadata-face-recognition-container sf-metadata-peoples-container" ref={containerRef} onScroll={handleScroll}>
|
||||||
|
{peoples.length > 0 && peoples.map((people) => {
|
||||||
|
return (
|
||||||
|
<People
|
||||||
|
key={people._id}
|
||||||
|
haveFreezed={haveFreezed}
|
||||||
|
people={people}
|
||||||
|
onOpenPeople={onOpenPeople}
|
||||||
|
onRename={onRename}
|
||||||
|
onFreezed={onFreezed}
|
||||||
|
onUnFreezed={onUnFreezed}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{isLoadingMore && (
|
||||||
|
<div className="sf-metadata-face-recognition-loading-more">
|
||||||
|
<CenteredLoading />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Peoples.propTypes = {
|
||||||
|
peoples: PropTypes.array,
|
||||||
|
onOpenPeople: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Peoples;
|
@ -0,0 +1,75 @@
|
|||||||
|
.sf-metadata-peoples-container .sf-metadata-people-info {
|
||||||
|
height: 56px;
|
||||||
|
width: 48%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-peoples-container .sf-metadata-people-info:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-peoples-container .sf-metadata-people-info:not(.readonly) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-peoples-container .sf-metadata-people-info .sf-metadata-people-info-img {
|
||||||
|
height: 36px;
|
||||||
|
width: 36px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-peoples-container .sf-metadata-people-info .sf-metadata-people-info-name-count {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-peoples-container .sf-metadata-people-info .sf-metadata-people-info-name {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-peoples-container .sf-metadata-people-info .sf-metadata-people-info-name-display {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-peoples-container .sf-metadata-people-info .sf-metadata-people-info-renaming {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 2px 3px;
|
||||||
|
width: 20rem;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 22px;
|
||||||
|
line-height: 16px;
|
||||||
|
border-radius: 2px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
vertical-align: middle;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-peoples-container .sf-metadata-people-info .sf-metadata-people-info-renaming:focus {
|
||||||
|
border-color: #1991eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-peoples-container .sf-metadata-people-info .sf-metadata-people-info-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-peoples-container .sf-metadata-people-info .sf-metadata-people-info-op {
|
||||||
|
width: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-peoples-container .sf-metadata-people-info-op .sf-dropdown-toggle {
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
@ -0,0 +1,124 @@
|
|||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Input } from 'reactstrap';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import isHotkey from 'is-hotkey';
|
||||||
|
import { gettext, siteRoot, thumbnailDefaultSize } from '../../../../../utils/constants';
|
||||||
|
import OpMenu from './op-menu';
|
||||||
|
import { isEnter } from '../../../../utils/hotkey';
|
||||||
|
import { Utils } from '../../../../../utils/utils';
|
||||||
|
import { getFileNameFromRecord, getParentDirFromRecord } from '../../../../utils/cell';
|
||||||
|
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const People = ({ haveFreezed, people, onOpenPeople, onRename, onFreezed, onUnFreezed }) => {
|
||||||
|
|
||||||
|
const similarPhotoURL = useMemo(() => {
|
||||||
|
const similarPhoto = people._similar_photo;
|
||||||
|
if (!similarPhoto) return '';
|
||||||
|
const repoID = window.sfMetadataContext.getSetting('repoID');
|
||||||
|
const fileName = getFileNameFromRecord(similarPhoto);
|
||||||
|
const parentDir = getParentDirFromRecord(similarPhoto);
|
||||||
|
const path = Utils.encodePath(Utils.joinPath(parentDir, fileName));
|
||||||
|
return `${siteRoot}thumbnail/${repoID}/${thumbnailDefaultSize}${path}`;
|
||||||
|
}, [people._similar_photo]);
|
||||||
|
|
||||||
|
const photosCount = useMemo(() => {
|
||||||
|
return Array.isArray(people._photo_links) ? people._photo_links.length : 0;
|
||||||
|
}, [people._photo_links]);
|
||||||
|
|
||||||
|
const [name, setName] = useState(people._name || gettext('Person image'));
|
||||||
|
const [renaming, setRenaming] = useState(false);
|
||||||
|
const [active, setActive] = useState(false);
|
||||||
|
const readonly = !window.sfMetadataContext.canModify();
|
||||||
|
|
||||||
|
const onMouseEnter = useCallback(() => {
|
||||||
|
if (haveFreezed) return;
|
||||||
|
setActive(true);
|
||||||
|
}, [haveFreezed]);
|
||||||
|
|
||||||
|
const onMouseLeave = useCallback(() => {
|
||||||
|
if (haveFreezed) return;
|
||||||
|
setActive(false);
|
||||||
|
}, [haveFreezed]);
|
||||||
|
|
||||||
|
const setRenamingState = useCallback(() => {
|
||||||
|
onFreezed();
|
||||||
|
setRenaming(true);
|
||||||
|
}, [onFreezed]);
|
||||||
|
|
||||||
|
const onBlur = useCallback(() => {
|
||||||
|
if (!name) return;
|
||||||
|
setRenaming(false);
|
||||||
|
if (name !== (people._name || gettext('Person image'))) {
|
||||||
|
onUnFreezed();
|
||||||
|
onRename(people._id, name, people._name);
|
||||||
|
}
|
||||||
|
}, [people, name, onRename, onUnFreezed]);
|
||||||
|
|
||||||
|
const onChange = useCallback((event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
if (value === name) return;
|
||||||
|
setName(value);
|
||||||
|
}, [name]);
|
||||||
|
|
||||||
|
const onKeyDown = useCallback((event) => {
|
||||||
|
if (isEnter(event)) {
|
||||||
|
onBlur();
|
||||||
|
return;
|
||||||
|
} else if (isHotkey('esc', event)) {
|
||||||
|
setName(people.name);
|
||||||
|
setRenaming(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [people, onBlur]);
|
||||||
|
|
||||||
|
const _onUnFreezed = useCallback(() => {
|
||||||
|
onUnFreezed();
|
||||||
|
setActive(false);
|
||||||
|
}, [onUnFreezed]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames('sf-metadata-people-info px-3 d-flex justify-content-between align-items-center', {
|
||||||
|
'readonly': readonly,
|
||||||
|
})}
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
|
onDoubleClick={haveFreezed ? () => {} : () => onOpenPeople(people)}
|
||||||
|
>
|
||||||
|
<div className="sf-metadata-people-info-img mr-2">
|
||||||
|
<img src={similarPhotoURL} alt={name} height={36} width={36} />
|
||||||
|
</div>
|
||||||
|
<div className="sf-metadata-people-info-name-count">
|
||||||
|
<div className="sf-metadata-people-info-name">
|
||||||
|
{renaming ? (
|
||||||
|
<Input className="sf-metadata-people-info-renaming" autoFocus value={name} onChange={onChange} onBlur={onBlur} onKeyDown={onKeyDown} />
|
||||||
|
) : (
|
||||||
|
<div className="sf-metadata-people-info-name-display">{name}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="sf-metadata-people-info-count">
|
||||||
|
{photosCount + ' ' + gettext('items')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!readonly && people._is_someone && (
|
||||||
|
<div className="sf-metadata-people-info-op">
|
||||||
|
{active && (
|
||||||
|
<OpMenu onRename={setRenamingState} onFreezed={onFreezed} onUnFreezed={_onUnFreezed} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
People.propTypes = {
|
||||||
|
haveFreezed: PropTypes.bool,
|
||||||
|
people: PropTypes.object.isRequired,
|
||||||
|
onOpenPeople: PropTypes.func,
|
||||||
|
onFreezed: PropTypes.func,
|
||||||
|
onUnFreezed: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default People;
|
@ -0,0 +1,65 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Dropdown, DropdownMenu, DropdownToggle, DropdownItem } from 'reactstrap';
|
||||||
|
import { gettext } from '../../../../../../utils/constants';
|
||||||
|
|
||||||
|
const OpMenu = ({ onRename, onFreezed, onUnFreezed }) => {
|
||||||
|
let [isShow, setShow] = useState(false);
|
||||||
|
|
||||||
|
const toggle = useCallback((event) => {
|
||||||
|
const dataset = event.target ? event.target.dataset : null;
|
||||||
|
if (dataset && dataset.toggle && dataset.toggle === 'rename') {
|
||||||
|
onRename();
|
||||||
|
setShow(!isShow);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isShow) {
|
||||||
|
onUnFreezed();
|
||||||
|
} else {
|
||||||
|
onFreezed();
|
||||||
|
}
|
||||||
|
setShow(!isShow);
|
||||||
|
}, [isShow, onRename, onFreezed, onUnFreezed, setShow]);
|
||||||
|
|
||||||
|
const onClick = useCallback((event) => {
|
||||||
|
toggle(event);
|
||||||
|
}, [toggle]);
|
||||||
|
|
||||||
|
const onItemClick = useCallback((event) => {
|
||||||
|
toggle(event);
|
||||||
|
}, [toggle]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
setShow = () => {};
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown isOpen={isShow} toggle={toggle}>
|
||||||
|
<DropdownToggle
|
||||||
|
tag="i"
|
||||||
|
role="button"
|
||||||
|
tabIndex="0"
|
||||||
|
className="sf-dropdown-toggle sf3-font-more sf3-font"
|
||||||
|
title={gettext('More operations')}
|
||||||
|
aria-label={gettext('More operations')}
|
||||||
|
onClick={onClick}
|
||||||
|
data-toggle="dropdown"
|
||||||
|
/>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownItem data-toggle="rename" onClick={onItemClick}>{gettext('Rename')}</DropdownItem>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
OpMenu.propTypes = {
|
||||||
|
onRename: PropTypes.func,
|
||||||
|
onFreezed: PropTypes.func,
|
||||||
|
onUnFreezed: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OpMenu;
|
||||||
|
|
@ -0,0 +1,56 @@
|
|||||||
|
.sf-metadata-people-photos-container {
|
||||||
|
padding: 0 !important;
|
||||||
|
overflow-y: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-people-photos-container .sf-metadata-people-photos-header {
|
||||||
|
height: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-people-photos-container .sf-metadata-icon-btn {
|
||||||
|
margin-left: -4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-people-photos-container .sf-metadata-icon-btn:hover {
|
||||||
|
background-color: #EFEFEF;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-people-photos-container .sf-metadata-people-photos-header .sf-metadata-people-photos-header-back {
|
||||||
|
font-size: 14px;
|
||||||
|
height: 24px;
|
||||||
|
min-width: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: -5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-people-photos-container .sf-metadata-people-photos-header .sf-metadata-people-photos-header-back:hover {
|
||||||
|
background-color: #DBDBDB;
|
||||||
|
background-color: #EFEFEF;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-people-photos-container .sf-metadata-people-photos-header-back .sf3-font-arrow {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-people-photos-container .sf-metadata-people-photos-header .sf-metadata-people-name {
|
||||||
|
margin-left: 4px;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-people-photos-container .sf-metadata-gallery-container {
|
||||||
|
height: calc(100% - 48px);
|
||||||
|
}
|
@ -0,0 +1,153 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import deepCopy from 'deep-copy';
|
||||||
|
import { CenteredLoading } from '@seafile/sf-metadata-ui-component';
|
||||||
|
import metadataAPI from '../../../api';
|
||||||
|
import Metadata from '../../../model/metadata';
|
||||||
|
import { normalizeColumns } from '../../../utils/column';
|
||||||
|
import { gettext } from '../../../../utils/constants';
|
||||||
|
import { Utils } from '../../../../utils/utils';
|
||||||
|
import toaster from '../../../../components/toast';
|
||||||
|
import Gallery from '../../gallery/main';
|
||||||
|
import { useMetadataView } from '../../../hooks/metadata-view';
|
||||||
|
import { PER_LOAD_NUMBER, VIEW_TYPE, VIEW_TYPE_DEFAULT_SORTS, EVENT_BUS_TYPE } from '../../../constants';
|
||||||
|
|
||||||
|
import './index.css';
|
||||||
|
import '../../gallery/index.css';
|
||||||
|
|
||||||
|
const PeoplePhotos = ({ people, onClose, onDeletePeoplePhotos }) => {
|
||||||
|
const [isLoading, setLoading] = useState(true);
|
||||||
|
const [isLoadingMore, setLoadingMore] = useState(false);
|
||||||
|
const [metadata, setMetadata] = useState({ rows: [] });
|
||||||
|
const repoID = window.sfMetadataContext.getSetting('repoID');
|
||||||
|
|
||||||
|
const { deleteFilesCallback } = useMetadataView();
|
||||||
|
|
||||||
|
const onLoadMore = useCallback(async () => {
|
||||||
|
if (isLoadingMore) return;
|
||||||
|
if (!metadata.hasMore) return;
|
||||||
|
setLoadingMore(true);
|
||||||
|
|
||||||
|
metadataAPI.getPeoplePhotos(repoID, people._id, metadata.recordsCount, PER_LOAD_NUMBER).then(res => {
|
||||||
|
const rows = res?.data?.results || [];
|
||||||
|
let newMetadata = deepCopy(metadata);
|
||||||
|
if (Array.isArray(rows) && rows.length > 0) {
|
||||||
|
newMetadata.rows.push(...rows);
|
||||||
|
rows.forEach(record => {
|
||||||
|
newMetadata.row_ids.push(record._id);
|
||||||
|
newMetadata.id_row_map[record._id] = record;
|
||||||
|
});
|
||||||
|
const loadedCount = rows.length;
|
||||||
|
newMetadata.hasMore = loadedCount === PER_LOAD_NUMBER;
|
||||||
|
newMetadata.recordsCount = newMetadata.row_ids.length;
|
||||||
|
} else {
|
||||||
|
newMetadata.hasMore = false;
|
||||||
|
}
|
||||||
|
setMetadata(newMetadata);
|
||||||
|
setLoadingMore(false);
|
||||||
|
}).catch(error => {
|
||||||
|
const errorMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errorMessage);
|
||||||
|
setLoadingMore(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
}, [isLoadingMore, metadata, people, repoID]);
|
||||||
|
|
||||||
|
const deletedByIds = useCallback((ids) => {
|
||||||
|
if (!Array.isArray(ids) || ids.length === 0) return;
|
||||||
|
const newMetadata = deepCopy(metadata);
|
||||||
|
const idNeedDeletedMap = ids.reduce((currIdNeedDeletedMap, rowId) => ({ ...currIdNeedDeletedMap, [rowId]: true }), {});
|
||||||
|
newMetadata.rows = newMetadata.rows.filter((row) => !idNeedDeletedMap[row._id]);
|
||||||
|
newMetadata.row_ids = newMetadata.row_ids.filter((id) => !idNeedDeletedMap[id]);
|
||||||
|
|
||||||
|
// delete rows in id_row_map
|
||||||
|
ids.forEach(rowId => {
|
||||||
|
delete newMetadata.id_row_map[rowId];
|
||||||
|
});
|
||||||
|
newMetadata.recordsCount = newMetadata.row_ids.length;
|
||||||
|
setMetadata(newMetadata);
|
||||||
|
|
||||||
|
if (newMetadata.rows.length === 0) {
|
||||||
|
onClose && onClose();
|
||||||
|
}
|
||||||
|
onDeletePeoplePhotos && onDeletePeoplePhotos(people._id, ids);
|
||||||
|
}, [metadata, onClose, people, onDeletePeoplePhotos]);
|
||||||
|
|
||||||
|
const handelDelete = useCallback((deletedImages, callback) => {
|
||||||
|
if (!deletedImages.length) return;
|
||||||
|
let recordIds = [];
|
||||||
|
let paths = [];
|
||||||
|
let fileNames = [];
|
||||||
|
deletedImages.forEach((record) => {
|
||||||
|
const { id, path: parentDir, name } = record || {};
|
||||||
|
if (parentDir && name) {
|
||||||
|
const path = Utils.joinPath(parentDir, name);
|
||||||
|
paths.push(path);
|
||||||
|
fileNames.push(name);
|
||||||
|
recordIds.push(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.sfMetadataContext.batchDeleteFiles(repoID, paths).then(res => {
|
||||||
|
callback && callback();
|
||||||
|
deletedByIds(recordIds);
|
||||||
|
deleteFilesCallback(paths, fileNames);
|
||||||
|
let msg = fileNames.length > 1
|
||||||
|
? gettext('Successfully deleted {name} and {n} other items')
|
||||||
|
: gettext('Successfully deleted {name}');
|
||||||
|
msg = msg.replace('{name}', fileNames[0])
|
||||||
|
.replace('{n}', fileNames.length - 1);
|
||||||
|
toaster.success(msg);
|
||||||
|
}).catch(error => {
|
||||||
|
toaster.danger(gettext('Failed to delete records'));
|
||||||
|
});
|
||||||
|
}, [deleteFilesCallback, repoID, deletedByIds]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const repoID = window.sfMetadataContext.getSetting('repoID');
|
||||||
|
metadataAPI.getPeoplePhotos(repoID, people._id, 0, PER_LOAD_NUMBER).then(res => {
|
||||||
|
const rows = res?.data?.results || [];
|
||||||
|
const columns = normalizeColumns(res?.data?.metadata);
|
||||||
|
let metadata = new Metadata({ rows, columns, view: { sorts: VIEW_TYPE_DEFAULT_SORTS[VIEW_TYPE.GALLERY] } });
|
||||||
|
if (rows.length < PER_LOAD_NUMBER) {
|
||||||
|
metadata.hasMore = false;
|
||||||
|
}
|
||||||
|
setMetadata(metadata);
|
||||||
|
setLoading(false);
|
||||||
|
}).catch(error => {
|
||||||
|
const errorMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errorMessage);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_VIEW_TOOLBAR, true);
|
||||||
|
return () => {
|
||||||
|
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_VIEW_TOOLBAR, false);
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (isLoading) return (<CenteredLoading />);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sf-metadata-face-recognition-container sf-metadata-people-photos-container">
|
||||||
|
<div className="sf-metadata-people-photos-header">
|
||||||
|
<div className="sf-metadata-people-photos-header-back" onClick={onClose}>
|
||||||
|
<i className="sf3-font sf3-font-arrow rotate-180"></i>
|
||||||
|
</div>
|
||||||
|
<div className="sf-metadata-people-name">{people._name || gettext('Person image')}</div>
|
||||||
|
</div>
|
||||||
|
<Gallery metadata={metadata} isLoadingMore={isLoadingMore} onLoadMore={onLoadMore} onDelete={handelDelete} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
PeoplePhotos.propTypes = {
|
||||||
|
people: PropTypes.object,
|
||||||
|
onClose: PropTypes.func,
|
||||||
|
onDelete: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PeoplePhotos;
|
197
frontend/src/metadata/views/gallery/content.js
Normal file
197
frontend/src/metadata/views/gallery/content.js
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
import React, { useState, useCallback, useMemo, useRef } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import EmptyTip from '../../../components/empty-tip';
|
||||||
|
import { gettext } from '../../../utils/constants';
|
||||||
|
import { GALLERY_DATE_MODE } from '../../constants';
|
||||||
|
import Image from './image';
|
||||||
|
|
||||||
|
const Content = ({
|
||||||
|
groups,
|
||||||
|
overScan,
|
||||||
|
columns,
|
||||||
|
size,
|
||||||
|
gap,
|
||||||
|
mode,
|
||||||
|
selectedImages,
|
||||||
|
onImageSelect,
|
||||||
|
onImageClick,
|
||||||
|
onImageDoubleClick,
|
||||||
|
onImageRightClick
|
||||||
|
}) => {
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const imageRef = useRef(null);
|
||||||
|
const animationFrameRef = useRef(null);
|
||||||
|
|
||||||
|
const [isSelecting, setIsSelecting] = useState(false);
|
||||||
|
const [selectionStart, setSelectionStart] = useState(null);
|
||||||
|
|
||||||
|
const imageHeight = useMemo(() => size + gap, [size, gap]);
|
||||||
|
const selectedImageIds = useMemo(() => selectedImages.map(img => img.id), [selectedImages]);
|
||||||
|
|
||||||
|
const handleMouseDown = useCallback((e) => {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
if (e.ctrlKey || e.metaKey || e.shiftKey) return;
|
||||||
|
|
||||||
|
setIsSelecting(true);
|
||||||
|
setSelectionStart({ x: e.clientX, y: e.clientY });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleMouseMove = useCallback((e) => {
|
||||||
|
if (!isSelecting) return;
|
||||||
|
|
||||||
|
if (animationFrameRef.current) {
|
||||||
|
cancelAnimationFrame(animationFrameRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
animationFrameRef.current = requestAnimationFrame(() => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const selectionEnd = { x: e.clientX, y: e.clientY };
|
||||||
|
const selected = [];
|
||||||
|
|
||||||
|
groups.forEach(group => {
|
||||||
|
group.children.forEach((row) => {
|
||||||
|
row.children.forEach((img) => {
|
||||||
|
const imgElement = document.getElementById(img.id);
|
||||||
|
if (imgElement) {
|
||||||
|
const rect = imgElement.getBoundingClientRect();
|
||||||
|
if (
|
||||||
|
rect.left < Math.max(selectionStart.x, selectionEnd.x) &&
|
||||||
|
rect.right > Math.min(selectionStart.x, selectionEnd.x) &&
|
||||||
|
rect.top < Math.max(selectionStart.y, selectionEnd.y) &&
|
||||||
|
rect.bottom > Math.min(selectionStart.y, selectionEnd.y)
|
||||||
|
) {
|
||||||
|
selected.push(img);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onImageSelect(selected);
|
||||||
|
});
|
||||||
|
}, [groups, isSelecting, selectionStart, onImageSelect]);
|
||||||
|
|
||||||
|
const handleMouseUp = useCallback((e) => {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsSelecting(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderDisplayGroup = useCallback((group) => {
|
||||||
|
const { top: overScanTop, bottom: overScanBottom } = overScan;
|
||||||
|
const { name, children, height, top, paddingTop } = group;
|
||||||
|
|
||||||
|
// group not in rendering area, return empty div
|
||||||
|
if (top >= overScanBottom || top + height <= overScanTop) {
|
||||||
|
return (<div key={name} className="w-100" style={{ height, flexShrink: 0 }}></div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
let childrenStartIndex = children.findIndex(r => r.top >= overScanTop);
|
||||||
|
let childrenEndIndex = children.findIndex(r => r.top >= overScanBottom);
|
||||||
|
|
||||||
|
// group in rendering area, but the image not need to render. eg: overScan: { top: 488, bottom: 1100 }, group: { top: 0, height: 521 },
|
||||||
|
// in this time, part of an image is in the rendering area, don't render image
|
||||||
|
if (childrenStartIndex === -1 && childrenEndIndex === -1) {
|
||||||
|
return (<div key={name} className="w-100" style={{ height, flexShrink: 0 }}></div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
childrenStartIndex = Math.max(childrenStartIndex, 0);
|
||||||
|
if (childrenEndIndex === -1) {
|
||||||
|
childrenEndIndex = children.length;
|
||||||
|
}
|
||||||
|
if (childrenEndIndex > 0) {
|
||||||
|
childrenEndIndex = childrenEndIndex - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={name}
|
||||||
|
className="metadata-gallery-date-group"
|
||||||
|
style={{ height, paddingTop }}
|
||||||
|
>
|
||||||
|
{mode !== GALLERY_DATE_MODE.ALL && childrenStartIndex === 0 && (
|
||||||
|
<div className="metadata-gallery-date-tag">{name || gettext('Empty')}</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
ref={imageRef}
|
||||||
|
className="metadata-gallery-image-list"
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: `repeat(${columns}, 1fr)`,
|
||||||
|
paddingTop: childrenStartIndex * imageHeight,
|
||||||
|
paddingBottom: (children.length - 1 - childrenEndIndex) * imageHeight,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children.slice(childrenStartIndex, childrenEndIndex + 1).map((row) => {
|
||||||
|
return row.children.map((img) => {
|
||||||
|
const isSelected = selectedImageIds.includes(img.id);
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
key={img.src}
|
||||||
|
isSelected={isSelected}
|
||||||
|
img={img}
|
||||||
|
size={size}
|
||||||
|
onClick={(e) => onImageClick(e, img)}
|
||||||
|
onDoubleClick={(e) => onImageDoubleClick(e, img)}
|
||||||
|
onContextMenu={(e) => onImageRightClick(e, img)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, [overScan, columns, size, imageHeight, mode, selectedImageIds, onImageClick, onImageDoubleClick, onImageRightClick]);
|
||||||
|
|
||||||
|
if (!Array.isArray(groups) || groups.length === 0) {
|
||||||
|
return <EmptyTip text={gettext('No record')}/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className='metadata-gallery-main'
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
|
>
|
||||||
|
{groups.map((group) => {
|
||||||
|
return renderDisplayGroup(group);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Content.propTypes = {
|
||||||
|
groups: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
children: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
top: PropTypes.number.isRequired,
|
||||||
|
children: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
src: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
})).isRequired,
|
||||||
|
})).isRequired,
|
||||||
|
height: PropTypes.number.isRequired,
|
||||||
|
top: PropTypes.number.isRequired,
|
||||||
|
paddingTop: PropTypes.number.isRequired,
|
||||||
|
})),
|
||||||
|
overScan: PropTypes.shape({
|
||||||
|
top: PropTypes.number.isRequired,
|
||||||
|
bottom: PropTypes.number.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
columns: PropTypes.number.isRequired,
|
||||||
|
size: PropTypes.number.isRequired,
|
||||||
|
gap: PropTypes.number.isRequired,
|
||||||
|
mode: PropTypes.string,
|
||||||
|
selectedImages: PropTypes.array.isRequired,
|
||||||
|
onImageSelect: PropTypes.func.isRequired,
|
||||||
|
onImageClick: PropTypes.func.isRequired,
|
||||||
|
onImageDoubleClick: PropTypes.func.isRequired,
|
||||||
|
onImageRightClick: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Content;
|
@ -1,135 +1,19 @@
|
|||||||
import React, { useMemo, useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { CenteredLoading } from '@seafile/sf-metadata-ui-component';
|
|
||||||
import metadataAPI from '../../api';
|
|
||||||
import URLDecorator from '../../../utils/url-decorator';
|
|
||||||
import toaster from '../../../components/toast';
|
import toaster from '../../../components/toast';
|
||||||
import Main from './main';
|
import Main from './main';
|
||||||
import ContextMenu from './context-menu';
|
|
||||||
import ImageDialog from '../../../components/dialog/image-dialog';
|
|
||||||
import ZipDownloadDialog from '../../../components/dialog/zip-download-dialog';
|
|
||||||
import ModalPortal from '../../../components/modal-portal';
|
|
||||||
import { useMetadataView } from '../../hooks/metadata-view';
|
import { useMetadataView } from '../../hooks/metadata-view';
|
||||||
import { Utils } from '../../../utils/utils';
|
import { Utils } from '../../../utils/utils';
|
||||||
import { getDateDisplayString, getFileNameFromRecord, getParentDirFromRecord } from '../../utils/cell';
|
import { gettext } from '../../../utils/constants';
|
||||||
import { siteRoot, fileServerRoot, useGoFileserver, gettext, thumbnailSizeForGrid, thumbnailSizeForOriginal } from '../../../utils/constants';
|
import { PER_LOAD_NUMBER } from '../../constants';
|
||||||
import { EVENT_BUS_TYPE, PER_LOAD_NUMBER, PRIVATE_COLUMN_KEY, GALLERY_DATE_MODE, DATE_TAG_HEIGHT, GALLERY_IMAGE_GAP } from '../../constants';
|
|
||||||
import { getRowById } from '../../utils/table';
|
|
||||||
import { getEventClassName } from '../../utils/common';
|
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
const Gallery = () => {
|
const Gallery = () => {
|
||||||
const [isFirstLoading, setFirstLoading] = useState(true);
|
|
||||||
const [isLoadingMore, setLoadingMore] = useState(false);
|
const [isLoadingMore, setLoadingMore] = useState(false);
|
||||||
const [zoomGear, setZoomGear] = useState(0);
|
|
||||||
const [containerWidth, setContainerWidth] = useState(0);
|
|
||||||
const [overScan, setOverScan] = useState({ top: 0, bottom: 0 });
|
|
||||||
const [mode, setMode] = useState(GALLERY_DATE_MODE.DAY);
|
|
||||||
const [isImagePopupOpen, setIsImagePopupOpen] = useState(false);
|
|
||||||
const [isZipDialogOpen, setIsZipDialogOpen] = useState(false);
|
|
||||||
const [imageIndex, setImageIndex] = useState(0);
|
|
||||||
const [selectedImages, setSelectedImages] = useState([]);
|
|
||||||
|
|
||||||
const containerRef = useRef(null);
|
const { metadata, store, deleteFilesCallback } = useMetadataView();
|
||||||
const renderMoreTimer = useRef(null);
|
|
||||||
const lastState = useRef({ visibleAreaFirstImage: { groupIndex: 0, rowIndex: 0 } });
|
|
||||||
|
|
||||||
const { metadata, store, updateCurrentDirent, deleteFilesCallback } = useMetadataView();
|
const onLoadMore = useCallback(async () => {
|
||||||
const repoID = window.sfMetadataContext.getSetting('repoID');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateCurrentDirent();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Number of images per row
|
|
||||||
const columns = useMemo(() => {
|
|
||||||
return 8 - zoomGear;
|
|
||||||
}, [zoomGear]);
|
|
||||||
|
|
||||||
const imageSize = useMemo(() => {
|
|
||||||
return (containerWidth - (columns - 1) * 2 - 32) / columns;
|
|
||||||
}, [containerWidth, columns]);
|
|
||||||
|
|
||||||
const dateMode = useMemo(() => {
|
|
||||||
switch (mode) {
|
|
||||||
case GALLERY_DATE_MODE.YEAR:
|
|
||||||
return 'YYYY';
|
|
||||||
case GALLERY_DATE_MODE.MONTH:
|
|
||||||
return 'YYYY-MM';
|
|
||||||
case GALLERY_DATE_MODE.DAY:
|
|
||||||
return 'YYYY-MM-DD';
|
|
||||||
default:
|
|
||||||
return 'YYYY-MM-DD';
|
|
||||||
}
|
|
||||||
}, [mode]);
|
|
||||||
|
|
||||||
const groups = useMemo(() => {
|
|
||||||
if (isFirstLoading) return [];
|
|
||||||
const firstSort = metadata.view.sorts[0];
|
|
||||||
let init = metadata.rows.filter(row => Utils.imageCheck(getFileNameFromRecord(row)))
|
|
||||||
.reduce((_init, record) => {
|
|
||||||
const id = record[PRIVATE_COLUMN_KEY.ID];
|
|
||||||
const fileName = getFileNameFromRecord(record);
|
|
||||||
const parentDir = getParentDirFromRecord(record);
|
|
||||||
const path = Utils.encodePath(Utils.joinPath(parentDir, fileName));
|
|
||||||
const date = mode !== GALLERY_DATE_MODE.ALL ? getDateDisplayString(record[firstSort.column_key], dateMode) : '';
|
|
||||||
const img = {
|
|
||||||
id,
|
|
||||||
name: fileName,
|
|
||||||
path: parentDir,
|
|
||||||
url: `${siteRoot}lib/${repoID}/file${path}`,
|
|
||||||
src: `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForGrid}${path}`,
|
|
||||||
thumbnail: `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForOriginal}${path}`,
|
|
||||||
downloadURL: `${fileServerRoot}repos/${repoID}/files${path}?op=download`,
|
|
||||||
date: date,
|
|
||||||
};
|
|
||||||
let _group = _init.find(g => g.name === date);
|
|
||||||
if (_group) {
|
|
||||||
_group.children.push(img);
|
|
||||||
} else {
|
|
||||||
_init.push({
|
|
||||||
name: date,
|
|
||||||
children: [img],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return _init;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
let _groups = [];
|
|
||||||
const imageHeight = imageSize + GALLERY_IMAGE_GAP;
|
|
||||||
const paddingTop = mode === GALLERY_DATE_MODE.ALL ? 0 : DATE_TAG_HEIGHT;
|
|
||||||
init.forEach((_init, index) => {
|
|
||||||
const { children, ...__init } = _init;
|
|
||||||
let top = 0;
|
|
||||||
let rows = [];
|
|
||||||
if (index > 0) {
|
|
||||||
const lastGroup = _groups[index - 1];
|
|
||||||
const { top: lastGroupTop, height: lastGroupHeight } = lastGroup;
|
|
||||||
top = lastGroupTop + lastGroupHeight;
|
|
||||||
}
|
|
||||||
children.forEach((child, childIndex) => {
|
|
||||||
const rowIndex = ~~(childIndex / columns);
|
|
||||||
if (!rows[rowIndex]) rows[rowIndex] = { top: paddingTop + top + rowIndex * imageHeight, children: [] };
|
|
||||||
child.groupIndex = index;
|
|
||||||
child.rowIndex = rowIndex;
|
|
||||||
rows[rowIndex].children.push(child);
|
|
||||||
});
|
|
||||||
|
|
||||||
const height = rows.length * imageHeight + paddingTop;
|
|
||||||
_groups.push({
|
|
||||||
...__init,
|
|
||||||
top,
|
|
||||||
height,
|
|
||||||
paddingTop,
|
|
||||||
children: rows
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return _groups;
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [isFirstLoading, metadata, metadata.recordsCount, repoID, columns, imageSize, mode]);
|
|
||||||
|
|
||||||
const loadMore = useCallback(async () => {
|
|
||||||
if (isLoadingMore) return;
|
if (isLoadingMore) return;
|
||||||
if (!metadata.hasMore) return;
|
if (!metadata.hasMore) return;
|
||||||
setLoadingMore(true);
|
setLoadingMore(true);
|
||||||
@ -146,207 +30,12 @@ const Gallery = () => {
|
|||||||
|
|
||||||
}, [isLoadingMore, metadata, store]);
|
}, [isLoadingMore, metadata, store]);
|
||||||
|
|
||||||
useEffect(() => {
|
const handleDelete = useCallback((deletedImages, callback) => {
|
||||||
const gear = window.sfMetadataContext.localStorage.getItem('zoom-gear', 0) || 0;
|
if (!deletedImages.length) return;
|
||||||
setZoomGear(gear);
|
|
||||||
|
|
||||||
const mode = window.sfMetadataContext.localStorage.getItem('gallery-group-by', GALLERY_DATE_MODE.DAY) || GALLERY_DATE_MODE.DAY;
|
|
||||||
setMode(mode);
|
|
||||||
|
|
||||||
const switchGalleryModeSubscribe = window.sfMetadataContext.eventBus.subscribe(
|
|
||||||
EVENT_BUS_TYPE.SWITCH_GALLERY_GROUP_BY,
|
|
||||||
(mode) => {
|
|
||||||
setMode(mode);
|
|
||||||
window.sfMetadataContext.localStorage.setItem('gallery-group-by', mode);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const container = containerRef.current;
|
|
||||||
if (container) {
|
|
||||||
const { offsetWidth, clientHeight } = container;
|
|
||||||
setContainerWidth(offsetWidth);
|
|
||||||
|
|
||||||
// Calculate initial overScan information
|
|
||||||
const columns = 8 - gear;
|
|
||||||
const imageSize = (offsetWidth - columns * 2 - 2) / columns;
|
|
||||||
setOverScan({ top: 0, bottom: clientHeight + (imageSize + GALLERY_IMAGE_GAP) * 2 });
|
|
||||||
}
|
|
||||||
setFirstLoading(false);
|
|
||||||
|
|
||||||
// resize
|
|
||||||
const handleResize = () => {
|
|
||||||
if (!container) return;
|
|
||||||
setContainerWidth(container.offsetWidth);
|
|
||||||
};
|
|
||||||
const resizeObserver = new ResizeObserver(handleResize);
|
|
||||||
container && resizeObserver.observe(container);
|
|
||||||
|
|
||||||
// op
|
|
||||||
const modifyGalleryZoomGearSubscribe = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.MODIFY_GALLERY_ZOOM_GEAR, (zoomGear) => {
|
|
||||||
window.sfMetadataContext.localStorage.setItem('zoom-gear', zoomGear);
|
|
||||||
setZoomGear(zoomGear);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
container && resizeObserver.unobserve(container);
|
|
||||||
modifyGalleryZoomGearSubscribe();
|
|
||||||
switchGalleryModeSubscribe();
|
|
||||||
renderMoreTimer.current && clearTimeout(renderMoreTimer.current);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!imageSize || imageSize < 0) return;
|
|
||||||
if (imageSize === lastState.current.imageSize) return;
|
|
||||||
const perImageOffset = imageSize - lastState.current.imageSize;
|
|
||||||
const { groupIndex, rowIndex } = lastState.current.visibleAreaFirstImage;
|
|
||||||
const rowOffset = groups.reduce((previousValue, current, currentIndex) => {
|
|
||||||
if (currentIndex < groupIndex) {
|
|
||||||
return previousValue + current.children.length;
|
|
||||||
}
|
|
||||||
return previousValue;
|
|
||||||
}, 0) + rowIndex;
|
|
||||||
const topOffset = rowOffset * perImageOffset + groupIndex * (mode === GALLERY_DATE_MODE.ALL ? 0 : DATE_TAG_HEIGHT);
|
|
||||||
containerRef.current.scrollTop = containerRef.current.scrollTop + topOffset;
|
|
||||||
lastState.current = { ...lastState.current, imageSize };
|
|
||||||
}, [imageSize, groups, mode]);
|
|
||||||
|
|
||||||
const handleScroll = useCallback(() => {
|
|
||||||
if (!containerRef.current) return;
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
|
|
||||||
if (scrollTop + clientHeight >= scrollHeight - 10) {
|
|
||||||
loadMore();
|
|
||||||
} else {
|
|
||||||
renderMoreTimer.current && clearTimeout(renderMoreTimer.current);
|
|
||||||
renderMoreTimer.current = setTimeout(() => {
|
|
||||||
const { scrollTop, clientHeight } = containerRef.current;
|
|
||||||
const overScanTop = Math.max(0, scrollTop - (imageSize + GALLERY_IMAGE_GAP) * 3);
|
|
||||||
const overScanBottom = scrollTop + clientHeight + (imageSize + GALLERY_IMAGE_GAP) * 3;
|
|
||||||
let groupIndex = 0;
|
|
||||||
let rowIndex = 0;
|
|
||||||
let flag = false;
|
|
||||||
for (let i = 0; i < groups.length; i++) {
|
|
||||||
const group = groups[i];
|
|
||||||
for (let j = 0; j < group.children.length; j++) {
|
|
||||||
const row = group.children[j];
|
|
||||||
if (row.top >= scrollTop) {
|
|
||||||
groupIndex = i;
|
|
||||||
rowIndex = j;
|
|
||||||
flag = true;
|
|
||||||
}
|
|
||||||
if (flag) break;
|
|
||||||
}
|
|
||||||
if (flag) break;
|
|
||||||
}
|
|
||||||
lastState.current = { ...lastState.current, visibleAreaFirstImage: { groupIndex, rowIndex } };
|
|
||||||
setOverScan({ top: overScanTop, bottom: overScanBottom });
|
|
||||||
renderMoreTimer.current = null;
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
}, [imageSize, loadMore, renderMoreTimer, groups]);
|
|
||||||
|
|
||||||
const imageItems = useMemo(() => {
|
|
||||||
return groups.flatMap(group => group.children.flatMap(row => row.children));
|
|
||||||
}, [groups]);
|
|
||||||
|
|
||||||
const updateSelectedImage = useCallback((image = null) => {
|
|
||||||
const imageInfo = image ? getRowById(metadata, image.id) : null;
|
|
||||||
if (!imageInfo) {
|
|
||||||
updateCurrentDirent();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
updateCurrentDirent({
|
|
||||||
type: 'file',
|
|
||||||
name: image.name,
|
|
||||||
path: image.path,
|
|
||||||
file_tags: []
|
|
||||||
});
|
|
||||||
}, [metadata, updateCurrentDirent]);
|
|
||||||
|
|
||||||
const handleClick = useCallback((event, image) => {
|
|
||||||
if (event.metaKey || event.ctrlKey) {
|
|
||||||
setSelectedImages(prev =>
|
|
||||||
prev.includes(image) ? prev.filter(img => img !== image) : [...prev, image]
|
|
||||||
);
|
|
||||||
updateSelectedImage(image);
|
|
||||||
} else if (event.shiftKey && selectedImages.length > 0) {
|
|
||||||
const lastSelected = selectedImages[selectedImages.length - 1];
|
|
||||||
const start = imageItems.indexOf(lastSelected);
|
|
||||||
const end = imageItems.indexOf(image);
|
|
||||||
const range = imageItems.slice(Math.min(start, end), Math.max(start, end) + 1);
|
|
||||||
setSelectedImages(prev => Array.from(new Set([...prev, ...range])));
|
|
||||||
updateSelectedImage(null);
|
|
||||||
} else {
|
|
||||||
setSelectedImages([image]);
|
|
||||||
updateSelectedImage(image);
|
|
||||||
}
|
|
||||||
}, [imageItems, selectedImages, updateSelectedImage]);
|
|
||||||
|
|
||||||
const handleDoubleClick = useCallback((event, image) => {
|
|
||||||
const index = imageItems.findIndex(item => item.id === image.id);
|
|
||||||
setImageIndex(index);
|
|
||||||
setIsImagePopupOpen(true);
|
|
||||||
}, [imageItems]);
|
|
||||||
|
|
||||||
const handleRightClick = useCallback((event, image) => {
|
|
||||||
event.preventDefault();
|
|
||||||
const index = imageItems.findIndex(item => item.id === image.id);
|
|
||||||
if (isNaN(index) || index === -1) return;
|
|
||||||
|
|
||||||
setSelectedImages(prev => prev.length < 2 ? [image] : [...prev]);
|
|
||||||
}, [imageItems]);
|
|
||||||
|
|
||||||
const moveToPrevImage = () => {
|
|
||||||
const imageItemsLength = imageItems.length;
|
|
||||||
setImageIndex((prevState) => (prevState + imageItemsLength - 1) % imageItemsLength);
|
|
||||||
};
|
|
||||||
|
|
||||||
const moveToNextImage = () => {
|
|
||||||
const imageItemsLength = imageItems.length;
|
|
||||||
setImageIndex((prevState) => (prevState + 1) % imageItemsLength);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImageSelection = useCallback((selectedImages) => {
|
|
||||||
setSelectedImages(selectedImages);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const closeImagePopup = () => {
|
|
||||||
setIsImagePopupOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDownload = useCallback(() => {
|
|
||||||
if (selectedImages.length) {
|
|
||||||
if (selectedImages.length === 1) {
|
|
||||||
const image = selectedImages[0];
|
|
||||||
let direntPath = image.path === '/' ? image.name : Utils.joinPath(image.path, image.name);
|
|
||||||
let url = URLDecorator.getUrl({ type: 'download_file_url', repoID: repoID, filePath: direntPath });
|
|
||||||
location.href = url;
|
|
||||||
} else {
|
|
||||||
if (!useGoFileserver) {
|
|
||||||
setIsZipDialogOpen(true);
|
|
||||||
} else {
|
|
||||||
const dirents = selectedImages.map(image => {
|
|
||||||
const value = image.path === '/' ? image.name : `${image.path}/${image.name}`;
|
|
||||||
return value;
|
|
||||||
});
|
|
||||||
metadataAPI.zipDownload(repoID, '/', dirents).then((res) => {
|
|
||||||
const zipToken = res.data['zip_token'];
|
|
||||||
location.href = `${fileServerRoot}zip/${zipToken}`;
|
|
||||||
}).catch(error => {
|
|
||||||
const errMessage = Utils.getErrorMsg(error);
|
|
||||||
toaster.danger(errMessage);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [repoID, selectedImages]);
|
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
|
||||||
if (!selectedImages.length) return;
|
|
||||||
let recordsIds = [];
|
let recordsIds = [];
|
||||||
let paths = [];
|
let paths = [];
|
||||||
let fileNames = [];
|
let fileNames = [];
|
||||||
selectedImages.forEach((record) => {
|
deletedImages.forEach((record) => {
|
||||||
const { path: parentDir, name } = record || {};
|
const { path: parentDir, name } = record || {};
|
||||||
if (parentDir && name) {
|
if (parentDir && name) {
|
||||||
const path = Utils.joinPath(parentDir, name);
|
const path = Utils.joinPath(parentDir, name);
|
||||||
@ -360,7 +49,7 @@ const Gallery = () => {
|
|||||||
toaster.danger(error);
|
toaster.danger(error);
|
||||||
},
|
},
|
||||||
success_callback: () => {
|
success_callback: () => {
|
||||||
setSelectedImages([]);
|
callback && callback();
|
||||||
deleteFilesCallback(paths, fileNames);
|
deleteFilesCallback(paths, fileNames);
|
||||||
let msg = fileNames.length > 1
|
let msg = fileNames.length > 1
|
||||||
? gettext('Successfully deleted {name} and {n} other items')
|
? gettext('Successfully deleted {name} and {n} other items')
|
||||||
@ -370,75 +59,11 @@ const Gallery = () => {
|
|||||||
toaster.success(msg);
|
toaster.success(msg);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [selectedImages, store, deleteFilesCallback]);
|
}, [store, deleteFilesCallback]);
|
||||||
|
|
||||||
const closeZipDialog = () => {
|
|
||||||
setIsZipDialogOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClickOutside = useCallback((event) => {
|
|
||||||
const className = getEventClassName(event);
|
|
||||||
const isClickInsideImage = className.includes('metadata-gallery-image-item') || className.includes('metadata-gallery-grid-image');
|
|
||||||
|
|
||||||
if (!isClickInsideImage && containerRef.current.contains(event.target)) {
|
|
||||||
handleImageSelection([]);
|
|
||||||
updateSelectedImage();
|
|
||||||
}
|
|
||||||
}, [handleImageSelection, updateSelectedImage]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sf-metadata-container" onMouseDown={handleClickOutside}>
|
<div className="sf-metadata-container">
|
||||||
<div className={`sf-metadata-gallery-container sf-metadata-gallery-container-${mode}`} ref={containerRef} onScroll={handleScroll} >
|
<Main isLoadingMore={isLoadingMore} metadata={metadata} onDelete={handleDelete} onLoadMore={onLoadMore} />
|
||||||
{!isFirstLoading && (
|
|
||||||
<>
|
|
||||||
<Main
|
|
||||||
groups={groups}
|
|
||||||
size={imageSize}
|
|
||||||
columns={columns}
|
|
||||||
overScan={overScan}
|
|
||||||
gap={GALLERY_IMAGE_GAP}
|
|
||||||
mode={mode}
|
|
||||||
selectedImages={selectedImages}
|
|
||||||
onImageSelect={handleImageSelection}
|
|
||||||
onImageClick={handleClick}
|
|
||||||
onImageDoubleClick={handleDoubleClick}
|
|
||||||
onImageRightClick={handleRightClick}
|
|
||||||
/>
|
|
||||||
{isLoadingMore &&
|
|
||||||
<div className="sf-metadata-gallery-loading-more">
|
|
||||||
<CenteredLoading />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<ContextMenu
|
|
||||||
getContentRect={() => containerRef.current.getBoundingClientRect()}
|
|
||||||
getContainerRect={() => containerRef.current.getBoundingClientRect()}
|
|
||||||
onDownload={handleDownload}
|
|
||||||
onDelete={handleDelete}
|
|
||||||
/>
|
|
||||||
{isImagePopupOpen && (
|
|
||||||
<ModalPortal>
|
|
||||||
<ImageDialog
|
|
||||||
imageItems={imageItems}
|
|
||||||
imageIndex={imageIndex}
|
|
||||||
closeImagePopup={closeImagePopup}
|
|
||||||
moveToPrevImage={moveToPrevImage}
|
|
||||||
moveToNextImage={moveToNextImage}
|
|
||||||
/>
|
|
||||||
</ModalPortal>
|
|
||||||
)}
|
|
||||||
{isZipDialogOpen &&
|
|
||||||
<ModalPortal>
|
|
||||||
<ZipDownloadDialog
|
|
||||||
repoID={repoID}
|
|
||||||
path={'/'}
|
|
||||||
target={selectedImages.map(image => image.path === '/' ? image.name : `${image.path}/${image.name}`)}
|
|
||||||
toggleDialog={closeZipDialog}
|
|
||||||
/>
|
|
||||||
</ModalPortal>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,197 +1,416 @@
|
|||||||
import React, { useState, useCallback, useMemo, useRef } from 'react';
|
import React, { useMemo, useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import EmptyTip from '../../../components/empty-tip';
|
import { CenteredLoading } from '@seafile/sf-metadata-ui-component';
|
||||||
import { gettext } from '../../../utils/constants';
|
import metadataAPI from '../../api';
|
||||||
import { GALLERY_DATE_MODE } from '../../constants';
|
import URLDecorator from '../../../utils/url-decorator';
|
||||||
import Image from './image';
|
import toaster from '../../../components/toast';
|
||||||
|
import Content from './content';
|
||||||
|
import ContextMenu from './context-menu';
|
||||||
|
import ImageDialog from '../../../components/dialog/image-dialog';
|
||||||
|
import ZipDownloadDialog from '../../../components/dialog/zip-download-dialog';
|
||||||
|
import ModalPortal from '../../../components/modal-portal';
|
||||||
|
import { useMetadataView } from '../../hooks/metadata-view';
|
||||||
|
import { Utils } from '../../../utils/utils';
|
||||||
|
import { getDateDisplayString, getFileNameFromRecord, getParentDirFromRecord } from '../../utils/cell';
|
||||||
|
import { siteRoot, fileServerRoot, useGoFileserver, thumbnailSizeForGrid, thumbnailSizeForOriginal } from '../../../utils/constants';
|
||||||
|
import { EVENT_BUS_TYPE, PRIVATE_COLUMN_KEY, GALLERY_DATE_MODE, DATE_TAG_HEIGHT, GALLERY_IMAGE_GAP } from '../../constants';
|
||||||
|
import { getRowById } from '../../utils/table';
|
||||||
|
import { getEventClassName } from '../../utils/common';
|
||||||
|
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore }) => {
|
||||||
|
const [isFirstLoading, setFirstLoading] = useState(true);
|
||||||
|
const [zoomGear, setZoomGear] = useState(0);
|
||||||
|
const [containerWidth, setContainerWidth] = useState(0);
|
||||||
|
const [overScan, setOverScan] = useState({ top: 0, bottom: 0 });
|
||||||
|
const [mode, setMode] = useState(GALLERY_DATE_MODE.DAY);
|
||||||
|
const [isImagePopupOpen, setIsImagePopupOpen] = useState(false);
|
||||||
|
const [isZipDialogOpen, setIsZipDialogOpen] = useState(false);
|
||||||
|
const [imageIndex, setImageIndex] = useState(0);
|
||||||
|
const [selectedImages, setSelectedImages] = useState([]);
|
||||||
|
|
||||||
const GalleryMain = ({
|
|
||||||
groups,
|
|
||||||
overScan,
|
|
||||||
columns,
|
|
||||||
size,
|
|
||||||
gap,
|
|
||||||
mode,
|
|
||||||
selectedImages,
|
|
||||||
onImageSelect,
|
|
||||||
onImageClick,
|
|
||||||
onImageDoubleClick,
|
|
||||||
onImageRightClick
|
|
||||||
}) => {
|
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
const imageRef = useRef(null);
|
const renderMoreTimer = useRef(null);
|
||||||
const animationFrameRef = useRef(null);
|
const lastState = useRef({ visibleAreaFirstImage: { groupIndex: 0, rowIndex: 0 } });
|
||||||
|
|
||||||
const [isSelecting, setIsSelecting] = useState(false);
|
const repoID = window.sfMetadataContext.getSetting('repoID');
|
||||||
const [selectionStart, setSelectionStart] = useState(null);
|
const { updateCurrentDirent } = useMetadataView();
|
||||||
|
|
||||||
const imageHeight = useMemo(() => size + gap, [size, gap]);
|
useEffect(() => {
|
||||||
const selectedImageIds = useMemo(() => selectedImages.map(img => img.id), [selectedImages]);
|
updateCurrentDirent();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
const handleMouseDown = useCallback((e) => {
|
|
||||||
if (e.button !== 0) return;
|
|
||||||
if (e.ctrlKey || e.metaKey || e.shiftKey) return;
|
|
||||||
|
|
||||||
setIsSelecting(true);
|
|
||||||
setSelectionStart({ x: e.clientX, y: e.clientY });
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleMouseMove = useCallback((e) => {
|
// Number of images per row
|
||||||
if (!isSelecting) return;
|
const columns = useMemo(() => {
|
||||||
|
return 8 - zoomGear;
|
||||||
|
}, [zoomGear]);
|
||||||
|
|
||||||
if (animationFrameRef.current) {
|
const imageSize = useMemo(() => {
|
||||||
cancelAnimationFrame(animationFrameRef.current);
|
return (containerWidth - (columns - 1) * 2 - 32) / columns;
|
||||||
|
}, [containerWidth, columns]);
|
||||||
|
|
||||||
|
const dateMode = useMemo(() => {
|
||||||
|
switch (mode) {
|
||||||
|
case GALLERY_DATE_MODE.YEAR:
|
||||||
|
return 'YYYY';
|
||||||
|
case GALLERY_DATE_MODE.MONTH:
|
||||||
|
return 'YYYY-MM';
|
||||||
|
case GALLERY_DATE_MODE.DAY:
|
||||||
|
return 'YYYY-MM-DD';
|
||||||
|
default:
|
||||||
|
return 'YYYY-MM-DD';
|
||||||
}
|
}
|
||||||
|
}, [mode]);
|
||||||
|
|
||||||
animationFrameRef.current = requestAnimationFrame(() => {
|
const groups = useMemo(() => {
|
||||||
e.preventDefault();
|
if (isFirstLoading) return [];
|
||||||
e.stopPropagation();
|
const firstSort = metadata.view.sorts[0];
|
||||||
|
let init = metadata.rows.filter(row => Utils.imageCheck(getFileNameFromRecord(row)))
|
||||||
const selectionEnd = { x: e.clientX, y: e.clientY };
|
.reduce((_init, record) => {
|
||||||
const selected = [];
|
const id = record[PRIVATE_COLUMN_KEY.ID];
|
||||||
|
const fileName = getFileNameFromRecord(record);
|
||||||
groups.forEach(group => {
|
const parentDir = getParentDirFromRecord(record);
|
||||||
group.children.forEach((row) => {
|
const path = Utils.encodePath(Utils.joinPath(parentDir, fileName));
|
||||||
row.children.forEach((img) => {
|
const date = mode !== GALLERY_DATE_MODE.ALL ? getDateDisplayString(record[firstSort.column_key], dateMode) : '';
|
||||||
const imgElement = document.getElementById(img.id);
|
const img = {
|
||||||
if (imgElement) {
|
id,
|
||||||
const rect = imgElement.getBoundingClientRect();
|
name: fileName,
|
||||||
if (
|
path: parentDir,
|
||||||
rect.left < Math.max(selectionStart.x, selectionEnd.x) &&
|
url: `${siteRoot}lib/${repoID}/file${path}`,
|
||||||
rect.right > Math.min(selectionStart.x, selectionEnd.x) &&
|
src: `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForGrid}${path}`,
|
||||||
rect.top < Math.max(selectionStart.y, selectionEnd.y) &&
|
thumbnail: `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForOriginal}${path}`,
|
||||||
rect.bottom > Math.min(selectionStart.y, selectionEnd.y)
|
downloadURL: `${fileServerRoot}repos/${repoID}/files${path}?op=download`,
|
||||||
) {
|
date: date,
|
||||||
selected.push(img);
|
};
|
||||||
}
|
let _group = _init.find(g => g.name === date);
|
||||||
}
|
if (_group) {
|
||||||
|
_group.children.push(img);
|
||||||
|
} else {
|
||||||
|
_init.push({
|
||||||
|
name: date,
|
||||||
|
children: [img],
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
return _init;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
let _groups = [];
|
||||||
|
const imageHeight = imageSize + GALLERY_IMAGE_GAP;
|
||||||
|
const paddingTop = mode === GALLERY_DATE_MODE.ALL ? 0 : DATE_TAG_HEIGHT;
|
||||||
|
init.forEach((_init, index) => {
|
||||||
|
const { children, ...__init } = _init;
|
||||||
|
let top = 0;
|
||||||
|
let rows = [];
|
||||||
|
if (index > 0) {
|
||||||
|
const lastGroup = _groups[index - 1];
|
||||||
|
const { top: lastGroupTop, height: lastGroupHeight } = lastGroup;
|
||||||
|
top = lastGroupTop + lastGroupHeight;
|
||||||
|
}
|
||||||
|
children.forEach((child, childIndex) => {
|
||||||
|
const rowIndex = ~~(childIndex / columns);
|
||||||
|
if (!rows[rowIndex]) rows[rowIndex] = { top: paddingTop + top + rowIndex * imageHeight, children: [] };
|
||||||
|
child.groupIndex = index;
|
||||||
|
child.rowIndex = rowIndex;
|
||||||
|
rows[rowIndex].children.push(child);
|
||||||
});
|
});
|
||||||
|
|
||||||
onImageSelect(selected);
|
const height = rows.length * imageHeight + paddingTop;
|
||||||
|
_groups.push({
|
||||||
|
...__init,
|
||||||
|
top,
|
||||||
|
height,
|
||||||
|
paddingTop,
|
||||||
|
children: rows
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}, [groups, isSelecting, selectionStart, onImageSelect]);
|
return _groups;
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [isFirstLoading, metadata, metadata.recordsCount, repoID, columns, imageSize, mode]);
|
||||||
|
|
||||||
const handleMouseUp = useCallback((e) => {
|
useEffect(() => {
|
||||||
if (e.button !== 0) return;
|
const gear = window.sfMetadataContext.localStorage.getItem('zoom-gear', 0) || 0;
|
||||||
|
setZoomGear(gear);
|
||||||
|
|
||||||
e.preventDefault();
|
const mode = window.sfMetadataContext.localStorage.getItem('gallery-group-by', GALLERY_DATE_MODE.DAY) || GALLERY_DATE_MODE.DAY;
|
||||||
e.stopPropagation();
|
setMode(mode);
|
||||||
setIsSelecting(false);
|
|
||||||
|
const switchGalleryModeSubscribe = window.sfMetadataContext.eventBus.subscribe(
|
||||||
|
EVENT_BUS_TYPE.SWITCH_GALLERY_GROUP_BY,
|
||||||
|
(mode) => {
|
||||||
|
setMode(mode);
|
||||||
|
window.sfMetadataContext.localStorage.setItem('gallery-group-by', mode);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (container) {
|
||||||
|
const { offsetWidth, clientHeight } = container;
|
||||||
|
setContainerWidth(offsetWidth);
|
||||||
|
|
||||||
|
// Calculate initial overScan information
|
||||||
|
const columns = 8 - gear;
|
||||||
|
const imageSize = (offsetWidth - columns * 2 - 2) / columns;
|
||||||
|
setOverScan({ top: 0, bottom: clientHeight + (imageSize + GALLERY_IMAGE_GAP) * 2 });
|
||||||
|
}
|
||||||
|
setFirstLoading(false);
|
||||||
|
|
||||||
|
// resize
|
||||||
|
const handleResize = () => {
|
||||||
|
if (!container) return;
|
||||||
|
setContainerWidth(container.offsetWidth);
|
||||||
|
};
|
||||||
|
const resizeObserver = new ResizeObserver(handleResize);
|
||||||
|
container && resizeObserver.observe(container);
|
||||||
|
|
||||||
|
// op
|
||||||
|
const modifyGalleryZoomGearSubscribe = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.MODIFY_GALLERY_ZOOM_GEAR, (zoomGear) => {
|
||||||
|
window.sfMetadataContext.localStorage.setItem('zoom-gear', zoomGear);
|
||||||
|
setZoomGear(zoomGear);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
container && resizeObserver.unobserve(container);
|
||||||
|
modifyGalleryZoomGearSubscribe();
|
||||||
|
switchGalleryModeSubscribe();
|
||||||
|
renderMoreTimer.current && clearTimeout(renderMoreTimer.current);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const renderDisplayGroup = useCallback((group) => {
|
useEffect(() => {
|
||||||
const { top: overScanTop, bottom: overScanBottom } = overScan;
|
if (!imageSize || imageSize < 0) return;
|
||||||
const { name, children, height, top, paddingTop } = group;
|
if (imageSize === lastState.current.imageSize) return;
|
||||||
|
const perImageOffset = imageSize - lastState.current.imageSize;
|
||||||
|
const { groupIndex, rowIndex } = lastState.current.visibleAreaFirstImage;
|
||||||
|
const rowOffset = groups.reduce((previousValue, current, currentIndex) => {
|
||||||
|
if (currentIndex < groupIndex) {
|
||||||
|
return previousValue + current.children.length;
|
||||||
|
}
|
||||||
|
return previousValue;
|
||||||
|
}, 0) + rowIndex;
|
||||||
|
const topOffset = rowOffset * perImageOffset + groupIndex * (mode === GALLERY_DATE_MODE.ALL ? 0 : DATE_TAG_HEIGHT);
|
||||||
|
containerRef.current.scrollTop = containerRef.current.scrollTop + topOffset;
|
||||||
|
lastState.current = { ...lastState.current, imageSize };
|
||||||
|
}, [imageSize, groups, mode]);
|
||||||
|
|
||||||
// group not in rendering area, return empty div
|
const handleScroll = useCallback(() => {
|
||||||
if (top >= overScanBottom || top + height <= overScanTop) {
|
if (!containerRef.current) return;
|
||||||
return (<div key={name} className="w-100" style={{ height, flexShrink: 0 }}></div>);
|
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
|
||||||
|
if (scrollTop + clientHeight >= scrollHeight - 10) {
|
||||||
|
onLoadMore();
|
||||||
|
} else {
|
||||||
|
renderMoreTimer.current && clearTimeout(renderMoreTimer.current);
|
||||||
|
renderMoreTimer.current = setTimeout(() => {
|
||||||
|
const { scrollTop, clientHeight } = containerRef.current;
|
||||||
|
const overScanTop = Math.max(0, scrollTop - (imageSize + GALLERY_IMAGE_GAP) * 3);
|
||||||
|
const overScanBottom = scrollTop + clientHeight + (imageSize + GALLERY_IMAGE_GAP) * 3;
|
||||||
|
let groupIndex = 0;
|
||||||
|
let rowIndex = 0;
|
||||||
|
let flag = false;
|
||||||
|
for (let i = 0; i < groups.length; i++) {
|
||||||
|
const group = groups[i];
|
||||||
|
for (let j = 0; j < group.children.length; j++) {
|
||||||
|
const row = group.children[j];
|
||||||
|
if (row.top >= scrollTop) {
|
||||||
|
groupIndex = i;
|
||||||
|
rowIndex = j;
|
||||||
|
flag = true;
|
||||||
|
}
|
||||||
|
if (flag) break;
|
||||||
|
}
|
||||||
|
if (flag) break;
|
||||||
|
}
|
||||||
|
lastState.current = { ...lastState.current, visibleAreaFirstImage: { groupIndex, rowIndex } };
|
||||||
|
setOverScan({ top: overScanTop, bottom: overScanBottom });
|
||||||
|
renderMoreTimer.current = null;
|
||||||
|
}, 200);
|
||||||
}
|
}
|
||||||
|
}, [imageSize, onLoadMore, renderMoreTimer, groups]);
|
||||||
|
|
||||||
let childrenStartIndex = children.findIndex(r => r.top >= overScanTop);
|
const imageItems = useMemo(() => {
|
||||||
let childrenEndIndex = children.findIndex(r => r.top >= overScanBottom);
|
return groups.flatMap(group => group.children.flatMap(row => row.children));
|
||||||
|
}, [groups]);
|
||||||
|
|
||||||
// group in rendering area, but the image not need to render. eg: overScan: { top: 488, bottom: 1100 }, group: { top: 0, height: 521 },
|
const updateSelectedImage = useCallback((image = null) => {
|
||||||
// in this time, part of an image is in the rendering area, don't render image
|
const imageInfo = image ? getRowById(metadata, image.id) : null;
|
||||||
if (childrenStartIndex === -1 && childrenEndIndex === -1) {
|
if (!imageInfo) {
|
||||||
return (<div key={name} className="w-100" style={{ height, flexShrink: 0 }}></div>);
|
updateCurrentDirent();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
updateCurrentDirent({
|
||||||
|
type: 'file',
|
||||||
|
name: image.name,
|
||||||
|
path: image.path,
|
||||||
|
file_tags: []
|
||||||
|
});
|
||||||
|
}, [metadata, updateCurrentDirent]);
|
||||||
|
|
||||||
childrenStartIndex = Math.max(childrenStartIndex, 0);
|
const handleClick = useCallback((event, image) => {
|
||||||
if (childrenEndIndex === -1) {
|
if (event.metaKey || event.ctrlKey) {
|
||||||
childrenEndIndex = children.length;
|
setSelectedImages(prev =>
|
||||||
|
prev.includes(image) ? prev.filter(img => img !== image) : [...prev, image]
|
||||||
|
);
|
||||||
|
updateSelectedImage(image);
|
||||||
|
} else if (event.shiftKey && selectedImages.length > 0) {
|
||||||
|
const lastSelected = selectedImages[selectedImages.length - 1];
|
||||||
|
const start = imageItems.indexOf(lastSelected);
|
||||||
|
const end = imageItems.indexOf(image);
|
||||||
|
const range = imageItems.slice(Math.min(start, end), Math.max(start, end) + 1);
|
||||||
|
setSelectedImages(prev => Array.from(new Set([...prev, ...range])));
|
||||||
|
updateSelectedImage(null);
|
||||||
|
} else {
|
||||||
|
setSelectedImages([image]);
|
||||||
|
updateSelectedImage(image);
|
||||||
}
|
}
|
||||||
if (childrenEndIndex > 0) {
|
}, [imageItems, selectedImages, updateSelectedImage]);
|
||||||
childrenEndIndex = childrenEndIndex - 1;
|
|
||||||
|
const handleDoubleClick = useCallback((event, image) => {
|
||||||
|
const index = imageItems.findIndex(item => item.id === image.id);
|
||||||
|
setImageIndex(index);
|
||||||
|
setIsImagePopupOpen(true);
|
||||||
|
}, [imageItems]);
|
||||||
|
|
||||||
|
const handleRightClick = useCallback((event, image) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const index = imageItems.findIndex(item => item.id === image.id);
|
||||||
|
if (isNaN(index) || index === -1) return;
|
||||||
|
|
||||||
|
setSelectedImages(prev => prev.length < 2 ? [image] : [...prev]);
|
||||||
|
}, [imageItems]);
|
||||||
|
|
||||||
|
const moveToPrevImage = () => {
|
||||||
|
const imageItemsLength = imageItems.length;
|
||||||
|
setImageIndex((prevState) => (prevState + imageItemsLength - 1) % imageItemsLength);
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveToNextImage = () => {
|
||||||
|
const imageItemsLength = imageItems.length;
|
||||||
|
setImageIndex((prevState) => (prevState + 1) % imageItemsLength);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImageSelection = useCallback((selectedImages) => {
|
||||||
|
setSelectedImages(selectedImages);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeImagePopup = () => {
|
||||||
|
setIsImagePopupOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownload = useCallback(() => {
|
||||||
|
if (!selectedImages.length) return;
|
||||||
|
if (selectedImages.length === 1) {
|
||||||
|
const image = selectedImages[0];
|
||||||
|
let direntPath = image.path === '/' ? image.name : Utils.joinPath(image.path, image.name);
|
||||||
|
let url = URLDecorator.getUrl({ type: 'download_file_url', repoID: repoID, filePath: direntPath });
|
||||||
|
location.href = url;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
if (!useGoFileserver) {
|
||||||
|
setIsZipDialogOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dirents = selectedImages.map(image => {
|
||||||
|
const value = image.path === '/' ? image.name : `${image.path}/${image.name}`;
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
metadataAPI.zipDownload(repoID, '/', dirents).then((res) => {
|
||||||
|
const zipToken = res.data['zip_token'];
|
||||||
|
location.href = `${fileServerRoot}zip/${zipToken}`;
|
||||||
|
}).catch(error => {
|
||||||
|
const errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
}, [repoID, selectedImages]);
|
||||||
|
|
||||||
return (
|
const handleDelete = useCallback(() => {
|
||||||
<div
|
if (!selectedImages.length) return;
|
||||||
key={name}
|
onDelete(selectedImages, () => {
|
||||||
className="metadata-gallery-date-group"
|
setSelectedImages([]);
|
||||||
style={{ height, paddingTop }}
|
});
|
||||||
>
|
}, [selectedImages, onDelete]);
|
||||||
{mode !== GALLERY_DATE_MODE.ALL && childrenStartIndex === 0 && (
|
|
||||||
<div className="metadata-gallery-date-tag">{name || gettext('Empty')}</div>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
ref={imageRef}
|
|
||||||
className="metadata-gallery-image-list"
|
|
||||||
style={{
|
|
||||||
gridTemplateColumns: `repeat(${columns}, 1fr)`,
|
|
||||||
paddingTop: childrenStartIndex * imageHeight,
|
|
||||||
paddingBottom: (children.length - 1 - childrenEndIndex) * imageHeight,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children.slice(childrenStartIndex, childrenEndIndex + 1).map((row) => {
|
|
||||||
return row.children.map((img) => {
|
|
||||||
const isSelected = selectedImageIds.includes(img.id);
|
|
||||||
return (
|
|
||||||
<Image
|
|
||||||
key={img.src}
|
|
||||||
isSelected={isSelected}
|
|
||||||
img={img}
|
|
||||||
size={size}
|
|
||||||
onClick={(e) => onImageClick(e, img)}
|
|
||||||
onDoubleClick={(e) => onImageDoubleClick(e, img)}
|
|
||||||
onContextMenu={(e) => onImageRightClick(e, img)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}, [overScan, columns, size, imageHeight, mode, selectedImageIds, onImageClick, onImageDoubleClick, onImageRightClick]);
|
|
||||||
|
|
||||||
if (!Array.isArray(groups) || groups.length === 0) {
|
const closeZipDialog = () => {
|
||||||
return <EmptyTip text={gettext('No record')}/>;
|
setIsZipDialogOpen(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const handleClickOutside = useCallback((event) => {
|
||||||
|
const className = getEventClassName(event);
|
||||||
|
const isClickInsideImage = className.includes('metadata-gallery-image-item') || className.includes('metadata-gallery-grid-image');
|
||||||
|
|
||||||
|
if (!isClickInsideImage && containerRef.current.contains(event.target)) {
|
||||||
|
handleImageSelection([]);
|
||||||
|
updateSelectedImage();
|
||||||
|
}
|
||||||
|
}, [handleImageSelection, updateSelectedImage]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
ref={containerRef}
|
<div
|
||||||
className='metadata-gallery-main'
|
className={`sf-metadata-gallery-container sf-metadata-gallery-container-${mode}`}
|
||||||
onMouseDown={handleMouseDown}
|
ref={containerRef}
|
||||||
onMouseMove={handleMouseMove}
|
onScroll={handleScroll}
|
||||||
onMouseUp={handleMouseUp}
|
onMouseDown={handleClickOutside}
|
||||||
>
|
>
|
||||||
{groups.map((group) => {
|
{!isFirstLoading && (
|
||||||
return renderDisplayGroup(group);
|
<>
|
||||||
})}
|
<Content
|
||||||
</div>
|
groups={groups}
|
||||||
|
size={imageSize}
|
||||||
|
columns={columns}
|
||||||
|
overScan={overScan}
|
||||||
|
gap={GALLERY_IMAGE_GAP}
|
||||||
|
mode={mode}
|
||||||
|
selectedImages={selectedImages}
|
||||||
|
onImageSelect={handleImageSelection}
|
||||||
|
onImageClick={handleClick}
|
||||||
|
onImageDoubleClick={handleDoubleClick}
|
||||||
|
onImageRightClick={handleRightClick}
|
||||||
|
/>
|
||||||
|
{isLoadingMore &&
|
||||||
|
<div className="sf-metadata-gallery-loading-more">
|
||||||
|
<CenteredLoading />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ContextMenu
|
||||||
|
getContentRect={() => containerRef.current.getBoundingClientRect()}
|
||||||
|
getContainerRect={() => containerRef.current.getBoundingClientRect()}
|
||||||
|
onDownload={handleDownload}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
/>
|
||||||
|
{isImagePopupOpen && (
|
||||||
|
<ModalPortal>
|
||||||
|
<ImageDialog
|
||||||
|
imageItems={imageItems}
|
||||||
|
imageIndex={imageIndex}
|
||||||
|
closeImagePopup={closeImagePopup}
|
||||||
|
moveToPrevImage={moveToPrevImage}
|
||||||
|
moveToNextImage={moveToNextImage}
|
||||||
|
/>
|
||||||
|
</ModalPortal>
|
||||||
|
)}
|
||||||
|
{isZipDialogOpen && (
|
||||||
|
<ModalPortal>
|
||||||
|
<ZipDownloadDialog
|
||||||
|
repoID={repoID}
|
||||||
|
path={'/'}
|
||||||
|
target={selectedImages.map(image => image.path === '/' ? image.name : `${image.path}/${image.name}`)}
|
||||||
|
toggleDialog={closeZipDialog}
|
||||||
|
/>
|
||||||
|
</ModalPortal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
GalleryMain.propTypes = {
|
Main.propTypes = {
|
||||||
groups: PropTypes.arrayOf(PropTypes.shape({
|
isLoadingMore: PropTypes.bool,
|
||||||
name: PropTypes.string.isRequired,
|
metadata: PropTypes.object,
|
||||||
children: PropTypes.arrayOf(PropTypes.shape({
|
onDelete: PropTypes.func,
|
||||||
top: PropTypes.number.isRequired,
|
onLoadMore: PropTypes.func
|
||||||
children: PropTypes.arrayOf(PropTypes.shape({
|
|
||||||
src: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
})).isRequired,
|
|
||||||
})).isRequired,
|
|
||||||
height: PropTypes.number.isRequired,
|
|
||||||
top: PropTypes.number.isRequired,
|
|
||||||
paddingTop: PropTypes.number.isRequired,
|
|
||||||
})),
|
|
||||||
overScan: PropTypes.shape({
|
|
||||||
top: PropTypes.number.isRequired,
|
|
||||||
bottom: PropTypes.number.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
columns: PropTypes.number.isRequired,
|
|
||||||
size: PropTypes.number.isRequired,
|
|
||||||
gap: PropTypes.number.isRequired,
|
|
||||||
mode: PropTypes.string,
|
|
||||||
selectedImages: PropTypes.array.isRequired,
|
|
||||||
onImageSelect: PropTypes.func.isRequired,
|
|
||||||
onImageClick: PropTypes.func.isRequired,
|
|
||||||
onImageDoubleClick: PropTypes.func.isRequired,
|
|
||||||
onImageRightClick: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GalleryMain;
|
export default Main;
|
||||||
|
@ -23,7 +23,7 @@ import DeleteFolderDialog from '../../components/dialog/delete-folder-dialog';
|
|||||||
import { EVENT_BUS_TYPE } from '../../components/common/event-bus-type';
|
import { EVENT_BUS_TYPE } from '../../components/common/event-bus-type';
|
||||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||||
import { MetadataProvider, CollaboratorsProvider } from '../../metadata/hooks';
|
import { MetadataProvider, CollaboratorsProvider } from '../../metadata/hooks';
|
||||||
import { LIST_MODE, METADATA_MODE, FACE_RECOGNITION_MODE, DIRENT_DETAIL_MODE } from '../../components/dir-view-mode/constants';
|
import { LIST_MODE, METADATA_MODE, DIRENT_DETAIL_MODE } from '../../components/dir-view-mode/constants';
|
||||||
import CurDirPath from '../../components/cur-dir-path';
|
import CurDirPath from '../../components/cur-dir-path';
|
||||||
import DirTool from '../../components/cur-dir-path/dir-tool';
|
import DirTool from '../../components/cur-dir-path/dir-tool';
|
||||||
import Detail from '../../components/dirent-detail';
|
import Detail from '../../components/dirent-detail';
|
||||||
@ -547,19 +547,6 @@ class LibContentView extends React.Component {
|
|||||||
window.history.pushState({ url: url, path: '' }, '', url);
|
window.history.pushState({ url: url, path: '' }, '', url);
|
||||||
};
|
};
|
||||||
|
|
||||||
showFaceRecognition = (filePath, viewId) => {
|
|
||||||
const repoID = this.props.repoID;
|
|
||||||
const repoInfo = this.state.currentRepoInfo;
|
|
||||||
this.setState({
|
|
||||||
currentMode: FACE_RECOGNITION_MODE,
|
|
||||||
path: filePath,
|
|
||||||
viewId: viewId,
|
|
||||||
isDirentDetailShow: false
|
|
||||||
});
|
|
||||||
const url = `${siteRoot}library/${repoID}/${encodeURIComponent(repoInfo.repo_name)}/?view=${encodeURIComponent(viewId)}`;
|
|
||||||
window.history.pushState({ url: url, path: '' }, '', url);
|
|
||||||
};
|
|
||||||
|
|
||||||
hideFileMetadata = () => {
|
hideFileMetadata = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
currentMode: LIST_MODE,
|
currentMode: LIST_MODE,
|
||||||
@ -1891,10 +1878,6 @@ class LibContentView extends React.Component {
|
|||||||
if (node.path !== this.state.path) {
|
if (node.path !== this.state.path) {
|
||||||
this.showFileMetadata(node.path, node.view_id || '0000', node.view_type || VIEW_TYPE.TABLE);
|
this.showFileMetadata(node.path, node.view_id || '0000', node.view_type || VIEW_TYPE.TABLE);
|
||||||
}
|
}
|
||||||
} else if (Utils.isFaceRecognition(node?.object?.type)) {
|
|
||||||
if (node.path !== this.state.path) {
|
|
||||||
this.showFaceRecognition(node.path, node.view_id || '0000');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
let url = siteRoot + 'lib/' + repoID + '/file' + Utils.encodePath(node.path);
|
let url = siteRoot + 'lib/' + repoID + '/file' + Utils.encodePath(node.path);
|
||||||
let dirent = node.object;
|
let dirent = node.object;
|
||||||
@ -2032,7 +2015,7 @@ class LibContentView extends React.Component {
|
|||||||
isDirentSelected: false,
|
isDirentSelected: false,
|
||||||
isAllDirentSelected: false,
|
isAllDirentSelected: false,
|
||||||
});
|
});
|
||||||
if (this.state.currentMode === METADATA_MODE || this.state.currentMode === FACE_RECOGNITION_MODE) {
|
if (this.state.currentMode === METADATA_MODE) {
|
||||||
this.setState({
|
this.setState({
|
||||||
currentMode: cookie.load('seafile_view_mode') || LIST_MODE,
|
currentMode: cookie.load('seafile_view_mode') || LIST_MODE,
|
||||||
});
|
});
|
||||||
@ -2422,6 +2405,7 @@ class LibContentView extends React.Component {
|
|||||||
getMarkDownFileName={this.getMarkDownFileName}
|
getMarkDownFileName={this.getMarkDownFileName}
|
||||||
openMarkdownFile={this.openMarkdownFile}
|
openMarkdownFile={this.openMarkdownFile}
|
||||||
updateCurrentDirent={this.updateCurrentDirent}
|
updateCurrentDirent={this.updateCurrentDirent}
|
||||||
|
closeDirentDetail={this.closeDirentDetail}
|
||||||
/>
|
/>
|
||||||
:
|
:
|
||||||
<div className="message err-tip">{gettext('Folder does not exist.')}</div>
|
<div className="message err-tip">{gettext('Folder does not exist.')}</div>
|
||||||
|
BIN
media/favicons/face-recognition-view.png
Normal file
BIN
media/favicons/face-recognition-view.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
@ -13,7 +13,8 @@ from seahub.api2.authentication import TokenAuthentication
|
|||||||
from seahub.repo_metadata.models import RepoMetadata, RepoMetadataViews
|
from seahub.repo_metadata.models import RepoMetadata, RepoMetadataViews
|
||||||
from seahub.views import check_folder_permission
|
from seahub.views import check_folder_permission
|
||||||
from seahub.repo_metadata.utils import add_init_metadata_task, gen_unique_id, init_metadata, \
|
from seahub.repo_metadata.utils import add_init_metadata_task, gen_unique_id, init_metadata, \
|
||||||
get_unmodifiable_columns, can_read_metadata, init_faces, add_init_face_recognition_task, get_metadata_by_faces, extract_file_details
|
get_unmodifiable_columns, can_read_metadata, init_faces, add_init_face_recognition_task, \
|
||||||
|
extract_file_details, get_someone_similar_faces
|
||||||
from seahub.repo_metadata.metadata_server_api import MetadataServerAPI, list_metadata_view_records
|
from seahub.repo_metadata.metadata_server_api import MetadataServerAPI, list_metadata_view_records
|
||||||
from seahub.utils.timeutils import datetime_to_isoformat_timestr
|
from seahub.utils.timeutils import datetime_to_isoformat_timestr
|
||||||
from seahub.utils.repo import is_repo_admin
|
from seahub.utils.repo import is_repo_admin
|
||||||
@ -831,7 +832,7 @@ class FacesRecords(APIView):
|
|||||||
|
|
||||||
def get(self, request, repo_id):
|
def get(self, request, repo_id):
|
||||||
start = request.GET.get('start', 0)
|
start = request.GET.get('start', 0)
|
||||||
limit = request.GET.get('limit', 100)
|
limit = request.GET.get('limit', 1000)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
start = int(start)
|
start = int(start)
|
||||||
@ -853,19 +854,18 @@ class FacesRecords(APIView):
|
|||||||
error_msg = f'The metadata module is disabled for repo {repo_id}.'
|
error_msg = f'The metadata module is disabled for repo {repo_id}.'
|
||||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
permission = check_folder_permission(request, repo_id, '/')
|
|
||||||
if not permission:
|
|
||||||
error_msg = 'Permission denied.'
|
|
||||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
|
||||||
|
|
||||||
repo = seafile_api.get_repo(repo_id)
|
repo = seafile_api.get_repo(repo_id)
|
||||||
if not repo:
|
if not repo:
|
||||||
error_msg = 'Library %s not found.' % repo_id
|
error_msg = 'Library %s not found.' % repo_id
|
||||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
if not can_read_metadata(request, repo_id):
|
||||||
|
error_msg = 'Permission denied.'
|
||||||
|
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||||
|
|
||||||
metadata_server_api = MetadataServerAPI(repo_id, request.user.username)
|
metadata_server_api = MetadataServerAPI(repo_id, request.user.username)
|
||||||
|
|
||||||
from seafevents.repo_metadata.utils import METADATA_TABLE, FACES_TABLE
|
from seafevents.repo_metadata.utils import FACES_TABLE
|
||||||
|
|
||||||
try:
|
try:
|
||||||
metadata = metadata_server_api.get_metadata()
|
metadata = metadata_server_api.get_metadata()
|
||||||
@ -880,7 +880,7 @@ class FacesRecords(APIView):
|
|||||||
if not faces_table_id:
|
if not faces_table_id:
|
||||||
return api_error(status.HTTP_404_NOT_FOUND, 'face recognition not be used')
|
return api_error(status.HTTP_404_NOT_FOUND, 'face recognition not be used')
|
||||||
|
|
||||||
sql = f'SELECT * FROM `{FACES_TABLE.name}` WHERE `{FACES_TABLE.columns.photo_links.name}` IS NOT NULL LIMIT {start}, {limit}'
|
sql = f'SELECT `{FACES_TABLE.columns.id.name}`, `{FACES_TABLE.columns.name.name}`, `{FACES_TABLE.columns.photo_links.name}`, `{FACES_TABLE.columns.vector.name}` FROM `{FACES_TABLE.name}` WHERE `{FACES_TABLE.columns.photo_links.name}` IS NOT NULL LIMIT {start}, {limit}'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
query_result = metadata_server_api.query_rows(sql)
|
query_result = metadata_server_api.query_rows(sql)
|
||||||
@ -889,49 +889,60 @@ class FacesRecords(APIView):
|
|||||||
error_msg = 'Internal Server Error'
|
error_msg = 'Internal Server Error'
|
||||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
faces = query_result.get('results')
|
faces_records = query_result.get('results')
|
||||||
|
metadata_columns = query_result.get('metadata', [])
|
||||||
|
metadata_columns.append({
|
||||||
|
'key': '_similar_photo',
|
||||||
|
'type': 'text',
|
||||||
|
'name': '_similar_photo',
|
||||||
|
'data': None,
|
||||||
|
})
|
||||||
|
metadata_columns.append({
|
||||||
|
'key': '_is_someone',
|
||||||
|
'type': 'checkbox',
|
||||||
|
'name': '_is_someone',
|
||||||
|
'data': None,
|
||||||
|
})
|
||||||
|
|
||||||
if not faces:
|
if not faces_records:
|
||||||
error_msg = 'Records not found'
|
return Response({
|
||||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
'metadata': metadata_columns,
|
||||||
|
'results': [],
|
||||||
|
})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
query_result = get_metadata_by_faces(faces, metadata_server_api)
|
similar_result = get_someone_similar_faces(faces_records, metadata_server_api)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
error_msg = 'Internal Server Error'
|
error_msg = 'Internal Server Error'
|
||||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
if not query_result:
|
|
||||||
error_msg = 'Records not found'
|
|
||||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
|
||||||
|
|
||||||
classify_result = dict()
|
if not query_result:
|
||||||
for row in query_result:
|
return Response({
|
||||||
link_row_ids = [item['row_id'] for item in row.get(METADATA_TABLE.columns.face_links.name, [])]
|
'metadata': metadata_columns,
|
||||||
|
'results': [],
|
||||||
|
})
|
||||||
|
|
||||||
|
similar_result_dict = dict()
|
||||||
|
for row in similar_result:
|
||||||
|
similar_result_dict[row['_id']] = row
|
||||||
|
|
||||||
|
for record in faces_records:
|
||||||
|
vector = record.get(FACES_TABLE.columns.vector.name, None)
|
||||||
|
record['_is_someone'] = bool(vector)
|
||||||
|
if FACES_TABLE.columns.vector.name in record:
|
||||||
|
del record[FACES_TABLE.columns.vector.name]
|
||||||
|
link_row_ids = [item['row_id'] for item in record.get(FACES_TABLE.columns.photo_links.name, [])]
|
||||||
if not link_row_ids:
|
if not link_row_ids:
|
||||||
continue
|
continue
|
||||||
for link_row_id in link_row_ids:
|
link_row_id = link_row_ids[0]
|
||||||
if link_row_id not in classify_result:
|
if link_row_id in similar_result_dict:
|
||||||
classify_result[link_row_id] = []
|
record['_similar_photo'] = similar_result_dict[link_row_id]
|
||||||
file_name = row.get(METADATA_TABLE.columns.file_name.name, '')
|
|
||||||
parent_dir = row.get(METADATA_TABLE.columns.parent_dir.name, '')
|
|
||||||
size = row.get(METADATA_TABLE.columns.size.name, 0)
|
|
||||||
mtime = row.get('_mtime')
|
|
||||||
classify_result[link_row_id].append({
|
|
||||||
'path': os.path.join(parent_dir, file_name),
|
|
||||||
'file_name': file_name,
|
|
||||||
'parent_dir': parent_dir,
|
|
||||||
'size': size,
|
|
||||||
'mtime': mtime
|
|
||||||
})
|
|
||||||
|
|
||||||
id_to_name = {item.get(FACES_TABLE.columns.id.name): item.get(FACES_TABLE.columns.name.name, '') for item in faces}
|
return Response({
|
||||||
classify_result = [{
|
'metadata': metadata_columns,
|
||||||
'record_id': key,
|
'results': faces_records,
|
||||||
'name': id_to_name.get(key, ''),
|
})
|
||||||
'link_photos': value
|
|
||||||
} for key, value in classify_result.items()]
|
|
||||||
return Response({'results': classify_result})
|
|
||||||
|
|
||||||
|
|
||||||
class FacesRecord(APIView):
|
class FacesRecord(APIView):
|
||||||
@ -999,6 +1010,91 @@ class FacesRecord(APIView):
|
|||||||
return Response({'success': True})
|
return Response({'success': True})
|
||||||
|
|
||||||
|
|
||||||
|
class PeoplePhotos(APIView):
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
|
||||||
|
def get(self, request, repo_id, people_id):
|
||||||
|
start = request.GET.get('start', 0)
|
||||||
|
limit = request.GET.get('limit', 1000)
|
||||||
|
|
||||||
|
try:
|
||||||
|
start = int(start)
|
||||||
|
limit = int(limit)
|
||||||
|
except:
|
||||||
|
start = 0
|
||||||
|
limit = 1000
|
||||||
|
|
||||||
|
if start < 0:
|
||||||
|
error_msg = 'start invalid'
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
if limit < 0:
|
||||||
|
error_msg = 'limit invalid'
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
metadata = RepoMetadata.objects.filter(repo_id=repo_id).first()
|
||||||
|
if not metadata or not metadata.enabled:
|
||||||
|
error_msg = f'The metadata module is disabled for repo {repo_id}.'
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
repo = seafile_api.get_repo(repo_id)
|
||||||
|
if not repo:
|
||||||
|
error_msg = 'Library %s not found.' % repo_id
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
if not can_read_metadata(request, repo_id):
|
||||||
|
error_msg = 'Permission denied.'
|
||||||
|
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||||
|
|
||||||
|
metadata_server_api = MetadataServerAPI(repo_id, request.user.username)
|
||||||
|
from seafevents.repo_metadata.utils import METADATA_TABLE, FACES_TABLE
|
||||||
|
|
||||||
|
try:
|
||||||
|
metadata = metadata_server_api.get_metadata()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
tables = metadata.get('tables', [])
|
||||||
|
faces_table_id = [table['id'] for table in tables if table['name'] == FACES_TABLE.name]
|
||||||
|
faces_table_id = faces_table_id[0] if faces_table_id else None
|
||||||
|
if not faces_table_id:
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, 'face recognition not be used')
|
||||||
|
|
||||||
|
sql = f'SELECT `{FACES_TABLE.columns.photo_links.name}` FROM `{FACES_TABLE.name}` WHERE `{FACES_TABLE.columns.id.name}` = "{people_id}" LIMIT {start}, {limit}'
|
||||||
|
|
||||||
|
try:
|
||||||
|
query_result = metadata_server_api.query_rows(sql)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
faces_records = query_result.get('results')
|
||||||
|
|
||||||
|
if not faces_records:
|
||||||
|
return Response({'metadata': [], 'results': []})
|
||||||
|
|
||||||
|
faces_record = faces_records[0]
|
||||||
|
|
||||||
|
try:
|
||||||
|
record_ids = [item['row_id'] for item in faces_record.get(FACES_TABLE.columns.photo_links.name, [])]
|
||||||
|
selected_ids = record_ids[start:limit]
|
||||||
|
selected_ids_str = ', '.join(["'%s'" % id for id in selected_ids])
|
||||||
|
|
||||||
|
sql = f'SELECT `{METADATA_TABLE.columns.id.name}`, `{METADATA_TABLE.columns.parent_dir.name}`, `{METADATA_TABLE.columns.file_name.name}`, `{METADATA_TABLE.columns.file_ctime.name}` FROM `{METADATA_TABLE.name}` WHERE `{METADATA_TABLE.columns.id.name}` IN ({selected_ids_str})'
|
||||||
|
someone_photos_result = metadata_server_api.query_rows(sql)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
return Response(someone_photos_result)
|
||||||
|
|
||||||
|
|
||||||
class FaceRecognitionManage(APIView):
|
class FaceRecognitionManage(APIView):
|
||||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
permission_classes = (IsAuthenticated, )
|
permission_classes = (IsAuthenticated, )
|
||||||
|
@ -47,6 +47,18 @@ def list_metadata_view_records(repo_id, user, view, start=0, limit=1000):
|
|||||||
metadata_server_api = MetadataServerAPI(repo_id, user)
|
metadata_server_api = MetadataServerAPI(repo_id, user)
|
||||||
columns = metadata_server_api.list_columns(METADATA_TABLE.id).get('columns')
|
columns = metadata_server_api.list_columns(METADATA_TABLE.id).get('columns')
|
||||||
sql = gen_view_data_sql(METADATA_TABLE, columns, view, start, limit, user)
|
sql = gen_view_data_sql(METADATA_TABLE, columns, view, start, limit, user)
|
||||||
|
|
||||||
|
# Remove face-vectors from the query SQL because they are too large
|
||||||
|
query_fields_str = ''
|
||||||
|
for column in columns:
|
||||||
|
column_name = column.get('name')
|
||||||
|
if column_name == METADATA_TABLE.columns.face_vectors.name:
|
||||||
|
continue
|
||||||
|
column_name_str = '`%s`, ' % column_name
|
||||||
|
query_fields_str += column_name_str
|
||||||
|
query_fields_str = query_fields_str.strip(', ')
|
||||||
|
sql = sql.replace('*', query_fields_str)
|
||||||
|
|
||||||
response_results = metadata_server_api.query_rows(sql, [])
|
response_results = metadata_server_api.query_rows(sql, [])
|
||||||
return response_results
|
return response_results
|
||||||
|
|
||||||
@ -70,7 +82,7 @@ class MetadataServerAPI:
|
|||||||
|
|
||||||
def gen_headers(self):
|
def gen_headers(self):
|
||||||
payload = {
|
payload = {
|
||||||
'exp': int(time.time()) + 3600,
|
'exp': int(time.time()) + 3600,
|
||||||
'base_id': self.base_id,
|
'base_id': self.base_id,
|
||||||
'user': self.user
|
'user': self.user
|
||||||
}
|
}
|
||||||
@ -88,7 +100,7 @@ class MetadataServerAPI:
|
|||||||
if response.status_code == 404:
|
if response.status_code == 404:
|
||||||
return {'success': True}
|
return {'success': True}
|
||||||
return parse_response(response)
|
return parse_response(response)
|
||||||
|
|
||||||
def insert_rows(self, table_id, rows):
|
def insert_rows(self, table_id, rows):
|
||||||
url = f'{METADATA_SERVER_URL}/api/v1/base/{self.base_id}/rows'
|
url = f'{METADATA_SERVER_URL}/api/v1/base/{self.base_id}/rows'
|
||||||
data = {
|
data = {
|
||||||
@ -97,7 +109,7 @@ class MetadataServerAPI:
|
|||||||
}
|
}
|
||||||
response = requests.post(url, json=data, headers=self.headers, timeout=self.timeout)
|
response = requests.post(url, json=data, headers=self.headers, timeout=self.timeout)
|
||||||
return parse_response(response)
|
return parse_response(response)
|
||||||
|
|
||||||
def update_rows(self, table_id, rows):
|
def update_rows(self, table_id, rows):
|
||||||
url = f'{METADATA_SERVER_URL}/api/v1/base/{self.base_id}/rows'
|
url = f'{METADATA_SERVER_URL}/api/v1/base/{self.base_id}/rows'
|
||||||
data = {
|
data = {
|
||||||
@ -153,7 +165,7 @@ class MetadataServerAPI:
|
|||||||
}
|
}
|
||||||
response = requests.post(url, json=data, headers=self.headers, timeout=self.timeout)
|
response = requests.post(url, json=data, headers=self.headers, timeout=self.timeout)
|
||||||
return parse_response(response)
|
return parse_response(response)
|
||||||
|
|
||||||
def delete_column(self, table_id, column_key):
|
def delete_column(self, table_id, column_key):
|
||||||
url = f'{METADATA_SERVER_URL}/api/v1/base/{self.base_id}/columns'
|
url = f'{METADATA_SERVER_URL}/api/v1/base/{self.base_id}/columns'
|
||||||
data = {
|
data = {
|
||||||
@ -162,7 +174,7 @@ class MetadataServerAPI:
|
|||||||
}
|
}
|
||||||
response = requests.delete(url, json=data, headers=self.headers, timeout=self.timeout)
|
response = requests.delete(url, json=data, headers=self.headers, timeout=self.timeout)
|
||||||
return parse_response(response)
|
return parse_response(response)
|
||||||
|
|
||||||
def update_column(self, table_id, column):
|
def update_column(self, table_id, column):
|
||||||
url = f'{METADATA_SERVER_URL}/api/v1/base/{self.base_id}/columns'
|
url = f'{METADATA_SERVER_URL}/api/v1/base/{self.base_id}/columns'
|
||||||
data = {
|
data = {
|
||||||
|
@ -29,24 +29,24 @@ def add_init_face_recognition_task(params):
|
|||||||
return json.loads(resp.content)['task_id']
|
return json.loads(resp.content)['task_id']
|
||||||
|
|
||||||
|
|
||||||
def get_metadata_by_faces(faces, metadata_server_api):
|
def get_someone_similar_faces(faces, metadata_server_api):
|
||||||
from seafevents.repo_metadata.utils import METADATA_TABLE, FACES_TABLE
|
from seafevents.repo_metadata.utils import METADATA_TABLE, FACES_TABLE
|
||||||
sql = f'SELECT * FROM `{METADATA_TABLE.name}` WHERE `{METADATA_TABLE.columns.id.name}` IN ('
|
sql = f'SELECT `{METADATA_TABLE.columns.id.name}`, `{METADATA_TABLE.columns.parent_dir.name}`, `{METADATA_TABLE.columns.file_name.name}` FROM `{METADATA_TABLE.name}` WHERE `{METADATA_TABLE.columns.id.name}` IN ('
|
||||||
parameters = []
|
parameters = []
|
||||||
query_result = []
|
query_result = []
|
||||||
for face in faces:
|
for face in faces:
|
||||||
link_row_ids = [item['row_id'] for item in face.get(FACES_TABLE.columns.photo_links.name, [])]
|
link_row_ids = [item['row_id'] for item in face.get(FACES_TABLE.columns.photo_links.name, [])]
|
||||||
if not link_row_ids:
|
if not link_row_ids:
|
||||||
continue
|
continue
|
||||||
for link_row_id in link_row_ids:
|
link_row_id = link_row_ids[0]
|
||||||
sql += '?, '
|
sql += '?, '
|
||||||
parameters.append(link_row_id)
|
parameters.append(link_row_id)
|
||||||
if len(parameters) >= 10000:
|
if len(parameters) >= 10000:
|
||||||
sql = sql.rstrip(', ') + ');'
|
sql = sql.rstrip(', ') + ');'
|
||||||
results = metadata_server_api.query_rows(sql, parameters).get('results', [])
|
results = metadata_server_api.query_rows(sql, parameters).get('results', [])
|
||||||
query_result.extend(results)
|
query_result.extend(results)
|
||||||
sql = f'SELECT * FROM `{METADATA_TABLE.name}` WHERE `{METADATA_TABLE.columns.id.name}` IN ('
|
sql = f'SELECT `{METADATA_TABLE.columns.id.name}`, `{METADATA_TABLE.columns.parent_dir.name}`, `{METADATA_TABLE.columns.file_name.name}` FROM `{METADATA_TABLE.name}` WHERE `{METADATA_TABLE.columns.id.name}` IN ('
|
||||||
parameters = []
|
parameters = []
|
||||||
|
|
||||||
if parameters:
|
if parameters:
|
||||||
sql = sql.rstrip(', ') + ');'
|
sql = sql.rstrip(', ') + ');'
|
||||||
|
@ -214,7 +214,7 @@ from seahub.api2.endpoints.wiki2 import Wikis2View, Wiki2View, Wiki2ConfigView,
|
|||||||
from seahub.api2.endpoints.subscription import SubscriptionView, SubscriptionPlansView, SubscriptionLogsView
|
from seahub.api2.endpoints.subscription import SubscriptionView, SubscriptionPlansView, SubscriptionLogsView
|
||||||
from seahub.api2.endpoints.metadata_manage import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecordInfo, \
|
from seahub.api2.endpoints.metadata_manage import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecordInfo, \
|
||||||
MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView, FacesRecords, \
|
MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView, FacesRecords, \
|
||||||
FaceRecognitionManage, FacesRecord, MetadataExtractFileDetails
|
FaceRecognitionManage, FacesRecord, MetadataExtractFileDetails, PeoplePhotos
|
||||||
from seahub.api2.endpoints.user_list import UserListView
|
from seahub.api2.endpoints.user_list import UserListView
|
||||||
from seahub.api2.endpoints.seahub_io import SeahubIOStatus
|
from seahub.api2.endpoints.seahub_io import SeahubIOStatus
|
||||||
|
|
||||||
@ -1060,6 +1060,7 @@ if settings.ENABLE_METADATA_MANAGEMENT:
|
|||||||
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/duplicate-view/$', MetadataViewsDuplicateView.as_view(), name='api-v2.1-metadata-view-duplicate'),
|
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/duplicate-view/$', MetadataViewsDuplicateView.as_view(), name='api-v2.1-metadata-view-duplicate'),
|
||||||
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/face-record/$', FacesRecord.as_view(), name='api-v2.1-metadata-face-record'),
|
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/face-record/$', FacesRecord.as_view(), name='api-v2.1-metadata-face-record'),
|
||||||
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/face-records/$', FacesRecords.as_view(), name='api-v2.1-metadata-face-records'),
|
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/face-records/$', FacesRecords.as_view(), name='api-v2.1-metadata-face-records'),
|
||||||
|
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/people-photos/(?P<people_id>.+)/$', PeoplePhotos.as_view(), name='api-v2.1-metadata-people-photos'),
|
||||||
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/face-recognition/$', FaceRecognitionManage.as_view(), name='api-v2.1-metadata-face-recognition'),
|
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/face-recognition/$', FaceRecognitionManage.as_view(), name='api-v2.1-metadata-face-recognition'),
|
||||||
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/extract-file-details/$', MetadataExtractFileDetails.as_view(), name='api-v2.1-metadata-extract-file-details'),
|
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/extract-file-details/$', MetadataExtractFileDetails.as_view(), name='api-v2.1-metadata-extract-file-details'),
|
||||||
]
|
]
|
||||||
|
Loading…
Reference in New Issue
Block a user