diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a3e828977a..7d0e2157d1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,12 +14,12 @@ "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@gatsbyjs/reach-router": "2.0.1", - "@seafile/react-image-lightbox": "^4.0.1", + "@seafile/react-image-lightbox": "4.0.2", "@seafile/resumablejs": "1.1.16", "@seafile/sdoc-editor": "^2.0.8", "@seafile/seafile-calendar": "0.0.28", "@seafile/seafile-editor": "2.0.0", - "@seafile/sf-metadata-ui-component": "^1.0.1", + "@seafile/sf-metadata-ui-component": "^1.0.2", "@seafile/stldraw-editor": "1.0.0", "@uiw/codemirror-extensions-langs": "^4.19.4", "@uiw/codemirror-themes": "^4.23.5", @@ -5213,10 +5213,9 @@ "license": "MIT" }, "node_modules/@seafile/react-image-lightbox": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@seafile/react-image-lightbox/-/react-image-lightbox-4.0.1.tgz", - "integrity": "sha512-4m1fMJWVertsvn4o7WqEfvMT8dW2hlm2Qp0CIxDPchRirIMcLQlHRWkMygWmU9m+FDOAw58YZr+9GJ3xbGn/4w==", - "license": "MIT", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@seafile/react-image-lightbox/-/react-image-lightbox-4.0.2.tgz", + "integrity": "sha512-rQy2X1JDltLE9hLcOQIee6dxW3UwUtWhjgbcOt/aq1BkqWG3mWzGdnHBUmFGfJMvwdbMgEPbDD3yDErBYy6P3w==", "dependencies": { "prop-types": "^15.8.1", "react-modal": "^3.16.1" @@ -5569,9 +5568,9 @@ } }, "node_modules/@seafile/sf-metadata-ui-component": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@seafile/sf-metadata-ui-component/-/sf-metadata-ui-component-1.0.1.tgz", - "integrity": "sha512-8uIfcVJMXz1pMe3dKcQOjlN5GB3gGX+dreTDgmLBJ8PyJgkLEGV0n3KdiQJMKLoLsHAj4uer8xZpSoKuf3LEiw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@seafile/sf-metadata-ui-component/-/sf-metadata-ui-component-1.0.2.tgz", + "integrity": "sha512-soyDV9z1E1wAeW1qFcLmSIRZucX60b1czFcth9yZX5TwPXYA51y07xeXBEPS6URQPQ+0Mx7fQ4eSCkssa/ZNkg==", "dependencies": { "@seafile/seafile-calendar": "0.0.28", "@seafile/seafile-editor": "2.0.0", diff --git a/frontend/package.json b/frontend/package.json index 95f48b9c51..bf68c69bbc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,12 +9,12 @@ "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@gatsbyjs/reach-router": "2.0.1", - "@seafile/react-image-lightbox": "4.0.1", + "@seafile/react-image-lightbox": "4.0.2", "@seafile/resumablejs": "1.1.16", "@seafile/sdoc-editor": "^2.0.8", "@seafile/seafile-calendar": "0.0.28", "@seafile/seafile-editor": "2.0.0", - "@seafile/sf-metadata-ui-component": "^1.0.1", + "@seafile/sf-metadata-ui-component": "^1.0.2", "@seafile/stldraw-editor": "1.0.0", "@uiw/codemirror-extensions-langs": "^4.19.4", "@uiw/codemirror-themes": "^4.23.5", diff --git a/frontend/src/assets/icons/left_arrow.svg b/frontend/src/assets/icons/left_arrow.svg new file mode 100644 index 0000000000..a1f6f436a0 --- /dev/null +++ b/frontend/src/assets/icons/left_arrow.svg @@ -0,0 +1,2 @@ + +view-back diff --git a/frontend/src/assets/icons/right_arrow.svg b/frontend/src/assets/icons/right_arrow.svg new file mode 100644 index 0000000000..ea25f85e22 --- /dev/null +++ b/frontend/src/assets/icons/right_arrow.svg @@ -0,0 +1,2 @@ + +view-forward diff --git a/frontend/src/components/dialog/image-dialog/index.css b/frontend/src/components/dialog/image-dialog/index.css new file mode 100644 index 0000000000..daf9c99574 --- /dev/null +++ b/frontend/src/components/dialog/image-dialog/index.css @@ -0,0 +1,101 @@ +.lightbox-side-panel { + width: 10px; + height: calc(100% - 100px); + display: flex; + flex-direction: column; + top: 50px; + right: 0; + position: absolute; + font-size: 1rem; + color: #fff; + background-color: #333; + border: 1px solid #666; + transition: width 0.3s ease; +} + +.lightbox-side-panel .cur-view-detail { + background-color: inherit; + border: none; +} + +.lightbox-side-panel .side-panel-controller { + display: flex; + justify-content: center; + align-items: center; + position: absolute; + top: 30px; + left: -40px; + width: 40px; + height: 48px; + background-color: #333; + color: #fff; + border: 1px solid #666; + border-right: none; + border-top-left-radius: 50%; + border-bottom-left-radius: 50%; + margin-right: -1px; + cursor: pointer; +} + +.lightbox-side-panel .side-panel-controller .expand-button { + width: 32px; + height: 32px; + opacity: 0.7; + border: none; +} + +.lightbox-side-panel .side-panel-controller:hover .expand-button { + opacity: 1; +} + +.lightbox-side-panel .detail-header { + width: 100%; + height: fit-content; + display: flex; + padding: 26px 16px; + border: none; + font-size: 1rem; +} + +.lightbox-side-panel .detail-header .detail-title .name, +.lightbox-side-panel .file-details-collapse .file-details-collapse-header .file-details-collapse-header-title, +.lightbox-side-panel .dirent-detail-item .dirent-detail-item-name, +.lightbox-side-panel .sf-metadata-number-property-detail-editor { + color: #fff; +} + +.lightbox-side-panel .detail-body { + scrollbar-color: #666 #333; + padding: 0 16px 8px; +} + +.lightbox-side-panel .file-details-collapse+.dirent-detail-people { + margin-top: 20px; +} + +.lightbox-side-panel .sf-metadata-ui.collaborator-item, +.lightbox-side-panel .sf-metadata-text-property-detail-editor:not(.formatter), +.lightbox-side-panel .sf-metadata-number-property-detail-editor:focus { + color: #212529; +} + +.lightbox-side-panel .sf-metadata-text-property-detail-editor:not(.formatter) { + cursor: text; +} + +.lightbox-side-panel .dirent-detail-item .dirent-detail-item-value:not(.editable) .sf-metadata-record-cell-empty:empty::before, +.lightbox-side-panel .sf-metadata-property-detail-editor:empty::before, +.lightbox-side-panel .sf-metadata-property-detail-capture-information-item .dirent-detail-item-value:empty::before, +.lightbox-side-panel .file-details-collapse .file-details-collapse-header .sf3-font-down, +.lightbox-side-panel .sf-metadata-number-property-detail-editor::placeholder { + color: #999; +} + +.lightbox-side-panel .file-details-collapse .file-details-collapse-header .file-details-collapse-header-operation:hover, +.lightbox-side-panel .dirent-detail-item .dirent-detail-item-value:hover { + background-color: #666; +} + +.lightbox-side-panel .dirent-detail-people { + border-top: 1px solid #999; +} diff --git a/frontend/src/components/dialog/image-dialog.js b/frontend/src/components/dialog/image-dialog/index.js similarity index 63% rename from frontend/src/components/dialog/image-dialog.js rename to frontend/src/components/dialog/image-dialog/index.js index 5c44fd903f..a5a7b4eb3b 100644 --- a/frontend/src/components/dialog/image-dialog.js +++ b/frontend/src/components/dialog/image-dialog/index.js @@ -1,13 +1,22 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useState } from 'react'; import PropTypes from 'prop-types'; -import { gettext } from '../../utils/constants'; +import { gettext } from '../../../utils/constants'; import Lightbox from '@seafile/react-image-lightbox'; -import { useMetadataAIOperations } from '../../hooks/metadata-ai-operation'; -import { SYSTEM_FOLDERS } from '../../constants'; +import { useMetadataAIOperations } from '../../../hooks/metadata-ai-operation'; +import EmbeddedFileDetails from '../../dirent-detail/embedded-file-details'; +import { SYSTEM_FOLDERS } from '../../../constants'; +import { Utils } from '../../../utils/utils'; +import Icon from '../../icon'; import '@seafile/react-image-lightbox/style.css'; +import './index.css'; + +const SIDE_PANEL_COLLAPSED_WIDTH = 10; +const SIDE_PANEL_EXPANDED_WIDTH = 300; + +const ImageDialog = ({ repoID, repoInfo, enableRotate: oldEnableRotate = true, imageItems, imageIndex, closeImagePopup, moveToPrevImage, moveToNextImage, onDeleteImage, onRotateImage, isCustomPermission }) => { + const [expanded, setExpanded] = useState(false); -const ImageDialog = ({ enableRotate: oldEnableRotate = true, imageItems, imageIndex, closeImagePopup, moveToPrevImage, moveToNextImage, onDeleteImage, onRotateImage }) => { const { enableOCR, enableMetadata, canModify, onOCR: onOCRAPI, OCRSuccessCallBack } = useMetadataAIOperations(); const downloadImage = useCallback((url) => { @@ -18,8 +27,13 @@ const ImageDialog = ({ enableRotate: oldEnableRotate = true, imageItems, imageIn window.open(imageItems[imageIndex].url, '_blank'); }, [imageItems, imageIndex]); + const onToggleSidePanel = useCallback(() => { + setExpanded(!expanded); + }, [expanded]); + const imageItemsLength = imageItems.length; if (imageItemsLength === 0) return null; + const id = imageItems[imageIndex].id; const name = imageItems[imageIndex].name; const mainImg = imageItems[imageIndex]; const nextImg = imageItems[(imageIndex + 1) % imageItemsLength]; @@ -39,6 +53,25 @@ const ImageDialog = ({ enableRotate: oldEnableRotate = true, imageItems, imageIn onOCR = () => onOCRAPI({ parentDir: mainImg.parentDir, fileName: mainImg.name }, { success_callback: OCRSuccessCallBack }); } + const renderSidePanel = () => { + const dirent = { id, name, type: 'file' }; + const path = Utils.joinPath(mainImg.parentDir, name); + + return ( +
+
+ +
+ {expanded && ()} +
+ + ); + }; + return ( onRotateImage(imageIndex, angle) : null} onOCR={onOCR} OCRLabel={gettext('OCR')} + sidePanel={!isCustomPermission ? { + render: renderSidePanel, + width: expanded ? SIDE_PANEL_EXPANDED_WIDTH : SIDE_PANEL_COLLAPSED_WIDTH, + } : null} /> ); }; diff --git a/frontend/src/components/dialog/lib-settings.js b/frontend/src/components/dialog/lib-settings.js index e1d1e4670a..bc2b3e53c7 100644 --- a/frontend/src/components/dialog/lib-settings.js +++ b/frontend/src/components/dialog/lib-settings.js @@ -46,8 +46,8 @@ const LibSettingsDialog = ({ repoID, currentRepoInfo, toggleDialog, tab }) => { const { encrypted, is_admin } = currentRepoInfo; const { enableMetadataManagement } = window.app.pageOptions; - const { enableFaceRecognition, updateEnableFaceRecognition } = useMetadata(); - const { enableMetadata, updateEnableMetadata, enableTags, tagsLang, updateEnableTags, enableOCR, updateEnableOCR } = useMetadataStatus(); + const { updateEnableFaceRecognition } = useMetadata(); + const { enableMetadata, updateEnableMetadata, enableTags, tagsLang, updateEnableTags, enableOCR, updateEnableOCR, enableFaceRecognition } = useMetadataStatus(); const enableHistorySetting = is_admin; // repo owner, admin of the department which the repo belongs to, and ... const enableAutoDelSetting = is_admin && enableRepoAutoDel; const enableExtendedPropertiesSetting = !encrypted && is_admin && enableMetadataManagement; diff --git a/frontend/src/components/dir-view-mode/dir-files.js b/frontend/src/components/dir-view-mode/dir-files.js index 8b576ad094..f6a57ba119 100644 --- a/frontend/src/components/dir-view-mode/dir-files.js +++ b/frontend/src/components/dir-view-mode/dir-files.js @@ -257,6 +257,7 @@ class DirFiles extends React.Component { thumbnail = `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForOriginal}${path}`; } return { + id: item.object.id, name, parentDir: node.parentNode.path, src, @@ -364,12 +365,11 @@ class DirFiles extends React.Component { render() { const { repoID, currentRepoInfo, userPerm } = this.props; const { encrypted: repoEncrypted } = currentRepoInfo; - + const { isCustomPermission, customPermission } = Utils.getUserPermission(userPerm); let canModifyFile = false; if (['rw', 'cloud-edit'].indexOf(userPerm) != -1) { canModifyFile = true; } else { - const { isCustomPermission, customPermission } = Utils.getUserPermission(userPerm); if (isCustomPermission) { const { modify } = customPermission.permission; canModifyFile = modify; @@ -461,6 +461,8 @@ class DirFiles extends React.Component { {this.state.isNodeImagePopupOpen && ( )} diff --git a/frontend/src/components/dirent-detail/detail/header/index.js b/frontend/src/components/dirent-detail/detail/header/index.js index 1ff83603a8..499db364f5 100644 --- a/frontend/src/components/dirent-detail/detail/header/index.js +++ b/frontend/src/components/dirent-detail/detail/header/index.js @@ -10,12 +10,16 @@ const Header = ({ title, icon, iconSize = 32, onClose, children, component = {} return (
- <div className="detail-control-container"> - {children} - <div className="detail-control" onClick={onClose}> - {closeIcon ? closeIcon : <Icon symbol="close" className="detail-control-close" />} + {(children || onClose) && ( + <div className="detail-control-container"> + {children} + {onClose && ( + <div className="detail-control" onClick={onClose}> + {closeIcon ? closeIcon : <Icon symbol="close" className="detail-control-close" />} + </div> + )} </div> - </div> + )} </div> ); }; @@ -26,7 +30,7 @@ Header.propTypes = { iconSize: PropTypes.number, component: PropTypes.object, children: PropTypes.any, - onClose: PropTypes.func.isRequired, + onClose: PropTypes.func, }; export default Header; diff --git a/frontend/src/components/dirent-detail/dirent-details/file-details/file-tag.js b/frontend/src/components/dirent-detail/dirent-details/file-details/file-tag.js new file mode 100644 index 0000000000..2d4c256ec6 --- /dev/null +++ b/frontend/src/components/dirent-detail/dirent-details/file-details/file-tag.js @@ -0,0 +1,61 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import PropTypes from 'prop-types'; +import { v4 as uuidV4 } from 'uuid'; +import classnames from 'classnames'; +import { getDirentPath } from '../utils'; +import { gettext } from '../../../../utils/constants'; +import EditFileTagPopover from '../../../popover/edit-filetag-popover'; +import FileTagList from '../../../file-tag-list'; + +const FileTag = ({ repoID, dirent, path, repoTags, fileTagList, onFileTagChanged }) => { + const [isEditFileTagShow, setEditFileTagShow] = useState(false); + const direntPath = useMemo(() => getDirentPath(dirent, path), [dirent, path]); + const tagListTitleID = useMemo(() => `detail-list-view-tags-${uuidV4()}`, []); + + const onEditFileTagToggle = useCallback(() => { + setEditFileTagShow(!isEditFileTagShow); + }, [isEditFileTagShow]); + + const fileTagChanged = useCallback(() => { + onFileTagChanged(dirent, direntPath); + }, [dirent, direntPath, onFileTagChanged]); + + return ( + <> + <div + className={classnames('sf-metadata-property-detail-tags', { 'tags-empty': !Array.isArray(fileTagList) || fileTagList.length === 0 })} + id={tagListTitleID} + onClick={onEditFileTagToggle} + > + {Array.isArray(fileTagList) && fileTagList.length > 0 ? ( + <FileTagList fileTagList={fileTagList} /> + ) : ( + <span className="empty-tip-text">{gettext('Empty')}</span> + )} + </div> + {isEditFileTagShow && + <EditFileTagPopover + repoID={repoID} + repoTags={repoTags} + filePath={direntPath} + fileTagList={fileTagList} + toggleCancel={onEditFileTagToggle} + onFileTagChanged={fileTagChanged} + target={tagListTitleID} + /> + } + </> + ); +}; + +FileTag.propTypes = { + repoID: PropTypes.string, + dirent: PropTypes.object, + path: PropTypes.string, + direntDetail: PropTypes.object, + repoTags: PropTypes.array, + fileTagList: PropTypes.array, + onFileTagChanged: PropTypes.func, +}; + +export default FileTag; diff --git a/frontend/src/components/dirent-detail/dirent-details/file-details/index.js b/frontend/src/components/dirent-detail/dirent-details/file-details/index.js index 0e1e433dca..ebdbb466c6 100644 --- a/frontend/src/components/dirent-detail/dirent-details/file-details/index.js +++ b/frontend/src/components/dirent-detail/dirent-details/file-details/index.js @@ -1,14 +1,9 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; -import { v4 as uuidV4 } from 'uuid'; import { Formatter } from '@seafile/sf-metadata-ui-component'; -import classnames from 'classnames'; -import { getDirentPath } from '../utils'; import DetailItem from '../../detail-item'; import { CellType, PRIVATE_COLUMN_KEY } from '../../../../metadata/constants'; import { gettext } from '../../../../utils/constants'; -import EditFileTagPopover from '../../../popover/edit-filetag-popover'; -import FileTagList from '../../../file-tag-list'; import { Utils } from '../../../../utils/utils'; import { MetadataDetails, useMetadataDetails } from '../../../../metadata'; import ObjectUtils from '../../../../metadata/utils/object-utils'; @@ -16,6 +11,8 @@ import { getCellValueByColumn, getDateDisplayString, decimalToExposureTime } fro import Collapse from './collapse'; import { useMetadataStatus } from '../../../../hooks'; import { CAPTURE_INFO_SHOW_KEY } from '../../../../constants'; +import People from '../../people'; +import FileTag from './file-tag'; import './index.css'; @@ -58,14 +55,11 @@ const getImageInfoValue = (key, value) => { } }; -const FileDetails = React.memo(({ repoID, dirent, path, direntDetail, onFileTagChanged, repoTags, fileTagList }) => { - const [isEditFileTagShow, setEditFileTagShow] = useState(false); +const FileDetails = React.memo(({ repoID, dirent, path, direntDetail, isShowRepoTags = true, repoTags, fileTagList, onFileTagChanged }) => { const [isCaptureInfoShow, setCaptureInfoShow] = useState(false); - const { enableMetadataManagement, enableMetadata } = useMetadataStatus(); + const { enableFaceRecognition, enableMetadata } = useMetadataStatus(); const { record } = useMetadataDetails(); - const direntPath = useMemo(() => getDirentPath(dirent, path), [dirent, path]); - const tagListTitleID = useMemo(() => `detail-list-view-tags-${uuidV4()}`, []); const sizeField = useMemo(() => ({ type: 'size', name: gettext('Size') }), []); const lastModifierField = useMemo(() => ({ type: CellType.LAST_MODIFIER, name: gettext('Last modifier') }), []); const lastModifiedTimeField = useMemo(() => ({ type: CellType.MTIME, name: gettext('Last modified time') }), []); @@ -76,14 +70,6 @@ const FileDetails = React.memo(({ repoID, dirent, path, direntDetail, onFileTagC setCaptureInfoShow(savedValue); }, []); - const onEditFileTagToggle = useCallback(() => { - setEditFileTagShow(!isEditFileTagShow); - }, [isEditFileTagShow]); - - const fileTagChanged = useCallback(() => { - onFileTagChanged(dirent, direntPath); - }, [dirent, direntPath, onFileTagChanged]); - const dom = ( <> <DetailItem field={sizeField} className="sf-metadata-property-detail-formatter"> @@ -104,22 +90,12 @@ const FileDetails = React.memo(({ repoID, dirent, path, direntDetail, onFileTagC <DetailItem field={lastModifiedTimeField} className="sf-metadata-property-detail-formatter"> <Formatter field={lastModifiedTimeField} value={direntDetail.last_modified}/> </DetailItem> - {window.app.pageOptions.enableFileTags && !enableMetadata && ( + {isShowRepoTags && window.app.pageOptions.enableFileTags && !enableMetadata && ( <DetailItem field={tagsField} className="sf-metadata-property-detail-formatter"> - <div - className={classnames('sf-metadata-property-detail-tags', { 'tags-empty': !Array.isArray(fileTagList) || fileTagList.length === 0 })} - id={tagListTitleID} - onClick={onEditFileTagToggle} - > - {Array.isArray(fileTagList) && fileTagList.length > 0 ? ( - <FileTagList fileTagList={fileTagList} /> - ) : ( - <span className="empty-tip-text">{gettext('Empty')}</span> - )} - </div> + <FileTag repoID={repoID} dirent={dirent} path={path} repoTags={repoTags} fileTagList={fileTagList} onFileTagChanged={onFileTagChanged} /> </DetailItem> )} - {enableMetadataManagement && enableMetadata && ( + {enableMetadata && ( <MetadataDetails /> )} </> @@ -154,22 +130,15 @@ const FileDetails = React.memo(({ repoID, dirent, path, direntDetail, onFileTagC return ( <> {component} - {isEditFileTagShow && - <EditFileTagPopover - repoID={repoID} - repoTags={repoTags} - filePath={direntPath} - fileTagList={fileTagList} - toggleCancel={onEditFileTagToggle} - onFileTagChanged={fileTagChanged} - target={tagListTitleID} - /> - } + {enableFaceRecognition && Utils.imageCheck(dirent.name) && ( + <People repoID={repoID} record={record} /> + )} </> ); }, (props, nextProps) => { - const { repoID, repoInfo, dirent, path, direntDetail, repoTags, fileTagList } = props; + const { repoID, repoInfo, dirent, path, direntDetail, isShowRepoTags, repoTags, fileTagList } = props; const isChanged = ( + isShowRepoTags !== nextProps.isShowRepoTags || repoID !== nextProps.repoID || path !== nextProps.path || !ObjectUtils.isSameObject(repoInfo, nextProps.repoInfo) || @@ -182,11 +151,14 @@ const FileDetails = React.memo(({ repoID, dirent, path, direntDetail, onFileTagC }); FileDetails.propTypes = { + isShowRepoTags: PropTypes.bool, repoID: PropTypes.string, repoInfo: PropTypes.object, dirent: PropTypes.object, path: PropTypes.string, direntDetail: PropTypes.object, + repoTags: PropTypes.array, + fileTagList: PropTypes.array, onFileTagChanged: PropTypes.func, }; diff --git a/frontend/src/components/dirent-detail/embedded-file-details/file-details.js b/frontend/src/components/dirent-detail/embedded-file-details/file-details.js deleted file mode 100644 index 2ed2ae5894..0000000000 --- a/frontend/src/components/dirent-detail/embedded-file-details/file-details.js +++ /dev/null @@ -1,49 +0,0 @@ -import React, { useMemo } from 'react'; -import PropTypes from 'prop-types'; -import { Formatter } from '@seafile/sf-metadata-ui-component'; -import DetailItem from '../detail-item'; -import { CellType } from '../../../metadata/constants'; -import { gettext } from '../../../utils/constants'; -import { Utils } from '../../../utils/utils'; -import { MetadataDetails } from '../../../metadata'; -import { useMetadataStatus } from '../../../hooks'; - -const FileDetails = ({ direntDetail }) => { - const { enableMetadata } = useMetadataStatus(); - - const sizeField = useMemo(() => ({ type: 'size', name: gettext('Size') }), []); - const lastModifierField = useMemo(() => ({ type: CellType.LAST_MODIFIER, name: gettext('Last modifier') }), []); - const lastModifiedTimeField = useMemo(() => ({ type: CellType.MTIME, name: gettext('Last modified time') }), []); - - return ( - <> - <DetailItem field={sizeField} className="sf-metadata-property-detail-formatter"> - <Formatter field={sizeField} value={Utils.bytesToSize(direntDetail.size)} /> - </DetailItem> - <DetailItem field={lastModifierField} className="sf-metadata-property-detail-formatter"> - <Formatter - field={lastModifierField} - value={direntDetail.last_modifier_email} - collaborators={[{ - name: direntDetail.last_modifier_name, - contact_email: direntDetail.last_modifier_contact_email, - email: direntDetail.last_modifier_email, - avatar_url: direntDetail.last_modifier_avatar, - }]} - /> - </DetailItem > - <DetailItem field={lastModifiedTimeField} className="sf-metadata-property-detail-formatter"> - <Formatter field={lastModifiedTimeField} value={direntDetail.last_modified}/> - </DetailItem> - {enableMetadata && ( - <MetadataDetails /> - )} - </> - ); -}; - -FileDetails.propTypes = { - direntDetail: PropTypes.object, -}; - -export default FileDetails; diff --git a/frontend/src/components/dirent-detail/embedded-file-details/index.js b/frontend/src/components/dirent-detail/embedded-file-details/index.js index 26dcdc6d4f..c42a48e9fb 100644 --- a/frontend/src/components/dirent-detail/embedded-file-details/index.js +++ b/frontend/src/components/dirent-detail/embedded-file-details/index.js @@ -1,11 +1,11 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { seafileAPI } from '../../../utils/seafile-api'; import { Utils } from '../../../utils/utils'; import toaster from '../../toast'; import { Header, Body } from '../detail'; -import FileDetails from './file-details'; +import FileDetails from '../dirent-details/file-details'; import { MetadataContext } from '../../../metadata'; import { MetadataDetailsProvider } from '../../../metadata/hooks'; import { AI, Settings } from '../../../metadata/components/metadata-details'; @@ -16,20 +16,38 @@ const EmbeddedFileDetails = ({ repoID, repoInfo, dirent, path, onClose, width = const { headerComponent } = component; const [direntDetail, setDirentDetail] = useState(''); + const isView = useMemo(() => { + const urlParams = new URLSearchParams(window.location.search); + return urlParams.has('view'); + }, []); + + const isTag = useMemo(() => { + const urlParams = new URLSearchParams(window.location.search); + return urlParams.has('tag'); + }, []); + useEffect(() => { - // init context - const context = new MetadataContext(); - window.sfMetadataContext = context; - window.sfMetadataContext.init({ repoID, repoInfo }); seafileAPI.getFileInfo(repoID, path).then(res => { setDirentDetail(res.data); }).catch(error => { const errMessage = Utils.getErrorMsg(error); toaster.danger(errMessage); }); + }, [repoID, path]); + + useEffect(() => { + if (isView || isTag) return; + + let isExistContext = true; + if (!window.sfMetadataContext) { + const context = new MetadataContext(); + window.sfMetadataContext = context; + window.sfMetadataContext.init({ repoID, repoInfo }); + isExistContext = false; + } return () => { - if (window.sfMetadataContext) { + if (window.sfMetadataContext && !isExistContext) { window.sfMetadataContext.destroy(); delete window['sfMetadataContext']; } @@ -53,14 +71,18 @@ const EmbeddedFileDetails = ({ repoID, repoInfo, dirent, path, onClose, width = })} style={{ width }} > - <Header title={dirent?.name || ''} icon={Utils.getDirentIcon(dirent, true)} onClose={onClose} component={headerComponent} > - <AI /> - <Settings /> + <Header title={dirent?.name || ''} icon={Utils.getDirentIcon(dirent, true)} onClose={onClose} component={headerComponent}> + {onClose && ( + <> + <AI /> + <Settings /> + </> + )} </Header> <Body> {dirent && direntDetail && ( <div className="detail-content"> - <FileDetails direntDetail={direntDetail} /> + <FileDetails repoID={repoID} isShowRepoTags={false} dirent={dirent} direntDetail={direntDetail} /> </div> )} </Body> @@ -75,7 +97,7 @@ EmbeddedFileDetails.propTypes = { path: PropTypes.string.isRequired, repoInfo: PropTypes.object.isRequired, component: PropTypes.object, - onClose: PropTypes.func.isRequired, + onClose: PropTypes.func, }; export default EmbeddedFileDetails; diff --git a/frontend/src/components/dirent-detail/people/index.css b/frontend/src/components/dirent-detail/people/index.css new file mode 100644 index 0000000000..f6807ae5a9 --- /dev/null +++ b/frontend/src/components/dirent-detail/people/index.css @@ -0,0 +1,15 @@ +.dirent-detail-people { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 20px; + border-top: 1px solid #eee; + padding-top: 20px; +} + +.dirent-detail-people .dirent-detail-people-item { + width: 32px; + height: 32px; + border-radius: 50%; + overflow: hidden; +} diff --git a/frontend/src/components/dirent-detail/people/index.js b/frontend/src/components/dirent-detail/people/index.js new file mode 100644 index 0000000000..7ae590d7ee --- /dev/null +++ b/frontend/src/components/dirent-detail/people/index.js @@ -0,0 +1,36 @@ +import React, { useCallback, useMemo } from 'react'; +import { PRIVATE_COLUMN_KEY } from '../../../metadata/constants'; +import { gettext, mediaUrl, siteRoot, thumbnailDefaultSize } from '../../../utils/constants'; +import { getCellValueByColumn } from '../../../metadata/utils/cell'; + +import './index.css'; + +const People = ({ repoID, record }) => { + const images = useMemo(() => { + if (!record) return []; + const faceLinks = getCellValueByColumn(record, { key: PRIVATE_COLUMN_KEY.FACE_LINKS }); + if (!faceLinks) return []; + return faceLinks.map(item => ({ + ...item, + url: `${siteRoot}thumbnail/${repoID}/${thumbnailDefaultSize}/_Internal/Faces/${item.row_id}.jpg` + })); + }, [repoID, record]); + + const onImgLoadError = useCallback((e) => { + e.target.src = `${mediaUrl}avatars/default.png`; + }, []); + + if (!images.length) return null; + + return ( + <div className="dirent-detail-people"> + {images.map(img => ( + <div className="dirent-detail-people-item" key={img.row_id} title={img.display_value || gettext('Unknown people')}> + <img src={img.url} alt={img.display_value || gettext('Unknown people')} onError={onImgLoadError} height={32} width={32} /> + </div> + ))} + </div> + ); +}; + +export default People; diff --git a/frontend/src/components/dirent-grid-view/dirent-grid-view.js b/frontend/src/components/dirent-grid-view/dirent-grid-view.js index 75e26fad5c..85ffbaaa7d 100644 --- a/frontend/src/components/dirent-grid-view/dirent-grid-view.js +++ b/frontend/src/components/dirent-grid-view/dirent-grid-view.js @@ -6,7 +6,7 @@ import { seafileAPI } from '../../utils/seafile-api'; import URLDecorator from '../../utils/url-decorator'; import Loading from '../loading'; import ModalPortal from '../modal-portal'; -import ImageDialog from '../../components/dialog/image-dialog'; +import ImageDialog from '../dialog/image-dialog'; import DirentGridItem from '../../components/dirent-grid-view/dirent-grid-item'; import ContextMenu from '../context-menu/context-menu'; import { hideMenu, showMenu } from '../context-menu/actions'; @@ -618,6 +618,7 @@ class DirentGridView extends React.Component { } return { + id: item.id, name, thumbnail, src, @@ -860,11 +861,11 @@ class DirentGridView extends React.Component { let canModifyFile = false; let canDeleteFile = false; + const { isCustomPermission, customPermission } = Utils.getUserPermission(userPerm); if (['rw', 'cloud-edit'].indexOf(userPerm) != -1) { canModifyFile = true; canDeleteFile = true; } else { - const { isCustomPermission, customPermission } = Utils.getUserPermission(userPerm); if (isCustomPermission) { const { modify, delete: canDelete } = customPermission.permission; canModifyFile = modify; @@ -1042,6 +1043,8 @@ class DirentGridView extends React.Component { {this.state.isImagePopupOpen && this.state.imageItems.length && ( <ModalPortal> <ImageDialog + repoID={this.props.repoID} + repoInfo={this.props.currentRepoInfo} imageItems={this.state.imageItems} imageIndex={this.state.imageIndex} closeImagePopup={this.closeImagePopup} @@ -1050,6 +1053,7 @@ class DirentGridView extends React.Component { onDeleteImage={(canDeleteFile && this.deleteImage) ? this.deleteImage : null} onRotateImage={this.rotateImage} enableRotate={canModifyFile} + isCustomPermission={isCustomPermission} /> </ModalPortal> )} diff --git a/frontend/src/components/dirent-list-view/dirent-list-view.js b/frontend/src/components/dirent-list-view/dirent-list-view.js index fdbb4827ff..b63c1a574f 100644 --- a/frontend/src/components/dirent-list-view/dirent-list-view.js +++ b/frontend/src/components/dirent-list-view/dirent-list-view.js @@ -205,6 +205,7 @@ class DirentListView extends React.Component { } return { + id: item.id, name, thumbnail, src, @@ -765,11 +766,11 @@ class DirentListView extends React.Component { let canModifyFile = false; let canDeleteFile = false; + const { isCustomPermission, customPermission } = Utils.getUserPermission(userPerm); if (['rw', 'cloud-edit'].indexOf(userPerm) != -1) { canModifyFile = true; canDeleteFile = true; } else { - const { isCustomPermission, customPermission } = Utils.getUserPermission(userPerm); if (isCustomPermission) { const { modify, delete: canDelete } = customPermission.permission; canModifyFile = modify; @@ -872,6 +873,8 @@ class DirentListView extends React.Component { {this.state.isImagePopupOpen && ( <ModalPortal> <ImageDialog + repoID={this.props.repoID} + repoInfo={this.props.currentRepoInfo} imageItems={this.state.imageItems} imageIndex={this.state.imageIndex} closeImagePopup={this.closeImagePopup} @@ -880,6 +883,7 @@ class DirentListView extends React.Component { onDeleteImage={(canDeleteFile && this.deleteImage) ? this.deleteImage : null} onRotateImage={this.rotateImage} enableRotate={canModifyFile} + isCustomPermission={isCustomPermission} /> </ModalPortal> )} diff --git a/frontend/src/hooks/metadata-status.js b/frontend/src/hooks/metadata-status.js index 16458cd8a4..a7dce0a5a0 100644 --- a/frontend/src/hooks/metadata-status.js +++ b/frontend/src/hooks/metadata-status.js @@ -21,6 +21,7 @@ export const MetadataStatusProvider = ({ repoID, repoInfo, hideMetadataView, chi const [enableOCR, setEnableOCR] = useState(false); const [detailsSettings, setDetailsSettings] = useState({}); const [isBeingBuilt, setIsBeingBuilt] = useState(false); + const [enableFaceRecognition, setEnableFaceRecognition] = useState(false); const cancelMetadataURL = useCallback((isSetRoot = false) => { // If attribute extension is turned off, unmark the URL @@ -39,6 +40,7 @@ export const MetadataStatusProvider = ({ repoID, repoInfo, hideMetadataView, chi setEnableMetadata(false); setEnableTags(false); setEnableOCR(false); + setEnableFaceRecognition(false); setDetailsSettings({}); setIsBeingBuilt(false); if (!enableMetadataManagement) { @@ -52,7 +54,8 @@ export const MetadataStatusProvider = ({ repoID, repoInfo, hideMetadataView, chi tags_enabled: enableTags, tags_lang: tagsLang, details_settings: detailsSettings, - ocr_enabled: enableOCR + ocr_enabled: enableOCR, + face_recognition_enabled: enableFaceRecognition, } = res.data; if (!enableMetadata) { cancelMetadataURL(); @@ -61,6 +64,7 @@ export const MetadataStatusProvider = ({ repoID, repoInfo, hideMetadataView, chi setTagsLang(tagsLang || 'en'); setDetailsSettings(JSON.parse(detailsSettings)); setEnableOCR(enableOCR); + setEnableFaceRecognition(enableFaceRecognition); setEnableMetadata(enableMetadata); setLoading(false); }).catch(error => { @@ -97,6 +101,11 @@ export const MetadataStatusProvider = ({ repoID, repoInfo, hideMetadataView, chi setEnableOCR(newValue); }, [enableOCR]); + const updateEnableFaceRecognition = useCallback((newValue) => { + if (newValue === enableFaceRecognition) return; + setEnableFaceRecognition(newValue); + }, [enableFaceRecognition]); + const modifyDetailsSettings = useCallback((update) => { metadataAPI.modifyMetadataDetailsSettings(repoID, update).then(res => { const newDetailsSettings = { ...detailsSettings, ...update }; @@ -122,6 +131,8 @@ export const MetadataStatusProvider = ({ repoID, repoInfo, hideMetadataView, chi modifyDetailsSettings, enableOCR, updateEnableOCR, + enableFaceRecognition, + updateEnableFaceRecognition, }} > {!isLoading && ( diff --git a/frontend/src/metadata/components/cell-editors/long-text-editor/index.css b/frontend/src/metadata/components/cell-editors/long-text-editor/index.css deleted file mode 100644 index 739dd34b72..0000000000 --- a/frontend/src/metadata/components/cell-editors/long-text-editor/index.css +++ /dev/null @@ -1,3 +0,0 @@ -.sf-metadata-long-text-editor-dialog { - z-index: 1049 !important; -} diff --git a/frontend/src/metadata/components/cell-editors/long-text-editor/index.js b/frontend/src/metadata/components/cell-editors/long-text-editor/index.js index dafacbdf70..1b9e6f5c75 100644 --- a/frontend/src/metadata/components/cell-editors/long-text-editor/index.js +++ b/frontend/src/metadata/components/cell-editors/long-text-editor/index.js @@ -9,8 +9,6 @@ import { lang, serviceURL } from '../../../../utils/constants'; import { LONG_TEXT_EXCEED_LIMIT_MESSAGE, LONG_TEXT_EXCEED_LIMIT_SUGGEST } from '../../../constants'; import i18n from '../../../../_i18n/i18n-seafile-editor'; -import './index.css'; - class LongTextEditor extends React.PureComponent { constructor(props) { diff --git a/frontend/src/metadata/components/cell-formatter/image-previewer.js b/frontend/src/metadata/components/cell-formatter/image-previewer.js index 0c3145f77c..ed64e89bca 100644 --- a/frontend/src/metadata/components/cell-formatter/image-previewer.js +++ b/frontend/src/metadata/components/cell-formatter/image-previewer.js @@ -98,6 +98,8 @@ const ImagePreviewer = ({ record, table, repoID, repoInfo, closeImagePopup, dele return ( <ModalPortal> <ImageDialog + repoID={repoID} + repoInfo={repoInfo} imageItems={imageItems} imageIndex={imageIndex} closeImagePopup={closeImagePopup} diff --git a/frontend/src/metadata/components/metadata-details/location/index.js b/frontend/src/metadata/components/metadata-details/location/index.js index 02d98d3d6d..bd3029706c 100644 --- a/frontend/src/metadata/components/metadata-details/location/index.js +++ b/frontend/src/metadata/components/metadata-details/location/index.js @@ -104,7 +104,7 @@ class Location extends React.Component { const gcPosition = wgs84_to_gcj02(position.lng, position.lat); const bdPosition = gcj02_to_bd09(gcPosition.lng, gcPosition.lat); const { lng, lat } = bdPosition; - this.map = new window.BMapGL.Map('sf-geolocation-map-container', { enableMapClick: false }); + this.map = new window.BMapGL.Map(this.ref, { enableMapClick: false }); const point = new window.BMapGL.Point(lng, lat); this.map.centerAndZoom(point, 16); this.map.enableScrollWheelZoom(true); diff --git a/frontend/src/metadata/hooks/metadata-details.js b/frontend/src/metadata/hooks/metadata-details.js index 37ad11052c..60a48fb22e 100644 --- a/frontend/src/metadata/hooks/metadata-details.js +++ b/frontend/src/metadata/hooks/metadata-details.js @@ -72,6 +72,7 @@ export const MetadataDetailsProvider = ({ repoID, repoInfo, path, dirent, dirent setRecord({ ...record, ...update }); if (window?.sfMetadataContext?.eventBus) { window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.LOCAL_RECORD_CHANGED, { recordId }, update); + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.LOCAL_RECORD_DETAIL_CHANGED, recordId, update); } }).catch(error => { const errorMsg = Utils.getErrorMsg(error); diff --git a/frontend/src/metadata/hooks/metadata.js b/frontend/src/metadata/hooks/metadata.js index a295d3e04f..9a5a867391 100644 --- a/frontend/src/metadata/hooks/metadata.js +++ b/frontend/src/metadata/hooks/metadata.js @@ -17,14 +17,13 @@ const MetadataContext = React.createContext(null); export const MetadataProvider = ({ repoID, currentPath, repoInfo, selectMetadataView, children }) => { const [isLoading, setLoading] = useState(true); - const [enableFaceRecognition, setEnableFaceRecognition] = useState(false); const [navigation, setNavigation] = useState([]); const [idViewMap, setIdViewMap] = useState({}); const collapsedFoldersIds = useRef([]); const originalTitleRef = useRef(document.title); - const { enableMetadata, isBeingBuilt, setIsBeingBuilt } = useMetadataStatus(); + const { enableMetadata, enableFaceRecognition, isBeingBuilt, setIsBeingBuilt, updateEnableFaceRecognition: updateEnableFaceRecognitionAPI } = useMetadataStatus(); const getCollapsedFolders = useCallback(() => { const strFoldedFolders = window.localStorage.getItem(`${CACHED_COLLAPSED_FOLDERS_PREFIX}-${repoID}`); @@ -70,27 +69,12 @@ export const MetadataProvider = ({ repoID, currentPath, repoInfo, selectMetadata }); return; } - setEnableFaceRecognition(false); setNavigation([]); setIdViewMap({}); setLoading(false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [enableMetadata]); - useEffect(() => { - if (!enableMetadata) { - setEnableFaceRecognition(false); - return; - } - metadataAPI.getFaceRecognitionStatus(repoID).then(res => { - setEnableFaceRecognition(res.data.enabled); - }).catch(error => { - const errorMsg = Utils.getErrorMsg(error); - toaster.danger(errorMsg); - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [enableMetadata]); - const getFirstView = useCallback(() => { const firstViewNav = navigation.find(item => item.type === VIEWS_TYPE_VIEW); const firstView = firstViewNav ? idViewMap[firstViewNav._id] : null; @@ -394,8 +378,8 @@ export const MetadataProvider = ({ repoID, currentPath, repoInfo, selectMetadata deleteView({ folderId, viewId: FACE_RECOGNITION_VIEW_ID, isSelected }); } } - setEnableFaceRecognition(newValue); - }, [enableFaceRecognition, currentPath, idViewMap, navigation, addView, deleteView]); + updateEnableFaceRecognitionAPI(newValue); + }, [enableFaceRecognition, currentPath, idViewMap, navigation, addView, deleteView, updateEnableFaceRecognitionAPI]); const modifyViewType = useCallback((viewId, update) => { metadataAPI.modifyView(repoID, viewId, update).then(res => { diff --git a/frontend/src/metadata/views/gallery/main.js b/frontend/src/metadata/views/gallery/main.js index 15f7b6fcd8..df185df183 100644 --- a/frontend/src/metadata/views/gallery/main.js +++ b/frontend/src/metadata/views/gallery/main.js @@ -35,6 +35,7 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, const lastState = useRef({ scrollPos: 0 }); const { repoID, updateCurrentDirent } = useMetadataView(); + const repoInfo = window.sfMetadataContext.getSetting('repoInfo'); const images = useMemo(() => { if (isFirstLoading) return []; @@ -424,6 +425,8 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, {isImagePopupOpen && ( <ModalPortal> <ImageDialog + repoID={repoID} + repoInfo={repoInfo} imageItems={images} imageIndex={imageIndex} closeImagePopup={closeImagePopup} diff --git a/frontend/src/shared-dir-view.js b/frontend/src/shared-dir-view.js index 06bdb04e09..3086b53f7f 100644 --- a/frontend/src/shared-dir-view.js +++ b/frontend/src/shared-dir-view.js @@ -922,12 +922,15 @@ class SharedDirView extends React.Component { {this.state.isImagePopupOpen && <ModalPortal> <ImageDialog + repoID={repoID} + repoInfo={{ 'permission': 'r' }} imageItems={this.state.imageItems} imageIndex={this.state.imageIndex} closeImagePopup={this.closeImagePopup} moveToPrevImage={this.moveToPrevImage} moveToNextImage={this.moveToNextImage} enableRotate={false} + isCustomPermission={true} /> </ModalPortal> } diff --git a/seahub/repo_metadata/apis.py b/seahub/repo_metadata/apis.py index a7d29c349f..bc6b5e6a3a 100644 --- a/seahub/repo_metadata/apis.py +++ b/seahub/repo_metadata/apis.py @@ -52,6 +52,7 @@ class MetadataManage(APIView): tags_lang = '' details_settings = '{}' is_ocr_enabled = False + face_recognition_enabled = False try: record = RepoMetadata.objects.filter(repo_id=repo_id).first() @@ -65,6 +66,8 @@ class MetadataManage(APIView): tags_lang = record.tags_lang if record.ocr_enabled: is_ocr_enabled = True + if record.face_recognition_enabled: + face_recognition_enabled = True except Exception as e: logger.error(e) error_msg = 'Internal Server Error' @@ -73,9 +76,10 @@ class MetadataManage(APIView): return Response({ 'enabled': is_enabled, 'tags_enabled': is_tags_enabled, + 'ocr_enabled': is_ocr_enabled, + 'face_recognition_enabled': face_recognition_enabled, 'tags_lang': tags_lang, 'details_settings': details_settings, - 'ocr_enabled': is_ocr_enabled }) def put(self, request, repo_id):