Merge branch 'bugfix/settings-backup' of github.com:ONLYOFFICE/AppServer into bugfix/settings-backup
This commit is contained in:
commit
68ce5c8764
@ -1,4 +1,5 @@
|
||||
{
|
||||
"ResetApplicationDescription": "Authenticator application configuration will be reset.",
|
||||
"ResetApplicationTitle": "Reset application configuration"
|
||||
"ResetApplicationTitle": "Reset application configuration",
|
||||
"SuccessResetApplication": "Application settings for authentication have been successfully reset"
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"ResetApplicationDescription": "Настройки приложения для аутентификации будут сброшены.",
|
||||
"ResetApplicationTitle": "Сбросить настройки приложения"
|
||||
"ResetApplicationTitle": "Сбросить настройки приложения",
|
||||
"SuccessResetApplication": "Настройки приложения для аутентификации успешно сброшены"
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ const withLoader = (WrappedComponent) => (Loader) => {
|
||||
|
||||
return (!isEditor && firstLoad && !isGallery) ||
|
||||
!isLoaded ||
|
||||
(isMobile && inLoad) ||
|
||||
(isMobile && inLoad && !firstLoad) ||
|
||||
(isLoadingFilesFind && !Loader) ||
|
||||
!tReady ||
|
||||
!isInit ? (
|
||||
|
@ -517,7 +517,6 @@ const ShellWrapper = inject(({ auth, backup }) => {
|
||||
setSnackbarExist,
|
||||
socketHelper,
|
||||
setTheme,
|
||||
getWhiteLabelLogoUrls,
|
||||
whiteLabelLogoUrls,
|
||||
} = settingsStore;
|
||||
const isBase = settingsStore.theme.isBase;
|
||||
|
@ -48,7 +48,7 @@ const CreateEvent = ({
|
||||
|
||||
setEventDialogVisible,
|
||||
eventDialogVisible,
|
||||
createWithoutDialog,
|
||||
keepNewFileName,
|
||||
}) => {
|
||||
const [headerTitle, setHeaderTitle] = React.useState(null);
|
||||
const [startValue, setStartValue] = React.useState("");
|
||||
@ -77,7 +77,7 @@ const CreateEvent = ({
|
||||
|
||||
if (!extension) return setEventDialogVisible(true);
|
||||
|
||||
if (!createWithoutDialog) {
|
||||
if (!keepNewFileName) {
|
||||
setEventDialogVisible(true);
|
||||
} else {
|
||||
onSave(null, title || defaultName);
|
||||
@ -289,6 +289,7 @@ export default inject(
|
||||
uploadDataStore,
|
||||
dialogsStore,
|
||||
oformsStore,
|
||||
settingsStore,
|
||||
}) => {
|
||||
const {
|
||||
setIsLoading,
|
||||
@ -321,7 +322,7 @@ export default inject(
|
||||
eventDialogVisible,
|
||||
} = dialogsStore;
|
||||
|
||||
const { createWithoutDialog } = filesStore;
|
||||
const { keepNewFileName } = settingsStore;
|
||||
|
||||
return {
|
||||
setEventDialogVisible,
|
||||
@ -352,7 +353,7 @@ export default inject(
|
||||
replaceFileStream,
|
||||
setEncryptionAccess,
|
||||
|
||||
createWithoutDialog,
|
||||
keepNewFileName,
|
||||
};
|
||||
}
|
||||
)(observer(CreateEvent));
|
||||
|
@ -22,9 +22,9 @@ const Dialog = ({
|
||||
onCancel,
|
||||
onClose,
|
||||
isCreateDialog,
|
||||
createWithoutDialog,
|
||||
setCreateWithoutDialog,
|
||||
extension,
|
||||
keepNewFileName,
|
||||
setKeepNewFileName,
|
||||
}) => {
|
||||
const [value, setValue] = useState("");
|
||||
const [isDisabled, setIsDisabled] = useState(false);
|
||||
@ -32,8 +32,8 @@ const Dialog = ({
|
||||
const [isChanged, setIsChanged] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
createWithoutDialog && isCreateDialog && setIsChecked(createWithoutDialog);
|
||||
}, [isCreateDialog, createWithoutDialog]);
|
||||
keepNewFileName && isCreateDialog && setIsChecked(keepNewFileName);
|
||||
}, [isCreateDialog, keepNewFileName]);
|
||||
|
||||
useEffect(() => {
|
||||
let input = document?.getElementById("create-text-input");
|
||||
@ -80,7 +80,7 @@ const Dialog = ({
|
||||
const onSaveAction = useCallback(
|
||||
(e) => {
|
||||
setIsDisabled(true);
|
||||
isCreateDialog && setCreateWithoutDialog(isChecked);
|
||||
isCreateDialog && setKeepNewFileName(isChecked);
|
||||
onSave && onSave(e, value);
|
||||
},
|
||||
[onSave, isCreateDialog, value, isChecked]
|
||||
@ -88,7 +88,7 @@ const Dialog = ({
|
||||
|
||||
const onCancelAction = useCallback((e) => {
|
||||
if (isChecked) {
|
||||
setCreateWithoutDialog(false);
|
||||
setKeepNewFileName(false);
|
||||
}
|
||||
onCancel && onCancel(e);
|
||||
}, []);
|
||||
@ -96,7 +96,7 @@ const Dialog = ({
|
||||
const onCloseAction = useCallback(
|
||||
(e) => {
|
||||
if (!isDisabled && isChecked) {
|
||||
setCreateWithoutDialog(false);
|
||||
setKeepNewFileName(false);
|
||||
}
|
||||
onClose && onClose(e);
|
||||
},
|
||||
@ -171,9 +171,9 @@ const Dialog = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default inject(({ auth, filesStore }) => {
|
||||
export default inject(({ auth, settingsStore }) => {
|
||||
const { folderFormValidation } = auth.settingsStore;
|
||||
const { createWithoutDialog, setCreateWithoutDialog } = filesStore;
|
||||
const { keepNewFileName, setKeepNewFileName } = settingsStore;
|
||||
|
||||
return { folderFormValidation, createWithoutDialog, setCreateWithoutDialog };
|
||||
return { folderFormValidation, keepNewFileName, setKeepNewFileName };
|
||||
})(observer(Dialog));
|
||||
|
@ -39,6 +39,8 @@ const Bar = (props) => {
|
||||
|
||||
showRoomQuotaBar,
|
||||
showStorageQuotaBar,
|
||||
|
||||
currentColorScheme,
|
||||
} = props;
|
||||
|
||||
const [barVisible, setBarVisible] = useState({
|
||||
@ -112,13 +114,7 @@ const Bar = (props) => {
|
||||
}, []);
|
||||
|
||||
const sendActivationLinkAction = () => {
|
||||
if (sendActivationLink) {
|
||||
sendActivationLink(t).finally(() => {
|
||||
return onCloseActivationBar();
|
||||
});
|
||||
} else {
|
||||
onCloseActivationBar();
|
||||
}
|
||||
sendActivationLink && sendActivationLink(t);
|
||||
};
|
||||
|
||||
const onCloseActivationBar = () => {
|
||||
@ -183,6 +179,7 @@ const Bar = (props) => {
|
||||
|
||||
return (isRoomQuota || isStorageQuota) && tReady ? (
|
||||
<QuotasBar
|
||||
currentColorScheme={currentColorScheme}
|
||||
isRoomQuota={isRoomQuota}
|
||||
{...quotasValue}
|
||||
onClick={onClickQuota}
|
||||
@ -191,6 +188,7 @@ const Bar = (props) => {
|
||||
/>
|
||||
) : withActivationBar && barVisible.confirmEmail && tReady ? (
|
||||
<ConfirmEmailBar
|
||||
currentColorScheme={currentColorScheme}
|
||||
onLoad={onLoad}
|
||||
onClick={sendActivationLinkAction}
|
||||
onClose={onCloseActivationBar}
|
||||
@ -221,6 +219,8 @@ export default inject(({ auth, profileActionsStore }) => {
|
||||
showStorageQuotaBar,
|
||||
} = auth.currentQuotaStore;
|
||||
|
||||
const { currentColorScheme } = auth.settingsStore;
|
||||
|
||||
return {
|
||||
isAdmin: user?.isAdmin,
|
||||
withActivationBar,
|
||||
@ -236,5 +236,7 @@ export default inject(({ auth, profileActionsStore }) => {
|
||||
|
||||
showRoomQuotaBar,
|
||||
showStorageQuotaBar,
|
||||
|
||||
currentColorScheme,
|
||||
};
|
||||
})(withTranslation(["Profile", "Common"])(withRouter(observer(Bar))));
|
||||
|
@ -11,10 +11,17 @@ const StyledLink = styled(Link)`
|
||||
line-height: 16px;
|
||||
font-weight: 400;
|
||||
|
||||
color: #316daa;
|
||||
color: ${(props) => props.currentColorScheme.main.accent};
|
||||
`;
|
||||
|
||||
const ConfirmEmailBar = ({ t, tReady, onClick, onClose, onLoad }) => {
|
||||
const ConfirmEmailBar = ({
|
||||
t,
|
||||
tReady,
|
||||
onClick,
|
||||
onClose,
|
||||
onLoad,
|
||||
currentColorScheme,
|
||||
}) => {
|
||||
return (
|
||||
tReady && (
|
||||
<SnackBar
|
||||
@ -22,7 +29,12 @@ const ConfirmEmailBar = ({ t, tReady, onClick, onClose, onLoad }) => {
|
||||
text={
|
||||
<>
|
||||
{t("ConfirmEmailDescription")}{" "}
|
||||
<StyledLink onClick={onClick}>{t("RequestActivation")}</StyledLink>
|
||||
<StyledLink
|
||||
currentColorScheme={currentColorScheme}
|
||||
onClick={onClick}
|
||||
>
|
||||
{t("RequestActivation")}
|
||||
</StyledLink>
|
||||
</>
|
||||
}
|
||||
isCampaigns={false}
|
||||
|
@ -11,7 +11,7 @@ const StyledLink = styled(Link)`
|
||||
line-height: 16px;
|
||||
font-weight: 400;
|
||||
|
||||
color: #316daa;
|
||||
color: ${(props) => props.currentColorScheme.main.accent};
|
||||
`;
|
||||
|
||||
const QuotasBar = ({
|
||||
@ -23,6 +23,7 @@ const QuotasBar = ({
|
||||
onClick,
|
||||
onClose,
|
||||
onLoad,
|
||||
currentColorScheme,
|
||||
}) => {
|
||||
const onClickAction = () => {
|
||||
onClick && onClick(isRoomQuota);
|
||||
@ -37,7 +38,10 @@ const QuotasBar = ({
|
||||
description: (
|
||||
<Trans i18nKey="RoomQuotaDescription" t={t}>
|
||||
You can archived the unnecessary rooms or
|
||||
<StyledLink onClick={onClickAction}>
|
||||
<StyledLink
|
||||
currentColorScheme={currentColorScheme}
|
||||
onClick={onClickAction}
|
||||
>
|
||||
{{ clickHere: t("ClickHere") }}
|
||||
</StyledLink>{" "}
|
||||
to find a better pricing plan for your portal.
|
||||
@ -50,7 +54,10 @@ const QuotasBar = ({
|
||||
description: (
|
||||
<Trans i18nKey="StorageQuotaDescription" t={t}>
|
||||
You can remove the unnecessary files or{" "}
|
||||
<StyledLink onClick={onClickAction}>
|
||||
<StyledLink
|
||||
currentColorScheme={currentColorScheme}
|
||||
onClick={onClickAction}
|
||||
>
|
||||
{{ clickHere: t("ClickHere") }}
|
||||
</StyledLink>{" "}
|
||||
to find a better pricing plan for your portal.
|
||||
|
@ -79,8 +79,6 @@ const AvatarEditorDialog = (props) => {
|
||||
const avatars = await createThumbnailsAvatar(profile.id, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 192,
|
||||
height: 192,
|
||||
tmpFile: res.data,
|
||||
});
|
||||
updateCreatedAvatar(avatars);
|
||||
|
@ -14,10 +14,11 @@ class ResetApplicationDialogComponent extends React.Component {
|
||||
}
|
||||
|
||||
resetApp = async () => {
|
||||
const { resetTfaApp, id, onClose, history } = this.props;
|
||||
const { t, resetTfaApp, id, onClose, history } = this.props;
|
||||
onClose && onClose();
|
||||
try {
|
||||
const res = await resetTfaApp(id);
|
||||
toastr.success(t("SuccessResetApplication"));
|
||||
if (res) history.push(res.replace(window.location.origin, ""));
|
||||
} catch (e) {
|
||||
toastr.error(e);
|
||||
|
@ -142,11 +142,11 @@ class FileRow extends Component {
|
||||
onCancelCurrentUpload = (e) => {
|
||||
//console.log("cancel upload ", e);
|
||||
const { id, action, fileId } = e.currentTarget.dataset;
|
||||
const { cancelCurrentUpload, cancelCurrentFileConversion } = this.props;
|
||||
const { t, cancelCurrentUpload, cancelCurrentFileConversion } = this.props;
|
||||
|
||||
return action === "convert"
|
||||
? cancelCurrentFileConversion(fileId)
|
||||
: cancelCurrentUpload(id);
|
||||
: cancelCurrentUpload(id, t);
|
||||
};
|
||||
|
||||
onMediaClick = (id) => {
|
||||
|
@ -10,26 +10,3 @@ export const thumbnailStatuses = {
|
||||
};
|
||||
|
||||
export const ADS_TIMEOUT = 300000; // 5 min
|
||||
|
||||
export const FilterGroups = Object.freeze({
|
||||
filterType: "filter-filterType",
|
||||
filterAuthor: "filter-author",
|
||||
filterFolders: "filter-folders",
|
||||
filterContent: "filter-withContent",
|
||||
roomFilterProviderType: "filter-provider-type",
|
||||
roomFilterType: "filter-type",
|
||||
roomFilterSubject: "filter-subject",
|
||||
roomFilterOwner: "filter-owner",
|
||||
roomFilterTags: "filter-tags",
|
||||
roomFilterFolders: "filter-withSubfolders",
|
||||
roomFilterContent: "filter-content",
|
||||
});
|
||||
|
||||
export const FilterKeys = Object.freeze({
|
||||
withSubfolders: "withSubfolders",
|
||||
excludeSubfolders: "excludeSubfolders",
|
||||
withContent: "withContent",
|
||||
me: "me",
|
||||
other: "other",
|
||||
user: "user",
|
||||
});
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
SendInviteDialog,
|
||||
DeleteUsersDialog,
|
||||
ChangeNameDialog,
|
||||
ResetApplicationDialog,
|
||||
} from "SRC_DIR/components/dialogs";
|
||||
|
||||
const Dialogs = ({
|
||||
@ -28,10 +29,12 @@ const Dialogs = ({
|
||||
disableDialogVisible,
|
||||
sendInviteDialogVisible,
|
||||
deleteDialogVisible,
|
||||
resetAuthDialogVisible,
|
||||
|
||||
changeNameVisible,
|
||||
setChangeNameVisible,
|
||||
profile,
|
||||
resetTfaApp,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
@ -101,6 +104,15 @@ const Dialogs = ({
|
||||
fromList
|
||||
/>
|
||||
)}
|
||||
|
||||
{resetAuthDialogVisible && (
|
||||
<ResetApplicationDialog
|
||||
visible={resetAuthDialogVisible}
|
||||
onClose={closeDialogs}
|
||||
resetTfaApp={resetTfaApp}
|
||||
id={data}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -121,6 +133,7 @@ export default inject(({ auth, peopleStore }) => {
|
||||
disableDialogVisible,
|
||||
sendInviteDialogVisible,
|
||||
deleteDialogVisible,
|
||||
resetAuthDialogVisible,
|
||||
} = peopleStore.dialogStore;
|
||||
|
||||
const { user: profile } = auth.userStore;
|
||||
@ -130,6 +143,10 @@ export default inject(({ auth, peopleStore }) => {
|
||||
setChangeNameVisible,
|
||||
} = peopleStore.targetUserStore;
|
||||
|
||||
const { tfaStore } = auth;
|
||||
|
||||
const { unlinkApp: resetTfaApp } = tfaStore;
|
||||
|
||||
return {
|
||||
changeEmail,
|
||||
changePassword,
|
||||
@ -145,9 +162,12 @@ export default inject(({ auth, peopleStore }) => {
|
||||
disableDialogVisible,
|
||||
sendInviteDialogVisible,
|
||||
deleteDialogVisible,
|
||||
resetAuthDialogVisible,
|
||||
|
||||
changeNameVisible,
|
||||
setChangeNameVisible,
|
||||
profile,
|
||||
|
||||
resetTfaApp,
|
||||
};
|
||||
})(observer(Dialogs));
|
||||
|
@ -11,7 +11,11 @@ import Loaders from "@docspace/common/components/Loaders";
|
||||
import { withLayoutSize } from "@docspace/common/utils";
|
||||
|
||||
import withPeopleLoader from "SRC_DIR/HOCs/withPeopleLoader";
|
||||
import { EmployeeType, PaymentsType } from "@docspace/common/constants";
|
||||
import {
|
||||
EmployeeType,
|
||||
EmployeeStatus,
|
||||
PaymentsType,
|
||||
} from "@docspace/common/constants";
|
||||
|
||||
const getStatus = (filterValues) => {
|
||||
const employeeStatus = result(
|
||||
@ -79,8 +83,12 @@ const SectionFilterContent = ({
|
||||
const newFilter = filter.clone();
|
||||
|
||||
if (status === 3) {
|
||||
newFilter.employeeStatus = 2;
|
||||
newFilter.employeeStatus = EmployeeStatus.Disabled;
|
||||
newFilter.activationStatus = null;
|
||||
} else if (status === 2) {
|
||||
console.log(status);
|
||||
newFilter.employeeStatus = EmployeeStatus.Active;
|
||||
newFilter.activationStatus = status;
|
||||
} else {
|
||||
newFilter.employeeStatus = null;
|
||||
newFilter.activationStatus = status;
|
||||
@ -93,6 +101,7 @@ const SectionFilterContent = ({
|
||||
newFilter.group = group;
|
||||
|
||||
newFilter.payments = payments;
|
||||
//console.log(newFilter);
|
||||
|
||||
setIsLoading(true);
|
||||
fetchPeople(newFilter, true).finally(() => setIsLoading(false));
|
||||
|
@ -9,10 +9,7 @@ import { isMobileOnly } from "react-device-detect";
|
||||
import find from "lodash/find";
|
||||
import result from "lodash/result";
|
||||
|
||||
import {
|
||||
FilterGroups,
|
||||
FilterKeys,
|
||||
} from "@docspace/client/src/helpers/filesConstants";
|
||||
import { FilterGroups, FilterKeys } from "@docspace/common/constants";
|
||||
|
||||
import { getUser } from "@docspace/common/api/people";
|
||||
import {
|
||||
@ -224,6 +221,10 @@ const SectionFilterContent = ({
|
||||
if (subjectId) {
|
||||
newFilter.subjectId = subjectId;
|
||||
|
||||
if (subjectId === FilterKeys.me) {
|
||||
newFilter.subjectId = `${userId}`;
|
||||
}
|
||||
|
||||
newFilter.subjectFilter = subjectFilter?.toString()
|
||||
? subjectFilter.toString()
|
||||
: FilterSubject.Member;
|
||||
@ -423,11 +424,12 @@ const SectionFilterContent = ({
|
||||
|
||||
if (roomsFilter.subjectId) {
|
||||
const user = await getUser(roomsFilter.subjectId);
|
||||
const isMe = userId === roomsFilter.subjectId;
|
||||
|
||||
let label = user.displayName;
|
||||
let label = isMe ? t("Common:MeLabel") : user.displayName;
|
||||
|
||||
const subject = {
|
||||
key: roomsFilter.subjectId,
|
||||
key: isMe ? FilterKeys.me : roomsFilter.subjectId,
|
||||
group: FilterGroups.roomFilterSubject,
|
||||
label: label,
|
||||
};
|
||||
@ -786,13 +788,25 @@ const SectionFilterContent = ({
|
||||
label: t("Common:Member"),
|
||||
isHeader: true,
|
||||
withoutSeparator: true,
|
||||
withMultiItems: true,
|
||||
},
|
||||
{
|
||||
id: "filter_author-me",
|
||||
key: FilterKeys.me,
|
||||
group: FilterGroups.roomFilterSubject,
|
||||
label: t("Common:MeLabel"),
|
||||
},
|
||||
{
|
||||
id: "filter_author-other",
|
||||
key: FilterKeys.other,
|
||||
group: FilterGroups.roomFilterSubject,
|
||||
label: t("Common:OtherLabel"),
|
||||
},
|
||||
{
|
||||
id: "filter_author-user",
|
||||
key: FilterKeys.user,
|
||||
group: FilterGroups.roomFilterSubject,
|
||||
label: t("Translations:ChooseFromList"),
|
||||
isSelector: true,
|
||||
displaySelectorType: "link",
|
||||
},
|
||||
];
|
||||
|
||||
@ -978,8 +992,7 @@ const SectionFilterContent = ({
|
||||
id: "filter_author-user",
|
||||
key: FilterKeys.user,
|
||||
group: FilterGroups.filterAuthor,
|
||||
label: t("Translations:ChooseFromList"),
|
||||
isSelector: true,
|
||||
displaySelectorType: "link",
|
||||
},
|
||||
];
|
||||
|
||||
@ -987,7 +1000,6 @@ const SectionFilterContent = ({
|
||||
|
||||
filterOptions.push(...typeOptions);
|
||||
}
|
||||
|
||||
return filterOptions;
|
||||
}, [
|
||||
t,
|
||||
|
@ -40,6 +40,7 @@ const MainProfile = (props) => {
|
||||
setChangeAvatarVisible,
|
||||
withActivationBar,
|
||||
sendActivationLink,
|
||||
currentColorScheme,
|
||||
} = props;
|
||||
|
||||
const role = getUserRole(profile);
|
||||
@ -63,11 +64,14 @@ const MainProfile = (props) => {
|
||||
editing={true}
|
||||
editAction={() => setChangeAvatarVisible(true)}
|
||||
/>
|
||||
<StyledInfo>
|
||||
<StyledInfo
|
||||
withActivationBar={withActivationBar}
|
||||
currentColorScheme={currentColorScheme}
|
||||
>
|
||||
<div className="rows-container">
|
||||
<div className="row">
|
||||
<div className="field">
|
||||
<Text as="div" color="#A3A9AE" className="label">
|
||||
<Text as="div" className="label">
|
||||
{t("Common:Name")}
|
||||
</Text>
|
||||
<Text fontWeight={600} truncate>
|
||||
@ -83,7 +87,7 @@ const MainProfile = (props) => {
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="field">
|
||||
<Text as="div" color="#A3A9AE" className="label">
|
||||
<Text as="div" className="label">
|
||||
{t("Common:Email")}
|
||||
</Text>
|
||||
<Text
|
||||
@ -92,26 +96,20 @@ const MainProfile = (props) => {
|
||||
fontWeight={600}
|
||||
>
|
||||
{profile.email}
|
||||
{withActivationBar && (
|
||||
<HelpButton
|
||||
className="send-again-icon"
|
||||
color={"#316daa"}
|
||||
tooltipContent={t("EmailNotVerified")}
|
||||
iconName={SendClockReactSvgUrl}
|
||||
/>
|
||||
)}
|
||||
</Text>
|
||||
|
||||
{withActivationBar && (
|
||||
<Text
|
||||
className="send-again-text"
|
||||
fontWeight={600}
|
||||
noSelect
|
||||
truncate
|
||||
<div
|
||||
className="send-again-container send-again-mobile"
|
||||
onClick={sendActivationLinkAction}
|
||||
>
|
||||
{t("SendAgain")}
|
||||
</Text>
|
||||
<ReactSVG
|
||||
className="send-again-icon"
|
||||
src={SendClockReactSvgUrl}
|
||||
/>
|
||||
<Text className="send-again-text" fontWeight={600} noSelect>
|
||||
{t("SendAgain")}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<IconButton
|
||||
@ -121,20 +119,15 @@ const MainProfile = (props) => {
|
||||
onClick={() => setChangeEmailVisible(true)}
|
||||
/>
|
||||
{withActivationBar && (
|
||||
<div className="send-again-container">
|
||||
<HelpButton
|
||||
<div
|
||||
className="send-again-container send-again-desktop"
|
||||
onClick={sendActivationLinkAction}
|
||||
>
|
||||
<ReactSVG
|
||||
className="send-again-icon"
|
||||
color={"#316daa"}
|
||||
tooltipContent={t("EmailNotVerified")}
|
||||
iconName={SendClockReactSvgUrl}
|
||||
src={SendClockReactSvgUrl}
|
||||
/>
|
||||
<Text
|
||||
className="send-again-text"
|
||||
fontWeight={600}
|
||||
noSelect
|
||||
truncate
|
||||
onClick={sendActivationLinkAction}
|
||||
>
|
||||
<Text className="send-again-text" fontWeight={600} noSelect>
|
||||
{t("SendAgain")}
|
||||
</Text>
|
||||
</div>
|
||||
@ -142,7 +135,7 @@ const MainProfile = (props) => {
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="field">
|
||||
<Text as="div" color="#A3A9AE" className="label">
|
||||
<Text as="div" className="label">
|
||||
{t("Common:Password")}
|
||||
</Text>
|
||||
<Text fontWeight={600}>********</Text>
|
||||
@ -199,6 +192,8 @@ export default inject(({ auth, peopleStore }) => {
|
||||
|
||||
const { withActivationBar, sendActivationLink } = auth.userStore;
|
||||
|
||||
const { currentColorScheme } = auth.settingsStore;
|
||||
|
||||
const {
|
||||
targetUser: profile,
|
||||
changeEmailVisible,
|
||||
@ -223,5 +218,6 @@ export default inject(({ auth, peopleStore }) => {
|
||||
setChangeAvatarVisible,
|
||||
withActivationBar,
|
||||
sendActivationLink,
|
||||
currentColorScheme,
|
||||
};
|
||||
})(observer(MainProfile));
|
||||
|
@ -79,7 +79,7 @@ const LanguagesCombo = (props) => {
|
||||
|
||||
return (
|
||||
<StyledRow>
|
||||
<Text as="div" color="#A3A9AE" className="label">
|
||||
<Text as="div" className="label">
|
||||
{t("Common:Language")}
|
||||
<HelpButton
|
||||
size={12}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import styled from "styled-components";
|
||||
import styled, { css } from "styled-components";
|
||||
import {
|
||||
hugeMobile,
|
||||
smallTablet,
|
||||
@ -49,6 +49,13 @@ export const StyledInfo = styled.div`
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.label {
|
||||
min-width: 75px;
|
||||
max-width: 75px;
|
||||
white-space: nowrap;
|
||||
color: ${(props) => props.theme.profile.main.descriptionTextColor};
|
||||
}
|
||||
|
||||
.rows-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -85,69 +92,71 @@ export const StyledInfo = styled.div`
|
||||
|
||||
.email-text-container {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.send-again-text {
|
||||
line-height: 15px;
|
||||
|
||||
color: #316daa;
|
||||
|
||||
border-bottom: 1px solid #316daa;
|
||||
|
||||
width: fit-content;
|
||||
|
||||
display: none;
|
||||
|
||||
@media ${smallTablet} {
|
||||
display: block;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.send-again-icon {
|
||||
display: none;
|
||||
|
||||
@media ${smallTablet} {
|
||||
display: block;
|
||||
padding-left: 5px;
|
||||
|
||||
max-width: 12px;
|
||||
max-height: 12px;
|
||||
}
|
||||
${(props) =>
|
||||
props.withActivationBar &&
|
||||
css`
|
||||
color: ${props.theme.profile.main.pendingEmailTextColor};
|
||||
`}
|
||||
}
|
||||
|
||||
.send-again-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
|
||||
margin-left: 8px;
|
||||
max-width: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
align-items: center;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
.send-again-text {
|
||||
display: block;
|
||||
height: 18px;
|
||||
|
||||
margin-left: 4px;
|
||||
.send-again-text {
|
||||
margin-left: 5px;
|
||||
|
||||
line-height: 15px;
|
||||
|
||||
color: ${(props) => props.currentColorScheme.main.accent};
|
||||
|
||||
border-bottom: 1px solid
|
||||
${(props) => props.currentColorScheme.main.accent};
|
||||
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.send-again-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media ${smallTablet} {
|
||||
display: none;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
|
||||
.send-again-text,
|
||||
.send-again-icon {
|
||||
display: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
div {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
|
||||
path {
|
||||
fill: ${(props) => props.currentColorScheme.main.accent};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
min-width: 75px;
|
||||
max-width: 75px;
|
||||
white-space: nowrap;
|
||||
.send-again-desktop {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.send-again-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
@ -165,19 +174,7 @@ export const StyledInfo = styled.div`
|
||||
gap: 2px;
|
||||
|
||||
.email-text-container {
|
||||
display: flex;
|
||||
|
||||
.send-again-icon {
|
||||
margin-left: 4px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
& > p {
|
||||
@ -199,8 +196,14 @@ export const StyledInfo = styled.div`
|
||||
min-width: 12px;
|
||||
}
|
||||
|
||||
.email-text-container {
|
||||
padding-left: 0px;
|
||||
.send-again-desktop {
|
||||
display: none;
|
||||
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.send-again-mobile {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import { isMobileOnly } from "react-device-detect";
|
||||
|
||||
import { StyledRow } from "./styled-main-profile";
|
||||
|
||||
const TimezoneCombo = ({title}) => {
|
||||
const TimezoneCombo = ({ title }) => {
|
||||
const { t } = useTranslation("Wizard");
|
||||
|
||||
const timezones = [{ key: "03", label: "(UTC) +03 Moscow" }];
|
||||
@ -17,7 +17,7 @@ const TimezoneCombo = ({title}) => {
|
||||
|
||||
return (
|
||||
<StyledRow title={title}>
|
||||
<Text as="div" color="#A3A9AE" className="label">
|
||||
<Text as="div" className="label">
|
||||
{t("Wizard:Timezone")}
|
||||
</Text>
|
||||
<ComboBox
|
||||
|
@ -28,10 +28,11 @@ const PersonalSettings = ({
|
||||
|
||||
t,
|
||||
showTitle,
|
||||
createWithoutDialog,
|
||||
setCreateWithoutDialog,
|
||||
|
||||
showAdminSettings,
|
||||
|
||||
keepNewFileName,
|
||||
setKeepNewFileName,
|
||||
}) => {
|
||||
const [isLoadingFavorites, setIsLoadingFavorites] = React.useState(false);
|
||||
const [isLoadingRecent, setIsLoadingRecent] = React.useState(false);
|
||||
@ -52,6 +53,10 @@ const PersonalSettings = ({
|
||||
setForceSave(!forceSave);
|
||||
}, [setForceSave, forceSave]);
|
||||
|
||||
const onChangeKeepNewFileName = React.useCallback(() => {
|
||||
setKeepNewFileName(!keepNewFileName);
|
||||
}, [setKeepNewFileName, keepNewFileName]);
|
||||
|
||||
const onChangeFavorites = React.useCallback(
|
||||
(e) => {
|
||||
setIsLoadingFavorites(true);
|
||||
@ -72,10 +77,6 @@ const PersonalSettings = ({
|
||||
[setIsLoadingRecent, setRecentSetting]
|
||||
);
|
||||
|
||||
const onChangeCheckbox = () => {
|
||||
setCreateWithoutDialog(!createWithoutDialog);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledSettings
|
||||
showTitle={showTitle}
|
||||
@ -91,8 +92,8 @@ const PersonalSettings = ({
|
||||
<ToggleButton
|
||||
className="toggle-btn"
|
||||
label={t("Common:DontAskAgain")}
|
||||
onChange={onChangeCheckbox}
|
||||
isChecked={createWithoutDialog}
|
||||
onChange={onChangeKeepNewFileName}
|
||||
isChecked={keepNewFileName}
|
||||
/>
|
||||
)}
|
||||
<ToggleButton
|
||||
@ -162,55 +163,56 @@ const PersonalSettings = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default inject(
|
||||
({ auth, settingsStore, treeFoldersStore, filesStore }) => {
|
||||
const {
|
||||
storeOriginalFiles,
|
||||
confirmDelete,
|
||||
updateIfExist,
|
||||
forcesave,
|
||||
export default inject(({ auth, settingsStore, treeFoldersStore }) => {
|
||||
const {
|
||||
storeOriginalFiles,
|
||||
confirmDelete,
|
||||
updateIfExist,
|
||||
forcesave,
|
||||
|
||||
setUpdateIfExist,
|
||||
setStoreOriginal,
|
||||
setUpdateIfExist,
|
||||
setStoreOriginal,
|
||||
|
||||
setConfirmDelete,
|
||||
setConfirmDelete,
|
||||
|
||||
setForceSave,
|
||||
setForceSave,
|
||||
|
||||
favoritesSection,
|
||||
recentSection,
|
||||
setFavoritesSetting,
|
||||
setRecentSetting,
|
||||
} = settingsStore;
|
||||
favoritesSection,
|
||||
recentSection,
|
||||
setFavoritesSetting,
|
||||
setRecentSetting,
|
||||
|
||||
const { myFolderId, commonFolderId } = treeFoldersStore;
|
||||
const { setCreateWithoutDialog, createWithoutDialog } = filesStore;
|
||||
keepNewFileName,
|
||||
setKeepNewFileName,
|
||||
} = settingsStore;
|
||||
|
||||
return {
|
||||
storeOriginalFiles,
|
||||
confirmDelete,
|
||||
updateIfExist,
|
||||
forceSave: forcesave,
|
||||
const { myFolderId, commonFolderId } = treeFoldersStore;
|
||||
|
||||
myFolderId,
|
||||
commonFolderId,
|
||||
isVisitor: auth.userStore.user.isVisitor,
|
||||
favoritesSection,
|
||||
recentSection,
|
||||
return {
|
||||
storeOriginalFiles,
|
||||
confirmDelete,
|
||||
updateIfExist,
|
||||
forceSave: forcesave,
|
||||
|
||||
setUpdateIfExist,
|
||||
setStoreOriginal,
|
||||
myFolderId,
|
||||
commonFolderId,
|
||||
isVisitor: auth.userStore.user.isVisitor,
|
||||
favoritesSection,
|
||||
recentSection,
|
||||
|
||||
setConfirmDelete,
|
||||
setUpdateIfExist,
|
||||
setStoreOriginal,
|
||||
|
||||
setForceSave,
|
||||
setConfirmDelete,
|
||||
|
||||
setFavoritesSetting,
|
||||
setRecentSetting,
|
||||
myFolderId,
|
||||
commonFolderId,
|
||||
setCreateWithoutDialog,
|
||||
createWithoutDialog,
|
||||
};
|
||||
}
|
||||
)(observer(PersonalSettings));
|
||||
setForceSave,
|
||||
|
||||
setFavoritesSetting,
|
||||
setRecentSetting,
|
||||
myFolderId,
|
||||
commonFolderId,
|
||||
|
||||
keepNewFileName,
|
||||
setKeepNewFileName,
|
||||
};
|
||||
})(observer(PersonalSettings));
|
||||
|
@ -415,8 +415,13 @@ class AccountsContextOptionsStore {
|
||||
};
|
||||
|
||||
onResetAuth = (item) => {
|
||||
toastr.warning("Work at progress");
|
||||
console.log(item);
|
||||
const {
|
||||
setDialogData,
|
||||
setResetAuthDialogVisible,
|
||||
} = this.peopleStore.dialogStore;
|
||||
|
||||
setResetAuthDialogVisible(true);
|
||||
setDialogData(item.id);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@ class DialogStore {
|
||||
disableDialogVisible = false;
|
||||
sendInviteDialogVisible = false;
|
||||
deleteDialogVisible = false;
|
||||
resetAuthDialogVisible = false;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
@ -64,6 +65,10 @@ class DialogStore {
|
||||
this.deleteDialogVisible = visible;
|
||||
};
|
||||
|
||||
setResetAuthDialogVisible = (visible) => {
|
||||
this.resetAuthDialogVisible = visible;
|
||||
};
|
||||
|
||||
closeDialogs = () => {
|
||||
this.setChangeEmailDialogVisible(false);
|
||||
this.setChangePasswordDialogVisible(false);
|
||||
@ -77,6 +82,7 @@ class DialogStore {
|
||||
|
||||
this.setSendInviteDialogVisible(false);
|
||||
this.setDeleteDialogVisible(false);
|
||||
this.setResetAuthDialogVisible(false);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,6 @@ import debounce from "lodash.debounce";
|
||||
|
||||
const { FilesFilter, RoomsFilter } = api;
|
||||
const storageViewAs = localStorage.getItem("viewAs");
|
||||
const storageCheckbox = JSON.parse(localStorage.getItem("createWithoutDialog"));
|
||||
|
||||
let requestCounter = 0;
|
||||
|
||||
@ -56,7 +55,6 @@ class FilesStore {
|
||||
|
||||
isLoaded = false;
|
||||
isLoading = false;
|
||||
createWithoutDialog = storageCheckbox ? true : false;
|
||||
|
||||
viewAs =
|
||||
isMobile && storageViewAs !== "tile" ? "row" : storageViewAs || "table";
|
||||
@ -569,11 +567,6 @@ class FilesStore {
|
||||
viewAs === "tile" && this.createThumbnails();
|
||||
};
|
||||
|
||||
setCreateWithoutDialog = (checked) => {
|
||||
this.createWithoutDialog = checked;
|
||||
localStorage.setItem("createWithoutDialog", JSON.stringify(checked));
|
||||
};
|
||||
|
||||
setPageItemsLength = (pageItemsLength) => {
|
||||
this.pageItemsLength = pageItemsLength;
|
||||
};
|
||||
@ -2184,6 +2177,7 @@ class FilesStore {
|
||||
this.setFilter(newFilter);
|
||||
this.setFiles(files);
|
||||
this.setFolders(folders);
|
||||
this.setTempActionFilesIds([]);
|
||||
});
|
||||
|
||||
return;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { makeAutoObservable } from "mobx";
|
||||
import { makeAutoObservable, runInAction } from "mobx";
|
||||
import {
|
||||
getInvitationLinks,
|
||||
getShortenedLink,
|
||||
@ -39,10 +39,12 @@ class InviteLinksStore {
|
||||
|
||||
const links = await getInvitationLinks();
|
||||
|
||||
this.setUserLink(links.userLink);
|
||||
this.setGuestLink(links.guestLink);
|
||||
this.setAdminLink(links.adminLink);
|
||||
this.setCollaboratorLink(links.collaboratorLink);
|
||||
runInAction(() => {
|
||||
this.setUserLink(links.userLink);
|
||||
this.setGuestLink(links.guestLink);
|
||||
this.setAdminLink(links.adminLink);
|
||||
this.setCollaboratorLink(links.collaboratorLink);
|
||||
});
|
||||
};
|
||||
|
||||
getShortenedLink = async (link, forUser = false) => {
|
||||
|
@ -30,6 +30,7 @@ class SettingsStore {
|
||||
favoritesSection = null;
|
||||
recentSection = null;
|
||||
hideConfirmConvertSave = null;
|
||||
keepNewFileName = null;
|
||||
chunkUploadSize = 1024 * 1023; // 1024 * 1023; //~0.999mb
|
||||
|
||||
settingsIsLoaded = false;
|
||||
@ -149,6 +150,12 @@ class SettingsStore {
|
||||
|
||||
setStoreForcesave = (val) => (this.storeForcesave = val);
|
||||
|
||||
setKeepNewFileName = (data) => {
|
||||
api.files
|
||||
.changeKeepNewFileName(data)
|
||||
.then((res) => this.setFilesSetting("keepNewFileName", res));
|
||||
};
|
||||
|
||||
setEnableThirdParty = async (data, setting) => {
|
||||
const res = await api.files.thirdParty(data);
|
||||
this.setFilesSetting(setting, res);
|
||||
|
@ -215,7 +215,31 @@ class UploadDataStore {
|
||||
this.setUploadData(newUploadData);
|
||||
};
|
||||
|
||||
cancelCurrentUpload = (id) => {
|
||||
cancelCurrentUpload = (id, t) => {
|
||||
if (this.isParallel) {
|
||||
runInAction(() => {
|
||||
const uploadedFilesHistory = this.uploadedFilesHistory.filter(
|
||||
(el) => el.uniqueId !== id
|
||||
);
|
||||
|
||||
const canceledFile = this.files.find((f) => f.uniqueId === id);
|
||||
const newPercent = this.getFilesPercent(canceledFile.file.size);
|
||||
canceledFile.cancel = true;
|
||||
canceledFile.percent = 100;
|
||||
canceledFile.action = "uploaded";
|
||||
|
||||
this.currentUploadNumber -= 1;
|
||||
this.uploadedFilesHistory = uploadedFilesHistory;
|
||||
this.percent = newPercent;
|
||||
const nextFileIndex = this.files.findIndex((f) => !f.inAction);
|
||||
|
||||
if (nextFileIndex !== -1) {
|
||||
this.startSessionFunc(nextFileIndex, t);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newFiles = this.files.filter((el) => el.uniqueId !== id);
|
||||
const uploadedFilesHistory = this.uploadedFilesHistory.filter(
|
||||
(el) => el.uniqueId !== id
|
||||
@ -543,7 +567,7 @@ class UploadDataStore {
|
||||
this.setConversionPercent(percent, !!error);
|
||||
|
||||
if (!file.error && file.fileInfo.version > 2) {
|
||||
this.filesStore.setHighlightFile({
|
||||
this.filesStore.setHighlightFile({
|
||||
highlightFileId: file.fileInfo.id,
|
||||
isFileHasExst: !file.fileInfo.fileExst,
|
||||
});
|
||||
@ -1440,6 +1464,7 @@ class UploadDataStore {
|
||||
filter,
|
||||
isEmptyLastPageAfterOperation,
|
||||
resetFilterPage,
|
||||
removeFiles,
|
||||
} = this.filesStore;
|
||||
|
||||
const {
|
||||
@ -1447,6 +1472,7 @@ class UploadDataStore {
|
||||
setSecondaryProgressBarData,
|
||||
label,
|
||||
} = this.secondaryProgressDataStore;
|
||||
const { withPaging } = this.authStore.settingsStore;
|
||||
|
||||
let receivedFolder = destFolderId;
|
||||
let updatedFolder = this.selectedFolderStore.id;
|
||||
@ -1459,6 +1485,14 @@ class UploadDataStore {
|
||||
if (!isCopy || destFolderId === this.selectedFolderStore.id) {
|
||||
let newFilter;
|
||||
|
||||
if (!withPaging) {
|
||||
removeFiles(fileIds, folderIds);
|
||||
this.clearActiveOperations(fileIds, folderIds);
|
||||
setTimeout(() => clearSecondaryProgressData(), TIMEOUT);
|
||||
this.dialogsStore.setIsFolderActions(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEmptyLastPageAfterOperation()) {
|
||||
newFilter = resetFilterPage();
|
||||
}
|
||||
|
@ -33,10 +33,6 @@ class UsersStore {
|
||||
filterData.pageCount = 100;
|
||||
}
|
||||
|
||||
if (filterData.employeeStatus === EmployeeStatus.Active) {
|
||||
filterData.employeeStatus = null;
|
||||
}
|
||||
|
||||
if (filterData.group && filterData.group === "root")
|
||||
filterData.group = undefined;
|
||||
|
||||
|
@ -703,6 +703,11 @@ export function forceSave(val) {
|
||||
return request({ method: "put", url: "files/forcesave", data });
|
||||
}
|
||||
|
||||
export function changeKeepNewFileName(val) {
|
||||
const data = { set: val };
|
||||
return request({ method: "put", url: "files/keepnewfilename", data });
|
||||
}
|
||||
|
||||
export function thirdParty(val) {
|
||||
const data = { set: val };
|
||||
return request({ method: "put", url: "files/thirdparty", data });
|
||||
|
@ -72,7 +72,7 @@ const FilterBlock = ({
|
||||
if (groupItem.key === currentFilter.key) {
|
||||
groupItem.isSelected = true;
|
||||
}
|
||||
if (groupItem.isSelector) {
|
||||
if (groupItem.displaySelectorType) {
|
||||
groupItem.isSelected = true;
|
||||
groupItem.selectedKey = currentFilter.key;
|
||||
groupItem.selectedLabel = currentFilter.label;
|
||||
@ -87,7 +87,7 @@ const FilterBlock = ({
|
||||
} else {
|
||||
item.groupItem.forEach((groupItem, idx) => {
|
||||
groupItem.isSelected = false;
|
||||
if (groupItem.isSelector) {
|
||||
if (groupItem.displaySelectorType) {
|
||||
groupItem.selectedKey = null;
|
||||
groupItem.selectedLabel = null;
|
||||
}
|
||||
@ -216,9 +216,12 @@ const FilterBlock = ({
|
||||
items.forEach((item) => {
|
||||
if (item.group === selectedValue.group) {
|
||||
item.groupItem.forEach((groupItem) => {
|
||||
if (groupItem.key === selectedValue.key || groupItem.isSelector) {
|
||||
if (
|
||||
groupItem.key === selectedValue.key ||
|
||||
groupItem.displaySelectorType
|
||||
) {
|
||||
groupItem.isSelected = true;
|
||||
if (groupItem.isSelector) {
|
||||
if (groupItem.displaySelectorType) {
|
||||
groupItem.selectedLabel = selectedValue.label;
|
||||
groupItem.selectedKey = selectedValue.key;
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import {
|
||||
import { ColorTheme, ThemeType } from "@docspace/common/components/ColorTheme";
|
||||
|
||||
import XIcon from "PUBLIC_DIR/images/x.react.svg";
|
||||
import { FilterGroups, FilterKeys } from "../../../constants";
|
||||
|
||||
const FilterBlockItem = ({
|
||||
group,
|
||||
@ -78,10 +79,17 @@ const FilterBlockItem = ({
|
||||
item.selectedKey === "me" ||
|
||||
item.selectedKey === "other" ? (
|
||||
<StyledFilterBlockItemSelector
|
||||
style={
|
||||
item?.displaySelectorType === "button"
|
||||
? {}
|
||||
: { height: "0", width: "0" }
|
||||
}
|
||||
key={item.key}
|
||||
onClick={(event) => showSelectorAction(event, isAuthor, item.group, [])}
|
||||
>
|
||||
<SelectorAddButton id="filter_add-author" />
|
||||
{item?.displaySelectorType === "button" && (
|
||||
<SelectorAddButton id="filter_add-author" />
|
||||
)}
|
||||
<StyledFilterBlockItemSelectorText noSelect={true}>
|
||||
{item.label}
|
||||
</StyledFilterBlockItemSelectorText>
|
||||
@ -177,14 +185,37 @@ const FilterBlockItem = ({
|
||||
};
|
||||
|
||||
const getTagItem = (item) => {
|
||||
const isAuthor = item.key === FilterKeys.user;
|
||||
|
||||
if (
|
||||
item.group === FilterGroups.filterAuthor ||
|
||||
item.group === FilterGroups.roomFilterSubject
|
||||
) {
|
||||
const [meItem, otherItem, userItem] = groupItem;
|
||||
|
||||
if (
|
||||
item.key === otherItem.key &&
|
||||
userItem?.isSelected &&
|
||||
!meItem?.isSelected
|
||||
)
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<ColorTheme
|
||||
key={item.key}
|
||||
isSelected={item.isSelected}
|
||||
name={`${item.label}-${item.key}`}
|
||||
id={item.id}
|
||||
onClick={() =>
|
||||
changeFilterValueAction(item.key, item.isSelected, item.isMultiSelect)
|
||||
onClick={
|
||||
item.key === FilterKeys.other
|
||||
? (event) => showSelectorAction(event, isAuthor, item.group, [])
|
||||
: () =>
|
||||
changeFilterValueAction(
|
||||
item.key,
|
||||
item.isSelected,
|
||||
item.isMultiSelect
|
||||
)
|
||||
}
|
||||
themeId={ThemeType.FilterBlockItemTag}
|
||||
>
|
||||
@ -214,7 +245,7 @@ const FilterBlockItem = ({
|
||||
withoutSeparator={withoutSeparator}
|
||||
>
|
||||
{groupItem.map((item) => {
|
||||
if (item.isSelector === true) return getSelectorItem(item);
|
||||
if (item.displaySelectorType) return getSelectorItem(item);
|
||||
if (item.isToggle === true) return getToggleItem(item);
|
||||
if (item.withOptions === true) return getWithOptionsItem(item);
|
||||
if (item.isCheckbox === true) return getCheckboxItem(item);
|
||||
|
@ -6,8 +6,9 @@ import MediaRotateLeftIcon from "PUBLIC_DIR/images/media.rotateleft.react.svg";
|
||||
import MediaRotateRightIcon from "PUBLIC_DIR/images/media.rotateright.react.svg";
|
||||
import MediaDeleteIcon from "PUBLIC_DIR/images/media.delete.react.svg";
|
||||
import MediaDownloadIcon from "PUBLIC_DIR/images/download.react.svg";
|
||||
import MediaFavoriteIcon from "PUBLIC_DIR/images/favorite.react.svg";
|
||||
// import MediaFavoriteIcon from "PUBLIC_DIR/images/favorite.react.svg";
|
||||
import ViewerSeparator from "PUBLIC_DIR/images/viewer.separator.react.svg";
|
||||
import { ToolbarActionType } from ".";
|
||||
|
||||
export const getCustomToolbar = (
|
||||
onDeleteClick: VoidFunction,
|
||||
@ -17,7 +18,7 @@ export const getCustomToolbar = (
|
||||
{
|
||||
key: "zoomOut",
|
||||
percent: true,
|
||||
actionType: 2,
|
||||
actionType: ToolbarActionType.ZoomOut,
|
||||
render: (
|
||||
<div className="iconContainer zoomOut">
|
||||
<MediaZoomOutIcon size="scale" />
|
||||
@ -26,11 +27,11 @@ export const getCustomToolbar = (
|
||||
},
|
||||
{
|
||||
key: "percent",
|
||||
actionType: 999,
|
||||
actionType: ToolbarActionType.Reset,
|
||||
},
|
||||
{
|
||||
key: "zoomIn",
|
||||
actionType: 1,
|
||||
actionType: ToolbarActionType.ZoomIn,
|
||||
render: (
|
||||
<div className="iconContainer zoomIn">
|
||||
<MediaZoomInIcon size="scale" />
|
||||
@ -39,7 +40,7 @@ export const getCustomToolbar = (
|
||||
},
|
||||
{
|
||||
key: "rotateLeft",
|
||||
actionType: 5,
|
||||
actionType: ToolbarActionType.RotateLeft,
|
||||
render: (
|
||||
<div className="iconContainer rotateLeft">
|
||||
<MediaRotateLeftIcon size="scale" />
|
||||
@ -48,7 +49,7 @@ export const getCustomToolbar = (
|
||||
},
|
||||
{
|
||||
key: "rotateRight",
|
||||
actionType: 6,
|
||||
actionType: ToolbarActionType.RotateRight,
|
||||
render: (
|
||||
<div className="iconContainer rotateRight">
|
||||
<MediaRotateRightIcon size="scale" />
|
||||
@ -67,7 +68,7 @@ export const getCustomToolbar = (
|
||||
},
|
||||
{
|
||||
key: "download",
|
||||
actionType: 102,
|
||||
actionType: ToolbarActionType.Download,
|
||||
render: (
|
||||
<div className="iconContainer download" style={{ height: "16px" }}>
|
||||
<MediaDownloadIcon size="scale" />
|
||||
@ -99,14 +100,5 @@ export const getCustomToolbar = (
|
||||
),
|
||||
onClick: onDeleteClick,
|
||||
},
|
||||
{
|
||||
key: "favorite",
|
||||
actionType: 104,
|
||||
render: (
|
||||
<div className="iconContainer viewer-favorite">
|
||||
<MediaFavoriteIcon size="scale" />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
@ -11,6 +11,8 @@ export const mediaTypes = Object.freeze({
|
||||
});
|
||||
|
||||
export enum KeyboardEventKeys {
|
||||
ArrowUp = "ArrowUp",
|
||||
ArrowDown = "ArrowDown",
|
||||
ArrowRight = "ArrowRight",
|
||||
ArrowLeft = "ArrowLeft",
|
||||
Escape = "Escape",
|
||||
@ -21,6 +23,20 @@ export enum KeyboardEventKeys {
|
||||
Digit1 = "Digit1",
|
||||
}
|
||||
|
||||
export enum ToolbarActionType {
|
||||
ZoomIn = 1,
|
||||
ZoomOut = 2,
|
||||
Prev = 3,
|
||||
Next = 4,
|
||||
RotateLeft = 5,
|
||||
RotateRight = 6,
|
||||
Reset = 7,
|
||||
Close = 8,
|
||||
ScaleX = 9,
|
||||
ScaleY = 10,
|
||||
Download = 11,
|
||||
}
|
||||
|
||||
export const mapSupplied = {
|
||||
".aac": { supply: "m4a", type: mediaTypes.audio },
|
||||
".flac": { supply: "mp3", type: mediaTypes.audio },
|
||||
|
@ -1,5 +1,11 @@
|
||||
import { isMobile } from "react-device-detect";
|
||||
import React, { useState, useCallback, useMemo, useEffect } from "react";
|
||||
import React, {
|
||||
useState,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
|
||||
import ViewerWrapper from "./sub-components/ViewerWrapper";
|
||||
|
||||
@ -18,7 +24,7 @@ import DuplicateReactSvgUrl from "PUBLIC_DIR/images/duplicate.react.svg?url";
|
||||
import DownloadReactSvgUrl from "PUBLIC_DIR/images/download.react.svg?url";
|
||||
import RenameReactSvgUrl from "PUBLIC_DIR/images/rename.react.svg?url";
|
||||
import TrashReactSvgUrl from "PUBLIC_DIR/images/trash.react.svg?url";
|
||||
import MoveReactSvgUrl from "PUBLIC_DIR/images/duplicate.react.svg?url";
|
||||
import MoveReactSvgUrl from "PUBLIC_DIR/images/move.react.svg?url";
|
||||
|
||||
function MediaViewer({
|
||||
playlistPos,
|
||||
@ -26,13 +32,15 @@ function MediaViewer({
|
||||
prevMedia,
|
||||
...props
|
||||
}: MediaViewerProps): JSX.Element {
|
||||
const TiffXMLHttpRequestRef = useRef<XMLHttpRequest>();
|
||||
|
||||
const [title, setTitle] = useState<string>("");
|
||||
const [fileUrl, setFileUrl] = useState<string>(() => {
|
||||
const [fileUrl, setFileUrl] = useState<string | undefined>(() => {
|
||||
const { playlist, currentFileId } = props;
|
||||
const item = playlist.find(
|
||||
(file) => file.fileId.toString() === currentFileId.toString()
|
||||
);
|
||||
return item?.src ?? "";
|
||||
return item?.src;
|
||||
});
|
||||
|
||||
const [targetFile, setTargetFile] = useState(() => {
|
||||
@ -89,10 +97,12 @@ function MediaViewer({
|
||||
if (!src) return onEmptyPlaylistError();
|
||||
|
||||
if (ext !== ".tif" && ext !== ".tiff" && src !== fileUrl) {
|
||||
TiffXMLHttpRequestRef.current?.abort();
|
||||
setFileUrl(src);
|
||||
}
|
||||
|
||||
if (ext === ".tiff" || ext === ".tif") {
|
||||
setFileUrl(undefined);
|
||||
fetchAndSetTiffDataURL(src);
|
||||
}
|
||||
|
||||
@ -108,18 +118,14 @@ function MediaViewer({
|
||||
(playlist[playlistPos].fileStatus & FileStatus.IsFavorite) ===
|
||||
FileStatus.IsFavorite
|
||||
);
|
||||
}, [
|
||||
props.playlist.length,
|
||||
props.files.length,
|
||||
props.currentFileId,
|
||||
playlistPos,
|
||||
]);
|
||||
}, [props.playlist, props.files.length, props.currentFileId, playlistPos]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("keydown", onKeydown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", onKeydown);
|
||||
TiffXMLHttpRequestRef.current?.abort();
|
||||
};
|
||||
}, [
|
||||
props.playlist.length,
|
||||
@ -298,38 +304,30 @@ function MediaViewer({
|
||||
|
||||
const onKeydown = (event: KeyboardEvent) => {
|
||||
const { code, ctrlKey } = event;
|
||||
if (props.deleteDialogVisible) return;
|
||||
|
||||
if (code in KeyboardEventKeys) {
|
||||
event.preventDefault();
|
||||
const includesKeyboardCode = [
|
||||
KeyboardEventKeys.KeyS,
|
||||
KeyboardEventKeys.Numpad1,
|
||||
KeyboardEventKeys.Digit1,
|
||||
KeyboardEventKeys.Space,
|
||||
].includes(code as KeyboardEventKeys);
|
||||
|
||||
if (!includesKeyboardCode || ctrlKey) event.preventDefault();
|
||||
}
|
||||
if (props.deleteDialogVisible) return;
|
||||
|
||||
switch (code) {
|
||||
case KeyboardEventKeys.ArrowLeft:
|
||||
if (document.fullscreenElement) return;
|
||||
|
||||
if (ctrlKey) {
|
||||
const rotateLeftElement = document.getElementsByClassName(
|
||||
"iconContainer rotateLeft"
|
||||
)?.[0] as HTMLElement | undefined;
|
||||
rotateLeftElement?.click();
|
||||
} else {
|
||||
prevMedia();
|
||||
}
|
||||
|
||||
if (!ctrlKey) prevMedia();
|
||||
break;
|
||||
|
||||
case KeyboardEventKeys.ArrowRight:
|
||||
if (document.fullscreenElement) return;
|
||||
|
||||
if (ctrlKey) {
|
||||
const rotateRightElement = document.getElementsByClassName(
|
||||
"iconContainer rotateRight"
|
||||
)?.[0] as HTMLElement | undefined;
|
||||
rotateRightElement?.click();
|
||||
} else {
|
||||
nextMedia();
|
||||
}
|
||||
if (!ctrlKey) nextMedia();
|
||||
|
||||
break;
|
||||
|
||||
@ -350,16 +348,6 @@ function MediaViewer({
|
||||
if (ctrlKey) onDownload();
|
||||
break;
|
||||
|
||||
case KeyboardEventKeys.Digit1:
|
||||
case KeyboardEventKeys.Numpad1:
|
||||
if (ctrlKey) {
|
||||
const resetElement = document.getElementsByClassName(
|
||||
"iconContainer reset"
|
||||
)?.[0] as HTMLElement | undefined;
|
||||
resetElement?.click();
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyboardEventKeys.Delete:
|
||||
onDelete();
|
||||
break;
|
||||
@ -376,7 +364,10 @@ function MediaViewer({
|
||||
const fetchAndSetTiffDataURL = useCallback((src: string) => {
|
||||
if (!window.Tiff) return;
|
||||
|
||||
TiffXMLHttpRequestRef.current?.abort();
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
TiffXMLHttpRequestRef.current = xhr;
|
||||
xhr.responseType = "arraybuffer";
|
||||
|
||||
xhr.open("GET", src);
|
||||
@ -399,7 +390,6 @@ function MediaViewer({
|
||||
}, [targetFile]);
|
||||
|
||||
const ext = getFileExtension(title);
|
||||
const images = useMemo(() => [{ src: fileUrl, alt: "" }], [fileUrl]);
|
||||
const audioIcon = useMemo(() => props.getIcon(96, ext), [ext]);
|
||||
const headerIcon = useMemo(() => props.getIcon(24, ext), [ext]);
|
||||
|
||||
@ -437,7 +427,7 @@ function MediaViewer({
|
||||
visible={props.visible}
|
||||
title={title}
|
||||
onClose={onClose}
|
||||
images={images}
|
||||
fileUrl={fileUrl}
|
||||
inactive={props.playlist.length <= 1}
|
||||
playlist={props.playlist}
|
||||
playlistPos={playlistPos}
|
||||
|
@ -0,0 +1,33 @@
|
||||
import React from "react";
|
||||
|
||||
import Text from "@docspace/components/text";
|
||||
import IconButton from "@docspace/components/icon-button";
|
||||
|
||||
import { ControlBtn } from "../../StyledComponents";
|
||||
|
||||
import ViewerMediaCloseSvgUrl from "PUBLIC_DIR/images/viewer.media.close.svg?url";
|
||||
|
||||
type DesktopDetailsProps = {
|
||||
title: string;
|
||||
onMaskClick: VoidFunction;
|
||||
};
|
||||
|
||||
function DesktopDetails({ onMaskClick, title }: DesktopDetailsProps) {
|
||||
return (
|
||||
<div className="details">
|
||||
<Text isBold fontSize="14px" className="title">
|
||||
{title}
|
||||
</Text>
|
||||
<ControlBtn onClick={onMaskClick} className="mediaPlayerClose">
|
||||
<IconButton
|
||||
color={"#fff"}
|
||||
iconName={ViewerMediaCloseSvgUrl}
|
||||
size={28}
|
||||
isClickable
|
||||
/>
|
||||
</ControlBtn>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DesktopDetails;
|
@ -0,0 +1,26 @@
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { getCustomToolbar } from "../../helpers/getCustomToolbar";
|
||||
|
||||
interface ImageViewerProps {
|
||||
src?: string;
|
||||
|
||||
isFistImage: boolean;
|
||||
isLastImage: boolean;
|
||||
panelVisible: boolean;
|
||||
mobileDetails: JSX.Element;
|
||||
toolbar: ReturnType<typeof getCustomToolbar>;
|
||||
|
||||
onPrev: VoidFunction;
|
||||
onNext: VoidFunction;
|
||||
onMask: VoidFunction;
|
||||
|
||||
resetToolbarVisibleTimer: VoidFunction;
|
||||
setIsOpenContextMenu: Dispatch<SetStateAction<boolean>>;
|
||||
generateContextMenu: (
|
||||
isOpen: boolean,
|
||||
right?: string,
|
||||
bottom?: string
|
||||
) => JSX.Element;
|
||||
}
|
||||
|
||||
export default ImageViewerProps;
|
@ -0,0 +1,30 @@
|
||||
import styled from "styled-components";
|
||||
import { animated } from "@react-spring/web";
|
||||
|
||||
export const ImageViewerContainer = styled.div<{ $backgroundBlack: boolean }>`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
|
||||
z-index: 300;
|
||||
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
|
||||
background-color: ${(props) =>
|
||||
props.$backgroundBlack ? "#000" : "rgba(55, 55, 55, 0.6)"};
|
||||
`;
|
||||
|
||||
export const ImageWrapper = styled.div<{ $isLoading: boolean }>`
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
visibility: ${(props) => (props.$isLoading ? "hidden" : "visible")};
|
||||
`;
|
||||
|
||||
export const Image = styled(animated.img)`
|
||||
will-change: opacity, transform, scale;
|
||||
`;
|
@ -0,0 +1,969 @@
|
||||
import { useGesture } from "@use-gesture/react";
|
||||
import { isMobile, isDesktop } from "react-device-detect";
|
||||
import { useSpring, config } from "@react-spring/web";
|
||||
import React, {
|
||||
SyntheticEvent,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import ViewerLoader from "../ViewerLoader";
|
||||
import ImageViewerToolbar from "../ImageViewerToolbar";
|
||||
|
||||
import {
|
||||
Image,
|
||||
ImageViewerContainer,
|
||||
ImageWrapper,
|
||||
} from "./ImageViewer.styled";
|
||||
|
||||
import ImageViewerProps from "./ImageViewer.props";
|
||||
import {
|
||||
ImperativeHandle,
|
||||
ToolbarItemType,
|
||||
} from "../ImageViewerToolbar/ImageViewerToolbar.props";
|
||||
import { ToolbarActionType, KeyboardEventKeys } from "../../helpers";
|
||||
|
||||
const MaxScale = 5;
|
||||
const MinScale = 0.5;
|
||||
const DefaultSpeedScale = 0.5;
|
||||
const RatioWheel = 400;
|
||||
|
||||
type BoundsType = {
|
||||
top: number;
|
||||
bottom: number;
|
||||
right: number;
|
||||
left: number;
|
||||
};
|
||||
|
||||
function ImageViewer({
|
||||
src,
|
||||
onPrev,
|
||||
onNext,
|
||||
onMask,
|
||||
isFistImage,
|
||||
isLastImage,
|
||||
panelVisible,
|
||||
generateContextMenu,
|
||||
setIsOpenContextMenu,
|
||||
resetToolbarVisibleTimer,
|
||||
mobileDetails,
|
||||
toolbar,
|
||||
}: ImageViewerProps) {
|
||||
const imgRef = useRef<HTMLImageElement>(null);
|
||||
const imgWrapperRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const unmountRef = useRef<boolean>(false);
|
||||
|
||||
const lastTapTimeRef = useRef<number>(0);
|
||||
const isDoubleTapRef = useRef<boolean>(false);
|
||||
const setTimeoutIDTapRef = useRef<NodeJS.Timeout>();
|
||||
const startAngleRef = useRef<number>(0);
|
||||
const scaleRef = useRef<number>(1);
|
||||
const toolbarRef = useRef<ImperativeHandle>(null);
|
||||
|
||||
const [scale, setScale] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [backgroundBlack, setBackgroundBlack] = useState<boolean>(() => false);
|
||||
|
||||
const [style, api] = useSpring(() => ({
|
||||
width: 0,
|
||||
height: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
scale: 5,
|
||||
rotate: 0,
|
||||
opacity: 1,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
unmountRef.current = false;
|
||||
|
||||
window.addEventListener("resize", resize);
|
||||
|
||||
return () => {
|
||||
setTimeoutIDTapRef.current && clearTimeout(setTimeoutIDTapRef.current);
|
||||
window.removeEventListener("resize", resize);
|
||||
unmountRef.current = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (unmountRef.current) return;
|
||||
setIsLoading(true);
|
||||
}, [src]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
function resize() {
|
||||
if (!imgRef.current || isLoading) return;
|
||||
|
||||
const naturalWidth = imgRef.current.naturalWidth;
|
||||
const naturalHeight = imgRef.current.naturalHeight;
|
||||
|
||||
const imagePositionAndSize = getImagePositionAndSize(
|
||||
naturalWidth,
|
||||
naturalHeight
|
||||
);
|
||||
if (imagePositionAndSize) {
|
||||
api.set(imagePositionAndSize);
|
||||
}
|
||||
}
|
||||
|
||||
const restartScaleAndSize = () => {
|
||||
if (!imgRef.current || style.scale.isAnimating) return;
|
||||
|
||||
const naturalWidth = imgRef.current.naturalWidth;
|
||||
const naturalHeight = imgRef.current.naturalHeight;
|
||||
|
||||
const imagePositionAndSize = getImagePositionAndSize(
|
||||
naturalWidth,
|
||||
naturalHeight
|
||||
);
|
||||
|
||||
if (!imagePositionAndSize) return;
|
||||
|
||||
const { x, y, width, height } = imagePositionAndSize;
|
||||
|
||||
const ratio = 1 / style.scale.get();
|
||||
|
||||
const point = calculateAdjustImage({ x, y }, ratio);
|
||||
|
||||
toolbarRef.current?.setPercentValue(1);
|
||||
|
||||
api.start({
|
||||
...point,
|
||||
width,
|
||||
height,
|
||||
scale: 1,
|
||||
});
|
||||
};
|
||||
|
||||
function getImagePositionAndSize(
|
||||
imageNaturalWidth: number,
|
||||
imageNaturalHeight: number
|
||||
) {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const {
|
||||
width: containerWidth,
|
||||
height: containerHeight,
|
||||
} = containerRef.current.getBoundingClientRect();
|
||||
|
||||
let width = Math.min(containerWidth, imageNaturalWidth);
|
||||
let height = (width / imageNaturalWidth) * imageNaturalHeight;
|
||||
|
||||
if (height > containerHeight) {
|
||||
height = containerHeight;
|
||||
width = (height / imageNaturalHeight) * imageNaturalWidth;
|
||||
}
|
||||
const x = (containerWidth - width) / 2;
|
||||
const y = (containerHeight - height) / 2;
|
||||
|
||||
return { width, height, x, y };
|
||||
}
|
||||
|
||||
function imageLoaded(event: SyntheticEvent<HTMLImageElement, Event>) {
|
||||
const naturalWidth = (event.target as HTMLImageElement).naturalWidth;
|
||||
const naturalHeight = (event.target as HTMLImageElement).naturalHeight;
|
||||
|
||||
const positionAndSize = getImagePositionAndSize(
|
||||
naturalWidth,
|
||||
naturalHeight
|
||||
);
|
||||
|
||||
if (!positionAndSize) return;
|
||||
|
||||
api.set({
|
||||
...positionAndSize,
|
||||
scale: 1,
|
||||
rotate: 0,
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
const compareTo = (a: number, b: number) => {
|
||||
return Math.trunc(a) > Math.trunc(b);
|
||||
};
|
||||
|
||||
const getSizeByAngle = (
|
||||
width: number,
|
||||
height: number,
|
||||
angle: number
|
||||
): [number, number] => {
|
||||
const { abs, cos, sin, PI } = Math;
|
||||
|
||||
const angleByRadians = (PI / 180) * angle;
|
||||
|
||||
const c = cos(angleByRadians);
|
||||
const s = sin(angleByRadians);
|
||||
const halfw = 0.5 * width;
|
||||
const halfh = 0.5 * height;
|
||||
const newWidth = 2 * (abs(c * halfw) + abs(s * halfh));
|
||||
const newHeight = 2 * (abs(s * halfw) + abs(c * halfh));
|
||||
|
||||
return [newWidth, newHeight];
|
||||
};
|
||||
|
||||
const getBounds = (
|
||||
diffScale: number = 1,
|
||||
angle: number = 0
|
||||
): BoundsType | null => {
|
||||
if (!imgRef.current || !containerRef.current) return null;
|
||||
|
||||
let imageBounds = imgRef.current.getBoundingClientRect();
|
||||
const containerBounds = containerRef.current.getBoundingClientRect();
|
||||
|
||||
const [width, height] = getSizeByAngle(
|
||||
imageBounds.width,
|
||||
imageBounds.height,
|
||||
angle
|
||||
);
|
||||
|
||||
if (diffScale !== 1)
|
||||
imageBounds = {
|
||||
...imageBounds,
|
||||
width: width * diffScale,
|
||||
height: height * diffScale,
|
||||
};
|
||||
else {
|
||||
imageBounds = {
|
||||
...imageBounds,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
}
|
||||
const originalWidth = imgRef.current.clientWidth;
|
||||
const widthOverhang = (imageBounds.width - originalWidth) / 2;
|
||||
|
||||
const originalHeight = imgRef.current.clientHeight;
|
||||
const heightOverhang = (imageBounds.height - originalHeight) / 2;
|
||||
|
||||
const isWidthOutContainer = imageBounds.width >= containerBounds.width;
|
||||
|
||||
const isHeightOutContainer = imageBounds.height >= containerBounds.height;
|
||||
|
||||
const bounds = {
|
||||
right: isWidthOutContainer
|
||||
? widthOverhang
|
||||
: containerBounds.width - imageBounds.width + widthOverhang,
|
||||
left: isWidthOutContainer
|
||||
? -(imageBounds.width - containerBounds.width) + widthOverhang
|
||||
: widthOverhang,
|
||||
bottom: isHeightOutContainer
|
||||
? heightOverhang
|
||||
: containerBounds.height - imageBounds.height + heightOverhang,
|
||||
top: isHeightOutContainer
|
||||
? -(imageBounds.height - containerBounds.height) + heightOverhang
|
||||
: heightOverhang,
|
||||
};
|
||||
|
||||
return bounds;
|
||||
};
|
||||
|
||||
const calculateAdjustBounds = (
|
||||
x: number,
|
||||
y: number,
|
||||
diffScale: number = 1,
|
||||
angle: number = 0
|
||||
) => {
|
||||
const bounds = getBounds(diffScale, angle);
|
||||
|
||||
if (!bounds) return { x, y };
|
||||
|
||||
const { left, right, top, bottom } = bounds;
|
||||
|
||||
if (x > right) {
|
||||
x = right;
|
||||
} else if (x < left) {
|
||||
x = left;
|
||||
}
|
||||
|
||||
if (y > bottom) {
|
||||
y = bottom;
|
||||
} else if (y < top) {
|
||||
y = top;
|
||||
}
|
||||
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
const calculateAdjustImage = (
|
||||
point: { x: number; y: number },
|
||||
diffScale: number = 1
|
||||
) => {
|
||||
if (!imgRef.current || !containerRef.current) return point;
|
||||
|
||||
// debugger;
|
||||
|
||||
let imageBounds = imgRef.current.getBoundingClientRect();
|
||||
const containerBounds = containerRef.current.getBoundingClientRect();
|
||||
|
||||
if (diffScale !== 1) {
|
||||
const { x, y, width, height } = imageBounds;
|
||||
|
||||
const newWidth = imageBounds.width * diffScale;
|
||||
const newHeight = imageBounds.height * diffScale;
|
||||
|
||||
const newX = x + width / 2 - newWidth / 2;
|
||||
const newY = y + height / 2 - newHeight / 2;
|
||||
|
||||
imageBounds = {
|
||||
...imageBounds,
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
left: newX,
|
||||
top: newY,
|
||||
right: newX + newWidth,
|
||||
bottom: newY + newHeight,
|
||||
x: newX,
|
||||
y: newY,
|
||||
};
|
||||
}
|
||||
|
||||
const originalWidth = imgRef.current.clientWidth;
|
||||
const widthOverhang = (imageBounds.width - originalWidth) / 2;
|
||||
|
||||
const originalHeight = imgRef.current.clientHeight;
|
||||
const heightOverhang = (imageBounds.height - originalHeight) / 2;
|
||||
|
||||
const isWidthOutContainer = imageBounds.width >= containerBounds.width;
|
||||
|
||||
const isHeightOutContainer = imageBounds.height >= containerBounds.height;
|
||||
|
||||
if (
|
||||
compareTo(imageBounds.left, containerBounds.left) &&
|
||||
isWidthOutContainer
|
||||
) {
|
||||
point.x = widthOverhang;
|
||||
} else if (
|
||||
compareTo(containerBounds.right, imageBounds.right) &&
|
||||
isWidthOutContainer
|
||||
) {
|
||||
point.x = containerBounds.width - imageBounds.width + widthOverhang;
|
||||
} else if (!isWidthOutContainer) {
|
||||
point.x = (containerBounds.width - imageBounds.width) / 2 + widthOverhang;
|
||||
}
|
||||
|
||||
if (
|
||||
compareTo(imageBounds.top, containerBounds.top) &&
|
||||
isHeightOutContainer
|
||||
) {
|
||||
point.y = heightOverhang;
|
||||
} else if (
|
||||
compareTo(containerBounds.bottom, imageBounds.bottom) &&
|
||||
isHeightOutContainer
|
||||
) {
|
||||
point.y = containerBounds.height - imageBounds.height + heightOverhang;
|
||||
} else if (!isHeightOutContainer) {
|
||||
point.y =
|
||||
(containerBounds.height - imageBounds.height) / 2 + heightOverhang;
|
||||
}
|
||||
|
||||
return point;
|
||||
};
|
||||
|
||||
const rotateImage = (dir: number) => {
|
||||
if (style.rotate.isAnimating) return;
|
||||
|
||||
const rotate = style.rotate.get() + dir * 90;
|
||||
|
||||
const point = calculateAdjustImage(
|
||||
calculateAdjustBounds(style.x.get(), style.y.get(), 1, rotate)
|
||||
);
|
||||
|
||||
api.start({
|
||||
...point,
|
||||
rotate,
|
||||
config: {
|
||||
// easing: easings.easeInBack,
|
||||
duration: 200,
|
||||
},
|
||||
onResolve(result) {
|
||||
api.start({
|
||||
...calculateAdjustImage({
|
||||
x: result.value.x,
|
||||
y: result.value.y,
|
||||
}),
|
||||
config: {
|
||||
duration: 100,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const zoomOut = () => {
|
||||
if (
|
||||
style.scale.isAnimating ||
|
||||
style.scale.get() <= MinScale ||
|
||||
!imgRef.current ||
|
||||
!containerRef.current
|
||||
)
|
||||
return;
|
||||
|
||||
const { width, height, x, y } = imgRef.current.getBoundingClientRect();
|
||||
const {
|
||||
width: containerWidth,
|
||||
height: containerHeight,
|
||||
} = containerRef.current.getBoundingClientRect();
|
||||
|
||||
const scale = Math.max(style.scale.get() - DefaultSpeedScale, MinScale);
|
||||
|
||||
const tx = ((containerWidth - width) / 2 - x) / style.scale.get();
|
||||
const ty = ((containerHeight - height) / 2 - y) / style.scale.get();
|
||||
|
||||
let dx = style.x.get() + DefaultSpeedScale * tx;
|
||||
let dy = style.y.get() + DefaultSpeedScale * ty;
|
||||
|
||||
const ratio = scale / style.scale.get();
|
||||
|
||||
const point = calculateAdjustImage(calculateAdjustBounds(dx, dy, ratio));
|
||||
toolbarRef.current?.setPercentValue(scale);
|
||||
|
||||
api.start({
|
||||
scale,
|
||||
...point,
|
||||
config: {
|
||||
duration: 300,
|
||||
},
|
||||
onResolve: (result) => {
|
||||
api.start({
|
||||
...calculateAdjustImage({
|
||||
x: result.value.x,
|
||||
y: result.value.y,
|
||||
}),
|
||||
config: {
|
||||
...config.default,
|
||||
duration: 100,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const zoomIn = () => {
|
||||
if (
|
||||
style.scale.isAnimating ||
|
||||
style.scale.get() >= MaxScale ||
|
||||
!imgRef.current ||
|
||||
!containerRef.current
|
||||
)
|
||||
return;
|
||||
|
||||
const { width, height, x, y } = imgRef.current.getBoundingClientRect();
|
||||
const {
|
||||
width: containerWidth,
|
||||
height: containerHeight,
|
||||
} = containerRef.current.getBoundingClientRect();
|
||||
|
||||
const tx = ((containerWidth - width) / 2 - x) / style.scale.get();
|
||||
const ty = ((containerHeight - height) / 2 - y) / style.scale.get();
|
||||
|
||||
const dx = style.x.get() - DefaultSpeedScale * tx;
|
||||
const dy = style.y.get() - DefaultSpeedScale * ty;
|
||||
|
||||
const scale = Math.min(style.scale.get() + DefaultSpeedScale, MaxScale);
|
||||
toolbarRef.current?.setPercentValue(scale);
|
||||
api.start({
|
||||
x: dx,
|
||||
y: dy,
|
||||
scale,
|
||||
config: {
|
||||
duration: 300,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
const { code, ctrlKey } = event;
|
||||
|
||||
switch (code) {
|
||||
case KeyboardEventKeys.ArrowLeft:
|
||||
case KeyboardEventKeys.ArrowRight:
|
||||
if (document.fullscreenElement) return;
|
||||
if (ctrlKey) {
|
||||
const dir = code === KeyboardEventKeys.ArrowRight ? 1 : -1;
|
||||
rotateImage(dir);
|
||||
}
|
||||
break;
|
||||
case KeyboardEventKeys.ArrowUp:
|
||||
zoomIn();
|
||||
break;
|
||||
case KeyboardEventKeys.ArrowDown:
|
||||
zoomOut();
|
||||
break;
|
||||
case KeyboardEventKeys.Digit1:
|
||||
case KeyboardEventKeys.Numpad1:
|
||||
if (ctrlKey) {
|
||||
restartScaleAndSize();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDoubleTapOrClick = (
|
||||
event:
|
||||
| TouchEvent
|
||||
| MouseEvent
|
||||
| React.MouseEvent<HTMLImageElement, MouseEvent>
|
||||
) => {
|
||||
if (style.scale.isAnimating) return;
|
||||
|
||||
if (style.scale.get() !== 1) {
|
||||
restartScaleAndSize();
|
||||
} else {
|
||||
zoomOnDoubleTap(event);
|
||||
}
|
||||
};
|
||||
|
||||
const zoomOnDoubleTap = (
|
||||
event:
|
||||
| TouchEvent
|
||||
| MouseEvent
|
||||
| React.MouseEvent<HTMLImageElement, MouseEvent>,
|
||||
scale = 1
|
||||
) => {
|
||||
if (
|
||||
!imgRef.current ||
|
||||
style.scale.get() >= MaxScale ||
|
||||
style.scale.isAnimating
|
||||
)
|
||||
return;
|
||||
|
||||
const isTouch = "touches" in event;
|
||||
|
||||
const pageX = isTouch ? event.touches[0].pageX : event.pageX;
|
||||
const pageY = isTouch ? event.touches[0].pageY : event.pageY;
|
||||
|
||||
const { width, height, x, y } = imgRef.current.getBoundingClientRect();
|
||||
const tx = (pageX - (x + width / 2)) / style.scale.get();
|
||||
const ty = (pageY - (y + height / 2)) / style.scale.get();
|
||||
|
||||
const dx = style.x.get() - scale * tx;
|
||||
const dy = style.y.get() - scale * ty;
|
||||
|
||||
const newScale = Math.min(style.scale.get() + scale, MaxScale);
|
||||
const ratio = newScale / style.scale.get();
|
||||
|
||||
const point = calculateAdjustImage(
|
||||
calculateAdjustBounds(dx, dy, ratio),
|
||||
ratio
|
||||
);
|
||||
|
||||
toolbarRef.current?.setPercentValue(newScale);
|
||||
|
||||
api.start({
|
||||
...point,
|
||||
scale: newScale,
|
||||
config: config.default,
|
||||
// onChange(result) {
|
||||
// api.start(maybeAdjustImage({ x: dx, y: dy }));
|
||||
// },
|
||||
onResolve() {
|
||||
api.start(calculateAdjustImage(calculateAdjustBounds(dx, dy, 1)));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
useGesture(
|
||||
{
|
||||
onDragStart: ({ pinching }) => {
|
||||
if (pinching) return;
|
||||
|
||||
setScale(style.scale.get());
|
||||
},
|
||||
onDrag: ({
|
||||
offset: [dx, dy],
|
||||
movement: [mdx, mdy],
|
||||
cancel,
|
||||
pinching,
|
||||
canceled,
|
||||
}) => {
|
||||
if (!imgRef.current) return;
|
||||
|
||||
if (isDoubleTapRef.current || unmountRef.current) {
|
||||
isDoubleTapRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (pinching || canceled) cancel();
|
||||
if (!imgRef.current || !containerRef.current) return;
|
||||
|
||||
api.start({
|
||||
x:
|
||||
style.scale.get() === 1 &&
|
||||
!isDesktop &&
|
||||
((isFistImage && mdx > 0) || (isLastImage && mdx < 0))
|
||||
? style.x.get()
|
||||
: dx,
|
||||
y: dy,
|
||||
opacity:
|
||||
style.scale.get() === 1 && !isDesktop && mdy > 0
|
||||
? imgRef.current.height / 10 / mdy
|
||||
: style.opacity.get(),
|
||||
immediate: true,
|
||||
config: config.default,
|
||||
});
|
||||
},
|
||||
|
||||
onDragEnd: ({ cancel, canceled, movement: [mdx, mdy] }) => {
|
||||
if (unmountRef.current || !imgRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (canceled || isDoubleTapRef.current) {
|
||||
isDoubleTapRef.current = false;
|
||||
cancel();
|
||||
}
|
||||
|
||||
if (style.scale.get() === 1 && !isDesktop) {
|
||||
if (mdx < -imgRef.current.width / 4) {
|
||||
return onNext();
|
||||
} else if (mdx > imgRef.current.width / 4) {
|
||||
return onPrev();
|
||||
}
|
||||
if (mdy > 150) {
|
||||
return onMask();
|
||||
}
|
||||
}
|
||||
|
||||
const newPoint = calculateAdjustImage({
|
||||
x: style.x.get(),
|
||||
y: style.y.get(),
|
||||
});
|
||||
|
||||
api.start({
|
||||
...newPoint,
|
||||
opacity: 1,
|
||||
config: config.default,
|
||||
});
|
||||
},
|
||||
|
||||
onPinchStart: ({ event, cancel }) => {
|
||||
if (event.target === containerRef.current) {
|
||||
cancel();
|
||||
} else {
|
||||
const roundedAngle = Math.round(style.rotate.get());
|
||||
startAngleRef.current = roundedAngle - (roundedAngle % 90);
|
||||
}
|
||||
},
|
||||
|
||||
onPinch: ({
|
||||
origin: [ox, oy],
|
||||
offset: [dScale, dRotate],
|
||||
lastOffset: [LScale],
|
||||
movement: [mScale],
|
||||
memo,
|
||||
first,
|
||||
canceled,
|
||||
event,
|
||||
pinching,
|
||||
cancel,
|
||||
}) => {
|
||||
if (
|
||||
canceled ||
|
||||
event.target === containerRef.current ||
|
||||
!imgRef.current
|
||||
)
|
||||
return memo;
|
||||
|
||||
if (!pinching) cancel();
|
||||
|
||||
if (first) {
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
x,
|
||||
y,
|
||||
} = imgRef.current.getBoundingClientRect();
|
||||
const tx = ox - (x + width / 2);
|
||||
const ty = oy - (y + height / 2);
|
||||
memo = [style.x.get(), style.y.get(), tx, ty];
|
||||
}
|
||||
|
||||
const x = memo[0] - (mScale - 1) * memo[2];
|
||||
const y = memo[1] - (mScale - 1) * memo[3];
|
||||
|
||||
const ratio = dScale / LScale;
|
||||
|
||||
const { x: dx, y: dy } = calculateAdjustImage({ x, y }, ratio);
|
||||
|
||||
const point = calculateAdjustBounds(dx, dy, ratio, dRotate);
|
||||
|
||||
scaleRef.current = dScale;
|
||||
|
||||
api.start({
|
||||
...point,
|
||||
scale: dScale,
|
||||
rotate: dRotate,
|
||||
delay: 0,
|
||||
onChange(result) {
|
||||
api.start({
|
||||
...calculateAdjustImage(
|
||||
{
|
||||
x: result.value.x,
|
||||
y: result.value.y,
|
||||
},
|
||||
ratio
|
||||
),
|
||||
delay: 0,
|
||||
config: {
|
||||
duration: 200,
|
||||
},
|
||||
});
|
||||
},
|
||||
config: config.default,
|
||||
});
|
||||
return memo;
|
||||
},
|
||||
onPinchEnd: ({
|
||||
movement: [, mRotate],
|
||||
direction: [, dirRotate],
|
||||
canceled,
|
||||
}) => {
|
||||
if (unmountRef.current || canceled) {
|
||||
return;
|
||||
}
|
||||
const rotate =
|
||||
Math.abs(mRotate / 90) > 1 / 3
|
||||
? Math.trunc(
|
||||
startAngleRef.current +
|
||||
90 *
|
||||
Math.max(Math.trunc(Math.abs(mRotate) / 90), 1) *
|
||||
dirRotate
|
||||
)
|
||||
: startAngleRef.current;
|
||||
|
||||
const newPoint = calculateAdjustImage({
|
||||
x: style.x.get(),
|
||||
y: style.y.get(),
|
||||
});
|
||||
|
||||
api.start({
|
||||
...newPoint,
|
||||
rotate,
|
||||
delay: 0,
|
||||
onResolve: () => {
|
||||
api.start({
|
||||
...calculateAdjustImage({
|
||||
x: style.x.get(),
|
||||
y: style.y.get(),
|
||||
}),
|
||||
delay: 0,
|
||||
config: {
|
||||
...config.default,
|
||||
duration: 200,
|
||||
},
|
||||
});
|
||||
},
|
||||
onChange(result) {
|
||||
api.start({
|
||||
...calculateAdjustImage({
|
||||
x: result.value.x,
|
||||
y: result.value.y,
|
||||
}),
|
||||
delay: 0,
|
||||
config: {
|
||||
...config.default,
|
||||
duration: 200,
|
||||
},
|
||||
});
|
||||
},
|
||||
config: config.default,
|
||||
});
|
||||
},
|
||||
|
||||
onClick: ({ pinching, event }) => {
|
||||
if (isDesktop && event.target === imgWrapperRef.current)
|
||||
return onMask();
|
||||
|
||||
if (!imgRef.current || !containerRef.current || pinching || isDesktop)
|
||||
return;
|
||||
|
||||
const time = new Date().getTime();
|
||||
|
||||
if (time - lastTapTimeRef.current < 300) {
|
||||
//on Double Tap
|
||||
lastTapTimeRef.current = 0;
|
||||
isDoubleTapRef.current = true;
|
||||
|
||||
handleDoubleTapOrClick(event);
|
||||
|
||||
clearTimeout(setTimeoutIDTapRef.current);
|
||||
} else {
|
||||
lastTapTimeRef.current = time;
|
||||
setTimeoutIDTapRef.current = setTimeout(() => {
|
||||
// onTap
|
||||
setBackgroundBlack((state) => !state);
|
||||
}, 300);
|
||||
}
|
||||
},
|
||||
onWheel: ({
|
||||
first,
|
||||
offset: [, yWheel],
|
||||
lastOffset: [, lYWheel],
|
||||
movement: [, mYWheel],
|
||||
pinching,
|
||||
memo,
|
||||
event,
|
||||
}) => {
|
||||
if (
|
||||
!imgRef.current ||
|
||||
pinching ||
|
||||
style.scale.isAnimating ||
|
||||
event.target !== imgRef.current
|
||||
)
|
||||
return memo;
|
||||
|
||||
const dScale = (-1 * yWheel) / RatioWheel;
|
||||
const lScale = (-1 * lYWheel) / RatioWheel;
|
||||
const mScale = (-1 * mYWheel) / RatioWheel;
|
||||
|
||||
if (first || !memo) {
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
x,
|
||||
y,
|
||||
} = imgRef.current.getBoundingClientRect();
|
||||
const tx = (event.pageX - (x + width / 2)) / style.scale.get();
|
||||
const ty = (event.pageY - (y + height / 2)) / style.scale.get();
|
||||
memo = [style.x.get(), style.y.get(), tx, ty];
|
||||
}
|
||||
const dx = memo[0] - mScale * memo[2];
|
||||
const dy = memo[1] - mScale * memo[3];
|
||||
|
||||
const ratio = dScale / lScale;
|
||||
|
||||
const point = calculateAdjustImage(
|
||||
calculateAdjustBounds(dx, dy, ratio),
|
||||
ratio
|
||||
);
|
||||
toolbarRef.current?.setPercentValue(dScale);
|
||||
api.start({
|
||||
...point,
|
||||
scale: dScale,
|
||||
config: {
|
||||
...config.default,
|
||||
duration: 300,
|
||||
},
|
||||
onResolve(result) {
|
||||
api.start(
|
||||
calculateAdjustImage(
|
||||
calculateAdjustBounds(result.value.x, result.value.y)
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return memo;
|
||||
},
|
||||
},
|
||||
{
|
||||
drag: {
|
||||
from: () => [style.x.get(), style.y.get()],
|
||||
axis: scale === 1 && !isDesktop ? "lock" : undefined,
|
||||
rubberband: isDesktop,
|
||||
bounds: () => {
|
||||
if (style.scale.get() === 1 && !isDesktop) return {};
|
||||
|
||||
return getBounds() ?? {};
|
||||
},
|
||||
},
|
||||
pinch: {
|
||||
scaleBounds: { min: MinScale, max: MaxScale },
|
||||
rubberband: false,
|
||||
from: () => [style.scale.get(), style.rotate.get()],
|
||||
threshold: [0.1, 5],
|
||||
pinchOnWheel: false,
|
||||
},
|
||||
|
||||
wheel: {
|
||||
from: () => [0, -style.scale.get() * RatioWheel],
|
||||
bounds: () => ({
|
||||
top: -MaxScale * RatioWheel,
|
||||
bottom: -MinScale * RatioWheel,
|
||||
}),
|
||||
axis: "y",
|
||||
},
|
||||
|
||||
target: containerRef,
|
||||
}
|
||||
);
|
||||
|
||||
const handleAction = (action: ToolbarActionType) => {
|
||||
resetToolbarVisibleTimer();
|
||||
|
||||
switch (action) {
|
||||
case ToolbarActionType.ZoomOut:
|
||||
zoomOut();
|
||||
break;
|
||||
case ToolbarActionType.ZoomIn:
|
||||
zoomIn();
|
||||
break;
|
||||
|
||||
case ToolbarActionType.RotateLeft:
|
||||
case ToolbarActionType.RotateRight:
|
||||
const dir = action === ToolbarActionType.RotateRight ? 1 : -1;
|
||||
rotateImage(dir);
|
||||
break;
|
||||
case ToolbarActionType.Reset:
|
||||
restartScaleAndSize();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
function toolbarEvent(item: ToolbarItemType) {
|
||||
if (item.onClick) {
|
||||
item.onClick();
|
||||
} else {
|
||||
handleAction(item.actionType);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isMobile && !backgroundBlack && mobileDetails}
|
||||
<ImageViewerContainer
|
||||
ref={containerRef}
|
||||
$backgroundBlack={backgroundBlack}
|
||||
>
|
||||
<ViewerLoader isLoading={isLoading} />
|
||||
<ImageWrapper ref={imgWrapperRef} $isLoading={isLoading}>
|
||||
<Image
|
||||
src={src}
|
||||
ref={imgRef}
|
||||
style={style}
|
||||
onDoubleClick={handleDoubleTapOrClick}
|
||||
onLoad={imageLoaded}
|
||||
/>
|
||||
</ImageWrapper>
|
||||
</ImageViewerContainer>
|
||||
{isDesktop && panelVisible && (
|
||||
<ImageViewerToolbar
|
||||
ref={toolbarRef}
|
||||
toolbar={toolbar}
|
||||
generateContextMenu={generateContextMenu}
|
||||
setIsOpenContextMenu={setIsOpenContextMenu}
|
||||
toolbarEvent={toolbarEvent}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImageViewer;
|
@ -0,0 +1,21 @@
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { getCustomToolbar } from "../../helpers/getCustomToolbar";
|
||||
|
||||
interface ImageViewerToolbarProps {
|
||||
toolbar: ReturnType<typeof getCustomToolbar>;
|
||||
generateContextMenu: (
|
||||
isOpen: boolean,
|
||||
right?: string,
|
||||
bottom?: string
|
||||
) => JSX.Element;
|
||||
setIsOpenContextMenu: Dispatch<SetStateAction<boolean>>;
|
||||
toolbarEvent: (item: ToolbarItemType) => void;
|
||||
}
|
||||
|
||||
export type ToolbarItemType = ReturnType<typeof getCustomToolbar>[number];
|
||||
|
||||
export type ImperativeHandle = {
|
||||
setPercentValue: (percent: number) => void;
|
||||
};
|
||||
|
||||
export default ImageViewerToolbarProps;
|
@ -0,0 +1,67 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
export const ImageViewerToolbarWrapper = styled.div`
|
||||
height: 48px;
|
||||
padding: 10px 24px;
|
||||
border-radius: 18px;
|
||||
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
z-index: 307;
|
||||
|
||||
transform: translateX(-50%);
|
||||
|
||||
text-align: center;
|
||||
transition: all 0.26s ease-out;
|
||||
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
`;
|
||||
|
||||
export const ListTools = styled.ul`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
`;
|
||||
export const ToolbarItem = styled.li<{
|
||||
$isSeparator?: boolean;
|
||||
$percent?: number;
|
||||
}>`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
height: 48px;
|
||||
width: ${(props) => (props.$isSeparator ? "33px" : "48px")};
|
||||
|
||||
&:hover {
|
||||
cursor: ${(props) => (props.$isSeparator ? "default" : "pointer")};
|
||||
}
|
||||
|
||||
.zoomPercent {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
path,
|
||||
rect {
|
||||
${(props) => (props.$percent !== 25 ? "fill: #fff;" : "fill: #BEBEBE;")}
|
||||
}
|
||||
}
|
||||
|
||||
.zoomOut,
|
||||
.zoomIn,
|
||||
.rotateLeft,
|
||||
.rotateRight {
|
||||
margin-top: 3px;
|
||||
}
|
||||
`;
|
@ -0,0 +1,111 @@
|
||||
import React, {
|
||||
ForwardedRef,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import ImageViewerToolbarProps, {
|
||||
ImperativeHandle,
|
||||
ToolbarItemType,
|
||||
} from "./ImageViewerToolbar.props";
|
||||
|
||||
import {
|
||||
ImageViewerToolbarWrapper,
|
||||
ListTools,
|
||||
ToolbarItem,
|
||||
} from "./imageViewerToolbar.styled";
|
||||
|
||||
import MediaContextMenu from "PUBLIC_DIR/images/vertical-dots.react.svg";
|
||||
|
||||
function ImageViewerToolbar(
|
||||
{
|
||||
toolbar,
|
||||
toolbarEvent,
|
||||
generateContextMenu,
|
||||
setIsOpenContextMenu,
|
||||
}: ImageViewerToolbarProps,
|
||||
ref: ForwardedRef<ImperativeHandle>
|
||||
) {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [percent, setPercent] = useState<number>(100);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => {
|
||||
return {
|
||||
setPercentValue(percent) {
|
||||
setPercent(Math.round(percent * 100));
|
||||
},
|
||||
};
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
function getContextMenu(item: ToolbarItemType) {
|
||||
const contextMenu = generateContextMenu(isOpen);
|
||||
return (
|
||||
<ToolbarItem
|
||||
style={{ position: "relative" }}
|
||||
key={item.key}
|
||||
onClick={() => {
|
||||
setIsOpenContextMenu((open) => !open);
|
||||
setIsOpen((open) => !open);
|
||||
}}
|
||||
data-key={item.key}
|
||||
>
|
||||
<div className="context" style={{ height: "16px" }}>
|
||||
<MediaContextMenu size="scale" />
|
||||
</div>
|
||||
{contextMenu}
|
||||
</ToolbarItem>
|
||||
);
|
||||
}
|
||||
|
||||
function getPercentCompoent() {
|
||||
return (
|
||||
<div
|
||||
className="iconContainer zoomPercent"
|
||||
style={{ width: "auto", color: "#fff", userSelect: "none" }}
|
||||
>
|
||||
{`${percent}%`}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderToolbarItem(item: ToolbarItemType) {
|
||||
if (item.key === "context-menu") {
|
||||
return getContextMenu(item);
|
||||
}
|
||||
|
||||
let content: JSX.Element | undefined = item?.render;
|
||||
|
||||
if (item.key === "percent") {
|
||||
content = getPercentCompoent();
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolbarItem
|
||||
$percent={item.percent ? percent : 100}
|
||||
$isSeparator={item.actionType === -1}
|
||||
key={item.key}
|
||||
onClick={() => {
|
||||
toolbarEvent(item);
|
||||
}}
|
||||
data-key={item.key}
|
||||
>
|
||||
{content}
|
||||
</ToolbarItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ImageViewerToolbarWrapper>
|
||||
<ListTools>{toolbar.map((item) => renderToolbarItem(item))}</ListTools>
|
||||
</ImageViewerToolbarWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef<ImperativeHandle, ImageViewerToolbarProps>(
|
||||
ImageViewerToolbar
|
||||
);
|
@ -34,16 +34,7 @@ function MobileDetails(
|
||||
return (
|
||||
<StyledMobileDetails>
|
||||
<BackArrow className="mobile-close" onClick={onMaskClick} />
|
||||
<Text
|
||||
fontSize="14px"
|
||||
color="#fff"
|
||||
className="title"
|
||||
as={undefined}
|
||||
tag={undefined}
|
||||
title={undefined}
|
||||
textAlign={undefined}
|
||||
fontWeight={undefined}
|
||||
>
|
||||
<Text fontSize="14px" color="#fff" className="title">
|
||||
{title}
|
||||
</Text>
|
||||
{!isPreviewFile && !isError && (
|
||||
|
@ -3,7 +3,7 @@ import { ContextMenuModel, PlaylistType } from "../../types";
|
||||
|
||||
interface ViewerProps {
|
||||
title: string;
|
||||
images: { src: string; alt: string }[];
|
||||
fileUrl?: string;
|
||||
isAudio: boolean;
|
||||
isVideo: boolean;
|
||||
visible: boolean;
|
||||
@ -13,10 +13,9 @@ interface ViewerProps {
|
||||
inactive: boolean;
|
||||
|
||||
audioIcon: string;
|
||||
zoomSpeed: number;
|
||||
errorTitle: string;
|
||||
headerIcon: string;
|
||||
customToolbar: () => ReturnType<typeof getCustomToolbar>;
|
||||
toolbar: ReturnType<typeof getCustomToolbar>;
|
||||
playlistPos: number;
|
||||
archiveRoom: boolean;
|
||||
isPreviewFile: boolean;
|
||||
@ -27,8 +26,8 @@ interface ViewerProps {
|
||||
onDownloadClick: VoidFunction;
|
||||
generateContextMenu: (
|
||||
isOpen: boolean,
|
||||
right: string,
|
||||
bottom: string
|
||||
right?: string,
|
||||
bottom?: string
|
||||
) => JSX.Element;
|
||||
onSetSelectionFile: VoidFunction;
|
||||
}
|
||||
|
@ -1,24 +1,20 @@
|
||||
import ReactDOM from "react-dom";
|
||||
import { isMobileOnly, isMobile } from "react-device-detect";
|
||||
import React, { useRef, useState, useEffect, useCallback } from "react";
|
||||
import { isMobileOnly } from "react-device-detect";
|
||||
|
||||
import Text from "@docspace/components/text";
|
||||
import IconButton from "@docspace/components/icon-button";
|
||||
import ContextMenu from "@docspace/components/context-menu";
|
||||
|
||||
import { StyledViewer } from "@docspace/components/viewer/styled-viewer";
|
||||
import ViewerPlayer from "@docspace/components/viewer/sub-components/viewer-player";
|
||||
import { StyledViewerContainer } from "../../StyledComponents";
|
||||
|
||||
import { ControlBtn, StyledViewerContainer } from "../../StyledComponents";
|
||||
|
||||
import MobileDetails from "../MobileDetails";
|
||||
import PrevButton from "../PrevButton";
|
||||
import NextButton from "../NextButton";
|
||||
import PrevButton from "../PrevButton";
|
||||
import ImageViewer from "../ImageViewer";
|
||||
import MobileDetails from "../MobileDetails";
|
||||
import DesktopDetails from "../DesktopDetails";
|
||||
import ViewerPlayer from "../ViewerPlayer/viewer-player";
|
||||
|
||||
import type ViewerProps from "./Viewer.props";
|
||||
|
||||
import ViewerMediaCloseSvgUrl from "PUBLIC_DIR/images/viewer.media.close.svg?url";
|
||||
|
||||
function Viewer(props: ViewerProps) {
|
||||
const timerIDRef = useRef<NodeJS.Timeout>();
|
||||
|
||||
@ -31,6 +27,8 @@ function Viewer(props: ViewerProps) {
|
||||
|
||||
const [imageTimer, setImageTimer] = useState<NodeJS.Timeout>();
|
||||
|
||||
const panelVisibleRef = useRef<boolean>(false);
|
||||
|
||||
const contextMenuRef = useRef<ContextMenu>(null);
|
||||
const videoElementRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
@ -49,20 +47,33 @@ function Viewer(props: ViewerProps) {
|
||||
return clearTimeout(timerIDRef.current);
|
||||
}, [isPlay, isOpenContextMenu, props.isImage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMobileOnly) return;
|
||||
|
||||
const resetTimer = () => {
|
||||
setPanelVisible(true);
|
||||
const resetToolbarVisibleTimer = () => {
|
||||
if (panelVisibleRef.current) {
|
||||
clearTimeout(timerIDRef.current);
|
||||
timerIDRef.current = setTimeout(() => setPanelVisible(false), 2500);
|
||||
setImageTimer(timerIDRef.current);
|
||||
};
|
||||
timerIDRef.current = setTimeout(() => {
|
||||
panelVisibleRef.current = false;
|
||||
setPanelVisible(false);
|
||||
}, 2500);
|
||||
} else {
|
||||
setPanelVisible(true);
|
||||
panelVisibleRef.current = true;
|
||||
|
||||
document.addEventListener("mousemove", resetTimer, { passive: true });
|
||||
timerIDRef.current = setTimeout(() => {
|
||||
panelVisibleRef.current = false;
|
||||
setPanelVisible(false);
|
||||
}, 2500);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isMobile) return;
|
||||
resetToolbarVisibleTimer();
|
||||
document.addEventListener("mousemove", resetToolbarVisibleTimer, {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", resetTimer);
|
||||
document.removeEventListener("mousemove", resetToolbarVisibleTimer);
|
||||
clearTimeout(timerIDRef.current);
|
||||
setPanelVisible(true);
|
||||
};
|
||||
@ -108,15 +119,15 @@ function Viewer(props: ViewerProps) {
|
||||
|
||||
const mobileDetails = (
|
||||
<MobileDetails
|
||||
onHide={onHide}
|
||||
isError={isError}
|
||||
title={props.title}
|
||||
icon={props.headerIcon}
|
||||
contextModel={props.contextModel}
|
||||
isPreviewFile={props.isPreviewFile}
|
||||
onHide={onHide}
|
||||
onContextMenu={onMobileContextMenu}
|
||||
onMaskClick={props.onMaskClick}
|
||||
ref={contextMenuRef}
|
||||
icon={props.headerIcon}
|
||||
onMaskClick={props.onMaskClick}
|
||||
contextModel={props.contextModel}
|
||||
onContextMenu={onMobileContextMenu}
|
||||
isPreviewFile={props.isPreviewFile}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -127,38 +138,11 @@ function Viewer(props: ViewerProps) {
|
||||
|
||||
return (
|
||||
<StyledViewerContainer visible={props.visible}>
|
||||
{!isFullscreen && !isMobileOnly && displayUI && (
|
||||
<div>
|
||||
<div className="details">
|
||||
<Text
|
||||
isBold
|
||||
fontSize="14px"
|
||||
className="title"
|
||||
title={undefined}
|
||||
tag={undefined}
|
||||
as={undefined}
|
||||
fontWeight={undefined}
|
||||
color={undefined}
|
||||
textAlign={undefined}
|
||||
>
|
||||
{props.title}
|
||||
</Text>
|
||||
<ControlBtn
|
||||
onClick={props.onMaskClick}
|
||||
className="mediaPlayerClose"
|
||||
>
|
||||
<IconButton
|
||||
color={"#fff"}
|
||||
iconName={ViewerMediaCloseSvgUrl}
|
||||
size={28}
|
||||
isClickable
|
||||
/>
|
||||
</ControlBtn>
|
||||
</div>
|
||||
</div>
|
||||
{!isFullscreen && !isMobile && panelVisible && (
|
||||
<DesktopDetails title={props.title} onMaskClick={props.onMaskClick} />
|
||||
)}
|
||||
|
||||
{props.playlist.length > 1 && !isFullscreen && displayUI && (
|
||||
{props.playlist.length > 1 && !isFullscreen && !isMobile && (
|
||||
<>
|
||||
{isNotFirstElement && <PrevButton prevClick={prevClick} />}
|
||||
{isNotLastElement && <NextButton nextClick={nextClick} />}
|
||||
@ -167,16 +151,19 @@ function Viewer(props: ViewerProps) {
|
||||
|
||||
{props.isImage
|
||||
? ReactDOM.createPortal(
|
||||
<StyledViewer
|
||||
{...props}
|
||||
displayUI={displayUI}
|
||||
<ImageViewer
|
||||
panelVisible={panelVisible}
|
||||
toolbar={props.toolbar}
|
||||
src={props.fileUrl}
|
||||
mobileDetails={mobileDetails}
|
||||
setIsOpenContextMenu={setIsOpenContextMenu}
|
||||
container={containerRef.current}
|
||||
imageTimer={imageTimer}
|
||||
onMaskClick={props.onMaskClick}
|
||||
setPanelVisible={setPanelVisible}
|
||||
onMask={props.onMaskClick}
|
||||
onPrev={props.onPrevClick}
|
||||
onNext={props.onNextClick}
|
||||
isLastImage={!isNotLastElement}
|
||||
isFistImage={!isNotFirstElement}
|
||||
generateContextMenu={props.generateContextMenu}
|
||||
setIsOpenContextMenu={setIsOpenContextMenu}
|
||||
resetToolbarVisibleTimer={resetToolbarVisibleTimer}
|
||||
/>,
|
||||
containerRef.current
|
||||
)
|
||||
|
@ -0,0 +1,48 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
const StyledLoaderWrapper = styled.div`
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const StyledLoader = styled.div`
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid #fff;
|
||||
border-bottom-color: transparent;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
animation: rotation 1s linear infinite;
|
||||
|
||||
@keyframes rotation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type ViewerLoader = {
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
export default function ViewerLoader({ isLoading }: ViewerLoader) {
|
||||
if (!isLoading) return <></>;
|
||||
|
||||
return (
|
||||
<StyledLoaderWrapper>
|
||||
<StyledLoader />
|
||||
</StyledLoaderWrapper>
|
||||
);
|
||||
}
|
@ -21,7 +21,7 @@ import Icon15x from "PUBLIC_DIR/images/media.viewer15x.react.svg";
|
||||
import Icon2x from "PUBLIC_DIR/images/media.viewer2x.react.svg";
|
||||
|
||||
import BigIconPlay from "PUBLIC_DIR/images/media.bgplay.react.svg";
|
||||
import { useSwipeable } from "../../react-swipeable";
|
||||
import { useSwipeable } from "@docspace/components/react-swipeable";
|
||||
import { MediaError } from "./media-error";
|
||||
|
||||
let iconWidth = 80;
|
||||
@ -422,6 +422,17 @@ export default function ViewerPlayer(props) {
|
||||
const [currentVolume, setCurrentVolume] = React.useState(stateVolume);
|
||||
const [globalTimer, setGlobalTimer] = React.useState(null);
|
||||
const speedIcons = [<Icon05x />, <Icon1x />, <Icon15x />, <Icon2x />];
|
||||
|
||||
const unmountedRef = React.useRef(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
unmountedRef.current = false;
|
||||
|
||||
return () => {
|
||||
unmountedRef.current = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handlers = useSwipeable({
|
||||
onSwiping: (e) => {
|
||||
const [width, height, left, top] = getVideoPosition(videoRef.current);
|
||||
@ -742,7 +753,7 @@ export default function ViewerPlayer(props) {
|
||||
const lasting = `${currentTime} / ${duration}`;
|
||||
|
||||
if (progress === 100 || !state.isPlaying) {
|
||||
videoRef.current.stop();
|
||||
videoRef.current.pause();
|
||||
} else {
|
||||
videoRef.current.play();
|
||||
}
|
||||
@ -842,14 +853,22 @@ export default function ViewerPlayer(props) {
|
||||
|
||||
React.useEffect(() => {
|
||||
if (videoRef && videoRef.current) {
|
||||
videoRef.current.addEventListener("error", (event) => {
|
||||
const onError = (event) => {
|
||||
if (unmountedRef.current) return;
|
||||
|
||||
setIsError(true);
|
||||
return dispatch(
|
||||
createAction(ACTION_TYPES.update, {
|
||||
loadingError: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
videoRef.current.addEventListener("error", onError);
|
||||
|
||||
return () => {
|
||||
videoRef.current?.removeEventListener("error", onError);
|
||||
};
|
||||
}
|
||||
}, [videoRef.current]);
|
||||
|
@ -4,7 +4,7 @@ interface ViewerWrapperProps {
|
||||
userAccess: boolean;
|
||||
visible: boolean;
|
||||
title: string;
|
||||
images: { src: string; alt: string }[];
|
||||
fileUrl?: string;
|
||||
inactive: boolean;
|
||||
playlist: PlaylistType[];
|
||||
playlistPos: number;
|
||||
|
@ -10,22 +10,16 @@ import { StyledDropDown } from "../StyledDropDown";
|
||||
import { StyledDropDownItem } from "../StyledDropDownItem";
|
||||
import ViewerWrapperProps from "./ViewerWrapper.props";
|
||||
|
||||
const DefaultSpeedZoom = 0.25;
|
||||
|
||||
function ViewerWrapper(props: ViewerWrapperProps) {
|
||||
const onClickContextItem = useCallback(
|
||||
(item: ContextMenuModel) => {
|
||||
if (isSeparator(item)) return;
|
||||
item.onClick();
|
||||
props.onClose();
|
||||
},
|
||||
[props.onClose]
|
||||
);
|
||||
const onClickContextItem = useCallback((item: ContextMenuModel) => {
|
||||
if (isSeparator(item)) return;
|
||||
item.onClick();
|
||||
}, []);
|
||||
|
||||
const generateContextMenu = (
|
||||
isOpen: boolean,
|
||||
right: string,
|
||||
bottom: string
|
||||
right?: string,
|
||||
bottom?: string
|
||||
) => {
|
||||
const model = props.contextModel();
|
||||
|
||||
@ -58,7 +52,7 @@ function ViewerWrapper(props: ViewerWrapperProps) {
|
||||
);
|
||||
};
|
||||
|
||||
const toolbars = useMemo(() => {
|
||||
const toolbar = useMemo(() => {
|
||||
const {
|
||||
onDeleteClick,
|
||||
onDownloadClick,
|
||||
@ -89,7 +83,7 @@ function ViewerWrapper(props: ViewerWrapperProps) {
|
||||
return (
|
||||
<Viewer
|
||||
title={props.title}
|
||||
images={props.images}
|
||||
fileUrl={props.fileUrl}
|
||||
isAudio={props.isAudio}
|
||||
isVideo={props.isVideo}
|
||||
visible={props.visible}
|
||||
@ -97,10 +91,9 @@ function ViewerWrapper(props: ViewerWrapperProps) {
|
||||
playlist={props.playlist}
|
||||
inactive={props.inactive}
|
||||
audioIcon={props.audioIcon}
|
||||
zoomSpeed={DefaultSpeedZoom}
|
||||
errorTitle={props.errorTitle}
|
||||
headerIcon={props.headerIcon}
|
||||
customToolbar={() => toolbars}
|
||||
toolbar={toolbar}
|
||||
playlistPos={props.playlistPos}
|
||||
archiveRoom={props.archiveRoom}
|
||||
isPreviewFile={props.isPreviewFile}
|
||||
|
@ -380,3 +380,26 @@ export const PortalFeaturesLimitations = Object.freeze({
|
||||
export const EDITOR_ID = "docspace_editor";
|
||||
|
||||
export const wrongPortalNameUrl = `https://www.onlyoffice.com/wrongportalname.aspx`;
|
||||
|
||||
export const FilterGroups = Object.freeze({
|
||||
filterType: "filter-filterType",
|
||||
filterAuthor: "filter-author",
|
||||
filterFolders: "filter-folders",
|
||||
filterContent: "filter-withContent",
|
||||
roomFilterProviderType: "filter-provider-type",
|
||||
roomFilterType: "filter-type",
|
||||
roomFilterSubject: "filter-subject",
|
||||
roomFilterOwner: "filter-owner",
|
||||
roomFilterTags: "filter-tags",
|
||||
roomFilterFolders: "filter-withSubfolders",
|
||||
roomFilterContent: "filter-content",
|
||||
});
|
||||
|
||||
export const FilterKeys = Object.freeze({
|
||||
withSubfolders: "withSubfolders",
|
||||
excludeSubfolders: "excludeSubfolders",
|
||||
withContent: "withContent",
|
||||
me: "me",
|
||||
other: "other",
|
||||
user: "user",
|
||||
});
|
||||
|
@ -79,7 +79,6 @@ class AuthStore {
|
||||
this.settingsStore.getCompanyInfoSettings()
|
||||
);
|
||||
}
|
||||
requests.push(this.settingsStore.getWhiteLabelLogoUrls());
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,7 +100,7 @@ class AuthStore {
|
||||
let success = false;
|
||||
if (this.isAuthenticated) {
|
||||
success = this.userStore.isLoaded && this.settingsStore.isLoaded;
|
||||
|
||||
|
||||
success && this.setLanguage();
|
||||
} else {
|
||||
success = this.settingsStore.isLoaded;
|
||||
@ -235,7 +234,7 @@ class AuthStore {
|
||||
get isAuthenticated() {
|
||||
return (
|
||||
this.settingsStore.isLoaded && !!this.settingsStore.socketUrl
|
||||
//|| //this.userStore.isAuthenticated
|
||||
//|| //this.userStore.isAuthenticated
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -86,29 +86,25 @@ class UserStore {
|
||||
sendActivationLink = (t) => {
|
||||
const { email, id } = this.user;
|
||||
|
||||
return api.people
|
||||
.resendUserInvites([id])
|
||||
.then(() => {
|
||||
toastr.success(
|
||||
<Trans
|
||||
i18nKey="MessageEmailActivationInstuctionsSentOnEmail"
|
||||
ns="People"
|
||||
t={t}
|
||||
>
|
||||
The email activation instructions have been sent to the
|
||||
<strong>{{ email: email }}</strong> email address
|
||||
</Trans>
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
this.setWithSendAgain(false);
|
||||
});
|
||||
return api.people.resendUserInvites([id]).then(() => {
|
||||
toastr.success(
|
||||
<Trans
|
||||
i18nKey="MessageEmailActivationInstuctionsSentOnEmail"
|
||||
ns="People"
|
||||
t={t}
|
||||
>
|
||||
The email activation instructions have been sent to the
|
||||
<strong>{{ email: email }}</strong> email address
|
||||
</Trans>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
get withActivationBar() {
|
||||
return (
|
||||
this.user &&
|
||||
this.user.activationStatus === EmployeeActivationStatus.Pending &&
|
||||
(this.user.activationStatus === EmployeeActivationStatus.Pending ||
|
||||
this.user.activationStatus === EmployeeActivationStatus.NotActivated) &&
|
||||
this.withSendAgain
|
||||
);
|
||||
}
|
||||
|
@ -265,7 +265,8 @@ class SelectionArea extends React.Component {
|
||||
e.target.closest(".tile-selected") ||
|
||||
e.target.closest(".table-row-selected") ||
|
||||
e.target.closest(".row-selected") ||
|
||||
!e.target.closest("#sectionScroll")
|
||||
!e.target.closest("#sectionScroll") ||
|
||||
e.target.closest(".table-container_row-checkbox")
|
||||
)
|
||||
return;
|
||||
|
||||
@ -292,7 +293,16 @@ class SelectionArea extends React.Component {
|
||||
y: scroll.scrollTop,
|
||||
};
|
||||
|
||||
onMove && onMove({ added: [], removed: [], clear: true });
|
||||
const threshold = 10;
|
||||
const { x1, y1 } = this.areaLocation;
|
||||
|
||||
if (
|
||||
Math.abs(e.clientX - x1) >= threshold ||
|
||||
Math.abs(e.clientY - y1) >= threshold
|
||||
) {
|
||||
onMove && onMove({ added: [], removed: [], clear: true });
|
||||
}
|
||||
|
||||
this.addListeners();
|
||||
|
||||
const itemsContainer = document.getElementsByClassName(itemsContainerClass);
|
||||
|
@ -3081,6 +3081,9 @@ const Base = {
|
||||
main: {
|
||||
background: "#F8F9F9",
|
||||
textColor: black,
|
||||
|
||||
descriptionTextColor: "#A3A9AE",
|
||||
pendingEmailTextColor: "#A3A9AE",
|
||||
},
|
||||
themePreview: {
|
||||
descriptionColor: "#A3A9AE",
|
||||
|
@ -3080,6 +3080,9 @@ const Dark = {
|
||||
main: {
|
||||
background: "#1f1f1f",
|
||||
textColor: white,
|
||||
|
||||
descriptionTextColor: "#858585",
|
||||
pendingEmailTextColor: "#858585",
|
||||
},
|
||||
themePreview: {
|
||||
descriptionColor: "#ADADAD",
|
||||
|
@ -1,255 +0,0 @@
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import {
|
||||
StyledViewer,
|
||||
StyledViewerContainer,
|
||||
StyledSwitchToolbar,
|
||||
StyledButtonScroll,
|
||||
StyledMobileDetails,
|
||||
} from "./styled-viewer";
|
||||
import ControlBtn from "./sub-components/control-btn";
|
||||
import Text from "@docspace/components/text";
|
||||
import IconButton from "@docspace/components/icon-button";
|
||||
|
||||
import { isMobile } from "react-device-detect";
|
||||
|
||||
import ViewerMediaCloseSvgUrl from "PUBLIC_DIR/images/viewer.media.close.svg?url";
|
||||
import MediaNextIcon from "PUBLIC_DIR/images/viewer.next.react.svg";
|
||||
import MediaPrevIcon from "PUBLIC_DIR/images/viewer.prew.react.svg";
|
||||
import ViewerPlayer from "./sub-components/viewer-player";
|
||||
|
||||
import MediaContextMenu from "PUBLIC_DIR/images/vertical-dots.react.svg";
|
||||
import ContextMenu from "@docspace/components/context-menu";
|
||||
import BackArrow from "PUBLIC_DIR/images/viewer.media.back.react.svg";
|
||||
|
||||
export const Viewer = (props) => {
|
||||
const {
|
||||
visible,
|
||||
onMaskClick,
|
||||
title,
|
||||
onNextClick,
|
||||
onPrevClick,
|
||||
playlistPos,
|
||||
playlist,
|
||||
isImage,
|
||||
isAudio,
|
||||
archiveRoom,
|
||||
audioIcon,
|
||||
contextModel,
|
||||
generateContextMenu,
|
||||
headerIcon,
|
||||
onSetSelectionFile,
|
||||
} = props;
|
||||
|
||||
let timer;
|
||||
|
||||
const defaultContainer = React.useRef(
|
||||
typeof document !== "undefined" ? document.createElement("div") : null
|
||||
);
|
||||
const [container, setContainer] = React.useState(props.container);
|
||||
const [panelVisible, setPanelVisible] = React.useState(true);
|
||||
const [isOpenContextMenu, setIsOpenContextMenu] = React.useState(false);
|
||||
const [isError, setIsError] = React.useState(false);
|
||||
const [isPlay, setIsPlay] = React.useState(null);
|
||||
const [globalTimer, setGlobalTimer] = React.useState(null);
|
||||
const [init, setInit] = React.useState(false);
|
||||
|
||||
const [imageTimer, setImageTimer] = React.useState(null);
|
||||
|
||||
const detailsContainerRef = React.useRef(null);
|
||||
const videoControls = React.useRef(null);
|
||||
const videoElement = React.useRef(null);
|
||||
const cm = React.useRef(null);
|
||||
|
||||
const [isFullscreen, setIsFullScreen] = React.useState(false);
|
||||
React.useEffect(() => {
|
||||
document.body.appendChild(defaultContainer.current);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if ((!isPlay || isOpenContextMenu) && (!isImage || isOpenContextMenu))
|
||||
return clearTimeout(timer);
|
||||
document.addEventListener("touchstart", onTouch);
|
||||
if (!isMobile) {
|
||||
document.addEventListener("mousemove", resetTimer);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", resetTimer);
|
||||
clearTimeout(timer);
|
||||
setPanelVisible(true);
|
||||
};
|
||||
}
|
||||
return () => document.removeEventListener("touchstart", onTouch);
|
||||
}, [isPlay, isOpenContextMenu, isImage]);
|
||||
|
||||
function resetTimer() {
|
||||
setPanelVisible(true);
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => setPanelVisible(false), 2500);
|
||||
setImageTimer(timer);
|
||||
}
|
||||
|
||||
const onTouch = (e, canTouch) => {
|
||||
if (e.target === videoElement.current || canTouch) {
|
||||
setPanelVisible((visible) => !visible);
|
||||
}
|
||||
};
|
||||
|
||||
const nextClick = () => {
|
||||
clearTimeout(imageTimer);
|
||||
onNextClick();
|
||||
};
|
||||
|
||||
const prevClick = () => {
|
||||
clearTimeout(imageTimer);
|
||||
onPrevClick();
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (props.visible && !init) {
|
||||
setInit(true);
|
||||
}
|
||||
}, [props.visible, init]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (props.container) {
|
||||
setContainer(props.container);
|
||||
} else {
|
||||
setContainer(defaultContainer.current);
|
||||
}
|
||||
}, [props.container]);
|
||||
|
||||
if (!init) {
|
||||
return null;
|
||||
}
|
||||
const onContextMenu = (e) => {
|
||||
setIsOpenContextMenu((open) => !open);
|
||||
onSetSelectionFile();
|
||||
cm.current.show(e);
|
||||
};
|
||||
|
||||
const contextMenuHeader = {
|
||||
icon: headerIcon,
|
||||
title: title,
|
||||
};
|
||||
|
||||
const mobileDetails = (
|
||||
<StyledMobileDetails>
|
||||
<BackArrow className="mobile-close" onClick={onMaskClick} />
|
||||
<Text fontSize="14px" color={"#fff"} className="title">
|
||||
{title}
|
||||
</Text>
|
||||
{!props.isPreviewFile && !isError && (
|
||||
<div className="details-context">
|
||||
<MediaContextMenu
|
||||
className="mobile-context"
|
||||
onClick={onContextMenu}
|
||||
/>
|
||||
<ContextMenu
|
||||
getContextModel={contextModel}
|
||||
ref={cm}
|
||||
withBackdrop={true}
|
||||
header={contextMenuHeader}
|
||||
onHide={() => setIsOpenContextMenu(false)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</StyledMobileDetails>
|
||||
);
|
||||
|
||||
const displayUI = (isMobile && isAudio) || panelVisible;
|
||||
|
||||
const viewerPortal = ReactDOM.createPortal(
|
||||
<StyledViewer
|
||||
{...props}
|
||||
displayUI={displayUI}
|
||||
mobileDetails={mobileDetails}
|
||||
setIsOpenContextMenu={setIsOpenContextMenu}
|
||||
container={container}
|
||||
imageTimer={imageTimer}
|
||||
onMaskClick={onMaskClick}
|
||||
setPanelVisible={setPanelVisible}
|
||||
generateContextMenu={generateContextMenu}
|
||||
/>,
|
||||
container
|
||||
);
|
||||
|
||||
const videoPortal = ReactDOM.createPortal(
|
||||
<ViewerPlayer
|
||||
{...props}
|
||||
onNextClick={nextClick}
|
||||
onPrevClick={prevClick}
|
||||
isAudio={isAudio}
|
||||
audioIcon={audioIcon}
|
||||
contextModel={contextModel}
|
||||
mobileDetails={mobileDetails}
|
||||
displayUI={displayUI}
|
||||
isOpenContextMenu={isOpenContextMenu}
|
||||
globalTimer={globalTimer}
|
||||
setGlobalTimer={setGlobalTimer}
|
||||
videoControls={videoControls}
|
||||
onTouch={onTouch}
|
||||
title={title}
|
||||
setIsPlay={setIsPlay}
|
||||
setIsOpenContextMenu={setIsOpenContextMenu}
|
||||
isPlay={isPlay}
|
||||
onMaskClick={onMaskClick}
|
||||
setPanelVisible={setPanelVisible}
|
||||
generateContextMenu={generateContextMenu}
|
||||
setIsFullScreen={setIsFullScreen}
|
||||
setIsError={setIsError}
|
||||
videoRef={videoElement}
|
||||
video={playlist[playlistPos]}
|
||||
activeIndex={playlistPos}
|
||||
/>,
|
||||
container
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledViewerContainer visible={visible}>
|
||||
{!isFullscreen && !isMobile && displayUI && (
|
||||
<div>
|
||||
<div className="details" ref={detailsContainerRef}>
|
||||
<Text isBold fontSize="14px" className="title">
|
||||
{title}
|
||||
</Text>
|
||||
<ControlBtn
|
||||
onClick={onMaskClick && onMaskClick}
|
||||
className="mediaPlayerClose"
|
||||
>
|
||||
<IconButton
|
||||
color={"#fff"}
|
||||
iconName={ViewerMediaCloseSvgUrl}
|
||||
size={28}
|
||||
isClickable
|
||||
/>
|
||||
</ControlBtn>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{playlist.length > 1 && !isFullscreen && displayUI && !isMobile && (
|
||||
<>
|
||||
{playlistPos !== 0 && (
|
||||
<StyledSwitchToolbar left onClick={prevClick}>
|
||||
<StyledButtonScroll orientation="left">
|
||||
<MediaPrevIcon />
|
||||
</StyledButtonScroll>
|
||||
</StyledSwitchToolbar>
|
||||
)}
|
||||
{playlistPos < playlist.length - 1 && (
|
||||
<>
|
||||
<StyledSwitchToolbar onClick={nextClick}>
|
||||
<StyledButtonScroll orientation="right">
|
||||
<MediaNextIcon />
|
||||
</StyledButtonScroll>
|
||||
</StyledSwitchToolbar>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{isImage ? <>{viewerPortal}</> : <>{videoPortal}</>}
|
||||
</StyledViewerContainer>
|
||||
);
|
||||
};
|
@ -1,264 +0,0 @@
|
||||
import styled from "styled-components";
|
||||
import { Base } from "@docspace/components/themes";
|
||||
import { ViewerBase } from "./sub-components/viewer-base";
|
||||
|
||||
const StyledViewerContainer = styled.div`
|
||||
color: ${(props) => props.theme.mediaViewer.color};
|
||||
display: ${(props) => (props.visible ? "block" : "none")};
|
||||
overflow: hidden;
|
||||
span {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 5px;
|
||||
margin-right: 10px;
|
||||
z-index: 305;
|
||||
}
|
||||
.deleteBtnContainer,
|
||||
.downloadBtnContainer {
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 4px 12px;
|
||||
line-height: 19px;
|
||||
svg {
|
||||
path {
|
||||
fill: ${(props) => props.theme.mediaViewer.fill};
|
||||
}
|
||||
}
|
||||
}
|
||||
.details {
|
||||
z-index: 307;
|
||||
padding-top: 21px;
|
||||
height: 64px;
|
||||
width: 100%;
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(0, 0, 0, 0.8) 100%
|
||||
);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
.title {
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
width: calc(100% - 50px);
|
||||
padding-left: 16px;
|
||||
box-sizing: border-box;
|
||||
color: ${(props) => props.theme.mediaViewer.titleColor};
|
||||
}
|
||||
}
|
||||
.mediaPlayerClose {
|
||||
position: fixed;
|
||||
top: 13px;
|
||||
right: 12px;
|
||||
height: 17px;
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
svg {
|
||||
path {
|
||||
fill: ${(props) => props.theme.mediaViewer.iconColor};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.containerVideo {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
StyledViewerContainer.defaultProps = { theme: Base };
|
||||
|
||||
const StyledViewer = styled(ViewerBase)`
|
||||
.react-viewer-inline {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
}
|
||||
.react-viewer-inline > .react-viewer-mask,
|
||||
.react-viewer-inline > .react-viewer-close,
|
||||
.react-viewer-inline > .react-viewer-canvas,
|
||||
.react-viewer-inline > .react-viewer-footer {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.react-viewer ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.react-viewer li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.react-viewer-footer {
|
||||
padding: 10px 24px;
|
||||
position: fixed;
|
||||
border-radius: 18px;
|
||||
bottom: 24px;
|
||||
z-index: 307;
|
||||
height: 48px;
|
||||
transition: all 0.26s ease-out;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
text-align: center;
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.react-viewer-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.react-viewer-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(55, 55, 55, 0.6);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.react-viewer-toolbar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
z-index: 308;
|
||||
}
|
||||
|
||||
.react-viewer-canvas {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: auto;
|
||||
user-select: none;
|
||||
}
|
||||
img.drag {
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
|
||||
.react-viewer-list {
|
||||
height: 50px;
|
||||
padding: 1px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.react-viewer-list > li {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 50px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
margin-right: 1px;
|
||||
}
|
||||
|
||||
.react-viewer-list > li > img {
|
||||
width: 60px;
|
||||
height: 50px;
|
||||
margin-left: -15px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.react-viewer-list > li.active > img {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledSwitchToolbar = styled.div`
|
||||
height: 100%;
|
||||
z-index: 306;
|
||||
position: fixed;
|
||||
width: 73px;
|
||||
background: inherit;
|
||||
display: block;
|
||||
opacity: 0;
|
||||
transition: all 0.3s;
|
||||
${(props) => (props.left ? "left: 0" : "right: 0")};
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledMobileDetails = styled.div`
|
||||
z-index: 307;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 53px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(0, 0, 0, 0.8) 100%
|
||||
);
|
||||
|
||||
svg {
|
||||
path {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-close {
|
||||
position: fixed;
|
||||
left: 21px;
|
||||
top: 22px;
|
||||
}
|
||||
|
||||
.mobile-context {
|
||||
position: fixed;
|
||||
right: 22px;
|
||||
top: 22px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
margin-top: 6px;
|
||||
width: calc(100% - 100px);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledButtonScroll = styled.div`
|
||||
z-index: 307;
|
||||
position: fixed;
|
||||
top: calc(50% - 20px);
|
||||
|
||||
${(props) => (props.orientation === "left" ? "left: 20px;" : "right: 20px;")}
|
||||
`;
|
||||
|
||||
export {
|
||||
StyledViewerContainer,
|
||||
StyledViewer,
|
||||
StyledSwitchToolbar,
|
||||
StyledButtonScroll,
|
||||
StyledMobileDetails,
|
||||
};
|
@ -1,34 +0,0 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import PropTypes from "prop-types";
|
||||
import { Base } from "@docspace/components/themes";
|
||||
|
||||
const StyledVideoControlBtn = styled.div`
|
||||
display: inline-block;
|
||||
height: 30px;
|
||||
line-height: 25px;
|
||||
margin: 5px;
|
||||
width: 40px;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) =>
|
||||
props.theme.mediaViewer.controlBtn.backgroundColor};
|
||||
}
|
||||
`;
|
||||
|
||||
StyledVideoControlBtn.defaultProps = { theme: Base };
|
||||
|
||||
const ControlBtn = (props) => {
|
||||
return (
|
||||
<StyledVideoControlBtn {...props}>{props.children}</StyledVideoControlBtn>
|
||||
);
|
||||
};
|
||||
|
||||
ControlBtn.propTypes = {
|
||||
children: PropTypes.any,
|
||||
};
|
||||
|
||||
export default ControlBtn;
|
@ -1,23 +0,0 @@
|
||||
import * as React from "react";
|
||||
|
||||
export const ActionType = {
|
||||
zoomIn: 1,
|
||||
zoomOut: 2,
|
||||
prev: 3,
|
||||
next: 4,
|
||||
rotateLeft: 5,
|
||||
rotateRight: 6,
|
||||
reset: 7,
|
||||
close: 8,
|
||||
scaleX: 9,
|
||||
scaleY: 10,
|
||||
download: 11,
|
||||
};
|
||||
|
||||
export default function Icon(props) {
|
||||
let prefixCls = "react-viewer-icon";
|
||||
|
||||
return (
|
||||
<i className={`${prefixCls} ${prefixCls}-${ActionType[props.type]}`}></i>
|
||||
);
|
||||
}
|
@ -1,387 +0,0 @@
|
||||
import React from "react";
|
||||
import { useGesture } from "@use-gesture/react";
|
||||
import { useSpring, animated } from "@react-spring/web";
|
||||
import styled from "styled-components";
|
||||
|
||||
const ImageWrapper = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
touch-action: none;
|
||||
`;
|
||||
|
||||
function MobileViewer({
|
||||
src,
|
||||
width,
|
||||
height,
|
||||
className,
|
||||
left,
|
||||
top,
|
||||
onPrev,
|
||||
onNext,
|
||||
onMask,
|
||||
isFistImage,
|
||||
isLastImage,
|
||||
setPanelVisible,
|
||||
}) {
|
||||
const imgRef = React.useRef(null);
|
||||
const containerRef = React.useRef(null);
|
||||
|
||||
const unmountRef = React.useRef(false);
|
||||
|
||||
const lastTapTimeRef = React.useRef(0);
|
||||
const isDoubleTapRef = React.useRef(false);
|
||||
const setTimeoutIDTapRef = React.useRef();
|
||||
const startAngleRef = React.useRef(0);
|
||||
|
||||
const [scale, setScale] = React.useState(1);
|
||||
|
||||
const [style, api] = useSpring(() => ({
|
||||
x: left,
|
||||
y: top,
|
||||
scale: 1,
|
||||
rotate: 0,
|
||||
opacity: 1,
|
||||
width: width,
|
||||
height: height,
|
||||
touchAction: "none",
|
||||
willChange: "transform, opacity, contents",
|
||||
}));
|
||||
React.useEffect(() => {
|
||||
const point = maybeAdjustImage({
|
||||
x: left,
|
||||
y: top,
|
||||
});
|
||||
|
||||
api.start({ ...point });
|
||||
}, [left, top]);
|
||||
|
||||
React.useEffect(() => {
|
||||
api.set({
|
||||
width,
|
||||
height,
|
||||
x: left,
|
||||
y: top,
|
||||
});
|
||||
}, [height, width]);
|
||||
|
||||
React.useEffect(() => {
|
||||
unmountRef.current = false;
|
||||
|
||||
return () => {
|
||||
setTimeoutIDTapRef.current && clearTimeout(setTimeoutIDTapRef.current);
|
||||
unmountRef.current = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const CompareTo = (a, b) => {
|
||||
return Math.trunc(a) > Math.trunc(b);
|
||||
};
|
||||
const maybeAdjustImage = (point) => {
|
||||
const imageBounds = imgRef.current.getBoundingClientRect();
|
||||
const containerBounds = imgRef.current.parentNode.getBoundingClientRect();
|
||||
|
||||
const originalWidth = imgRef.current.clientWidth;
|
||||
const widthOverhang = (imageBounds.width - originalWidth) / 2;
|
||||
|
||||
const originalHeight = imgRef.current.clientHeight;
|
||||
const heightOverhang = (imageBounds.height - originalHeight) / 2;
|
||||
|
||||
const isWidthOutContainer = imageBounds.width >= containerBounds.width;
|
||||
|
||||
const isHeightOutContainer = imageBounds.height >= containerBounds.height;
|
||||
|
||||
if (
|
||||
CompareTo(imageBounds.left, containerBounds.left) &&
|
||||
isWidthOutContainer
|
||||
) {
|
||||
point.x = widthOverhang;
|
||||
} else if (
|
||||
CompareTo(containerBounds.right, imageBounds.right) &&
|
||||
isWidthOutContainer
|
||||
) {
|
||||
point.x = -(imageBounds.width - containerBounds.width) + widthOverhang;
|
||||
} else if (!isWidthOutContainer) {
|
||||
point.x = (containerBounds.width - imageBounds.width) / 2 + widthOverhang;
|
||||
}
|
||||
|
||||
if (
|
||||
CompareTo(imageBounds.top, containerBounds.top) &&
|
||||
isHeightOutContainer
|
||||
) {
|
||||
point.y = heightOverhang;
|
||||
} else if (
|
||||
CompareTo(containerBounds.bottom, imageBounds.bottom) &&
|
||||
isHeightOutContainer
|
||||
) {
|
||||
point.y = -(imageBounds.height - containerBounds.height) + heightOverhang;
|
||||
} else if (!isHeightOutContainer) {
|
||||
point.y =
|
||||
(containerBounds.height - imageBounds.height) / 2 + heightOverhang;
|
||||
}
|
||||
|
||||
return point;
|
||||
};
|
||||
|
||||
useGesture(
|
||||
{
|
||||
onDragStart: ({ pinching }) => {
|
||||
if (pinching) return;
|
||||
|
||||
setScale(style.scale.get());
|
||||
},
|
||||
onDrag: ({
|
||||
offset: [dx, dy],
|
||||
movement: [mdx, mdy],
|
||||
cancel,
|
||||
pinching,
|
||||
canceled,
|
||||
}) => {
|
||||
if (isDoubleTapRef.current || unmountRef.current) {
|
||||
isDoubleTapRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (pinching || canceled) cancel();
|
||||
|
||||
api.start({
|
||||
x:
|
||||
style.scale.get() === 1 &&
|
||||
((isFistImage && mdx > 0) || (isLastImage && mdx < 0))
|
||||
? style.x.get()
|
||||
: dx,
|
||||
y: dy,
|
||||
opacity:
|
||||
style.scale.get() === 1 && mdy > 0
|
||||
? imgRef.current.height / 10 / mdy
|
||||
: style.opacity.get(),
|
||||
immediate: true,
|
||||
config: {
|
||||
duration: 0,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
onDragEnd: ({ cancel, canceled, movement: [mdx, mdy] }) => {
|
||||
if (unmountRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (canceled || isDoubleTapRef.current) {
|
||||
isDoubleTapRef.current = false;
|
||||
cancel();
|
||||
}
|
||||
if (style.scale.get() === 1) {
|
||||
if (mdx < -imgRef.current.width / 4) {
|
||||
return onNext();
|
||||
} else if (mdx > imgRef.current.width / 4) {
|
||||
return onPrev();
|
||||
}
|
||||
if (mdy > 150) {
|
||||
return onMask();
|
||||
}
|
||||
}
|
||||
|
||||
const newPoint = maybeAdjustImage({
|
||||
x: style.x.get(),
|
||||
y: style.y.get(),
|
||||
});
|
||||
|
||||
api.start({
|
||||
...newPoint,
|
||||
opacity: 1,
|
||||
});
|
||||
},
|
||||
|
||||
onPinchStart: ({ event, cancel }) => {
|
||||
if (event.target === containerRef.current) {
|
||||
cancel();
|
||||
} else {
|
||||
const roundedAngle = Math.round(style.rotate.get());
|
||||
startAngleRef.current = roundedAngle - (roundedAngle % 90);
|
||||
}
|
||||
},
|
||||
|
||||
onPinch: ({
|
||||
origin: [ox, oy],
|
||||
offset: [dScale, dRotate],
|
||||
movement: [mScale],
|
||||
memo,
|
||||
first,
|
||||
canceled,
|
||||
event,
|
||||
}) => {
|
||||
if (canceled || event.target === containerRef.current) return;
|
||||
|
||||
if (first) {
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
x,
|
||||
y,
|
||||
} = imgRef.current.getBoundingClientRect();
|
||||
const tx = ox - (x + width / 2);
|
||||
const ty = oy - (y + height / 2);
|
||||
memo = [style.x.get(), style.y.get(), tx, ty];
|
||||
}
|
||||
|
||||
const x = memo[0] - (mScale - 1) * memo[2];
|
||||
const y = memo[1] - (mScale - 1) * memo[3];
|
||||
|
||||
api.start({
|
||||
x,
|
||||
y,
|
||||
scale: dScale,
|
||||
rotate: dRotate,
|
||||
delay: 0,
|
||||
});
|
||||
return memo;
|
||||
},
|
||||
onPinchEnd: ({
|
||||
movement: [, mRotate],
|
||||
direction: [, dirRotate],
|
||||
canceled,
|
||||
}) => {
|
||||
if (unmountRef.current || canceled) {
|
||||
return;
|
||||
}
|
||||
const rotate =
|
||||
Math.abs(mRotate / 90) > 1 / 3
|
||||
? Math.trunc(
|
||||
startAngleRef.current +
|
||||
90 *
|
||||
Math.max(Math.trunc(Math.abs(mRotate) / 90), 1) *
|
||||
dirRotate
|
||||
)
|
||||
: startAngleRef.current;
|
||||
|
||||
const newPoint = maybeAdjustImage({
|
||||
x: style.x.get(),
|
||||
y: style.y.get(),
|
||||
});
|
||||
|
||||
api.start({
|
||||
rotate,
|
||||
...newPoint,
|
||||
delay: 0,
|
||||
onResolve: () => {
|
||||
const newPoint = maybeAdjustImage({
|
||||
x: style.x.get(),
|
||||
y: style.y.get(),
|
||||
});
|
||||
|
||||
api.start({
|
||||
...newPoint,
|
||||
immediate: true,
|
||||
delay: 0,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
onClick: () => {
|
||||
const time = new Date().getTime();
|
||||
|
||||
if (time - lastTapTimeRef.current < 300) {
|
||||
//on Double Tap
|
||||
lastTapTimeRef.current = 0;
|
||||
isDoubleTapRef.current = true;
|
||||
const imageWidth = imgRef.current.width;
|
||||
const imageHeight = imgRef.current.height;
|
||||
const containerBounds = imgRef.current.parentNode.getBoundingClientRect();
|
||||
|
||||
const deltaWidth = (containerBounds.width - imageWidth) / 2;
|
||||
const deltaHeight = (containerBounds.height - imageHeight) / 2;
|
||||
api.start({
|
||||
scale: 1,
|
||||
x: deltaWidth,
|
||||
y: deltaHeight,
|
||||
rotate: 0,
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
clearTimeout(setTimeoutIDTapRef.current);
|
||||
} else {
|
||||
lastTapTimeRef.current = time;
|
||||
setTimeoutIDTapRef.current = setTimeout(() => {
|
||||
// onTap
|
||||
|
||||
setPanelVisible((visible) => {
|
||||
let display = visible;
|
||||
const displayVisible =
|
||||
JSON.parse(localStorage.getItem("displayVisible")) || null;
|
||||
|
||||
if (displayVisible !== null) {
|
||||
display = !displayVisible;
|
||||
}
|
||||
|
||||
localStorage.setItem("displayVisible", display);
|
||||
return !visible;
|
||||
});
|
||||
}, 300);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
drag: {
|
||||
from: () => [style.x.get(), style.y.get()],
|
||||
axis: scale === 1 ? "lock" : undefined,
|
||||
bounds: () => {
|
||||
if (style.scale.get() === 1) return undefined;
|
||||
|
||||
const imageBounds = imgRef.current.getBoundingClientRect();
|
||||
const containerBounds = imgRef.current.parentNode.getBoundingClientRect();
|
||||
|
||||
const originalWidth = imgRef.current.clientWidth;
|
||||
const widthOverhang = (imageBounds.width - originalWidth) / 2;
|
||||
|
||||
const originalHeight = imgRef.current.clientHeight;
|
||||
const heightOverhang = (imageBounds.height - originalHeight) / 2;
|
||||
|
||||
const isWidthOutContainer =
|
||||
imageBounds.width >= containerBounds.width;
|
||||
|
||||
const isHeightOutContainer =
|
||||
imageBounds.height >= containerBounds.height;
|
||||
|
||||
const bounds = {
|
||||
right: isWidthOutContainer
|
||||
? widthOverhang
|
||||
: containerBounds.width - imageBounds.width + widthOverhang,
|
||||
left: isWidthOutContainer
|
||||
? -(imageBounds.width - containerBounds.width) + widthOverhang
|
||||
: widthOverhang,
|
||||
bottom: isHeightOutContainer
|
||||
? heightOverhang
|
||||
: containerBounds.height - imageBounds.height + heightOverhang,
|
||||
top: isHeightOutContainer
|
||||
? -(imageBounds.height - containerBounds.height) + heightOverhang
|
||||
: heightOverhang,
|
||||
};
|
||||
return bounds;
|
||||
},
|
||||
},
|
||||
pinch: {
|
||||
scaleBounds: { min: 0.5, max: 5 },
|
||||
rubberband: false,
|
||||
from: () => [style.scale.get(), style.rotate.get()],
|
||||
threshold: [0.1, 5],
|
||||
},
|
||||
|
||||
target: containerRef,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<ImageWrapper ref={containerRef}>
|
||||
<animated.img
|
||||
src={src}
|
||||
className={className}
|
||||
ref={imgRef}
|
||||
style={style}
|
||||
/>
|
||||
</ImageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default MobileViewer;
|
@ -1,758 +0,0 @@
|
||||
import * as React from "react";
|
||||
import ViewerImage from "./viewer-image";
|
||||
import classnames from "classnames";
|
||||
import ViewerToolbar, { defaultToolbars } from "./viewer-toolbar";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import Icon, { ActionType } from "./icon";
|
||||
|
||||
const ACTION_TYPES = {
|
||||
setVisible: "setVisible",
|
||||
setActiveIndex: "setActiveIndex",
|
||||
update: "update",
|
||||
clear: "clear",
|
||||
};
|
||||
|
||||
function createAction(type, payload) {
|
||||
return {
|
||||
type,
|
||||
payload: payload || {},
|
||||
};
|
||||
}
|
||||
|
||||
const ViewerBase = (props) => {
|
||||
const {
|
||||
visible = false,
|
||||
images = [],
|
||||
activeIndex = 0,
|
||||
zIndex = 300,
|
||||
drag = true,
|
||||
attribute = true,
|
||||
zoomable = true,
|
||||
rotatable = true,
|
||||
scalable = true,
|
||||
changeable = true,
|
||||
customToolbar = (toolbars) => toolbars,
|
||||
zoomSpeed = 0.25,
|
||||
disableKeyboardSupport = false,
|
||||
noResetZoomAfterChange = false,
|
||||
noLimitInitializationSize = false,
|
||||
defaultScale = 1,
|
||||
loop = true,
|
||||
disableMouseZoom = false,
|
||||
downloadable = false,
|
||||
noImgDetails = false,
|
||||
noToolbar = false,
|
||||
showTotal = true,
|
||||
totalName = "of",
|
||||
minScale = 0.1,
|
||||
generateContextMenu,
|
||||
mobileDetails,
|
||||
onNextClick,
|
||||
onPrevClick,
|
||||
onMaskClick,
|
||||
isPreviewFile,
|
||||
archiveRoom,
|
||||
} = props;
|
||||
|
||||
const initialState = {
|
||||
visible: false,
|
||||
visibleStart: false,
|
||||
transitionEnd: false,
|
||||
activeIndex: props.activeIndex,
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: 15,
|
||||
left: null,
|
||||
rotate: 0,
|
||||
imageWidth: 0,
|
||||
imageHeight: 0,
|
||||
scaleX: defaultScale,
|
||||
scaleY: defaultScale,
|
||||
loading: false,
|
||||
loadFailed: false,
|
||||
startLoading: false,
|
||||
percent: 100,
|
||||
withTransition: false,
|
||||
opacity: 1,
|
||||
};
|
||||
function setContainerWidthHeight() {
|
||||
let width = window.innerWidth;
|
||||
let height = window.innerHeight;
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
};
|
||||
}
|
||||
const containerSize = React.useRef(setContainerWidthHeight());
|
||||
const imageRef = React.useRef(null);
|
||||
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const footerHeight = 0;
|
||||
function reducer(state, action) {
|
||||
switch (action.type) {
|
||||
case ACTION_TYPES.setVisible:
|
||||
return {
|
||||
...state,
|
||||
visible: action.payload.visible,
|
||||
};
|
||||
case ACTION_TYPES.setActiveIndex:
|
||||
return {
|
||||
...state,
|
||||
activeIndex: action.payload.index,
|
||||
startLoading: true,
|
||||
};
|
||||
case ACTION_TYPES.update:
|
||||
return {
|
||||
...state,
|
||||
...action.payload,
|
||||
};
|
||||
case ACTION_TYPES.clear:
|
||||
return {
|
||||
...state,
|
||||
width: 0,
|
||||
height: 0,
|
||||
scaleX: defaultScale,
|
||||
scaleY: defaultScale,
|
||||
rotate: 1,
|
||||
imageWidth: 0,
|
||||
imageHeight: 0,
|
||||
loadFailed: false,
|
||||
top: 0,
|
||||
left: 0,
|
||||
loading: false,
|
||||
};
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
const viewerCore = React.useRef(null);
|
||||
const init = React.useRef(false);
|
||||
const currentLoadIndex = React.useRef(0);
|
||||
|
||||
const [state, dispatch] = React.useReducer(reducer, initialState);
|
||||
|
||||
React.useEffect(() => {
|
||||
init.current = true;
|
||||
|
||||
return () => {
|
||||
init.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
containerSize.current = setContainerWidthHeight();
|
||||
}, [props.container]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (visible) {
|
||||
if (init.current) {
|
||||
dispatch(
|
||||
createAction(ACTION_TYPES.setVisible, {
|
||||
visible: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
React.useEffect(() => {
|
||||
bindEvent();
|
||||
|
||||
return () => {
|
||||
bindEvent(true);
|
||||
};
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
//fix memory leak
|
||||
if (!init.current) return;
|
||||
|
||||
if (visible) {
|
||||
if (!props.container) {
|
||||
document.body.style.overflow = "hidden";
|
||||
if (document.body.scrollHeight > document.body.clientHeight) {
|
||||
document.body.style.paddingRight = "15px";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dispatch(createAction(ACTION_TYPES.clear, {}));
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = "";
|
||||
document.body.style.paddingRight = "";
|
||||
};
|
||||
}, [state.visible]);
|
||||
|
||||
React.useEffect(() => {
|
||||
//fix memory leak
|
||||
if (!init.current) return;
|
||||
|
||||
if (visible) {
|
||||
dispatch(
|
||||
createAction(ACTION_TYPES.setActiveIndex, {
|
||||
index: activeIndex,
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [activeIndex, visible, images]);
|
||||
|
||||
function loadImg(currentActiveIndex, isReset = false) {
|
||||
const timerId = setTimeout(() => {
|
||||
dispatch(
|
||||
createAction(ACTION_TYPES.update, {
|
||||
loading: true,
|
||||
})
|
||||
);
|
||||
}, 300);
|
||||
dispatch(
|
||||
createAction(ACTION_TYPES.update, {
|
||||
loadFailed: false,
|
||||
})
|
||||
);
|
||||
let activeImage = null;
|
||||
if (images.length > 0) {
|
||||
activeImage = images[currentActiveIndex];
|
||||
}
|
||||
|
||||
if (imageRef.current) {
|
||||
//abort previous image request
|
||||
imageRef.current.src = "";
|
||||
}
|
||||
|
||||
let loadComplete = false;
|
||||
let img = new Image();
|
||||
imageRef.current = img;
|
||||
img.src = activeImage.src;
|
||||
|
||||
img.onload = () => {
|
||||
clearTimeout(timerId);
|
||||
if (!init.current) {
|
||||
return;
|
||||
}
|
||||
if (!loadComplete) {
|
||||
loadImgSuccess(img.width, img.height, true);
|
||||
}
|
||||
};
|
||||
img.onerror = () => {
|
||||
clearTimeout(timerId);
|
||||
dispatch(
|
||||
createAction(ACTION_TYPES.update, {
|
||||
loading: false,
|
||||
loadFailed: false,
|
||||
startLoading: false,
|
||||
})
|
||||
);
|
||||
};
|
||||
if (img.complete) {
|
||||
loadComplete = true;
|
||||
loadImgSuccess(img.width, img.height, true);
|
||||
}
|
||||
function loadImgSuccess(imgWidth, imgHeight, success) {
|
||||
if (currentActiveIndex !== currentLoadIndex.current) {
|
||||
return;
|
||||
}
|
||||
let realImgWidth = imgWidth;
|
||||
let realImgHeight = imgHeight;
|
||||
|
||||
let [width, height] = getImgWidthHeight(realImgWidth, realImgHeight);
|
||||
let left = (containerSize.current.width - width) / 2;
|
||||
let top = (containerSize.current.height - height - footerHeight) / 2;
|
||||
|
||||
let scaleX = defaultScale;
|
||||
let scaleY = defaultScale;
|
||||
if (noResetZoomAfterChange && !isReset) {
|
||||
scaleX = state.scaleX;
|
||||
scaleY = state.scaleY;
|
||||
}
|
||||
props.setPanelVisible(true);
|
||||
dispatch(
|
||||
createAction(ACTION_TYPES.update, {
|
||||
width: width,
|
||||
height: height,
|
||||
left: left,
|
||||
top: top,
|
||||
imageWidth: imgWidth,
|
||||
imageHeight: imgHeight,
|
||||
loading: false,
|
||||
rotate: 0,
|
||||
scaleX: scaleX,
|
||||
scaleY: scaleY,
|
||||
loadFailed: !success,
|
||||
startLoading: false,
|
||||
percent: 100,
|
||||
opacity: 1,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (state.startLoading) {
|
||||
currentLoadIndex.current = state.activeIndex;
|
||||
loadImg(state.activeIndex);
|
||||
}
|
||||
}, [state.startLoading, state.activeIndex, images[state.activeIndex]?.src]);
|
||||
|
||||
function getImgWidthHeight(imgWidth, imgHeight) {
|
||||
const titleHeight = 0;
|
||||
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
let maxWidth = containerSize.current.width;
|
||||
|
||||
let maxHeight = containerSize.current.height - (footerHeight + titleHeight);
|
||||
|
||||
width = Math.min(maxWidth, imgWidth);
|
||||
height = (width / imgWidth) * imgHeight;
|
||||
|
||||
if (height > maxHeight) {
|
||||
height = maxHeight;
|
||||
width = (height / imgHeight) * imgWidth;
|
||||
}
|
||||
if (noLimitInitializationSize) {
|
||||
width = imgWidth;
|
||||
height = imgHeight;
|
||||
}
|
||||
return [width, height];
|
||||
}
|
||||
|
||||
function onPercentClick() {
|
||||
if (state.percent === 100) return;
|
||||
let imgCenterXY = getImageCenterXY();
|
||||
|
||||
const zoomCondition = state.percent < 100;
|
||||
|
||||
const direct = zoomCondition ? 1 : -1;
|
||||
const zoom = zoomCondition
|
||||
? (100 - state.percent) / 100
|
||||
: (state.percent - 100) / 100;
|
||||
|
||||
handleZoom(imgCenterXY.x, imgCenterXY.y, direct, zoom);
|
||||
}
|
||||
|
||||
function getActiveImage(activeIndex2 = undefined) {
|
||||
let activeImg2 = {
|
||||
src: "",
|
||||
alt: "",
|
||||
downloadUrl: "",
|
||||
};
|
||||
|
||||
let realActiveIndex = null;
|
||||
if (activeIndex2 !== undefined) {
|
||||
realActiveIndex = activeIndex2;
|
||||
} else {
|
||||
realActiveIndex = state.activeIndex;
|
||||
}
|
||||
if (images.length > 0 && realActiveIndex >= 0) {
|
||||
activeImg2 = images[realActiveIndex];
|
||||
}
|
||||
|
||||
return activeImg2;
|
||||
}
|
||||
|
||||
function handleDownload() {
|
||||
const activeImage = getActiveImage();
|
||||
if (activeImage.downloadUrl) {
|
||||
if (props.downloadInNewWindow) {
|
||||
window.open(activeImage.downloadUrl, "_blank");
|
||||
} else {
|
||||
location.href = activeImage.downloadUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleRotate(isRight = false) {
|
||||
dispatch(
|
||||
createAction(ACTION_TYPES.update, {
|
||||
withTransition: true,
|
||||
rotate: state.rotate + 90 * (isRight ? 1 : -1),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function handleDefaultAction(type) {
|
||||
switch (type) {
|
||||
case ActionType.zoomIn:
|
||||
let imgCenterXY = getImageCenterXY();
|
||||
handleZoom(imgCenterXY.x, imgCenterXY.y, 1, zoomSpeed);
|
||||
break;
|
||||
case ActionType.zoomOut:
|
||||
let imgCenterXY2 = getImageCenterXY();
|
||||
handleZoom(imgCenterXY2.x, imgCenterXY2.y, -1, zoomSpeed);
|
||||
break;
|
||||
case ActionType.rotateLeft:
|
||||
handleRotate();
|
||||
break;
|
||||
case ActionType.rotateRight:
|
||||
handleRotate(true);
|
||||
break;
|
||||
case ActionType.reset:
|
||||
loadImg(state.activeIndex, true);
|
||||
break;
|
||||
case ActionType.download:
|
||||
handleDownload();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handleAction(config) {
|
||||
handleDefaultAction(config.actionType);
|
||||
|
||||
if (config.onClick) {
|
||||
const activeImage = getActiveImage();
|
||||
config.onClick(activeImage);
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangeImgState(width, height, top, left) {
|
||||
dispatch(
|
||||
createAction(ACTION_TYPES.update, {
|
||||
width: width,
|
||||
height: height,
|
||||
top: top,
|
||||
left: left,
|
||||
withTransition: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function handleResize() {
|
||||
containerSize.current = setContainerWidthHeight();
|
||||
if (visible) {
|
||||
const [imgWidth, imgHeight] = getImgWidthHeight(
|
||||
state.imageWidth,
|
||||
state.imageHeight
|
||||
);
|
||||
|
||||
let left = (containerSize.current.width - imgWidth) / 2;
|
||||
let top =
|
||||
(containerSize.current.height - imgHeight - (footerHeight - 53)) / 2;
|
||||
|
||||
dispatch(
|
||||
createAction(ACTION_TYPES.update, {
|
||||
left: left,
|
||||
top: top,
|
||||
width: imgWidth,
|
||||
height: imgHeight,
|
||||
})
|
||||
);
|
||||
viewerCore;
|
||||
}
|
||||
}
|
||||
|
||||
function bindEvent(remove = false) {
|
||||
let funcName = "addEventListener";
|
||||
if (remove) {
|
||||
funcName = "removeEventListener";
|
||||
}
|
||||
if (!disableKeyboardSupport) {
|
||||
document[funcName]("keydown", handleKeydown, true);
|
||||
}
|
||||
if (viewerCore.current) {
|
||||
viewerCore.current[funcName]("wheel", handleMouseScroll, {
|
||||
passive: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(e) {
|
||||
let keyCode = e.keyCode || e.which || e.charCode;
|
||||
let isFeatrue = false;
|
||||
switch (keyCode) {
|
||||
// key: ↑
|
||||
case 38:
|
||||
handleDefaultAction(ActionType.zoomIn);
|
||||
isFeatrue = true;
|
||||
break;
|
||||
// key: ↓
|
||||
case 40:
|
||||
handleDefaultAction(ActionType.zoomOut);
|
||||
isFeatrue = true;
|
||||
break;
|
||||
// key: Ctrl + 1
|
||||
case 49:
|
||||
if (e.ctrlKey) {
|
||||
loadImg(state.activeIndex);
|
||||
isFeatrue = true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (isFeatrue) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseScroll(e) {
|
||||
if (disableMouseZoom) {
|
||||
return;
|
||||
}
|
||||
if (state.loading) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
let direct = 0;
|
||||
const value = e.deltaY;
|
||||
if (value === 0) {
|
||||
direct = 0;
|
||||
} else {
|
||||
direct = value > 0 ? -1 : 1;
|
||||
}
|
||||
if (direct !== 0) {
|
||||
let imgCenterXY = getImageCenterXY();
|
||||
handleZoom(imgCenterXY.x, imgCenterXY.y, direct, zoomSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
function getImageCenterXY() {
|
||||
return {
|
||||
x: state.left + state.width / 2,
|
||||
y: state.top + state.height / 2,
|
||||
};
|
||||
}
|
||||
|
||||
function handleResetZoom() {
|
||||
const [imgWidth, imgHeight] = getImgWidthHeight(
|
||||
state.imageWidth,
|
||||
state.imageHeight
|
||||
);
|
||||
const left = (containerSize.current.width - imgWidth) / 2;
|
||||
const top = (containerSize.current.height - imgHeight) / 2;
|
||||
dispatch(
|
||||
createAction(ACTION_TYPES.update, {
|
||||
width: imgWidth,
|
||||
height: imgHeight,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
top: top,
|
||||
left: left,
|
||||
loading: false,
|
||||
percent: 100,
|
||||
withTransition: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function handleZoom(targetX, targetY, direct, scale) {
|
||||
let imgCenterXY = getImageCenterXY();
|
||||
let diffX = targetX - imgCenterXY.x;
|
||||
let diffY = targetY - imgCenterXY.y;
|
||||
let top = 0;
|
||||
let left = 0;
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
let scaleX = 0;
|
||||
let scaleY = 0;
|
||||
let zoomPercent =
|
||||
direct === 1 ? state.percent + scale * 100 : state.percent - scale * 100;
|
||||
if (zoomPercent === 0) return;
|
||||
if (scale === 1) {
|
||||
zoomPercent = 100;
|
||||
}
|
||||
|
||||
let nowWidth = state.width;
|
||||
|
||||
if (nowWidth === 0) {
|
||||
const [imgWidth, imgHeight] = getImgWidthHeight(
|
||||
state.imageWidth,
|
||||
state.imageHeight
|
||||
);
|
||||
left = (containerSize.current.width - imgWidth) / 2;
|
||||
top = (containerSize.current.height - footerHeight - imgHeight) / 2;
|
||||
width = state.width + imgWidth;
|
||||
height = state.height + imgHeight;
|
||||
scaleX = scaleY = 1;
|
||||
} else {
|
||||
let directX = state.scaleX > 0 ? 1 : -1;
|
||||
let directY = state.scaleY > 0 ? 1 : -1;
|
||||
scaleX = state.scaleX + scale * direct * directX;
|
||||
scaleY = state.scaleY + scale * direct * directY;
|
||||
if (typeof props.maxScale !== "undefined") {
|
||||
if (Math.abs(scaleX) > props.maxScale) {
|
||||
scaleX = props.maxScale * directX;
|
||||
}
|
||||
if (Math.abs(scaleY) > props.maxScale) {
|
||||
scaleY = props.maxScale * directY;
|
||||
}
|
||||
}
|
||||
if (Math.abs(scaleX) < minScale) {
|
||||
scaleX = minScale * directX;
|
||||
}
|
||||
if (Math.abs(scaleY) < minScale) {
|
||||
scaleY = minScale * directY;
|
||||
}
|
||||
top = state.top + ((-direct * diffY) / state.scaleX) * scale * directX;
|
||||
left = state.left + ((-direct * diffX) / state.scaleY) * scale * directY;
|
||||
width = nowWidth;
|
||||
height = state.height;
|
||||
}
|
||||
dispatch(
|
||||
createAction(ACTION_TYPES.update, {
|
||||
width: width,
|
||||
scaleX: scaleX > 0 ? scaleX : 0,
|
||||
scaleY: scaleY > 0 ? scaleY : 0,
|
||||
height: height,
|
||||
top: top,
|
||||
left: left,
|
||||
loading: false,
|
||||
percent: zoomPercent,
|
||||
withTransition: true,
|
||||
})
|
||||
);
|
||||
|
||||
return [scaleX, scaleY];
|
||||
}
|
||||
|
||||
let currentTop = (containerSize.current.height - state.height) / 2;
|
||||
const prefixCls = "react-viewer";
|
||||
|
||||
const className = classnames(`${prefixCls}`, `${prefixCls}-transition`, {
|
||||
[`${prefixCls}-inline`]: props.container,
|
||||
[props.className]: props.className,
|
||||
});
|
||||
|
||||
let viewerStyle = {
|
||||
opacity: visible && state.visible ? 1 : 0,
|
||||
display: visible || state.visible ? "block" : "none",
|
||||
};
|
||||
|
||||
let activeImg = {
|
||||
src: "",
|
||||
alt: "",
|
||||
};
|
||||
|
||||
if (
|
||||
visible &&
|
||||
state.visible &&
|
||||
!state.loading &&
|
||||
state.activeIndex !== null &&
|
||||
!state.startLoading
|
||||
) {
|
||||
activeImg = getActiveImage();
|
||||
}
|
||||
|
||||
const displayVisible = JSON.parse(localStorage.getItem("displayVisible"));
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
style={viewerStyle}
|
||||
id="image-viewer"
|
||||
onTransitionEnd={() => {
|
||||
if (!visible) {
|
||||
dispatch(
|
||||
createAction(ACTION_TYPES.setVisible, {
|
||||
visible: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
}}
|
||||
ref={viewerCore}
|
||||
>
|
||||
{isMobile && !displayVisible && mobileDetails}
|
||||
<div
|
||||
className={`${prefixCls}-mask`}
|
||||
style={{
|
||||
zIndex: zIndex,
|
||||
backgroundColor: `${
|
||||
isMobile
|
||||
? !displayVisible
|
||||
? "rgba(55,55,55,0.6)"
|
||||
: "#000"
|
||||
: "rgba(55,55,55,0.6)"
|
||||
}`,
|
||||
}}
|
||||
/>
|
||||
<ViewerImage
|
||||
prefixCls={prefixCls}
|
||||
imgSrc={
|
||||
state.loadFailed
|
||||
? props.defaultImg.src || activeImg.src
|
||||
: activeImg.src
|
||||
}
|
||||
visible={visible}
|
||||
width={state.width}
|
||||
dispatch={dispatch}
|
||||
createAction={createAction}
|
||||
actionType={ACTION_TYPES}
|
||||
playlist={props.playlist}
|
||||
playlistPos={props.playlistPos}
|
||||
currentTop={currentTop}
|
||||
opacity={state.opacity}
|
||||
getImageCenterXY={getImageCenterXY}
|
||||
setPanelVisible={props.setPanelVisible}
|
||||
handleDefaultAction={handleDefaultAction}
|
||||
handleZoom={handleZoom}
|
||||
handleResetZoom={handleResetZoom}
|
||||
height={state.height}
|
||||
onNextClick={onNextClick}
|
||||
onPrevClick={onPrevClick}
|
||||
tpCache={state.tpCache}
|
||||
withTransition={state.withTransition}
|
||||
top={state.top}
|
||||
left={state.left}
|
||||
rotate={state.rotate}
|
||||
onMaskClick={onMaskClick}
|
||||
needUpdatePoint={state.needUpdatePoint}
|
||||
onChangeImgState={handleChangeImgState}
|
||||
onResize={handleResize}
|
||||
zIndex={zIndex + 5}
|
||||
scaleX={state.scaleX}
|
||||
containerSize={containerSize}
|
||||
scaleY={state.scaleY}
|
||||
loading={state.loading}
|
||||
drag={drag}
|
||||
container={props.container}
|
||||
/>
|
||||
{props.noFooter ||
|
||||
(!isMobile && props.displayUI && (
|
||||
<div className={`${prefixCls}-container`}>
|
||||
<div
|
||||
className={`${prefixCls}-footer`}
|
||||
style={{ zIndex: zIndex + 7 }}
|
||||
>
|
||||
{noToolbar || (
|
||||
<ViewerToolbar
|
||||
isMobileOnly={isMobile}
|
||||
imageTimer={props.imageTimer}
|
||||
prefixCls={prefixCls}
|
||||
onAction={handleAction}
|
||||
alt={activeImg.alt}
|
||||
width={state.imageWidth}
|
||||
height={state.imageHeight}
|
||||
percent={state.percent}
|
||||
attribute={attribute}
|
||||
isPreviewFile={isPreviewFile}
|
||||
archiveRoom={archiveRoom}
|
||||
setIsOpenContextMenu={props.setIsOpenContextMenu}
|
||||
zoomable={zoomable}
|
||||
rotatable={rotatable}
|
||||
onPercentClick={onPercentClick}
|
||||
generateContextMenu={generateContextMenu}
|
||||
scalable={scalable}
|
||||
changeable={changeable}
|
||||
downloadable={downloadable}
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
noImgDetails={noImgDetails}
|
||||
toolbars={customToolbar(defaultToolbars)}
|
||||
activeIndex={state.activeIndex}
|
||||
count={images.length}
|
||||
showTotal={showTotal}
|
||||
totalName={totalName}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { ViewerBase };
|
@ -1,501 +0,0 @@
|
||||
import * as React from "react";
|
||||
import classnames from "classnames";
|
||||
import ViewerLoading from "./viewer-loading";
|
||||
import { useSwipeable } from "../../react-swipeable";
|
||||
import { isIOS, isMobile } from "react-device-detect";
|
||||
import { ActionType } from "./icon";
|
||||
|
||||
import MobileViewer from "./mobile-viewer";
|
||||
function ViewerImage(props) {
|
||||
const {
|
||||
dispatch,
|
||||
createAction,
|
||||
actionType,
|
||||
playlist,
|
||||
playlistPos,
|
||||
containerSize,
|
||||
setPanelVisible,
|
||||
} = props;
|
||||
|
||||
const isMouseDown = React.useRef(false);
|
||||
const isZoomingRef = React.useRef(true);
|
||||
const imgRef = React.useRef(null);
|
||||
const dirRef = React.useRef("");
|
||||
const swipedRef = React.useRef(false);
|
||||
|
||||
const unMountedRef = React.useRef(false);
|
||||
const startPostionRef = React.useRef({ x: 0, y: 0 });
|
||||
const isDoubleTapRef = React.useRef(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
unMountedRef.current = false;
|
||||
|
||||
return () => (unMountedRef.current = true);
|
||||
}, []);
|
||||
|
||||
const prePosition = React.useRef({
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
const [position, setPosition] = React.useState({
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
|
||||
const CompareTo = (a, b) => {
|
||||
return Math.trunc(a) > Math.trunc(b);
|
||||
};
|
||||
|
||||
const maybeAdjustImage = (point) => {
|
||||
const imageBounds = imgRef.current.getBoundingClientRect();
|
||||
const containerBounds = imgRef.current.parentNode.getBoundingClientRect();
|
||||
|
||||
const originalWidth = imgRef.current.clientWidth;
|
||||
const widthOverhang = (imageBounds.width - originalWidth) / 2;
|
||||
|
||||
const originalHeight = imgRef.current.clientHeight;
|
||||
const heightOverhang = (imageBounds.height - originalHeight) / 2;
|
||||
|
||||
const isWidthOutContainer = imageBounds.width >= containerBounds.width;
|
||||
|
||||
const isHeightOutContainer = imageBounds.height >= containerBounds.height;
|
||||
|
||||
if (
|
||||
CompareTo(imageBounds.left, containerBounds.left) &&
|
||||
isWidthOutContainer
|
||||
) {
|
||||
point.x = widthOverhang;
|
||||
} else if (
|
||||
CompareTo(containerBounds.right, imageBounds.right) &&
|
||||
isWidthOutContainer
|
||||
) {
|
||||
point.x = -(imageBounds.width - containerBounds.width) + widthOverhang;
|
||||
} else if (!isWidthOutContainer) {
|
||||
point.x = (containerBounds.width - imageBounds.width) / 2 + widthOverhang;
|
||||
}
|
||||
|
||||
if (
|
||||
CompareTo(imageBounds.top, containerBounds.top) &&
|
||||
isHeightOutContainer
|
||||
) {
|
||||
point.y = heightOverhang;
|
||||
} else if (
|
||||
CompareTo(containerBounds.bottom, imageBounds.bottom) &&
|
||||
isHeightOutContainer
|
||||
) {
|
||||
point.y = -(imageBounds.height - containerBounds.height) + heightOverhang;
|
||||
} else if (!isHeightOutContainer) {
|
||||
point.y =
|
||||
(containerBounds.height - imageBounds.height) / 2 + heightOverhang;
|
||||
}
|
||||
|
||||
return point;
|
||||
};
|
||||
|
||||
const handlers = useSwipeable({
|
||||
onSwipeStart: (e) => {
|
||||
dirRef.current = e.dir;
|
||||
swipedRef.current = false;
|
||||
startPostionRef.current = {
|
||||
x: props.left,
|
||||
y: props.top,
|
||||
};
|
||||
},
|
||||
onSwiping: (e) => {
|
||||
if (
|
||||
e.piching ||
|
||||
!isZoomingRef.current ||
|
||||
unMountedRef.current ||
|
||||
!imgRef.current ||
|
||||
isDoubleTapRef.current
|
||||
)
|
||||
return;
|
||||
|
||||
let newPoint = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
|
||||
const isFistImage = playlistPos === 0;
|
||||
const isLastImage = playlistPos === playlist.length - 1;
|
||||
|
||||
const containerBounds = imgRef.current.parentNode.getBoundingClientRect();
|
||||
const imageBounds = imgRef.current.getBoundingClientRect();
|
||||
|
||||
const deltaWidth = (containerBounds.width - imageBounds.width) / 2;
|
||||
const deltaHeight = (containerBounds.height - imageBounds.height) / 2;
|
||||
|
||||
const originalWidth = imgRef.current.clientWidth;
|
||||
const widthOverhang = (imageBounds.width - originalWidth) / 2;
|
||||
|
||||
const originalHeight = imgRef.current.clientHeight;
|
||||
const heightOverhang = (imageBounds.height - originalHeight) / 2;
|
||||
|
||||
if (props.scaleX * props.scaleY <= 1) {
|
||||
switch (dirRef.current) {
|
||||
case "Down":
|
||||
newPoint.x = props.left;
|
||||
newPoint.y = e.deltaY + deltaHeight + heightOverhang;
|
||||
|
||||
break;
|
||||
case "Left":
|
||||
newPoint.x = isLastImage
|
||||
? props.left
|
||||
: e.deltaX + deltaWidth + widthOverhang;
|
||||
newPoint.y = props.top;
|
||||
break;
|
||||
case "Right":
|
||||
newPoint.x = isFistImage
|
||||
? props.left
|
||||
: e.deltaX + deltaWidth + widthOverhang;
|
||||
newPoint.y = props.top;
|
||||
break;
|
||||
default:
|
||||
newPoint.x = props.left;
|
||||
newPoint.y = props.top;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
const isWidthOutContainer = imageBounds.width >= containerBounds.width;
|
||||
|
||||
const isHeightOutContainer =
|
||||
imageBounds.height >= containerBounds.height;
|
||||
|
||||
const [vx, vy] = e.vxvy;
|
||||
|
||||
const absVx = Math.abs(vx) > 0 ? Math.abs(vx) + 1 : 0;
|
||||
const absVy = Math.abs(vy) > 0 ? Math.abs(vy) + 1 : 0;
|
||||
|
||||
const isImageHeightInsideContainer =
|
||||
imageBounds.top > containerBounds.top &&
|
||||
containerBounds.bottom > imageBounds.bottom;
|
||||
const isImageWidhtInsideContainer =
|
||||
imageBounds.left > containerBounds.left &&
|
||||
containerBounds.right > imageBounds.right;
|
||||
|
||||
const left = imageBounds.left + e.deltaX * absVx;
|
||||
const right = imageBounds.right + e.deltaX * absVx;
|
||||
|
||||
if (isWidthOutContainer) {
|
||||
if (left > containerBounds.left) {
|
||||
newPoint.x = widthOverhang;
|
||||
} else if (right < containerBounds.right) {
|
||||
newPoint.x =
|
||||
-(imageBounds.width - containerBounds.width) + widthOverhang;
|
||||
} else {
|
||||
newPoint.x = e.deltaX * absVx + startPostionRef.current.x;
|
||||
}
|
||||
} else if (isImageWidhtInsideContainer) {
|
||||
newPoint.x = props.left;
|
||||
} else {
|
||||
newPoint.x = e.deltaX * absVx + startPostionRef.current.x;
|
||||
}
|
||||
|
||||
const top = imageBounds.top + e.deltaY * absVy;
|
||||
const bottom = imageBounds.bottom + e.deltaY * absVy;
|
||||
|
||||
if (isHeightOutContainer) {
|
||||
if (top > containerBounds.top) {
|
||||
newPoint.y = heightOverhang;
|
||||
} else if (bottom < containerBounds.bottom) {
|
||||
newPoint.y =
|
||||
-(imageBounds.height - containerBounds.height) + heightOverhang;
|
||||
} else {
|
||||
newPoint.y = e.deltaY * absVy + startPostionRef.current.y;
|
||||
}
|
||||
} else if (isImageHeightInsideContainer) {
|
||||
newPoint.y = props.top;
|
||||
} else {
|
||||
newPoint.y = e.deltaY * absVy + startPostionRef.current.y;
|
||||
}
|
||||
}
|
||||
|
||||
const opacity =
|
||||
props.scaleX !== 1 && props.scaleY !== 1
|
||||
? 1
|
||||
: dirRef.current === "Down"
|
||||
? 2 -
|
||||
(imageBounds.height / 2 + props.top) / (containerBounds.height / 2)
|
||||
: 1;
|
||||
|
||||
const direction =
|
||||
Math.abs(e.deltaX) > Math.abs(e.deltaY) ? "horizontal" : "vertical";
|
||||
|
||||
return dispatch(
|
||||
createAction(actionType.update, {
|
||||
left: newPoint.x,
|
||||
top: newPoint.y,
|
||||
opacity: direction === "vertical" && e.deltaY > 0 ? opacity : 1,
|
||||
deltaX: 0,
|
||||
deltaY: 0,
|
||||
})
|
||||
);
|
||||
},
|
||||
onSwipedLeft: (e) => {
|
||||
if (
|
||||
(props.scaleX !== 1 && props.scaleY !== 1) ||
|
||||
e.piching ||
|
||||
!isZoomingRef.current
|
||||
)
|
||||
return;
|
||||
if (e.deltaX <= -100 && playlistPos !== playlist.length - 1) {
|
||||
swipedRef.current = true;
|
||||
props.onNextClick();
|
||||
}
|
||||
},
|
||||
onSwipedRight: (e) => {
|
||||
if (
|
||||
(props.scaleX !== 1 && props.scaleY !== 1) ||
|
||||
e.piching ||
|
||||
!isZoomingRef.current
|
||||
)
|
||||
return;
|
||||
|
||||
if (e.deltaX >= 100 && playlistPos !== 0) {
|
||||
swipedRef.current = true;
|
||||
props.onPrevClick();
|
||||
}
|
||||
},
|
||||
onSwipedDown: (e) => {
|
||||
if (unMountedRef.current) return;
|
||||
|
||||
if (e.deltaY > 200 && props.scaleX * props.scaleY === 1) {
|
||||
swipedRef.current = true;
|
||||
props.onMaskClick();
|
||||
}
|
||||
},
|
||||
onTap: (e) => {
|
||||
setPanelVisible((visible) => !visible);
|
||||
},
|
||||
onSwiped: (e) => {
|
||||
if (unMountedRef.current || isDoubleTapRef.current) return;
|
||||
console.log("onSwiped");
|
||||
let Point = {
|
||||
x: props.left,
|
||||
y: props.top,
|
||||
};
|
||||
dirRef.current = "";
|
||||
|
||||
setTimeout(() => {
|
||||
if (unMountedRef.current || swipedRef.current) return;
|
||||
const newPoint = maybeAdjustImage(Point);
|
||||
return dispatch(
|
||||
createAction(actionType.update, {
|
||||
left: newPoint.x,
|
||||
top: newPoint.y,
|
||||
deltaX: 0,
|
||||
deltaY: 0,
|
||||
opacity: 1,
|
||||
})
|
||||
);
|
||||
}, 200);
|
||||
},
|
||||
onTouchEndOrOnMouseUp: () => {
|
||||
dirRef.current = "";
|
||||
isDoubleTapRef.current = false;
|
||||
},
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
bindEvent(true);
|
||||
bindWindowResizeEvent(true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
bindWindowResizeEvent();
|
||||
|
||||
return () => {
|
||||
bindWindowResizeEvent(true);
|
||||
};
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (props.visible && props.drag) {
|
||||
bindEvent();
|
||||
}
|
||||
if (!props.visible && props.drag) {
|
||||
handleMouseUp({});
|
||||
}
|
||||
return () => {
|
||||
bindEvent(true);
|
||||
};
|
||||
}, [props.drag, props.visible]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let diffX = position.x - prePosition.current.x;
|
||||
let diffY = position.y - prePosition.current.y;
|
||||
prePosition.current = {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
};
|
||||
props.onChangeImgState(
|
||||
props.width,
|
||||
props.height,
|
||||
props.top + diffY,
|
||||
props.left + diffX
|
||||
);
|
||||
}, [position]);
|
||||
|
||||
function handleResize(e) {
|
||||
props.onResize();
|
||||
}
|
||||
|
||||
function handleMouseDown(e) {
|
||||
if (e.button !== 0) {
|
||||
return;
|
||||
}
|
||||
if (!props.visible || !props.drag) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
isMouseDown.current = true;
|
||||
prePosition.current = {
|
||||
x: e.nativeEvent.clientX,
|
||||
y: e.nativeEvent.clientY,
|
||||
};
|
||||
}
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
if (isMouseDown.current) {
|
||||
setPosition({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (e) => {
|
||||
if (e?.detail === 2) props.handleDefaultAction(ActionType.zoomIn);
|
||||
};
|
||||
|
||||
function handleResize(e) {
|
||||
props.onResize();
|
||||
}
|
||||
|
||||
function handleMouseUp(e) {
|
||||
isMouseDown.current = false;
|
||||
}
|
||||
|
||||
function onClose(e) {
|
||||
if (
|
||||
e.target === imgRef.current ||
|
||||
e.nativeEvent.pointerType === "touch" ||
|
||||
e.type === "touchend"
|
||||
)
|
||||
return;
|
||||
props.onMaskClick();
|
||||
}
|
||||
|
||||
function bindWindowResizeEvent(remove) {
|
||||
let funcName = "addEventListener";
|
||||
if (remove) {
|
||||
funcName = "removeEventListener";
|
||||
}
|
||||
window[funcName]("resize", handleResize, false);
|
||||
}
|
||||
|
||||
function bindEvent(remove) {
|
||||
let funcName = "addEventListener";
|
||||
if (remove) {
|
||||
funcName = "removeEventListener";
|
||||
}
|
||||
|
||||
document[funcName]("click", handleMouseUp, false);
|
||||
document[funcName]("mousemove", handleMouseMove, false);
|
||||
}
|
||||
|
||||
let imgStyle = {
|
||||
width: `${props.width}px`,
|
||||
height: `${props.height}px`,
|
||||
opacity: `${props.opacity}`,
|
||||
transition: `${props.withTransition ? "all .26s ease-out" : "none"}`,
|
||||
transform: `
|
||||
translateX(${props.left !== null ? props.left + "px" : "auto"}) translateY(${
|
||||
props.top
|
||||
}px)
|
||||
rotate(${props.rotate}deg) scaleX(${props.scaleX}) scaleY(${props.scaleY})`,
|
||||
};
|
||||
|
||||
const imgClass = classnames(`${props.prefixCls}-image`, {
|
||||
drag: props.drag,
|
||||
[`${props.prefixCls}-image-transition`]: !isMouseDown.current,
|
||||
});
|
||||
|
||||
let styleIndex = {
|
||||
zIndex: props.zIndex,
|
||||
};
|
||||
|
||||
let imgNode = null;
|
||||
|
||||
if (props.imgSrc !== "") {
|
||||
imgNode = isMobile ? (
|
||||
<MobileViewer
|
||||
className={imgClass}
|
||||
src={props.imgSrc}
|
||||
width={props.width}
|
||||
height={props.height}
|
||||
left={props.left}
|
||||
top={props.top}
|
||||
onPrev={props.onPrevClick}
|
||||
onNext={props.onNextClick}
|
||||
onMask={props.onMaskClick}
|
||||
isFistImage={playlistPos === 0}
|
||||
isLastImage={playlistPos === playlist.length - 1}
|
||||
setPanelVisible={setPanelVisible}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
className={imgClass}
|
||||
src={props.imgSrc}
|
||||
style={imgStyle}
|
||||
ref={imgRef}
|
||||
onMouseDown={handleMouseDown}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (props.loading) {
|
||||
imgNode = (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
height: `${window.innerHeight}px`,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<ViewerLoading />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
const events = isIOS ? { onTouchEnd: onClose } : { onClick: onClose };
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${props.prefixCls}-canvas`}
|
||||
style={styleIndex}
|
||||
{...events}
|
||||
>
|
||||
{imgNode}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${props.prefixCls}-canvas`}
|
||||
onClick={onClose}
|
||||
style={styleIndex}
|
||||
{...handlers}
|
||||
>
|
||||
{imgNode}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(ViewerImage);
|
@ -1,26 +0,0 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
const StyledLoader = styled.div`
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid #fff;
|
||||
border-bottom-color: transparent;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
animation: rotation 1s linear infinite;
|
||||
|
||||
@keyframes rotation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default function ViewerLoading() {
|
||||
return <StyledLoader />;
|
||||
}
|
@ -1,209 +0,0 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Icon, { ActionType } from "./icon";
|
||||
|
||||
import MediaContextMenu from "PUBLIC_DIR/images/vertical-dots.react.svg";
|
||||
|
||||
const ToolbarItem = styled.li`
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
|
||||
${(props) => (props.isSeparator ? "width: 33px;" : "width: 48px;")}
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.zoomPercent {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.zoomOut,
|
||||
.zoomIn,
|
||||
.rotateLeft,
|
||||
.rotateRight {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
path,
|
||||
rect {
|
||||
${(props) => (props.percent !== 25 ? "fill: #fff;" : "fill: #BEBEBE;")}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const defaultToolbars = [
|
||||
{
|
||||
key: "zoomIn",
|
||||
actionType: ActionType.zoomIn,
|
||||
},
|
||||
{
|
||||
key: "zoomOut",
|
||||
actionType: ActionType.zoomOut,
|
||||
},
|
||||
{
|
||||
key: "prev",
|
||||
actionType: ActionType.prev,
|
||||
noHover: true,
|
||||
},
|
||||
{
|
||||
key: "reset",
|
||||
actionType: ActionType.reset,
|
||||
},
|
||||
{
|
||||
key: "next",
|
||||
actionType: ActionType.next,
|
||||
noHover: true,
|
||||
},
|
||||
{
|
||||
key: "rotateLeft",
|
||||
actionType: ActionType.rotateLeft,
|
||||
},
|
||||
{
|
||||
key: "rotateRight",
|
||||
actionType: ActionType.rotateRight,
|
||||
},
|
||||
{
|
||||
key: "scaleX",
|
||||
actionType: ActionType.scaleX,
|
||||
},
|
||||
{
|
||||
key: "scaleY",
|
||||
actionType: ActionType.scaleY,
|
||||
},
|
||||
{
|
||||
key: "download",
|
||||
actionType: ActionType.download,
|
||||
},
|
||||
];
|
||||
|
||||
function deleteToolbarFromKey(toolbars, keys) {
|
||||
const targetToolbar = toolbars.filter((item) => keys.indexOf(item.key) < 0);
|
||||
|
||||
return targetToolbar;
|
||||
}
|
||||
|
||||
export default function ViewerToolbar(props) {
|
||||
function handleAction(config) {
|
||||
clearTimeout(props.imageTimer);
|
||||
if (config.key === "percent") return props.onPercentClick();
|
||||
props.onAction(config);
|
||||
}
|
||||
|
||||
const iconRef = React.useRef(null);
|
||||
|
||||
function renderAction(config) {
|
||||
let content = null;
|
||||
if (typeof ActionType[config.actionType] !== "undefined") {
|
||||
content = <Icon type={config.actionType} />;
|
||||
}
|
||||
if (config.render) {
|
||||
content = config.render;
|
||||
}
|
||||
|
||||
if (config.key === "percent") {
|
||||
content = (
|
||||
<div
|
||||
className="iconContainer zoomPercent"
|
||||
style={{ width: "auto", color: "#fff", userSelect: "none" }}
|
||||
>
|
||||
{`${props.percent}%`}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (config.key === "context-menu") {
|
||||
const contextMenu = props.generateContextMenu(props.isOpen);
|
||||
return (
|
||||
<ToolbarItem
|
||||
ref={iconRef}
|
||||
style={{ position: "relative" }}
|
||||
noHover={config.noHover}
|
||||
key={config.key}
|
||||
className={`${props.prefixCls}-btn`}
|
||||
onClick={() => {
|
||||
props.setIsOpenContextMenu((open) => !open);
|
||||
props.setIsOpen((open) => !open);
|
||||
}}
|
||||
data-key={config.key}
|
||||
>
|
||||
<div className="context" style={{ height: "16px" }}>
|
||||
<MediaContextMenu size="scale" />
|
||||
</div>
|
||||
{contextMenu}
|
||||
</ToolbarItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolbarItem
|
||||
percent={config.percent ? props.percent : 100}
|
||||
noHover={config.noHover}
|
||||
key={config.key}
|
||||
isSeparator={config.actionType === -1}
|
||||
className={`${props.prefixCls}-btn`}
|
||||
onClick={() => {
|
||||
handleAction(config);
|
||||
}}
|
||||
data-key={config.key}
|
||||
>
|
||||
{content}
|
||||
</ToolbarItem>
|
||||
);
|
||||
}
|
||||
let toolbars = props.toolbars;
|
||||
|
||||
if (!props.isMobileOnly) {
|
||||
toolbars = deleteToolbarFromKey(toolbars, ["delete", "favorite"]);
|
||||
}
|
||||
|
||||
if (props.isMobileOnly) {
|
||||
toolbars = deleteToolbarFromKey(toolbars, [
|
||||
"zoomIn",
|
||||
"zoomOut",
|
||||
"percent",
|
||||
"separator",
|
||||
"context-menu",
|
||||
]);
|
||||
}
|
||||
if (!props.zoomable) {
|
||||
toolbars = deleteToolbarFromKey(toolbars, ["zoomIn", "zoomOut"]);
|
||||
}
|
||||
if (!props.changeable) {
|
||||
toolbars = deleteToolbarFromKey(toolbars, ["prev", "next"]);
|
||||
}
|
||||
if (!props.rotatable) {
|
||||
toolbars = deleteToolbarFromKey(toolbars, ["rotateLeft", "rotateRight"]);
|
||||
}
|
||||
if (!props.scalable) {
|
||||
toolbars = deleteToolbarFromKey(toolbars, ["scaleX", "scaleY"]);
|
||||
}
|
||||
|
||||
if (props.isPreviewFile || props.archiveRoom) {
|
||||
toolbars = deleteToolbarFromKey(toolbars, [
|
||||
"context-menu",
|
||||
"context-separator",
|
||||
]);
|
||||
}
|
||||
// if (!props.downloadable) {
|
||||
// toolbars = deleteToolbarFromKey(toolbars, ["download"]);
|
||||
// }
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ul className={`${props.prefixCls}-toolbar`}>
|
||||
{toolbars.map((item) => {
|
||||
return renderAction(item);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -903,18 +903,15 @@ internal class FileDao : AbstractDao, IFileDao<int>
|
||||
await RecalculateFilesCountAsync(toFolderId);
|
||||
}
|
||||
|
||||
|
||||
var parentFoldersTask =
|
||||
filesDbContext.Tree
|
||||
.Where(r => r.FolderId == toFolderId)
|
||||
.OrderByDescending(r => r.Level)
|
||||
.ToListAsync();
|
||||
|
||||
var toUpdateFile = await q.FirstOrDefaultAsync(r => r.CurrentVersion);
|
||||
|
||||
if (toUpdateFile != null)
|
||||
{
|
||||
toUpdateFile.Folders = await parentFoldersTask;
|
||||
toUpdateFile.Folders = await filesDbContext.Tree
|
||||
.Where(r => r.FolderId == toFolderId)
|
||||
.OrderByDescending(r => r.Level)
|
||||
.ToListAsync();
|
||||
|
||||
_factoryIndexer.Update(toUpdateFile, UpdateAction.Replace, w => w.Folders);
|
||||
}
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user