Merge branch 'bugfix/settings-backup' of github.com:ONLYOFFICE/AppServer into bugfix/settings-backup

This commit is contained in:
Tatiana Lopaeva 2023-03-03 14:46:21 +03:00
commit 68ce5c8764
65 changed files with 1881 additions and 2891 deletions

View File

@ -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"
}

View File

@ -1,4 +1,5 @@
{
"ResetApplicationDescription": "Настройки приложения для аутентификации будут сброшены.",
"ResetApplicationTitle": "Сбросить настройки приложения"
"ResetApplicationTitle": "Сбросить настройки приложения",
"SuccessResetApplication": "Настройки приложения для аутентификации успешно сброшены"
}

View File

@ -55,7 +55,7 @@ const withLoader = (WrappedComponent) => (Loader) => {
return (!isEditor && firstLoad && !isGallery) ||
!isLoaded ||
(isMobile && inLoad) ||
(isMobile && inLoad && !firstLoad) ||
(isLoadingFilesFind && !Loader) ||
!tReady ||
!isInit ? (

View File

@ -517,7 +517,6 @@ const ShellWrapper = inject(({ auth, backup }) => {
setSnackbarExist,
socketHelper,
setTheme,
getWhiteLabelLogoUrls,
whiteLabelLogoUrls,
} = settingsStore;
const isBase = settingsStore.theme.isBase;

View File

@ -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));

View File

@ -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));

View File

@ -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))));

View File

@ -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}

View File

@ -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.

View File

@ -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);

View File

@ -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);

View File

@ -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) => {

View File

@ -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",
});

View File

@ -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));

View File

@ -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));

View File

@ -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,

View File

@ -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));

View File

@ -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}

View File

@ -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;
}
}
}

View File

@ -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

View File

@ -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));

View File

@ -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);
};
}

View File

@ -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);
};
}

View File

@ -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;

View File

@ -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) => {

View File

@ -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);

View File

@ -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();
}

View File

@ -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;

View File

@ -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 });

View File

@ -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;
}

View File

@ -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);

View File

@ -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>
),
},
];
};

View File

@ -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 },

View File

@ -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}

View File

@ -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;

View File

@ -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;

View File

@ -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;
`;

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
`;

View File

@ -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
);

View File

@ -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 && (

View File

@ -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;
}

View File

@ -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
)

View File

@ -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>
);
}

View File

@ -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]);

View File

@ -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;

View File

@ -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}

View File

@ -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",
});

View File

@ -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
);
}

View File

@ -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
);
}

View File

@ -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);

View File

@ -3081,6 +3081,9 @@ const Base = {
main: {
background: "#F8F9F9",
textColor: black,
descriptionTextColor: "#A3A9AE",
pendingEmailTextColor: "#A3A9AE",
},
themePreview: {
descriptionColor: "#A3A9AE",

View File

@ -3080,6 +3080,9 @@ const Dark = {
main: {
background: "#1f1f1f",
textColor: white,
descriptionTextColor: "#858585",
pendingEmailTextColor: "#858585",
},
themePreview: {
descriptionColor: "#ADADAD",

View File

@ -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>
);
};

View File

@ -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,
};

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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;

View File

@ -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 };

View File

@ -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);

View File

@ -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 />;
}

View File

@ -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>
);
}

View File

@ -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);
}
});