Merge pull request #534 from ONLYOFFICE/feature/VDR-watermarks
Feature/vdr-watermarks
This commit is contained in:
commit
2a30838dc1
@ -1,29 +1,42 @@
|
||||
{
|
||||
"ActivationRequired": "activation required",
|
||||
"AddWatermarkElements": "Add watermark elements",
|
||||
"AddStaticText": "Add static text",
|
||||
"Diagonal": "Diagonal",
|
||||
"ChooseRoomType": "Choose room type",
|
||||
"CreateRoomConfirmation": "Continue without connecting the storage?\nYou have selected a third-party storage option that is not connected yet. If you proceed without connecting the service, this option will not be added.",
|
||||
"CreateRoomWatermarksConfirmation": "You have not set a watermark to be applied to documents in this room. You can always add a watermark in the room editing settings. Continue without a watermark?",
|
||||
"CreateTagOption": "Create tag",
|
||||
"DisableRoomQuota": "Disable quota for this room",
|
||||
"FormRoomBarDescription": "This room is available to anyone with the link. External users will have Form Filler permission for all the files.",
|
||||
"Center": "Center",
|
||||
"Horizontal": "Horizontal",
|
||||
"Icon": "Icon",
|
||||
"MakeRoomPrivateDescription": "All files in this room will be encrypted.",
|
||||
"MakeRoomPrivateLimitationsWarningDescription": "With this feature, you can invite only existing {{productName}} users. After creating a room, you will not be able to change the list of users.",
|
||||
"MakeRoomPrivateTitle": "Make the Room Private",
|
||||
"PeopleSelectorInfo": "Only a room admin or a {{productName}} admin can become the owner of the room",
|
||||
"PublicRoomBarDescription": "This room is available to anyone with the link. External users will have View Only permission for all the files.",
|
||||
"ViewerInfo": "Viewer info",
|
||||
"Position": "Position",
|
||||
"PublicRoomSystemFoldersDescription": "System folders store copies of forms at different stages of completion. Forms that are being filled are stored in the In progress folder, and completed forms are stored in the Complete folder.",
|
||||
"PublicRoomSystemFoldersTitle": "System Folders",
|
||||
"RoomEditing": "Room editing",
|
||||
"RootFolderLabel": "Root folder",
|
||||
"StorageDescription": "Storage quota set per room. You can change this value or turn off storage limit.",
|
||||
"TagsPlaceholder": "Add a tag",
|
||||
"Text": "Text",
|
||||
"ThirdPartyStorageComboBoxPlaceholder": "Select storage",
|
||||
"ThirdPartyStorageDescription": "Use third-party services as data storage for this room. You can create a new folder or select the existing one in the connected storage.",
|
||||
"ThirdPartyStorageNoStorageAlert": "Before, you need to connect the corresponding service in the “Integration” section. Otherwise, the connection will not be possible.",
|
||||
"ThirdPartyStoragePermanentSettingDescription": "Files are stored in a third-party {{thirdpartyTitle}} storage in the \"{{thirdpartyFolderName}}\" folder.\n<strong>{{thirdpartyPath}}</strong>",
|
||||
"ThirdPartyStorageRoomAdminNoStorageAlert": "To connect a third-party storage, you need to add the corresponding service in the Integration section of {{productName}} settings. Contact {{productName}} owner or administrator to enable the integration.",
|
||||
"UserName": "User Name",
|
||||
"UserEmail": "User Email",
|
||||
"UserIPAddress": "User IP Address",
|
||||
"ViewOnlyRoomDescription": "Share any ready documents, reports, documentation, and other files for viewing.",
|
||||
"ViewOnlyRoomTitle": "View-only room",
|
||||
"WatermarkPreview": "Watermark Preview",
|
||||
"WatermarkPreviewHelp": "This image preview roughly shows how the watermark will be displayed in your files.",
|
||||
"AutomaticIndexing": "Automatic indexing",
|
||||
"AutomaticIndexingDescription": "Enable automatic indexing to index files and folders by serial number. Sorting by number will be set as default for all users.",
|
||||
"FileLifetime": "File lifetime",
|
||||
|
@ -51,6 +51,7 @@ const CreateRoomEvent = ({
|
||||
enableThirdParty,
|
||||
deleteThirdParty,
|
||||
startRoomType,
|
||||
isNotWatermarkSet,
|
||||
}) => {
|
||||
const { t } = useTranslation(["CreateEditRoomDialog", "Common", "Files"]);
|
||||
const [fetchedTags, setFetchedTags] = useState([]);
|
||||
@ -59,14 +60,17 @@ const CreateRoomEvent = ({
|
||||
setRoomParams(roomParams);
|
||||
setOnClose(onClose);
|
||||
|
||||
if (
|
||||
const notConnectedThirdparty =
|
||||
roomParams.storageLocation.isThirdparty &&
|
||||
!roomParams.storageLocation.storageFolderId
|
||||
) {
|
||||
!roomParams.storageLocation.storageFolderId;
|
||||
|
||||
if (notConnectedThirdparty || isNotWatermarkSet()) {
|
||||
setCreateRoomConfirmDialogVisible(true);
|
||||
} else {
|
||||
onCreateRoom(false, t);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
onCreateRoom(false, t);
|
||||
};
|
||||
|
||||
const fetchTagsAction = useCallback(async () => {
|
||||
@ -134,6 +138,8 @@ export default inject(
|
||||
setIsLoading,
|
||||
setOnClose,
|
||||
confirmDialogIsLoading,
|
||||
|
||||
isNotWatermarkSet,
|
||||
} = createEditRoomStore;
|
||||
|
||||
return {
|
||||
@ -151,6 +157,8 @@ export default inject(
|
||||
fetchThirdPartyProviders,
|
||||
enableThirdParty,
|
||||
deleteThirdParty,
|
||||
|
||||
isNotWatermarkSet,
|
||||
};
|
||||
},
|
||||
)(observer(CreateRoomEvent));
|
||||
|
@ -31,8 +31,13 @@ import isEqual from "lodash/isEqual";
|
||||
import { EditRoomDialog } from "../dialogs";
|
||||
import { Encoder } from "@docspace/shared/utils/encoder";
|
||||
import api from "@docspace/shared/api";
|
||||
import { getRoomInfo } from "@docspace/shared/api/rooms";
|
||||
import {
|
||||
deleteWatermarkSettings,
|
||||
getRoomInfo,
|
||||
getWatermarkSettings,
|
||||
} from "@docspace/shared/api/rooms";
|
||||
import { toastr } from "@docspace/shared/components/toast";
|
||||
import { setWatermarkSettings } from "@docspace/shared/api/rooms";
|
||||
|
||||
const EditRoomEvent = ({
|
||||
addActiveItems,
|
||||
@ -77,12 +82,17 @@ const EditRoomEvent = ({
|
||||
defaultRoomsQuota,
|
||||
isDefaultRoomsQuotaSet,
|
||||
changeRoomLifetime,
|
||||
setInitialWatermarks,
|
||||
getWatermarkRequest,
|
||||
watermarksSettings,
|
||||
isNotWatermarkSet,
|
||||
}) => {
|
||||
const { t } = useTranslation(["CreateEditRoomDialog", "Common", "Files"]);
|
||||
|
||||
const [fetchedTags, setFetchedTags] = useState([]);
|
||||
const [fetchedImage, setFetchedImage] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isInitLoading, setIsInitLoading] = useState(false);
|
||||
|
||||
const startTags = Object.values(item.tags);
|
||||
const startObjTags = startTags.map((tag, i) => ({ id: i, name: tag }));
|
||||
@ -193,6 +203,15 @@ const EditRoomEvent = ({
|
||||
if (removedTags.length)
|
||||
actions.push(removeTagsFromRoom(room.id, removedTags));
|
||||
|
||||
|
||||
|
||||
if (watermarksSettings && !isNotWatermarkSet()) {
|
||||
|
||||
const request = getWatermarkRequest(room, watermarksSettings);
|
||||
|
||||
actions.push(request);
|
||||
}
|
||||
|
||||
await Promise.all(actions);
|
||||
|
||||
if (!!item.logo.original && !roomParams.icon.uploadedFile) {
|
||||
@ -281,24 +300,28 @@ const EditRoomEvent = ({
|
||||
setFetchedImage(file);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const logo = item?.logo?.original ? item.logo.original : "";
|
||||
if (logo) {
|
||||
fetchLogoAction(logo);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchTagsAction = useCallback(async () => {
|
||||
const tags = await fetchTags();
|
||||
setFetchedTags(tags);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTagsAction();
|
||||
}, [fetchTagsAction]);
|
||||
|
||||
useEffect(() => {
|
||||
setCreateRoomDialogVisible(true);
|
||||
setIsInitLoading(true);
|
||||
|
||||
const logo = item?.logo?.original ? item.logo.original : "";
|
||||
|
||||
const requests = [fetchTags(), getWatermarkSettings(item.id)];
|
||||
|
||||
if (logo) requests.push(fetchLogoAction);
|
||||
|
||||
const fetchInfo = async () => {
|
||||
const [tags, watermarks] = await Promise.all(requests);
|
||||
|
||||
setFetchedTags(tags);
|
||||
|
||||
setInitialWatermarks(watermarks);
|
||||
|
||||
setIsInitLoading(false);
|
||||
};
|
||||
|
||||
fetchInfo();
|
||||
|
||||
return () => setCreateRoomDialogVisible(false);
|
||||
}, []);
|
||||
|
||||
@ -312,6 +335,7 @@ const EditRoomEvent = ({
|
||||
fetchedTags={fetchedTags}
|
||||
fetchedImage={fetchedImage}
|
||||
isLoading={isLoading}
|
||||
isInitLoading={isInitLoading}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -327,6 +351,7 @@ export default inject(
|
||||
filesSettingsStore,
|
||||
infoPanelStore,
|
||||
currentQuotaStore,
|
||||
createEditRoomStore,
|
||||
}) => {
|
||||
const {
|
||||
editRoom,
|
||||
@ -359,6 +384,12 @@ export default inject(
|
||||
const { updateInfoPanelSelection } = infoPanelStore;
|
||||
|
||||
const { defaultRoomsQuota, isDefaultRoomsQuotaSet } = currentQuotaStore;
|
||||
const {
|
||||
setInitialWatermarks,
|
||||
watermarksSettings,
|
||||
isNotWatermarkSet,
|
||||
getWatermarkRequest,
|
||||
} = createEditRoomStore;
|
||||
|
||||
return {
|
||||
defaultRoomsQuota,
|
||||
@ -397,6 +428,10 @@ export default inject(
|
||||
updateInfoPanelSelection,
|
||||
changeRoomOwner,
|
||||
changeRoomLifetime,
|
||||
setInitialWatermarks,
|
||||
watermarksSettings,
|
||||
isNotWatermarkSet,
|
||||
getWatermarkRequest,
|
||||
};
|
||||
},
|
||||
)(observer(EditRoomEvent));
|
||||
|
@ -25,6 +25,8 @@
|
||||
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { inject, observer } from "mobx-react";
|
||||
|
||||
import isEqual from "lodash/isEqual";
|
||||
import TagHandler from "./handlers/TagHandler";
|
||||
import SetRoomParams from "./sub-components/SetRoomParams";
|
||||
@ -42,6 +44,8 @@ const EditRoomDialog = ({
|
||||
fetchedRoomParams,
|
||||
fetchedTags,
|
||||
fetchedImage,
|
||||
isInitLoading,
|
||||
isEqualWatermarkChanges,
|
||||
}) => {
|
||||
const [isScrollLocked, setIsScrollLocked] = useState(false);
|
||||
const [isValidTitle, setIsValidTitle] = useState(true);
|
||||
@ -77,7 +81,8 @@ const EditRoomDialog = ({
|
||||
prevParams.icon.uploadedFile === currentParams.icon.uploadedFile) &&
|
||||
prevParams.quota === currentParams.quota &&
|
||||
prevParams.indexing === currentParams.indexing &&
|
||||
isEqual(prevParams.lifetime, currentParams.lifetime)
|
||||
isEqual(prevParams.lifetime, currentParams.lifetime) &&
|
||||
isEqualWatermarkChanges()
|
||||
);
|
||||
};
|
||||
|
||||
@ -132,6 +137,7 @@ const EditRoomDialog = ({
|
||||
visible={visible}
|
||||
onClose={onCloseAction}
|
||||
isScrollLocked={isScrollLocked}
|
||||
isLoading={isInitLoading}
|
||||
withFooterBorder
|
||||
>
|
||||
<ModalDialog.Header>
|
||||
@ -183,4 +189,10 @@ const EditRoomDialog = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default EditRoomDialog;
|
||||
export default inject(({ createEditRoomStore }) => {
|
||||
const { isEqualWatermarkChanges } = createEditRoomStore;
|
||||
|
||||
return {
|
||||
isEqualWatermarkChanges,
|
||||
};
|
||||
})(observer(EditRoomDialog));
|
||||
|
@ -223,6 +223,7 @@ const SetRoomParams = ({
|
||||
t={t}
|
||||
roomParams={roomParams}
|
||||
setRoomParams={setRoomParams}
|
||||
isEdit={isEdit}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
@ -1,16 +1,17 @@
|
||||
import { useState } from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { inject, observer } from "mobx-react";
|
||||
|
||||
import { Text } from "@docspace/shared/components/text";
|
||||
import { ToggleButton } from "@docspace/shared/components/toggle-button";
|
||||
|
||||
import FileLifetime from "./FileLifetime";
|
||||
import WatermarkBlock from "./Watermarks/WatermarkBlock";
|
||||
|
||||
const StyledVirtualDataRoomBlock = styled.div`
|
||||
.virtual-data-room-block {
|
||||
margin-bottom: 18px;
|
||||
:last-child {
|
||||
margin-bottom: -26px;
|
||||
}
|
||||
|
||||
.virtual-data-room-block_header {
|
||||
display: flex;
|
||||
@ -26,6 +27,9 @@ const StyledVirtualDataRoomBlock = styled.div`
|
||||
|
||||
color: ${({ theme }) => theme.editLink.text.color};
|
||||
}
|
||||
.virtual-data-room-block_content {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -64,14 +68,13 @@ const Block = ({
|
||||
);
|
||||
};
|
||||
|
||||
const VirtualDataRoomBlock = ({ t, roomParams, setRoomParams }) => {
|
||||
const VirtualDataRoomBlock = ({ t, roomParams, setRoomParams, isEdit }) => {
|
||||
const role = t("Translations:RoleViewer");
|
||||
|
||||
const [fileLifetimeChecked, setFileLifetimeChecked] = useState(
|
||||
!!roomParams?.lifetime,
|
||||
);
|
||||
const [copyAndDownloadChecked, setCopyAndDownloadChecked] = useState(false);
|
||||
const [watermarksChecked, setWatermarksChecked] = useState(false);
|
||||
|
||||
const onChangeAutomaticIndexing = () => {
|
||||
setRoomParams({ ...roomParams, indexing: !roomParams.indexing });
|
||||
@ -86,10 +89,6 @@ const VirtualDataRoomBlock = ({ t, roomParams, setRoomParams }) => {
|
||||
setCopyAndDownloadChecked(!copyAndDownloadChecked);
|
||||
};
|
||||
|
||||
const onChangeAddWatermarksToDocuments = () => {
|
||||
setWatermarksChecked(!watermarksChecked);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledVirtualDataRoomBlock>
|
||||
<Block
|
||||
@ -124,13 +123,8 @@ const VirtualDataRoomBlock = ({ t, roomParams, setRoomParams }) => {
|
||||
isDisabled={false}
|
||||
isChecked={copyAndDownloadChecked}
|
||||
></Block>
|
||||
<Block
|
||||
headerText={t("AddWatermarksToDocuments")}
|
||||
bodyText={t("AddWatermarksToDocumentsDescription")}
|
||||
onChange={onChangeAddWatermarksToDocuments}
|
||||
isDisabled={false}
|
||||
isChecked={watermarksChecked}
|
||||
></Block>
|
||||
|
||||
<WatermarkBlock BlockComponent={Block} t={t} isEdit={isEdit} />
|
||||
</StyledVirtualDataRoomBlock>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,334 @@
|
||||
// (c) Copyright Ascensio System SIA 2009-2024
|
||||
//
|
||||
// This program is a free software product.
|
||||
// You can redistribute it and/or modify it under the terms
|
||||
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
|
||||
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
|
||||
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
|
||||
// any third-party rights.
|
||||
//
|
||||
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
|
||||
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
|
||||
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
|
||||
//
|
||||
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
|
||||
//
|
||||
// The interactive user interfaces in modified source and object code versions of the Program must
|
||||
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
|
||||
//
|
||||
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
|
||||
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
|
||||
// trademark law for use of our trademarks.
|
||||
//
|
||||
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
|
||||
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
|
||||
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
|
||||
// (c) Copyright Ascensio System SIA 2009-2024
|
||||
//
|
||||
// This program is a free software product.
|
||||
// You can redistribute it and/or modify it under the terms
|
||||
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
|
||||
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
|
||||
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
|
||||
// any third-party rights.
|
||||
//
|
||||
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
|
||||
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
|
||||
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
|
||||
//
|
||||
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
|
||||
//
|
||||
// The interactive user interfaces in modified source and object code versions of the Program must
|
||||
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
|
||||
//
|
||||
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
|
||||
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
|
||||
// trademark law for use of our trademarks.
|
||||
//
|
||||
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
|
||||
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
|
||||
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { inject, observer } from "mobx-react";
|
||||
|
||||
import { Text } from "@docspace/shared/components/text";
|
||||
import { ComboBox } from "@docspace/shared/components/combobox";
|
||||
import { DropDownItem } from "@docspace/shared/components/drop-down-item";
|
||||
import { FileInput } from "@docspace/shared/components/file-input";
|
||||
import { imageProcessing } from "@docspace/shared/utils/common";
|
||||
import { ButtonDelete } from "@docspace/shared/components/image-editor";
|
||||
import { HelpButton } from "@docspace/shared/components/help-button";
|
||||
|
||||
import { StyledWatermark } from "./StyledComponent";
|
||||
|
||||
const scaleOptions = [
|
||||
{ key: 100, label: "100" },
|
||||
{ key: 200, label: "200" },
|
||||
{ key: 300, label: "300" },
|
||||
{ key: 400, label: "400" },
|
||||
{ key: 500, label: "500" },
|
||||
];
|
||||
|
||||
const rotateOptions = [
|
||||
{ key: 0, label: "0" },
|
||||
{ key: 30, label: "30" },
|
||||
{ key: 45, label: "45" },
|
||||
{ key: 60, label: "60" },
|
||||
{ key: 90, label: "90" },
|
||||
];
|
||||
const getInitialScale = (scale, isEdit) => {
|
||||
if (!isEdit || !scale) return scaleOptions[0];
|
||||
|
||||
return scaleOptions.find((item) => {
|
||||
return item.key === scale;
|
||||
});
|
||||
};
|
||||
|
||||
const getInitialRotate = (rotate, isEdit) => {
|
||||
if (!isEdit) return rotateOptions[0];
|
||||
|
||||
const item = rotateOptions.find((item) => {
|
||||
return item.key === rotate;
|
||||
});
|
||||
|
||||
return !item ? rotateOptions[0] : item;
|
||||
};
|
||||
|
||||
const ImageWatermark = ({
|
||||
isEdit,
|
||||
setWatermarks,
|
||||
initialWatermarksSettings,
|
||||
imageUrl,
|
||||
}) => {
|
||||
const { t } = useTranslation(["CreateEditRoomDialog", "Common"]);
|
||||
|
||||
const initialInfo = useRef(null);
|
||||
const previewRef = useRef(null);
|
||||
|
||||
if (initialInfo.current === null) {
|
||||
initialInfo.current = {
|
||||
rotate: getInitialRotate(initialWatermarksSettings?.rotate, isEdit),
|
||||
scale: getInitialScale(initialWatermarksSettings?.imageScale, isEdit),
|
||||
};
|
||||
}
|
||||
|
||||
const initialInfoRef = initialInfo.current;
|
||||
|
||||
useEffect(() => {
|
||||
const { enabled, isImage } = initialWatermarksSettings;
|
||||
|
||||
if (isEdit && enabled && isImage) {
|
||||
setWatermarks(initialWatermarksSettings);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setWatermarks({
|
||||
rotate: initialInfoRef.rotate.key,
|
||||
scale: initialInfoRef.scale.key,
|
||||
additions: 0,
|
||||
isImage: true,
|
||||
enabled: true,
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
URL.revokeObjectURL(previewRef.current);
|
||||
previewRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [selectedRotate, setRotate] = useState(initialInfoRef.rotate);
|
||||
const [selectedScale, setScale] = useState(initialInfoRef.scale);
|
||||
const [selectedImageUrl, setImageUrl] = useState(imageUrl);
|
||||
|
||||
const onInput = (file) => {
|
||||
imageProcessing(file)
|
||||
.then((f) => {
|
||||
if (f instanceof File) {
|
||||
setWatermarks({ image: f });
|
||||
|
||||
const img = new Image();
|
||||
|
||||
previewRef.current = URL.createObjectURL(f);
|
||||
img.src = previewRef.current;
|
||||
|
||||
img.onload = () => {
|
||||
setImageUrl(previewRef.current);
|
||||
};
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message === "recursion depth exceeded"
|
||||
) {
|
||||
toastr.error(t("Common:SizeImageLarge"));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onScaleChange = (item) => {
|
||||
setScale(item);
|
||||
|
||||
setWatermarks({ imageScale: item.key });
|
||||
};
|
||||
|
||||
const onRotateChange = (item) => {
|
||||
setRotate(item);
|
||||
|
||||
setWatermarks({ rotate: item.key });
|
||||
};
|
||||
|
||||
const onButtonClick = () => {
|
||||
if (previewRef.current) {
|
||||
URL.revokeObjectURL(previewRef.current);
|
||||
previewRef.current = null;
|
||||
}
|
||||
|
||||
setWatermarks({ image: null, imageUrl: null });
|
||||
setImageUrl("");
|
||||
};
|
||||
|
||||
const rotateItems = () => {
|
||||
const items = rotateOptions.map((item) => {
|
||||
return (
|
||||
<DropDownItem
|
||||
className="access-right-item"
|
||||
key={item.key}
|
||||
data-key={item.key}
|
||||
onClick={() => onRotateChange(item)}
|
||||
>
|
||||
{item.label}°
|
||||
</DropDownItem>
|
||||
);
|
||||
});
|
||||
|
||||
return <div style={{ display: "contents" }}>{items}</div>;
|
||||
};
|
||||
|
||||
const scaleItems = () => {
|
||||
const items = scaleOptions.map((item) => {
|
||||
return (
|
||||
<DropDownItem
|
||||
className="access-right-item"
|
||||
key={item.key}
|
||||
data-key={item.key}
|
||||
onClick={() => onScaleChange(item)}
|
||||
>
|
||||
{item.label}%
|
||||
</DropDownItem>
|
||||
);
|
||||
});
|
||||
|
||||
return <div style={{ display: "contents" }}>{items}</div>;
|
||||
};
|
||||
|
||||
// const onSelectFile = (fileInfo) => {
|
||||
// setWatermarks({ image: fileInfo });
|
||||
// };
|
||||
|
||||
console.log("selectedRotate", selectedRotate.key, selectedScale.key);
|
||||
return (
|
||||
<StyledWatermark
|
||||
rotate={selectedRotate.key}
|
||||
scale={selectedScale.key / 100}
|
||||
mainHeight={50}
|
||||
>
|
||||
{!selectedImageUrl && (
|
||||
<FileInput
|
||||
accept={["image/png", "image/jpeg"]}
|
||||
onInput={onInput}
|
||||
scale
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* <FilesSelectorInput
|
||||
onSelectFile={onSelectFile}
|
||||
filterParam={FilesSelectorFilterTypes.IMG}
|
||||
isSelect
|
||||
scale
|
||||
/> */}
|
||||
|
||||
{selectedImageUrl && (
|
||||
<div className="image-wrapper">
|
||||
<div>
|
||||
<div className="image-description">
|
||||
<Text fontWeight={600} className="image-watermark_text">
|
||||
{t("WatermarkPreview")}
|
||||
</Text>
|
||||
<HelpButton
|
||||
tooltipContent={
|
||||
<Text fontSize="12px">{t("WatermarkPreviewHelp")}</Text>
|
||||
}
|
||||
offsetRight={0}
|
||||
className="settings_unavailable"
|
||||
/>
|
||||
</div>
|
||||
<div className="image-watermark_wrapper">
|
||||
<img
|
||||
alt="logo"
|
||||
src={selectedImageUrl}
|
||||
className="header-logo-icon"
|
||||
/>
|
||||
</div>
|
||||
<ButtonDelete t={t} onClick={onButtonClick} />
|
||||
</div>
|
||||
|
||||
<div className="options-wrapper">
|
||||
<div>
|
||||
<Text fontWeight={600} lineHeight="20px">
|
||||
{t("Scale")}
|
||||
</Text>
|
||||
<ComboBox
|
||||
onSelect={onScaleChange}
|
||||
scaled
|
||||
scaledOptions
|
||||
advancedOptions={scaleItems()}
|
||||
options={[]}
|
||||
selectedOption={{}}
|
||||
>
|
||||
<div>{selectedScale.label}%</div>
|
||||
</ComboBox>
|
||||
</div>
|
||||
<div>
|
||||
<Text fontWeight={600} lineHeight="20px">
|
||||
{t("Rotate")}
|
||||
</Text>
|
||||
|
||||
<ComboBox
|
||||
onSelect={onRotateChange}
|
||||
scaled
|
||||
scaledOptions
|
||||
advancedOptions={rotateItems()}
|
||||
options={[]}
|
||||
selectedOption={{}}
|
||||
advancedOptionsCount={rotateOptions.length}
|
||||
fillIcon={false}
|
||||
>
|
||||
<div>{selectedRotate.label}°</div>
|
||||
</ComboBox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</StyledWatermark>
|
||||
);
|
||||
};
|
||||
|
||||
export default inject(({ createEditRoomStore }) => {
|
||||
const { setWatermarks, initialWatermarksSettings, watermarksSettings } =
|
||||
createEditRoomStore;
|
||||
|
||||
const { imageUrl } = watermarksSettings;
|
||||
|
||||
return {
|
||||
setWatermarks,
|
||||
initialWatermarksSettings,
|
||||
imageUrl,
|
||||
};
|
||||
})(observer(ImageWatermark));
|
@ -0,0 +1,90 @@
|
||||
// (c) Copyright Ascensio System SIA 2009-2024
|
||||
//
|
||||
// This program is a free software product.
|
||||
// You can redistribute it and/or modify it under the terms
|
||||
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
|
||||
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
|
||||
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
|
||||
// any third-party rights.
|
||||
//
|
||||
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
|
||||
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
|
||||
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
|
||||
//
|
||||
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
|
||||
//
|
||||
// The interactive user interfaces in modified source and object code versions of the Program must
|
||||
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
|
||||
//
|
||||
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
|
||||
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
|
||||
// trademark law for use of our trademarks.
|
||||
//
|
||||
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
|
||||
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
|
||||
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
const StyledWatermark = styled.div`
|
||||
margin-top: 16px;
|
||||
|
||||
.watermark-title {
|
||||
margin: 16px 0 8px 0;
|
||||
}
|
||||
.title-without-top {
|
||||
margin-top: 0px;
|
||||
}
|
||||
.watermark-checkbox {
|
||||
margin: 18px 0 0 0;
|
||||
}
|
||||
|
||||
.options-wrapper {
|
||||
display: grid;
|
||||
grid-template-rows: 56px 56px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 216px auto;
|
||||
gap: 16px;
|
||||
|
||||
.image-description {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: baseline;
|
||||
|
||||
.image-watermark_text {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.image-watermark_wrapper {
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
border: 1px solid #eceef1;
|
||||
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
width: 88%;
|
||||
height: 88%;
|
||||
transform: ${(props) =>
|
||||
`rotate(${props.rotate}deg) scale(${props.scale})`};
|
||||
|
||||
opacity: 0.4;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const StyledBody = styled.div`
|
||||
.types-content {
|
||||
}
|
||||
`;
|
||||
|
||||
export { StyledWatermark, StyledBody };
|
@ -0,0 +1,240 @@
|
||||
// (c) Copyright Ascensio System SIA 2009-2024
|
||||
//
|
||||
// This program is a free software product.
|
||||
// You can redistribute it and/or modify it under the terms
|
||||
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
|
||||
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
|
||||
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
|
||||
// any third-party rights.
|
||||
//
|
||||
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
|
||||
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
|
||||
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
|
||||
//
|
||||
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
|
||||
//
|
||||
// The interactive user interfaces in modified source and object code versions of the Program must
|
||||
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
|
||||
//
|
||||
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
|
||||
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
|
||||
// trademark law for use of our trademarks.
|
||||
//
|
||||
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
|
||||
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
|
||||
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { inject, observer } from "mobx-react";
|
||||
|
||||
import { TextInput } from "@docspace/shared/components/text-input";
|
||||
import { Text } from "@docspace/shared/components/text";
|
||||
import { ComboBox } from "@docspace/shared/components/combobox";
|
||||
import { WatermarkAdditions } from "@docspace/shared/enums";
|
||||
|
||||
import { StyledWatermark } from "./StyledComponent";
|
||||
import { Tabs, TabsTypes } from "@docspace/shared/components/tabs";
|
||||
|
||||
const tabsOptions = (t) => [
|
||||
{
|
||||
id: "UserName",
|
||||
name: t("UserName"),
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
id: "UserEmail",
|
||||
name: t("UserEmail"),
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
id: "UserIpAdress",
|
||||
name: t("UserIPAddress"),
|
||||
index: 2,
|
||||
},
|
||||
{
|
||||
id: "CurrentDate",
|
||||
name: t("Common:CurrentDate"),
|
||||
index: 3,
|
||||
},
|
||||
{
|
||||
id: "RoomName",
|
||||
name: t("Common:RoomName"),
|
||||
index: 4,
|
||||
},
|
||||
];
|
||||
|
||||
const getInitialState = (initialTab) => {
|
||||
const state = {
|
||||
UserName: false,
|
||||
UserEmail: false,
|
||||
UserIpAdress: false,
|
||||
CurrentDate: false,
|
||||
RoomName: false,
|
||||
};
|
||||
|
||||
initialTab.map((item) => {
|
||||
state[item.id] = true;
|
||||
});
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
const getInitialText = (text, isEdit) => {
|
||||
return isEdit && text ? text : "";
|
||||
};
|
||||
|
||||
const getInitialTabs = (additions, isEdit, t) => {
|
||||
const dataTabs = tabsOptions(t);
|
||||
|
||||
if (!isEdit || !additions) return [dataTabs[0]];
|
||||
|
||||
return dataTabs.filter((item) => additions & WatermarkAdditions[item.id]);
|
||||
};
|
||||
|
||||
const rotateOptions = (t) => [
|
||||
{ key: -45, label: t("Diagonal") },
|
||||
{ key: 0, label: t("Horizontal") },
|
||||
];
|
||||
|
||||
const getInitialRotate = (rotate, isEdit, t) => {
|
||||
const dataRotate = rotateOptions(t);
|
||||
|
||||
if (!isEdit) return dataRotate[0];
|
||||
|
||||
const item = dataRotate.find((item) => {
|
||||
return item.key === rotate;
|
||||
});
|
||||
|
||||
return !item ? dataRotate[0] : item;
|
||||
};
|
||||
|
||||
const ViewerInfoWatermark = ({
|
||||
isEdit,
|
||||
|
||||
setWatermarks,
|
||||
|
||||
initialWatermarksSettings,
|
||||
}) => {
|
||||
const { t } = useTranslation(["CreateEditRoomDialog", "Common"]);
|
||||
|
||||
const elements = useRef(null);
|
||||
const initialInfo = useRef(null);
|
||||
|
||||
if (initialInfo.current === null) {
|
||||
initialInfo.current = {
|
||||
dataRotate: rotateOptions(t),
|
||||
dataTabs: tabsOptions(t),
|
||||
tabs: getInitialTabs(initialWatermarksSettings?.additions, isEdit, t),
|
||||
rotate: getInitialRotate(initialWatermarksSettings?.rotate, isEdit, t),
|
||||
text: getInitialText(initialWatermarksSettings?.text, isEdit),
|
||||
};
|
||||
|
||||
elements.current = getInitialState(initialInfo.current.tabs);
|
||||
}
|
||||
|
||||
const initialInfoRef = initialInfo.current;
|
||||
|
||||
useEffect(() => {
|
||||
const { enabled, isImage } = initialWatermarksSettings;
|
||||
|
||||
if (isEdit && enabled && !isImage) {
|
||||
setWatermarks(initialWatermarksSettings);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setWatermarks({
|
||||
rotate: initialInfoRef.rotate.key,
|
||||
additions: WatermarkAdditions.UserName,
|
||||
isImage: false,
|
||||
enabled: true,
|
||||
image: "",
|
||||
imageWidth: 0,
|
||||
imageHeight: 0,
|
||||
imageScale: 0,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const [selectedPosition, setSelectedPosition] = useState(
|
||||
initialInfoRef.rotate,
|
||||
);
|
||||
const [textValue, setTextValue] = useState(initialInfoRef.text);
|
||||
|
||||
const onSelect = (item) => {
|
||||
let elementsData = elements.current;
|
||||
let flagsCount = 0;
|
||||
|
||||
const key = item.id;
|
||||
|
||||
elementsData[key] = !elementsData[item.id];
|
||||
|
||||
for (const key in elementsData) {
|
||||
const value = elementsData[key];
|
||||
|
||||
if (value) {
|
||||
flagsCount += WatermarkAdditions[key];
|
||||
}
|
||||
}
|
||||
|
||||
setWatermarks({ additions: flagsCount });
|
||||
};
|
||||
|
||||
const onPositionChange = (item) => {
|
||||
setSelectedPosition(item);
|
||||
|
||||
setWatermarks({ rotate: item.key });
|
||||
};
|
||||
|
||||
const onTextChange = (e) => {
|
||||
const { value } = e.target;
|
||||
setTextValue(value);
|
||||
|
||||
setWatermarks({ text: value });
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWatermark>
|
||||
<Text className="watermark-title" fontWeight={600} lineHeight="20px">
|
||||
{t("AddWatermarkElements")}
|
||||
</Text>
|
||||
|
||||
<Tabs
|
||||
items={initialInfoRef.dataTabs}
|
||||
selectedItems={initialInfoRef.tabs.map((item) => item.index)}
|
||||
onSelect={onSelect}
|
||||
type={TabsTypes.Secondary}
|
||||
multiple
|
||||
/>
|
||||
|
||||
<Text
|
||||
className="watermark-title title-without-top"
|
||||
fontWeight={600}
|
||||
lineHeight="20px"
|
||||
>
|
||||
{t("AddStaticText")}
|
||||
</Text>
|
||||
<TextInput scale value={textValue} tabIndex={1} onChange={onTextChange} />
|
||||
|
||||
<Text className="watermark-title" fontWeight={600} lineHeight="20px">
|
||||
{t("Position")}
|
||||
</Text>
|
||||
<ComboBox
|
||||
selectedOption={selectedPosition}
|
||||
options={initialInfoRef.dataRotate}
|
||||
onSelect={onPositionChange}
|
||||
scaled
|
||||
scaledOptions
|
||||
/>
|
||||
</StyledWatermark>
|
||||
);
|
||||
};
|
||||
|
||||
export default inject(({ createEditRoomStore }) => {
|
||||
const { setWatermarks, initialWatermarksSettings } = createEditRoomStore;
|
||||
|
||||
return {
|
||||
setWatermarks,
|
||||
initialWatermarksSettings,
|
||||
};
|
||||
})(observer(ViewerInfoWatermark));
|
@ -0,0 +1,102 @@
|
||||
// (c) Copyright Ascensio System SIA 2009-2024
|
||||
//
|
||||
// This program is a free software product.
|
||||
// You can redistribute it and/or modify it under the terms
|
||||
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
|
||||
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
|
||||
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
|
||||
// any third-party rights.
|
||||
//
|
||||
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
|
||||
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
|
||||
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
|
||||
//
|
||||
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
|
||||
//
|
||||
// The interactive user interfaces in modified source and object code versions of the Program must
|
||||
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
|
||||
//
|
||||
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
|
||||
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
|
||||
// trademark law for use of our trademarks.
|
||||
//
|
||||
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
|
||||
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
|
||||
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
// (c) Copyright Ascensio System SIA 2009-2024
|
||||
//
|
||||
// This program is a free software product.
|
||||
// You can redistribute it and/or modify it under the terms
|
||||
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
|
||||
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
|
||||
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
|
||||
// any third-party rights.
|
||||
//
|
||||
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
|
||||
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
|
||||
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
|
||||
//
|
||||
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
|
||||
//
|
||||
// The interactive user interfaces in modified source and object code versions of the Program must
|
||||
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
|
||||
//
|
||||
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
|
||||
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
|
||||
// trademark law for use of our trademarks.
|
||||
//
|
||||
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
|
||||
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
|
||||
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import { inject, observer } from "mobx-react";
|
||||
|
||||
import Watermarks from "./index";
|
||||
|
||||
const WatermarkBlock = ({
|
||||
BlockComponent,
|
||||
setWatermarks,
|
||||
isEdit = false,
|
||||
isWatermarks = false,
|
||||
resetWatermarks,
|
||||
t,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
return () => resetWatermarks();
|
||||
}, []);
|
||||
|
||||
const [watermarksChecked, setWatermarksChecked] = useState(
|
||||
isWatermarks && isEdit,
|
||||
);
|
||||
|
||||
const onChangeAddWatermarksToDocuments = () => {
|
||||
setWatermarksChecked(!watermarksChecked);
|
||||
|
||||
setWatermarks({ enabled: !watermarksChecked });
|
||||
};
|
||||
|
||||
return (
|
||||
<BlockComponent
|
||||
headerText={t("AddWatermarksToDocuments")}
|
||||
bodyText={t("AddWatermarksToDocumentsDescription")}
|
||||
onChange={onChangeAddWatermarksToDocuments}
|
||||
isDisabled={false}
|
||||
isChecked={watermarksChecked}
|
||||
>
|
||||
<Watermarks isEdit={isEdit} />
|
||||
</BlockComponent>
|
||||
);
|
||||
};
|
||||
|
||||
export default inject(({ createEditRoomStore }) => {
|
||||
const { setWatermarks, initialWatermarksSettings, resetWatermarks } =
|
||||
createEditRoomStore;
|
||||
|
||||
return {
|
||||
setWatermarks,
|
||||
isWatermarks: initialWatermarksSettings?.enabled,
|
||||
resetWatermarks,
|
||||
};
|
||||
})(observer(WatermarkBlock));
|
@ -0,0 +1,102 @@
|
||||
// (c) Copyright Ascensio System SIA 2009-2024
|
||||
//
|
||||
// This program is a free software product.
|
||||
// You can redistribute it and/or modify it under the terms
|
||||
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
|
||||
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
|
||||
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
|
||||
// any third-party rights.
|
||||
//
|
||||
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
|
||||
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
|
||||
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
|
||||
//
|
||||
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
|
||||
//
|
||||
// The interactive user interfaces in modified source and object code versions of the Program must
|
||||
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
|
||||
//
|
||||
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
|
||||
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
|
||||
// trademark law for use of our trademarks.
|
||||
//
|
||||
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
|
||||
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
|
||||
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { inject, observer } from "mobx-react";
|
||||
|
||||
import { RadioButtonGroup } from "@docspace/shared/components/radio-button-group";
|
||||
|
||||
import ViewerInfoWatermark from "./ViewerInfo";
|
||||
import { StyledBody } from "./StyledComponent";
|
||||
import ImageWatermark from "./ImageWatermark";
|
||||
|
||||
const imageWatermark = "image",
|
||||
viewerInfoWatermark = "viewerInfo";
|
||||
|
||||
const options = (t) => [
|
||||
{
|
||||
label: t("ViewerInfo"),
|
||||
value: viewerInfoWatermark,
|
||||
},
|
||||
{
|
||||
label: t("Common:Image"),
|
||||
value: imageWatermark,
|
||||
},
|
||||
];
|
||||
|
||||
const getOptionType = (additions, isEdit) => {
|
||||
if (isEdit) {
|
||||
return additions === 0 ? imageWatermark : viewerInfoWatermark;
|
||||
}
|
||||
|
||||
return viewerInfoWatermark;
|
||||
};
|
||||
const Watermarks = ({ isEdit, setWatermarks, initialWatermarksSettings }) => {
|
||||
const { t } = useTranslation(["CreateEditRoomDialog", "Common"]);
|
||||
const [type, setType] = useState(
|
||||
getOptionType(initialWatermarksSettings?.additions, isEdit),
|
||||
);
|
||||
|
||||
const onSelectType = (e) => {
|
||||
const { value } = e.target;
|
||||
|
||||
setType(value);
|
||||
setWatermarks({
|
||||
isImage: type === imageWatermark,
|
||||
});
|
||||
};
|
||||
|
||||
const typeOptions = options(t);
|
||||
|
||||
return (
|
||||
<StyledBody>
|
||||
<RadioButtonGroup
|
||||
name="watermarks-radiobutton"
|
||||
fontSize="13px"
|
||||
fontWeight="400"
|
||||
spacing="8px"
|
||||
options={typeOptions}
|
||||
selected={type}
|
||||
onClick={onSelectType}
|
||||
/>
|
||||
|
||||
{type === imageWatermark ? (
|
||||
<ImageWatermark isEdit={isEdit} />
|
||||
) : (
|
||||
<ViewerInfoWatermark isEdit={isEdit} />
|
||||
)}
|
||||
</StyledBody>
|
||||
);
|
||||
};
|
||||
|
||||
export default inject(({ createEditRoomStore }) => {
|
||||
const { setWatermarks, initialWatermarksSettings } = createEditRoomStore;
|
||||
return {
|
||||
setWatermarks,
|
||||
initialWatermarksSettings,
|
||||
};
|
||||
})(observer(Watermarks));
|
@ -29,6 +29,7 @@ import { ModalDialog } from "@docspace/shared/components/modal-dialog";
|
||||
import { withTranslation } from "react-i18next";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { Button } from "@docspace/shared/components/button";
|
||||
import { RoomsType } from "@docspace/shared/enums";
|
||||
|
||||
const CreateRoomConfirmDialog = ({
|
||||
t,
|
||||
@ -46,6 +47,10 @@ const CreateRoomConfirmDialog = ({
|
||||
|
||||
const onClose = () => setVisible(false);
|
||||
|
||||
const bodyText = RoomsType.VirtualDataRoom
|
||||
? t("CreateEditRoomDialog:CreateRoomWatermarksConfirmation")
|
||||
: t("CreateEditRoomDialog:CreateRoomConfirmation");
|
||||
|
||||
return (
|
||||
<ModalDialog
|
||||
visible={visible || confirmDialogIsLoading}
|
||||
@ -54,9 +59,7 @@ const CreateRoomConfirmDialog = ({
|
||||
zIndex={310}
|
||||
>
|
||||
<ModalDialog.Header>{t("Common:Warning")}</ModalDialog.Header>
|
||||
<ModalDialog.Body>
|
||||
{t("CreateEditRoomDialog:CreateRoomConfirmation")}
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Body>{bodyText}</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
<Button
|
||||
label={t("Common:ContinueButton")}
|
||||
|
@ -25,12 +25,15 @@
|
||||
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
|
||||
import { makeAutoObservable } from "mobx";
|
||||
import isEqual from "lodash/isEqual";
|
||||
|
||||
import { toastr } from "@docspace/shared/components/toast";
|
||||
import { isDesktop } from "@docspace/shared/utils";
|
||||
import FilesFilter from "@docspace/shared/api/files/filter";
|
||||
import { getCategoryUrl } from "SRC_DIR/helpers/utils";
|
||||
import { CategoryType } from "SRC_DIR/helpers/constants";
|
||||
import { RoomsType } from "@docspace/shared/enums";
|
||||
import { setWatermarkSettings } from "@docspace/shared/api/rooms";
|
||||
|
||||
class CreateEditRoomStore {
|
||||
roomParams = null;
|
||||
@ -46,6 +49,9 @@ class CreateEditRoomStore {
|
||||
settingsStore = null;
|
||||
infoPanelStore = null;
|
||||
currentQuotaStore = null;
|
||||
watermarksSettings = {};
|
||||
initialWatermarksSettings = {};
|
||||
isImageType = false;
|
||||
|
||||
constructor(
|
||||
filesStore,
|
||||
@ -91,6 +97,113 @@ class CreateEditRoomStore {
|
||||
this.onClose = onClose;
|
||||
};
|
||||
|
||||
setInitialWatermarks = (watermarksSettings) => {
|
||||
this.initialWatermarksSettings = !watermarksSettings
|
||||
? { enabled: false }
|
||||
: watermarksSettings;
|
||||
|
||||
this.initialWatermarksSettings.isImage =
|
||||
!!this.initialWatermarksSettings.imageUrl;
|
||||
|
||||
this.setWatermarks(this.initialWatermarksSettings);
|
||||
};
|
||||
|
||||
setWatermarks = (object) => {
|
||||
for (const [key, value] of Object.entries(object)) {
|
||||
this.watermarksSettings[key] = value;
|
||||
}
|
||||
};
|
||||
|
||||
resetWatermarks = () => {
|
||||
this.watermarksSettings = {};
|
||||
this.initialWatermarksSettings = {};
|
||||
};
|
||||
|
||||
isEqualWatermarkChanges = () => {
|
||||
return isEqual(this.watermarksSettings, this.initialWatermarksSettings);
|
||||
};
|
||||
|
||||
isNotWatermarkSet = () => {
|
||||
if (
|
||||
this.watermarksSettings.isImage &&
|
||||
!this.watermarksSettings.image &&
|
||||
!this.watermarksSettings.imageUrl
|
||||
)
|
||||
return true;
|
||||
|
||||
if (
|
||||
!this.watermarksSettings.isImage &&
|
||||
this.watermarksSettings.additions === 0
|
||||
)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
getWatermarkRequest = async (room) => {
|
||||
if (!this.watermarksSettings.isImage) {
|
||||
return setWatermarkSettings(room.id, {
|
||||
enabled: this.watermarksSettings.enabled,
|
||||
rotate: this.watermarksSettings.rotate,
|
||||
text: this.watermarksSettings.text,
|
||||
additions: this.watermarksSettings.additions,
|
||||
});
|
||||
}
|
||||
|
||||
const watermarkImage = this.watermarksSettings.image;
|
||||
const watermarksSettings = this.watermarksSettings;
|
||||
|
||||
const getMeta = (url, onSetInfo) => {
|
||||
//url for this.watermarksSettings.image.viewUrl
|
||||
const img = new Image();
|
||||
const imgUrl = url ?? URL.createObjectURL(watermarkImage);
|
||||
img.src = imgUrl;
|
||||
|
||||
img.onload = () => {
|
||||
URL.revokeObjectURL(imgUrl);
|
||||
onSetInfo(null, img);
|
||||
};
|
||||
|
||||
img.onerror = (err) => onSetInfo(err);
|
||||
};
|
||||
|
||||
if (!watermarkImage && this.watermarksSettings.imageUrl) {
|
||||
return setWatermarkSettings(room.id, {
|
||||
enabled: watermarksSettings.enabled,
|
||||
imageScale: watermarksSettings.imageScale,
|
||||
rotate: watermarksSettings.rotate,
|
||||
imageUrl: watermarksSettings.imageUrl,
|
||||
// imageId: watermarksSettings.image.id,
|
||||
imageWidth: watermarksSettings.imageWidth,
|
||||
imageHeight: watermarksSettings.imageHeight,
|
||||
});
|
||||
}
|
||||
|
||||
const { uploadRoomLogo } = this.filesStore;
|
||||
|
||||
const uploadWatermarkData = new FormData();
|
||||
uploadWatermarkData.append(0, watermarkImage);
|
||||
|
||||
const response = await uploadRoomLogo(uploadWatermarkData);
|
||||
|
||||
getMeta(null, (err, img) => {
|
||||
if (err) {
|
||||
toastr.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
return setWatermarkSettings(room.id, {
|
||||
enabled: watermarksSettings.enabled,
|
||||
imageScale: watermarksSettings.imageScale,
|
||||
rotate: watermarksSettings.rotate,
|
||||
imageUrl: response.data,
|
||||
// imageId: watermarksSettings.image.id,
|
||||
imageWidth: img.naturalWidth,
|
||||
imageHeight: img.naturalHeight,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
onCreateRoom = async (withConfirm = false, t) => {
|
||||
const roomParams = this.roomParams;
|
||||
|
||||
@ -158,10 +271,17 @@ class CreateEditRoomStore {
|
||||
|
||||
const actions = [];
|
||||
|
||||
const requests = [];
|
||||
|
||||
if (this.watermarksSettings.enabled && !this.isNotWatermarkSet()) {
|
||||
requests.push(this.getWatermarkRequest(room));
|
||||
}
|
||||
|
||||
// delete thirdparty account if not needed
|
||||
if (!isThirdparty && storageFolderId)
|
||||
await deleteThirdParty(thirdpartyAccount.providerId);
|
||||
requests.push(deleteThirdParty(thirdpartyAccount.providerId));
|
||||
|
||||
await Promise.all(requests);
|
||||
// create new tags
|
||||
for (let i = 0; i < createTagsData.length; i++) {
|
||||
actions.push(createTag(createTagsData[i]));
|
||||
@ -176,6 +296,7 @@ class CreateEditRoomStore {
|
||||
await uploadRoomLogo(uploadLogoData).then(async (response) => {
|
||||
const url = URL.createObjectURL(roomParams.icon.uploadedFile);
|
||||
const img = new Image();
|
||||
|
||||
img.onload = async () => {
|
||||
const { x, y, zoom } = roomParams.icon;
|
||||
try {
|
||||
|
117
packages/common/components/Watermarks/index.js
Normal file
117
packages/common/components/Watermarks/index.js
Normal file
@ -0,0 +1,117 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
const Watermark = ({
|
||||
text,
|
||||
rotate,
|
||||
image,
|
||||
color,
|
||||
|
||||
isSemitransparent,
|
||||
|
||||
children,
|
||||
}) => {
|
||||
const setCtxText = (ctx) => {
|
||||
const getColor = () => {
|
||||
if (color)
|
||||
return Array.isArray(color)
|
||||
? `rgb(${color[0]}, ${color[1]}, ${color[2]}, 1)`
|
||||
: color;
|
||||
|
||||
if (isSemitransparent) return "rgba(223, 226, 227, 1)";
|
||||
|
||||
return "rgba(208, 213, 218, 1)";
|
||||
};
|
||||
|
||||
ctx.fillStyle = getColor();
|
||||
ctx.textAlign = "center";
|
||||
ctx.font = `${13}px Arial`;
|
||||
};
|
||||
|
||||
const setCtxCenteredText = (imgContent, ctx) => {
|
||||
ctx.translate(imgContent.width / 2, imgContent.height / 2);
|
||||
};
|
||||
|
||||
const setCtxRotate = (ctx) => {
|
||||
const angle = (Math.PI / 180) * Number(rotate);
|
||||
ctx.rotate(angle);
|
||||
};
|
||||
|
||||
const setCtxTextWrap = (ctx, canvas) => {
|
||||
let line = "",
|
||||
marginTop = 0,
|
||||
marginLeft = 0,
|
||||
lineHeight = 15;
|
||||
|
||||
for (var n = 0; n < text.length; n++) {
|
||||
let testLine = line + text[n];
|
||||
let testWidth = ctx.measureText(testLine).width;
|
||||
|
||||
const percentWidth = ((canvas.width - testWidth) * 100) / canvas.width;
|
||||
|
||||
if (
|
||||
(percentWidth < 32 && text[n] === " ") ||
|
||||
testWidth > canvas.width - 4
|
||||
) {
|
||||
ctx.fillText(line, marginLeft, marginTop);
|
||||
line = text[n];
|
||||
marginTop += lineHeight;
|
||||
} else {
|
||||
line = testLine;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.fillText(line, marginLeft, marginTop);
|
||||
};
|
||||
|
||||
const getContent = (canvas, ctx, imgContent, imgWidth, imgHeight) => {
|
||||
setCtxText(ctx);
|
||||
|
||||
ctx.drawImage(imgContent, 0, 0, imgWidth, imgHeight);
|
||||
|
||||
setCtxCenteredText(imgContent, ctx);
|
||||
setCtxRotate(ctx);
|
||||
setCtxTextWrap(ctx, canvas);
|
||||
};
|
||||
|
||||
const drawCanvas = (drawContent, ctx, canvas) => {
|
||||
canvas.width = drawContent.naturalWidth;
|
||||
canvas.height = drawContent.naturalHeight;
|
||||
|
||||
getContent(
|
||||
canvas,
|
||||
ctx,
|
||||
drawContent || "",
|
||||
drawContent.naturalWidth,
|
||||
drawContent.naturalHeight
|
||||
);
|
||||
};
|
||||
|
||||
const renderWatermark = () => {
|
||||
const canvas = document.querySelector("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
drawCanvas(img, ctx, canvas);
|
||||
};
|
||||
img.onerror = () => {
|
||||
drawCanvas(text);
|
||||
};
|
||||
img.crossOrigin = "anonymous";
|
||||
img.referrerPolicy = "no-referrer";
|
||||
|
||||
img.src = image;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
renderWatermark();
|
||||
}, [text, rotate, isSemitransparent]);
|
||||
|
||||
return (
|
||||
<canvas>
|
||||
<div>{children}</div>
|
||||
</canvas>
|
||||
);
|
||||
};
|
||||
export default Watermark;
|
@ -506,3 +506,39 @@ export function getExportRoomIndexProgress() {
|
||||
url: `files/rooms/indexexport`,
|
||||
}) as Promise<TExportRoomIndexTask>;
|
||||
}
|
||||
|
||||
export function setWatermarkSettings(
|
||||
roomId: number | string,
|
||||
data: {
|
||||
enabled: boolean;
|
||||
rotate: number;
|
||||
text: string;
|
||||
additions: number;
|
||||
imageScale: number;
|
||||
imageUrl: string;
|
||||
imageWidth: string;
|
||||
imageHeight: string;
|
||||
},
|
||||
) {
|
||||
const options = {
|
||||
method: "put",
|
||||
url: `files/rooms/${roomId}/watermark`,
|
||||
data,
|
||||
};
|
||||
|
||||
return request(options);
|
||||
}
|
||||
|
||||
export function getWatermarkSettings(roomId: number | string) {
|
||||
return request({
|
||||
method: "get",
|
||||
url: `files/rooms/${roomId}/watermark`,
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteWatermarkSettings(roomId: number | string) {
|
||||
return request({
|
||||
method: "delete",
|
||||
url: `files/rooms/${roomId}/watermark`,
|
||||
});
|
||||
}
|
||||
|
123
packages/shared/components/image-editor/ButtonDelete/index.tsx
Normal file
123
packages/shared/components/image-editor/ButtonDelete/index.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
// (c) Copyright Ascensio System SIA 2009-2024
|
||||
//
|
||||
// This program is a free software product.
|
||||
// You can redistribute it and/or modify it under the terms
|
||||
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
|
||||
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
|
||||
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
|
||||
// any third-party rights.
|
||||
//
|
||||
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
|
||||
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
|
||||
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
|
||||
//
|
||||
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
|
||||
//
|
||||
// The interactive user interfaces in modified source and object code versions of the Program must
|
||||
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
|
||||
//
|
||||
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
|
||||
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
|
||||
// trademark law for use of our trademarks.
|
||||
//
|
||||
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
|
||||
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
|
||||
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
// (c) Copyright Ascensio System SIA 2009-2024
|
||||
//
|
||||
// This program is a free software product.
|
||||
// You can redistribute it and/or modify it under the terms
|
||||
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
|
||||
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
|
||||
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
|
||||
// any third-party rights.
|
||||
//
|
||||
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
|
||||
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
|
||||
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
|
||||
//
|
||||
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
|
||||
//
|
||||
// The interactive user interfaces in modified source and object code versions of the Program must
|
||||
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
|
||||
//
|
||||
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
|
||||
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
|
||||
// trademark law for use of our trademarks.
|
||||
//
|
||||
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
|
||||
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
|
||||
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
|
||||
import { ReactSVG } from "react-svg";
|
||||
import styled from "styled-components";
|
||||
|
||||
import TrashReactSvgUrl from "PUBLIC_DIR/images/trash.react.svg?url";
|
||||
import { TTranslation } from "../../../types";
|
||||
|
||||
const StyledButton = styled.div`
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 6px 0;
|
||||
background: ${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton.background};
|
||||
border: 1px solid
|
||||
${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton.borderColor};
|
||||
border-radius: 3px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
transition: all 0.2s ease;
|
||||
&:hover {
|
||||
background: ${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton
|
||||
.hoverBackground};
|
||||
border: 1px solid
|
||||
${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton
|
||||
.hoverBorderColor};
|
||||
}
|
||||
|
||||
&-text {
|
||||
user-select: none;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
color: ${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton.color};
|
||||
}
|
||||
|
||||
svg {
|
||||
path {
|
||||
fill: ${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton.iconColor};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const ButtonDelete = ({
|
||||
onClick,
|
||||
t,
|
||||
}: {
|
||||
onClick: (e: React.MouseEvent) => void;
|
||||
t: TTranslation;
|
||||
}) => {
|
||||
return (
|
||||
<StyledButton
|
||||
className="icon_cropper-delete_button"
|
||||
onClick={onClick}
|
||||
title={t("Common:Delete")}
|
||||
>
|
||||
<ReactSVG src={TrashReactSvgUrl} />
|
||||
<div className="icon_cropper-delete_button-text">
|
||||
{t("Common:Delete")}
|
||||
</div>
|
||||
</StyledButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default ButtonDelete;
|
@ -25,15 +25,11 @@
|
||||
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import resizeImage from "resize-image";
|
||||
import { imageProcessing } from "@docspace/shared/utils/common";
|
||||
|
||||
import DropzoneComponent from "../../dropzone";
|
||||
import { toastr } from "../../toast";
|
||||
|
||||
const ONE_MEGABYTE = 1024 * 1024;
|
||||
const COMPRESSION_RATIO = 2;
|
||||
const NO_COMPRESSION_RATIO = 1;
|
||||
|
||||
const Dropzone = ({
|
||||
t,
|
||||
setUploadedFile,
|
||||
@ -57,70 +53,12 @@ const Dropzone = ({
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function resizeRecursiveAsync(
|
||||
img: { width: number; height: number },
|
||||
canvas: HTMLCanvasElement,
|
||||
compressionRatio = COMPRESSION_RATIO,
|
||||
depth = 0,
|
||||
): Promise<unknown> {
|
||||
const data = resizeImage.resize(
|
||||
// @ts-expect-error canvas
|
||||
canvas,
|
||||
img.width / compressionRatio,
|
||||
img.height / compressionRatio,
|
||||
resizeImage.JPEG,
|
||||
);
|
||||
|
||||
const file = await fetch(data)
|
||||
.then((res) => res.blob())
|
||||
.then((blob) => {
|
||||
const f = new File([blob], "File name", {
|
||||
type: "image/jpg",
|
||||
});
|
||||
return f;
|
||||
});
|
||||
|
||||
// const stepMessage = `Step ${depth + 1}`;
|
||||
// const sizeMessage = `size = ${file.size} bytes`;
|
||||
// const compressionRatioMessage = `compressionRatio = ${compressionRatio}`;
|
||||
|
||||
// console.log(`${stepMessage} ${sizeMessage} ${compressionRatioMessage}`);
|
||||
|
||||
if (file.size < maxImageSize) {
|
||||
return file;
|
||||
}
|
||||
|
||||
if (depth > 5) {
|
||||
// console.log("start");
|
||||
throw new Error("recursion depth exceeded");
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
return resolve(file);
|
||||
}).then(() =>
|
||||
resizeRecursiveAsync(img, canvas, compressionRatio + 1, depth + 1),
|
||||
);
|
||||
}
|
||||
|
||||
const onDrop = async ([file]: File[]) => {
|
||||
timer.current = setTimeout(() => {
|
||||
setLoadingFile(true);
|
||||
}, 50);
|
||||
try {
|
||||
const imageBitMap = await createImageBitmap(file);
|
||||
|
||||
const width = imageBitMap.width;
|
||||
const height = imageBitMap.height;
|
||||
|
||||
// @ts-expect-error imageBitMap
|
||||
const canvas = resizeImage.resize2Canvas(imageBitMap, width, height);
|
||||
|
||||
resizeRecursiveAsync(
|
||||
{ width, height },
|
||||
canvas,
|
||||
file.size > maxImageSize ? COMPRESSION_RATIO : NO_COMPRESSION_RATIO,
|
||||
)
|
||||
imageProcessing(file)
|
||||
.then((f) => {
|
||||
if (mount.current) {
|
||||
if (f instanceof File) setUploadedFile(f);
|
||||
|
@ -32,12 +32,12 @@ import AvatarEditor, { Position } from "react-avatar-editor";
|
||||
import ZoomMinusReactSvgUrl from "PUBLIC_DIR/images/zoom-minus.react.svg?url";
|
||||
import ZoomPlusReactSvgUrl from "PUBLIC_DIR/images/zoom-plus.react.svg?url";
|
||||
import IconCropperGridSvgUrl from "PUBLIC_DIR/images/icon-cropper-grid.svg?url";
|
||||
import TrashReactSvgUrl from "PUBLIC_DIR/images/trash.react.svg?url";
|
||||
|
||||
import { Slider } from "../../slider";
|
||||
import { IconButton } from "../../icon-button";
|
||||
import { StyledImageCropper } from "../ImageEditor.styled";
|
||||
import { ImageCropperProps } from "../ImageEditor.types";
|
||||
import ButtonDelete from "../ButtonDelete";
|
||||
|
||||
const ImageCropper = ({
|
||||
t,
|
||||
@ -132,16 +132,8 @@ const ImageCropper = ({
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="icon_cropper-delete_button"
|
||||
onClick={handleDeleteImage}
|
||||
title={t("Common:Delete")}
|
||||
>
|
||||
<ReactSVG src={TrashReactSvgUrl} />
|
||||
<div className="icon_cropper-delete_button-text">
|
||||
{t("Common:Delete")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ButtonDelete onClick={handleDeleteImage} t={t} />
|
||||
|
||||
{typeof uploadedFile !== "string" &&
|
||||
uploadedFile?.name &&
|
||||
|
@ -63,50 +63,6 @@ const StyledImageCropper = styled.div<{ disableImageRescaling?: boolean }>`
|
||||
`};
|
||||
}
|
||||
|
||||
.icon_cropper-delete_button {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 6px 0;
|
||||
background: ${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton.background};
|
||||
border: 1px solid
|
||||
${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton.borderColor};
|
||||
border-radius: 3px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
transition: all 0.2s ease;
|
||||
&:hover {
|
||||
background: ${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton
|
||||
.hoverBackground};
|
||||
border: 1px solid
|
||||
${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton
|
||||
.hoverBorderColor};
|
||||
}
|
||||
|
||||
&-text {
|
||||
user-select: none;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
color: ${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton.color};
|
||||
}
|
||||
|
||||
svg {
|
||||
path {
|
||||
fill: ${(props) =>
|
||||
props.theme.createEditRoomDialog.iconCropper.deleteButton.iconColor};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon_cropper-zoom-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -27,6 +27,7 @@
|
||||
import React from "react";
|
||||
import Dropzone from "./Dropzone";
|
||||
import ImageCropper from "./ImageCropper";
|
||||
import ButtonDelete from "./ButtonDelete";
|
||||
import { ImageEditorProps } from "./ImageEditor.types";
|
||||
import AvatarPreview from "./AvatarPreview";
|
||||
|
||||
@ -77,4 +78,4 @@ const ImageEditor = ({
|
||||
);
|
||||
};
|
||||
|
||||
export { ImageEditor, AvatarPreview };
|
||||
export { ImageEditor, AvatarPreview, Dropzone, ButtonDelete };
|
||||
|
@ -24,7 +24,5 @@
|
||||
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
|
||||
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
||||
|
||||
export const INDEX_NOT_FOUND = -1;
|
||||
|
||||
export const OFFSET_RIGHT = 48;
|
||||
export const OFFSET_LEFT = 48;
|
||||
|
@ -31,9 +31,14 @@ import { TabsTypes } from "./Tabs.enums";
|
||||
|
||||
export const StyledTabs = styled.div<{
|
||||
stickyTop?: string;
|
||||
multiple: boolean;
|
||||
}>`
|
||||
${(props) =>
|
||||
props.multiple &&
|
||||
css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`};
|
||||
|
||||
.sticky {
|
||||
height: 33px;
|
||||
@ -128,6 +133,7 @@ export const ScrollbarTabs = styled(Scrollbar)<{
|
||||
|
||||
export const TabList = styled.div<{
|
||||
$type?: TabsTypes;
|
||||
multiple: boolean;
|
||||
}>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -136,6 +142,13 @@ export const TabList = styled.div<{
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
|
||||
${(props) =>
|
||||
props.multiple &&
|
||||
css`
|
||||
flex-wrap: wrap;
|
||||
height: fit-content;
|
||||
`};
|
||||
|
||||
gap: ${(props) => (props.$type === TabsTypes.Primary ? "20px" : "8px")};
|
||||
|
||||
border-bottom: ${(props) =>
|
||||
|
@ -39,26 +39,26 @@ import {
|
||||
} from "./Tabs.styled";
|
||||
import { TabsProps, TTabItem } from "./Tabs.types";
|
||||
import { TabsTypes } from "./Tabs.enums";
|
||||
import { OFFSET_RIGHT, OFFSET_LEFT, INDEX_NOT_FOUND } from "./Tabs.constants";
|
||||
import { OFFSET_RIGHT, OFFSET_LEFT } from "./Tabs.constants";
|
||||
|
||||
const Tabs = (props: TabsProps) => {
|
||||
const {
|
||||
items,
|
||||
selectedItemId,
|
||||
selectedItems = [],
|
||||
type = TabsTypes.Primary,
|
||||
stickyTop,
|
||||
onSelect,
|
||||
multiple = false,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
let selectedItemIndex = items.findIndex((item) => item.id === selectedItemId);
|
||||
if (selectedItemIndex === INDEX_NOT_FOUND) {
|
||||
selectedItemIndex = 0;
|
||||
}
|
||||
const selectedItemIndex = !selectedItemId
|
||||
? 0
|
||||
: items.findIndex((item) => item.id === selectedItemId);
|
||||
|
||||
const [currentItem, setCurrentItem] = useState<TTabItem>(
|
||||
items[selectedItemIndex],
|
||||
);
|
||||
const [currentItem, setCurrentItem] = useState(selectedItemIndex);
|
||||
const [multipleItems, setMultipleItems] = useState(selectedItems);
|
||||
|
||||
const tabsRef = useRef<HTMLDivElement>(null);
|
||||
const scrollRef = useRef<ScrollbarType>(null);
|
||||
@ -67,8 +67,8 @@ const Tabs = (props: TabsProps) => {
|
||||
const isViewLastTab = useViewTab(scrollRef, tabsRef, items.length - 1);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentItem(items[selectedItemIndex]);
|
||||
}, [selectedItemIndex, items]);
|
||||
if (!multiple) setCurrentItem(selectedItemIndex);
|
||||
}, [selectedItemIndex, items, multiple]);
|
||||
|
||||
const scrollToTab = (index: number): void => {
|
||||
if (!scrollRef.current || !tabsRef.current) return;
|
||||
@ -95,19 +95,42 @@ const Tabs = (props: TabsProps) => {
|
||||
};
|
||||
|
||||
const setSelectedItem = (selectedTabItem: TTabItem, index: number): void => {
|
||||
setCurrentItem(selectedTabItem);
|
||||
scrollToTab(index);
|
||||
onSelect?.(selectedTabItem);
|
||||
if (multiple) {
|
||||
const indexOperation = () => {
|
||||
const newArray = [...multipleItems];
|
||||
|
||||
const deletionIndex = newArray.indexOf(index);
|
||||
|
||||
if (deletionIndex !== -1) {
|
||||
newArray.splice(deletionIndex, 1);
|
||||
|
||||
return newArray;
|
||||
}
|
||||
|
||||
newArray.push(index);
|
||||
return newArray;
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledTabs {...rest} stickyTop={stickyTop}>
|
||||
<div className="sticky">
|
||||
{!isViewFirstTab && <div className="blur-ahead" />}
|
||||
<ScrollbarTabs ref={scrollRef} autoHide={false} noScrollY $type={type}>
|
||||
<TabList ref={tabsRef} $type={type}>
|
||||
const updatedActiveTab = indexOperation();
|
||||
|
||||
setMultipleItems(updatedActiveTab);
|
||||
onSelect?.(selectedTabItem);
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentItem(index);
|
||||
onSelect?.(selectedTabItem);
|
||||
|
||||
scrollToTab(index);
|
||||
};
|
||||
|
||||
const renderContent = (
|
||||
<TabList ref={tabsRef} $type={type} multiple={multiple}>
|
||||
{items.map((item, index) => {
|
||||
const isActive = item.id === currentItem.id;
|
||||
const isActive = multiple
|
||||
? multipleItems.indexOf(index) !== -1
|
||||
: index === currentItem;
|
||||
|
||||
return (
|
||||
<Tab
|
||||
key={item.id}
|
||||
@ -125,13 +148,31 @@ const Tabs = (props: TabsProps) => {
|
||||
);
|
||||
})}
|
||||
</TabList>
|
||||
);
|
||||
return (
|
||||
<StyledTabs {...rest} stickyTop={stickyTop} multiple={multiple}>
|
||||
{multiple && renderContent}
|
||||
|
||||
{!multiple && (
|
||||
<div className="sticky">
|
||||
{!isViewFirstTab && <div className="blur-ahead" />}
|
||||
|
||||
<ScrollbarTabs
|
||||
ref={scrollRef}
|
||||
autoHide={false}
|
||||
noScrollY
|
||||
$type={type}
|
||||
>
|
||||
{renderContent}
|
||||
</ScrollbarTabs>
|
||||
|
||||
{!isViewLastTab && <div className="blur-back" />}
|
||||
</div>
|
||||
|
||||
)}
|
||||
<div className="sticky-indent" />
|
||||
|
||||
<div className="tabs-body">{currentItem?.content}</div>
|
||||
{!multiple && (
|
||||
<div className="tabs-body">{items[currentItem]?.content}</div>
|
||||
)}
|
||||
</StyledTabs>
|
||||
);
|
||||
};
|
||||
|
@ -44,10 +44,13 @@ export interface TabsProps {
|
||||
items: TTabItem[];
|
||||
/** Selected item of tabs. */
|
||||
selectedItemId?: number | string;
|
||||
selectedItems?: number[];
|
||||
/** Theme for displaying tabs. */
|
||||
type?: TabsTypes;
|
||||
/** Tab indentation for sticky positioning. */
|
||||
stickyTop?: string;
|
||||
/** Enables multiple select */
|
||||
multiple?: boolean;
|
||||
/** Sets a callback function that is triggered when the tab is selected. */
|
||||
onSelect?: (element: TTabItem) => void;
|
||||
}
|
||||
|
@ -550,6 +550,18 @@ export const enum EditorConfigErrorType {
|
||||
TenantQuotaException = "ASC.Core.Tenants.TenantQuotaException",
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum for watermarks.
|
||||
* @readonly
|
||||
*/
|
||||
export const enum WatermarkAdditions {
|
||||
UserName = 1,
|
||||
UserEmail = 2,
|
||||
UserIpAdress = 4,
|
||||
CurrentDate = 8,
|
||||
RoomName = 16,
|
||||
}
|
||||
|
||||
export const enum RoomsStorageFilter {
|
||||
internal = 1,
|
||||
thirdparty = 2,
|
||||
|
@ -102,7 +102,8 @@ export const convertFilesToItems: (
|
||||
filterParam?: string | number,
|
||||
) => {
|
||||
const items = files.map((file) => {
|
||||
const { id, title, security, folderId, rootFolderType, fileExst } = file;
|
||||
const { id, title, security, folderId, rootFolderType, fileExst, viewUrl } =
|
||||
file;
|
||||
|
||||
const icon = getIcon(fileExst || DEFAULT_FILE_EXTS);
|
||||
const label = getTitleWithoutExtension(file, false);
|
||||
@ -117,6 +118,7 @@ export const convertFilesToItems: (
|
||||
rootFolderType,
|
||||
isDisabled: !filterParam,
|
||||
fileExst,
|
||||
viewUrl,
|
||||
};
|
||||
});
|
||||
return items;
|
||||
|
@ -134,6 +134,7 @@ const FilesSelector = ({
|
||||
title: string;
|
||||
path?: string[];
|
||||
fileExst?: string;
|
||||
viewUrl?: string;
|
||||
inPublic?: boolean;
|
||||
} | null>(null);
|
||||
const [total, setTotal] = React.useState<number>(0);
|
||||
@ -347,6 +348,7 @@ const FilesSelector = ({
|
||||
id: item.id,
|
||||
title: item.label,
|
||||
fileExst: item.fileExst,
|
||||
viewUrl: item.viewUrl,
|
||||
inPublic,
|
||||
});
|
||||
|
||||
|
@ -61,7 +61,6 @@ const {
|
||||
strongBlue,
|
||||
|
||||
darkRed,
|
||||
|
||||
darkErrorStatus,
|
||||
charlestonGreen,
|
||||
outerSpace,
|
||||
|
@ -33,6 +33,7 @@ import moment from "moment-timezone";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { I18nextProviderProps } from "react-i18next";
|
||||
import sjcl from "sjcl";
|
||||
import resizeImage from "resize-image";
|
||||
|
||||
import { flagsIcons } from "@docspace/shared/utils/image-flags";
|
||||
|
||||
@ -1136,3 +1137,66 @@ export function setLanguageForUnauthorized(culture: string) {
|
||||
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
export const imageProcessing = async (file: File, maxSize?: number) => {
|
||||
const ONE_MEGABYTE = 1024 * 1024;
|
||||
const COMPRESSION_RATIO = 2;
|
||||
const NO_COMPRESSION_RATIO = 1;
|
||||
|
||||
const maxImageSize = maxSize ?? ONE_MEGABYTE;
|
||||
const imageBitMap = await createImageBitmap(file);
|
||||
|
||||
const width = imageBitMap.width;
|
||||
const height = imageBitMap.height;
|
||||
|
||||
// @ts-expect-error imageBitMap
|
||||
const canvas = resizeImage.resize2Canvas(imageBitMap, width, height);
|
||||
|
||||
async function resizeRecursiveAsync(
|
||||
img: { width: number; height: number },
|
||||
compressionRatio = COMPRESSION_RATIO,
|
||||
depth = 0,
|
||||
): Promise<unknown> {
|
||||
const data = resizeImage.resize(
|
||||
// @ts-expect-error canvas
|
||||
canvas,
|
||||
img.width / compressionRatio,
|
||||
img.height / compressionRatio,
|
||||
resizeImage.JPEG,
|
||||
);
|
||||
|
||||
const newFile = await fetch(data)
|
||||
.then((res) => res.blob())
|
||||
.then((blob) => {
|
||||
const f = new File([blob], "File name", {
|
||||
type: "image/jpg",
|
||||
});
|
||||
return f;
|
||||
});
|
||||
|
||||
// const stepMessage = `Step ${depth + 1}`;
|
||||
// const sizeMessage = `size = ${file.size} bytes`;
|
||||
// const compressionRatioMessage = `compressionRatio = ${compressionRatio}`;
|
||||
|
||||
// console.log(`${stepMessage} ${sizeMessage} ${compressionRatioMessage}`);
|
||||
|
||||
if (file.size < maxImageSize) {
|
||||
return file;
|
||||
}
|
||||
|
||||
if (depth > 5) {
|
||||
// console.log("start");
|
||||
throw new Error("recursion depth exceeded");
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
return resolve(newFile);
|
||||
}).then(() => resizeRecursiveAsync(img, compressionRatio + 1, depth + 1));
|
||||
}
|
||||
|
||||
return resizeRecursiveAsync(
|
||||
{ width, height },
|
||||
file.size > maxImageSize ? COMPRESSION_RATIO : NO_COMPRESSION_RATIO,
|
||||
);
|
||||
};
|
||||
|
@ -109,6 +109,7 @@
|
||||
"Culture_vi": "Tiếng Việt (Việt Nam)",
|
||||
"Culture_zh-CN": "中文(简体,中国)",
|
||||
"Custom": "Custom",
|
||||
"CurrentDate": "Current Date",
|
||||
"CustomFilter": "Custom filter",
|
||||
"CustomQuota": "Custom quota",
|
||||
"CustomRoomDescription": "Apply your own settings to use this room for any custom purpose.",
|
||||
@ -368,6 +369,7 @@
|
||||
"ReviewRoomTitle": "Review room",
|
||||
"Role": "Role",
|
||||
"Room": "Room",
|
||||
"RoomName": "Room Name",
|
||||
"RoomAdmin": "Room admin",
|
||||
"RoomList": "Room list",
|
||||
"Rooms": "Rooms",
|
||||
|
Loading…
Reference in New Issue
Block a user