feat: camera widget (#8069)
* feat: Camera Widget -- Scaffold the basic structure of the widget * feat: Camera Widget -- Prototype a feature, taking picture * feat: Camera Widget -- Add types for MediaRecorder -- Define media capture status and action types -- Prototype basic video recording, playing features * feat: Camera Widget -- Implement video player -- Add timer for recording and playing video -- Add permission and error handling logic -- Add device selectors * feat: Camera Widget -- Place control buttons above device inputs layer -- Make the widget fully responsive * feat: Camera Widget -- Change the color of caret-down icon to white -- Remove overlaying of web cam and video player -- Add some padding for device inputs * feat: Camera Widget -- Add black background to the container of the widget * feat: Camera Widget -- Change the widget icon * feat: Camera Widget -- Implement the mute feature of a mic or a camera * feat: Camera Widget -- Check media device permissions before getting started * feat: Camera Widget -- Add a fullscreen control * feat: Camera Widget -- Set error text color to white -- Change the layout of control panel * feat: Camera Widget -- Apply layout change for control panel according to app layout change * feat: Camera Widget -- Add a new derived property, videoURL * feat: Switch Group Widget -- Adopt theme changes * feat: Camera Widget -- Make background grey in case of both error and disabled status * feat: Camera Widget -- Update npm dependencies * feat: Camera Widget -- Fix on #8788, using muted property * feat: Camera Widget -- Show off the microphone setting icon only if the current mode is video -- Set isMirrored property to true by default * feat: Camera Widget -- Add photo viewer * feat: Camera Widget -- Add onImageCapture, onRecordingStart, onRecordingStop actions instead of onMediaCapture * feat: Camera Widget -- Expose meta properties for the widget * feat: Camera Widget -- Fix on responsiveness issue * feat: Camera Widget -- Add type definitions for MediaStream recording * feat: Camera Widget -- Hide isMirroed property for video mode * feat: Camera Widget -- Wrap all the controls with TooltipComponent * feat: Camera Widget -- Implement enter, exit full screen feature * feat: Camera Widget -- Add a widget icon for entity explorer * feat: Camera Widget -- Fix on the typo for the label of onRecordingStop property * feat: Camera Widget -- Enable/disable media tracks * feat: Camera Widget -- Set the video's height to 100% in fullscreen mode * feat: Camera Widget -- Add overlayers on Webcam * feat: Camera Widget -- Set position to relative on fullscreen wrapper div -- Set the photo viewer's height to 100% * feat: Camera Widget -- Add image, mediaCaptureStatus, timer meta properties to keep UI states when the widget is dragged * feat: Camera Widget -- Refactor code base, eliminating commented code blocks * feat: Camera Widget -- Revert all the changes needed for keeping status when the widget is dragged -- Set mirroed property to false for video mode
|
|
@ -102,6 +102,7 @@
|
|||
"react-dnd-touch-backend": "^9.4.0",
|
||||
"react-documents": "^1.0.4",
|
||||
"react-dom": "^16.7.0",
|
||||
"react-full-screen": "^1.1.0",
|
||||
"react-google-maps": "^9.4.5",
|
||||
"react-google-recaptcha": "^2.1.0",
|
||||
"react-helmet": "^5.2.1",
|
||||
|
|
@ -128,6 +129,7 @@
|
|||
"react-transition-group": "^4.3.0",
|
||||
"react-use-gesture": "^7.0.4",
|
||||
"react-virtuoso": "^1.9.0",
|
||||
"react-webcam": "^6.0.0",
|
||||
"react-window": "^1.8.6",
|
||||
"react-zoom-pan-pinch": "^1.6.1",
|
||||
"redux": "^4.0.1",
|
||||
|
|
@ -202,6 +204,7 @@
|
|||
"@types/chance": "^1.0.7",
|
||||
"@types/codemirror": "^0.0.96",
|
||||
"@types/deep-diff": "^1.0.0",
|
||||
"@types/dom-mediacapture-record": "^1.0.11",
|
||||
"@types/downloadjs": "^1.4.2",
|
||||
"@types/draft-js": "^0.11.1",
|
||||
"@types/emoji-mart": "^3.0.4",
|
||||
|
|
|
|||
3
app/client/src/assets/icons/widget/camera.svg
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<path fill="#4b4848" d="M9 3h6l2 2h4a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h4l2-2zm3 16a6 6 0 1 0 0-12 6 6 0 0 0 0 12zm0-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 283 B |
4
app/client/src/assets/icons/widget/camera/camera-muted.svg
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.6364 5C16.1382 5 16.5455 5.392 16.5455 5.875V9.55001L21.2846 6.35625C21.49 6.21801 21.7736 6.26613 21.9182 6.46476C21.9709 6.53826 22 6.62575 22 6.715V17.285C22 17.5265 21.7963 17.7226 21.5454 17.7226C21.4518 17.7226 21.3609 17.6945 21.2846 17.6438L16.5455 14.45V18.125C16.5455 18.608 16.1382 19 15.6364 19H2.90909C2.40728 19 2 18.608 2 18.125V5.875C2 5.392 2.40728 5 2.90909 5H15.6364ZM14.7272 6.75H3.81818V17.2501H14.7272V6.75ZM20.1818 9.23588L16.5455 11.6859V12.3141L20.1818 14.7641V9.23588Z" fill="white"/>
|
||||
<path d="M21.5 2.5L2 21.5" stroke="white" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 711 B |
3
app/client/src/assets/icons/widget/camera/camera-offline.svg
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.5861 20.0001H2.00007C1.73485 20.0001 1.4805 19.8947 1.29296 19.7072C1.10542 19.5196 1.00007 19.2653 1.00007 19.0001V5.00007C1.00007 4.73485 1.10542 4.4805 1.29296 4.29296C1.4805 4.10542 1.73485 4.00007 2.00007 4.00007H2.58607L0.393066 1.80807L1.80807 0.393066L21.6071 20.1931L20.1921 21.6071L18.5861 20.0001ZM4.58607 6.00007H3.00007V18.0001H16.5861L14.4061 15.8201C13.3486 16.657 12.0205 17.0762 10.6742 16.9981C9.32795 16.92 8.05726 16.35 7.10368 15.3965C6.1501 14.4429 5.5801 13.1722 5.502 11.8259C5.4239 10.4796 5.84315 9.15151 6.68007 8.09407L4.58607 6.00007ZM8.11007 9.52507C7.64905 10.1988 7.43805 11.0125 7.51359 11.8254C7.58912 12.6383 7.94644 13.3992 8.5237 13.9764C9.10097 14.5537 9.86187 14.911 10.6747 14.9865C11.4876 15.0621 12.3013 14.8511 12.9751 14.3901L8.11007 9.52507ZM21.0001 16.7851L19.0001 14.7851V6.00007H15.1721L13.1721 4.00007H8.82807L8.52107 4.30707L7.10707 2.89307L8.00007 2.00007H14.0001L16.0001 4.00007H20.0001C20.2653 4.00007 20.5196 4.10542 20.7072 4.29296C20.8947 4.4805 21.0001 4.73485 21.0001 5.00007V16.7861V16.7851ZM10.2631 6.05007C11.1024 5.93646 11.9567 6.01825 12.7592 6.28905C13.5618 6.55985 14.2909 7.01236 14.8899 7.61128C15.4888 8.21021 15.9413 8.93937 16.2121 9.74191C16.4829 10.5445 16.5647 11.3987 16.4511 12.2381L14.1131 9.90007C13.7783 9.25128 13.2499 8.72282 12.6011 8.38807L10.2631 6.05007Z" fill="#858282"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
3
app/client/src/assets/icons/widget/camera/camera.svg
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="20" height="14" viewBox="0 0 20 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.3334 0.333496C13.7934 0.333496 14.1667 0.706829 14.1667 1.16683V4.66683L18.5109 1.62516C18.6992 1.4935 18.9592 1.53933 19.0917 1.7285C19.14 1.7985 19.1667 1.88183 19.1667 1.96683V12.0335C19.1667 12.2635 18.98 12.4502 18.75 12.4502C18.6642 12.4502 18.5809 12.4235 18.5109 12.3752L14.1667 9.3335V12.8335C14.1667 13.2935 13.7934 13.6668 13.3334 13.6668H1.66671C1.20671 13.6668 0.833374 13.2935 0.833374 12.8335V1.16683C0.833374 0.706829 1.20671 0.333496 1.66671 0.333496H13.3334ZM12.5 2.00016H2.50004V12.0002H12.5V2.00016ZM17.5 4.36766L14.1667 6.701V7.29933L17.5 9.63266V4.36766Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 709 B |
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16">
|
||||
<path d="M5.5 0a.5.5 0 0 1 .5.5v4A1.5 1.5 0 0 1 4.5 6h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5zm5 0a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 10 4.5v-4a.5.5 0 0 1 .5-.5zM0 10.5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 6 11.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5zm10 1a1.5 1.5 0 0 1 1.5-1.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4z" fill="white" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 522 B |
3
app/client/src/assets/icons/widget/camera/fullscreen.svg
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="18" height="16" viewBox="0 0 18 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.6667 0.5H17.3333V5.5H15.6667V2.16667H12.3333V0.5H15.6667ZM2.33332 0.5H5.66666V2.16667H2.33332V5.5H0.666656V0.5H2.33332ZM15.6667 13.8333V10.5H17.3333V15.5H12.3333V13.8333H15.6667ZM2.33332 13.8333H5.66666V15.5H0.666656V10.5H2.33332V13.8333Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 371 B |
3
app/client/src/assets/icons/widget/camera/microphone-muted.svg
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="17" height="18" viewBox="0 0 17 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.3929 0C16.2411 0 16.0893 0.0642857 15.9679 0.192857L11.5357 4.88571V3.21429C11.5357 1.44643 10.1696 0 8.5 0C6.83036 0 5.46429 1.44643 5.46429 3.21429V9.64286C5.46429 10.125 5.58571 10.575 5.7375 10.9929L4.82679 11.9571C4.43214 11.2821 4.21964 10.4786 4.21964 9.64286V7.71429C4.21964 7.36071 3.94643 7.07143 3.6125 7.07143C3.27857 7.07143 3.00536 7.36071 3.00536 7.71429V9.64286C3.00536 10.8321 3.33929 11.9571 3.94643 12.8893L0.182143 16.9071C0.0607143 17.0036 0 17.1643 0 17.3571C0 17.7107 0.273214 18 0.607143 18C0.789286 18 0.941071 17.9357 1.03214 17.8071L11.2929 6.94286C11.3232 6.91071 11.3839 6.87857 11.4143 6.81429L16.8179 1.09286C16.9393 0.996429 17 0.803571 17 0.642857C17 0.289286 16.7268 0 16.3929 0ZM13.3571 7.07143C13.0232 7.07143 12.75 7.36071 12.75 7.71429V9.64286C12.75 12.1179 10.8982 14.1107 8.56071 14.1429C8.53036 14.1429 8.5 14.1429 8.46964 14.1429C8.43929 14.1429 8.40893 14.1429 8.40893 14.1429C7.92321 14.1429 7.4375 14.0464 7.0125 13.8857C6.95179 13.8536 6.86071 13.8536 6.8 13.8536C6.46607 13.8536 6.19286 14.1429 6.19286 14.4964C6.19286 14.7857 6.375 15.0107 6.61786 15.1071C7.0125 15.2679 7.4375 15.3643 7.89286 15.4286V16.7143H6.07143C5.7375 16.7143 5.46429 17.0036 5.46429 17.3571C5.46429 17.7107 5.7375 18 6.07143 18H8.40893C8.43929 18 8.46964 18 8.5 18C8.53036 18 8.56071 18 8.59107 18H10.9286C11.2625 18 11.5357 17.7107 11.5357 17.3571C11.5357 17.0036 11.2625 16.7143 10.9286 16.7143H9.10714V15.3964C11.8393 15.075 13.9643 12.6321 13.9643 9.64286V7.71429C13.9643 7.36071 13.6911 7.07143 13.3571 7.07143Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
3
app/client/src/assets/icons/widget/camera/microphone.svg
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="20" viewBox="0 0 16 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.99994 2.50016C7.3369 2.50016 6.70102 2.76355 6.23218 3.2324C5.76333 3.70124 5.49994 4.33712 5.49994 5.00016V8.3335C5.49994 8.99654 5.76333 9.63242 6.23218 10.1013C6.70102 10.5701 7.3369 10.8335 7.99994 10.8335C8.66298 10.8335 9.29887 10.5701 9.76771 10.1013C10.2366 9.63242 10.4999 8.99654 10.4999 8.3335V5.00016C10.4999 4.33712 10.2366 3.70124 9.76771 3.2324C9.29887 2.76355 8.66298 2.50016 7.99994 2.50016ZM7.99994 0.833496C8.54712 0.833496 9.08893 0.94127 9.59446 1.15066C10.1 1.36006 10.5593 1.66697 10.9462 2.05388C11.3331 2.4408 11.64 2.90012 11.8494 3.40565C12.0588 3.91117 12.1666 4.45299 12.1666 5.00016V8.3335C12.1666 9.43856 11.7276 10.4984 10.9462 11.2798C10.1648 12.0612 9.10501 12.5002 7.99994 12.5002C6.89487 12.5002 5.83507 12.0612 5.05366 11.2798C4.27226 10.4984 3.83328 9.43856 3.83328 8.3335V5.00016C3.83328 3.89509 4.27226 2.83529 5.05366 2.05388C5.83507 1.27248 6.89487 0.833496 7.99994 0.833496ZM0.545776 9.16683H2.22494C2.42685 10.5541 3.1215 11.8224 4.1818 12.7396C5.24211 13.6567 6.59718 14.1615 7.99911 14.1615C9.40104 14.1615 10.7561 13.6567 11.8164 12.7396C12.8767 11.8224 13.5714 10.5541 13.7733 9.16683H15.4533C15.2638 10.8574 14.5055 12.4335 13.3026 13.6364C12.0998 14.8394 10.5238 15.598 8.83328 15.7877V19.1668H7.16661V15.7877C5.47589 15.5982 3.89976 14.8397 2.69676 13.6367C1.49375 12.4337 0.735287 10.8575 0.545776 9.16683Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -92,6 +92,9 @@ export enum EventType {
|
|||
ON_RECORDING_COMPLETE = "ON_RECORDING_COMPLETE",
|
||||
ON_SWITCH_GROUP_SELECTION_CHANGE = "ON_SWITCH_GROUP_SELECTION_CHANGE",
|
||||
ON_JS_FUNCTION_EXECUTE = "ON_JS_FUNCTION_EXECUTE",
|
||||
ON_CAMERA_IMAGE_CAPTURE = "ON_CAMERA_IMAGE_CAPTURE",
|
||||
ON_CAMERA_VIDEO_RECORDING_START = "ON_CAMERA_VIDEO_RECORDING_START",
|
||||
ON_CAMERA_VIDEO_RECORDING_STOP = "ON_CAMERA_VIDEO_RECORDING_STOP",
|
||||
}
|
||||
|
||||
export interface PageAction {
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ export const GLOBAL_STYLE_TOOLTIP_CLASSNAME = "ads-global-tooltip";
|
|||
export const TooltipStyles = createGlobalStyle<{
|
||||
theme: Theme;
|
||||
}>`
|
||||
.${Classes.PORTAL} {
|
||||
.${Classes.TOOLTIP}.${GLOBAL_STYLE_TOOLTIP_CLASSNAME} {
|
||||
max-width: 350px;
|
||||
.${Classes.PORTAL} .${Classes.TOOLTIP}.${GLOBAL_STYLE_TOOLTIP_CLASSNAME}, .${
|
||||
Classes.TOOLTIP
|
||||
}.${GLOBAL_STYLE_TOOLTIP_CLASSNAME} {
|
||||
max-width: 350px;
|
||||
overflow-wrap: anywhere;
|
||||
.${Classes.POPOVER_CONTENT} {
|
||||
padding: 10px 12px;
|
||||
|
|
@ -30,6 +31,5 @@ export const TooltipStyles = createGlobalStyle<{
|
|||
.${CsClasses.BP3_POPOVER_ARROW_FILL} {
|
||||
fill: ${(props) => props.theme.colors.tooltip.darkBg};
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import React, { JSXElementConstructor } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Colors } from "constants/Colors";
|
||||
import { IconProps, IconWrapper } from "constants/IconConstants";
|
||||
import { ReactComponent as SpinnerIcon } from "assets/icons/widget/alert.svg";
|
||||
import { ReactComponent as ButtonIcon } from "assets/icons/widget/button.svg";
|
||||
|
|
@ -35,8 +38,7 @@ import { ReactComponent as CheckboxGroupIcon } from "assets/icons/widget/checkbo
|
|||
import { ReactComponent as AudioRecorderIcon } from "assets/icons/widget/audio-recorder.svg";
|
||||
import { ReactComponent as ButtonGroupIcon } from "assets/icons/widget/button-group.svg";
|
||||
import { ReactComponent as SwitchGroupIcon } from "assets/icons/widget/switch-group.svg";
|
||||
import styled from "styled-components";
|
||||
import { Colors } from "constants/Colors";
|
||||
import { ReactComponent as CameraIcon } from "assets/icons/widget/camera.svg";
|
||||
|
||||
/* eslint-disable react/display-name */
|
||||
|
||||
|
|
@ -231,6 +233,11 @@ export const WidgetIcons: {
|
|||
<SwitchGroupIcon />
|
||||
</StyledIconWrapper>
|
||||
),
|
||||
CAMERA_WIDGET: (props: IconProps) => (
|
||||
<StyledIconWrapper {...props}>
|
||||
<CameraIcon />
|
||||
</StyledIconWrapper>
|
||||
),
|
||||
};
|
||||
|
||||
export type WidgetIcon = typeof WidgetIcons[keyof typeof WidgetIcons];
|
||||
|
|
|
|||
|
|
@ -418,3 +418,34 @@ export const getPageURL = (
|
|||
page.pageId,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert Base64 string to Blob
|
||||
* @param base64Data
|
||||
* @param contentType
|
||||
* @param sliceSize
|
||||
* @returns
|
||||
*/
|
||||
export const base64ToBlob = (
|
||||
base64Data: string,
|
||||
contentType = "",
|
||||
sliceSize = 512,
|
||||
) => {
|
||||
const byteCharacters = atob(base64Data);
|
||||
const byteArrays = [];
|
||||
|
||||
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
|
||||
const slice = byteCharacters.slice(offset, offset + sliceSize);
|
||||
|
||||
const byteNumbers = new Array(slice.length);
|
||||
for (let i = 0; i < slice.length; i++) {
|
||||
byteNumbers[i] = slice.charCodeAt(i);
|
||||
}
|
||||
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
byteArrays.push(byteArray);
|
||||
}
|
||||
|
||||
const blob = new Blob(byteArrays, { type: contentType });
|
||||
return blob;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -114,6 +114,10 @@ import SwitchGroupWidget, {
|
|||
|
||||
import log from "loglevel";
|
||||
|
||||
import CameraWidget, {
|
||||
CONFIG as CAMERA_WIDGET_CONFIG,
|
||||
} from "widgets/CameraWidget";
|
||||
|
||||
export const registerWidgets = () => {
|
||||
const start = performance.now();
|
||||
registerWidget(CanvasWidget, CANVAS_WIDGET_CONFIG);
|
||||
|
|
@ -158,6 +162,7 @@ export const registerWidgets = () => {
|
|||
registerWidget(SingleSelectTreeWidget, SINGLE_SELECT_TREE_WIDGET_CONFIG);
|
||||
registerWidget(SwitchGroupWidget, SWITCH_GROUP_WIDGET_CONFIG);
|
||||
registerWidget(AudioWidget, AUDIO_WIDGET_CONFIG);
|
||||
registerWidget(CameraWidget, CAMERA_WIDGET_CONFIG);
|
||||
|
||||
log.debug("Widget registration took: ", performance.now() - start, "ms");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -430,6 +430,17 @@ export const entityDefinitions: Record<string, unknown> = {
|
|||
"!url": "https://docs.appsmith.com/widget-reference/switch-group",
|
||||
selectedValues: "[string]",
|
||||
},
|
||||
CAMERA_WIDGET: {
|
||||
"!doc":
|
||||
"Camera widget allows users to take a picture or record videos through their system camera using browser permissions.",
|
||||
"!url": "https://docs.appsmith.com/widget-reference/camera",
|
||||
imageBlobURL: "string",
|
||||
imageDataURL: "string",
|
||||
imageRawBinary: "string",
|
||||
videoBlobURL: "string",
|
||||
videoDataURL: "string",
|
||||
videoRawBinary: "string",
|
||||
},
|
||||
};
|
||||
|
||||
export const GLOBAL_DEFS = {
|
||||
|
|
|
|||
1207
app/client/src/widgets/CameraWidget/component/index.tsx
Normal file
47
app/client/src/widgets/CameraWidget/constants.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// This file contains common constants which can be used across the widget configuration file (index.ts), widget and component folders.
|
||||
export enum CameraModeTypes {
|
||||
CAMERA = "CAMERA",
|
||||
VIDEO = "VIDEO",
|
||||
}
|
||||
|
||||
export type CameraMode = keyof typeof CameraModeTypes;
|
||||
|
||||
export enum MediaCaptureStatusTypes {
|
||||
IMAGE_DEFAULT = "IMAGE_DEFAULT",
|
||||
IMAGE_CAPTURED = "IMAGE_CAPTURED",
|
||||
IMAGE_SAVED = "IMAGE_SAVED",
|
||||
VIDEO_DEFAULT = "VIDEO_DEFAULT",
|
||||
VIDEO_RECORDING = "VIDEO_RECORDING",
|
||||
VIDEO_CAPTURED = "VIDEO_CAPTURED",
|
||||
VIDEO_PLAYING = "VIDEO_PLAYING",
|
||||
VIDEO_PAUSED = "VIDEO_PAUSED",
|
||||
VIDEO_SAVED = "VIDEO_SAVED",
|
||||
VIDEO_PLAYING_AFTER_SAVE = "VIDEO_PLAYING_AFTER_SAVE",
|
||||
VIDEO_PAUSED_AFTER_SAVE = "VIDEO_PAUSED_AFTER_SAVE",
|
||||
}
|
||||
|
||||
export type MediaCaptureStatus = keyof typeof MediaCaptureStatusTypes;
|
||||
|
||||
export enum MediaCaptureActionTypes {
|
||||
IMAGE_CAPTURE = "IMAGE_CAPTURE",
|
||||
IMAGE_SAVE = "IMAGE_SAVE",
|
||||
IMAGE_DISCARD = "IMAGE_DISCARD",
|
||||
IMAGE_REFRESH = "IMAGE_REFRESH",
|
||||
RECORDING_START = "RECORDING_START",
|
||||
RECORDING_STOP = "RECORDING_STOP",
|
||||
RECORDING_DISCARD = "RECORDING_DISCARD",
|
||||
RECORDING_SAVE = "RECORDING_SAVE",
|
||||
VIDEO_PLAY = "VIDEO_PLAY",
|
||||
VIDEO_PAUSE = "VIDEO_PAUSE",
|
||||
VIDEO_PLAY_AFTER_SAVE = "VIDEO_PLAY_AFTER_SAVE",
|
||||
VIDEO_PAUSE_AFTER_SAVE = "VIDEO_PAUSE_AFTER_SAVE",
|
||||
VIDEO_REFRESH = "VIDEO_REFRESH",
|
||||
}
|
||||
|
||||
export type MediaCaptureAction = keyof typeof MediaCaptureActionTypes;
|
||||
|
||||
export enum DeviceTypes {
|
||||
MICROPHONE = "MICROPHONE",
|
||||
CAMERA = "CAMERA",
|
||||
}
|
||||
export type DeviceType = keyof typeof DeviceTypes;
|
||||
3
app/client/src/widgets/CameraWidget/icon.svg
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><g fill="#4b4848">
|
||||
<path fill="none" d="M0 0h24v24H0z"/><path d="M9 3h6l2 2h4a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h4l2-2zm3 16a6 6 0 1 0 0-12 6 6 0 0 0 0 12zm0-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8z"/>
|
||||
</g></svg>
|
||||
|
After Width: | Height: | Size: 315 B |
30
app/client/src/widgets/CameraWidget/index.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import Widget from "./widget";
|
||||
import IconSVG from "./icon.svg";
|
||||
import { GRID_DENSITY_MIGRATION_V1 } from "widgets/constants";
|
||||
import { CameraModeTypes } from "./constants";
|
||||
|
||||
export const CONFIG = {
|
||||
type: Widget.getWidgetType(),
|
||||
name: "Camera", // The display name which will be made in uppercase and show in the widgets panel ( can have spaces )
|
||||
iconSVG: IconSVG,
|
||||
needsMeta: true, // Defines if this widget adds any meta properties
|
||||
isCanvas: false, // Defines if this widget has a canvas within in which we can drop other widgets
|
||||
defaults: {
|
||||
widgetName: "Camera",
|
||||
rows: 8.25 * GRID_DENSITY_MIGRATION_V1,
|
||||
columns: 6.25 * GRID_DENSITY_MIGRATION_V1,
|
||||
mode: CameraModeTypes.CAMERA,
|
||||
isDisabled: false,
|
||||
isVisible: true,
|
||||
isMirrored: true,
|
||||
version: 1,
|
||||
},
|
||||
properties: {
|
||||
derived: Widget.getDerivedPropertiesMap(),
|
||||
default: Widget.getDefaultPropertiesMap(),
|
||||
meta: Widget.getMetaPropertiesMap(),
|
||||
config: Widget.getPropertyPaneConfig(),
|
||||
},
|
||||
};
|
||||
|
||||
export default Widget;
|
||||
275
app/client/src/widgets/CameraWidget/widget/index.tsx
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
import React from "react";
|
||||
|
||||
import BaseWidget, { WidgetProps, WidgetState } from "widgets/BaseWidget";
|
||||
import { DerivedPropertiesMap } from "utils/WidgetFactory";
|
||||
import { ValidationTypes } from "constants/WidgetValidation";
|
||||
import { WIDGET_PADDING } from "constants/WidgetConstants";
|
||||
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
|
||||
import { base64ToBlob, createBlobUrl } from "utils/AppsmithUtils";
|
||||
import { FileDataTypes } from "widgets/constants";
|
||||
|
||||
import CameraComponent from "../component";
|
||||
import {
|
||||
CameraMode,
|
||||
CameraModeTypes,
|
||||
MediaCaptureStatusTypes,
|
||||
} from "../constants";
|
||||
|
||||
class CameraWidget extends BaseWidget<CameraWidgetProps, WidgetState> {
|
||||
static getPropertyPaneConfig() {
|
||||
return [
|
||||
{
|
||||
sectionName: "General",
|
||||
children: [
|
||||
{
|
||||
propertyName: "mode",
|
||||
label: "Mode",
|
||||
controlType: "DROP_DOWN",
|
||||
helpText: "Whether a picture is taken or a video is recorded",
|
||||
options: [
|
||||
{
|
||||
label: "Camera",
|
||||
value: "CAMERA",
|
||||
},
|
||||
{
|
||||
label: "Video",
|
||||
value: "VIDEO",
|
||||
},
|
||||
],
|
||||
isBindProperty: false,
|
||||
isTriggerProperty: false,
|
||||
validation: {
|
||||
type: ValidationTypes.TEXT,
|
||||
params: {
|
||||
allowedValues: ["CAMERA", "VIDEO"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
propertyName: "isDisabled",
|
||||
label: "Disabled",
|
||||
controlType: "SWITCH",
|
||||
helpText: "Disables clicks to this widget",
|
||||
isJSConvertible: true,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: false,
|
||||
validation: { type: ValidationTypes.BOOLEAN },
|
||||
},
|
||||
{
|
||||
propertyName: "isVisible",
|
||||
label: "Visible",
|
||||
helpText: "Controls the visibility of the widget",
|
||||
controlType: "SWITCH",
|
||||
isJSConvertible: true,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: false,
|
||||
validation: { type: ValidationTypes.BOOLEAN },
|
||||
},
|
||||
{
|
||||
propertyName: "isMirrored",
|
||||
label: "Mirrored",
|
||||
helpText: "Show camera preview and get the screenshot mirrored",
|
||||
controlType: "SWITCH",
|
||||
hidden: (props: CameraWidgetProps) =>
|
||||
props.mode === CameraModeTypes.VIDEO,
|
||||
dependencies: ["mode"],
|
||||
isJSConvertible: true,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: false,
|
||||
validation: { type: ValidationTypes.BOOLEAN },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
sectionName: "Actions",
|
||||
children: [
|
||||
{
|
||||
helpText: "Triggers an action when the image is captured",
|
||||
propertyName: "onImageCapture",
|
||||
label: "OnImageCapture",
|
||||
controlType: "ACTION_SELECTOR",
|
||||
hidden: (props: CameraWidgetProps) =>
|
||||
props.mode === CameraModeTypes.VIDEO,
|
||||
dependencies: ["mode"],
|
||||
isJSConvertible: true,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: true,
|
||||
},
|
||||
{
|
||||
helpText: "Triggers an action when the video recording get started",
|
||||
propertyName: "onRecordingStart",
|
||||
label: "OnRecordingStart",
|
||||
controlType: "ACTION_SELECTOR",
|
||||
hidden: (props: CameraWidgetProps) =>
|
||||
props.mode === CameraModeTypes.CAMERA,
|
||||
dependencies: ["mode"],
|
||||
isJSConvertible: true,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: true,
|
||||
},
|
||||
{
|
||||
helpText: "Triggers an action when the video recording stops",
|
||||
propertyName: "onRecordingStop",
|
||||
label: "OnRecordingStop",
|
||||
controlType: "ACTION_SELECTOR",
|
||||
hidden: (props: CameraWidgetProps) =>
|
||||
props.mode === CameraModeTypes.CAMERA,
|
||||
dependencies: ["mode"],
|
||||
isJSConvertible: true,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
static getDerivedPropertiesMap(): DerivedPropertiesMap {
|
||||
return {};
|
||||
}
|
||||
|
||||
static getDefaultPropertiesMap(): Record<string, string> {
|
||||
return {};
|
||||
}
|
||||
|
||||
static getMetaPropertiesMap(): Record<string, any> {
|
||||
return {
|
||||
image: null,
|
||||
imageBlobURL: undefined,
|
||||
imageDataURL: undefined,
|
||||
imageRawBinary: undefined,
|
||||
mediaCaptureStatus: MediaCaptureStatusTypes.IMAGE_DEFAULT,
|
||||
timer: undefined,
|
||||
videoBlobURL: undefined,
|
||||
videoDataURL: undefined,
|
||||
videoRawBinary: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
static getWidgetType(): string {
|
||||
return "CAMERA_WIDGET";
|
||||
}
|
||||
|
||||
getPageView() {
|
||||
const {
|
||||
bottomRow,
|
||||
isDisabled,
|
||||
isMirrored,
|
||||
leftColumn,
|
||||
mode,
|
||||
parentColumnSpace,
|
||||
parentRowSpace,
|
||||
rightColumn,
|
||||
topRow,
|
||||
videoBlobURL,
|
||||
} = this.props;
|
||||
|
||||
const height = (bottomRow - topRow) * parentRowSpace - WIDGET_PADDING * 2;
|
||||
const width =
|
||||
(rightColumn - leftColumn) * parentColumnSpace - WIDGET_PADDING * 2;
|
||||
|
||||
return (
|
||||
<CameraComponent
|
||||
disabled={isDisabled}
|
||||
height={height}
|
||||
mirrored={isMirrored}
|
||||
mode={mode}
|
||||
onImageCapture={this.handleImageCapture}
|
||||
onRecordingStart={this.handleRecordingStart}
|
||||
onRecordingStop={this.handleRecordingStop}
|
||||
videoBlobURL={videoBlobURL}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
handleImageCapture = (image?: string | null) => {
|
||||
if (!image) {
|
||||
URL.revokeObjectURL(this.props.imageBlobURL);
|
||||
|
||||
this.props.updateWidgetMetaProperty("imageBlobURL", undefined);
|
||||
this.props.updateWidgetMetaProperty("imageDataURL", undefined);
|
||||
this.props.updateWidgetMetaProperty("imageRawBinary", undefined);
|
||||
return;
|
||||
}
|
||||
const base64Data = image.split(",")[1];
|
||||
const imageBlob = base64ToBlob(base64Data, "image/webp");
|
||||
const blobURL = URL.createObjectURL(imageBlob);
|
||||
const blobIdForBase64 = createBlobUrl(imageBlob, FileDataTypes.Base64);
|
||||
const blobIdForRaw = createBlobUrl(imageBlob, FileDataTypes.Binary);
|
||||
|
||||
this.props.updateWidgetMetaProperty("imageBlobURL", blobURL);
|
||||
this.props.updateWidgetMetaProperty("imageDataURL", blobIdForBase64, {
|
||||
triggerPropertyName: "onImageCapture",
|
||||
dynamicString: this.props.onImageCapture,
|
||||
event: {
|
||||
type: EventType.ON_CAMERA_IMAGE_CAPTURE,
|
||||
},
|
||||
});
|
||||
this.props.updateWidgetMetaProperty("imageRawBinary", blobIdForRaw, {
|
||||
triggerPropertyName: "onImageCapture",
|
||||
dynamicString: this.props.onImageCapture,
|
||||
event: {
|
||||
type: EventType.ON_CAMERA_IMAGE_CAPTURE,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
handleRecordingStart = () => {
|
||||
if (this.props.onRecordingStart) {
|
||||
super.executeAction({
|
||||
triggerPropertyName: "onRecordingStart",
|
||||
dynamicString: this.props.onRecordingStart,
|
||||
event: {
|
||||
type: EventType.ON_CAMERA_VIDEO_RECORDING_START,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleRecordingStop = (video?: Blob | null) => {
|
||||
if (!video) {
|
||||
if (this.props.videoBlobURL) {
|
||||
URL.revokeObjectURL(this.props.videoBlobURL);
|
||||
}
|
||||
|
||||
this.props.updateWidgetMetaProperty("videoBlobURL", undefined);
|
||||
this.props.updateWidgetMetaProperty("videoDataURL", undefined);
|
||||
this.props.updateWidgetMetaProperty("videoRawBinary", undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const blobURL = URL.createObjectURL(video);
|
||||
const blobIdForBase64 = createBlobUrl(video, FileDataTypes.Base64);
|
||||
const blobIdForRaw = createBlobUrl(video, FileDataTypes.Binary);
|
||||
|
||||
this.props.updateWidgetMetaProperty("videoBlobURL", blobURL);
|
||||
this.props.updateWidgetMetaProperty("videoDataURL", blobIdForBase64, {
|
||||
triggerPropertyName: "onRecordingStop",
|
||||
dynamicString: this.props.onRecordingStop,
|
||||
event: {
|
||||
type: EventType.ON_CAMERA_VIDEO_RECORDING_STOP,
|
||||
},
|
||||
});
|
||||
this.props.updateWidgetMetaProperty("videoRawBinary", blobIdForRaw, {
|
||||
triggerPropertyName: "onRecordingStop",
|
||||
dynamicString: this.props.onRecordingStop,
|
||||
event: {
|
||||
type: EventType.ON_CAMERA_VIDEO_RECORDING_STOP,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export interface CameraWidgetProps extends WidgetProps {
|
||||
isDisabled: boolean;
|
||||
isMirrored: boolean;
|
||||
isVisible: boolean;
|
||||
mode: CameraMode;
|
||||
onImageCapture?: string;
|
||||
onRecordingStart?: string;
|
||||
onRecordingStop?: string;
|
||||
videoBlobURL?: string;
|
||||
}
|
||||
|
||||
export default CameraWidget;
|
||||
|
|
@ -2771,6 +2771,11 @@
|
|||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/@types/deep-diff/-/deep-diff-1.0.0.tgz"
|
||||
|
||||
"@types/dom-mediacapture-record@^1.0.11":
|
||||
version "1.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.11.tgz#f61b17e6131d76629d4039b02634c7e786b82c3a"
|
||||
integrity sha512-ODVOH95x08arZhbQOjH3no7Iye64akdO+55nM+IGtTzpu2ACKr9CQTrI//CCVieIjlI/eL+rK1hQjMycxIgylQ==
|
||||
|
||||
"@types/dom4@^2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmjs.org/@types/dom4/-/dom4-2.0.1.tgz"
|
||||
|
|
@ -8038,6 +8043,11 @@ fs.realpath@^1.0.0:
|
|||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
|
||||
|
||||
fscreen@^1.0.2:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/fscreen/-/fscreen-1.2.0.tgz#1a8c88e06bc16a07b473ad96196fb06d6657f59e"
|
||||
integrity sha512-hlq4+BU0hlPmwsFjwGGzZ+OZ9N/wq9Ljg/sq3pX+2CD7hrJsX9tJgWWK/wiNTFM212CLHWhicOoqwXyZGGetJg==
|
||||
|
||||
fsevents@^1.2.7:
|
||||
version "1.2.13"
|
||||
resolved "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz"
|
||||
|
|
@ -13598,6 +13608,13 @@ react-fast-compare@^3.0.0, react-fast-compare@^3.0.1:
|
|||
version "3.2.0"
|
||||
resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz"
|
||||
|
||||
react-full-screen@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-full-screen/-/react-full-screen-1.1.0.tgz#696c586da25652ed10d88046f8dc0b6620f4ef96"
|
||||
integrity sha512-ivL/HrcfHhEUJWmgoiDKP7Xfy127LGz9x3VnwVxljJ0ky1D1YqJmXjhxnuEhfqT3yociJy/HCk9/yyJ3HEAjaw==
|
||||
dependencies:
|
||||
fscreen "^1.0.2"
|
||||
|
||||
react-google-maps@^9.4.5:
|
||||
version "9.4.5"
|
||||
resolved "https://registry.npmjs.org/react-google-maps/-/react-google-maps-9.4.5.tgz"
|
||||
|
|
@ -13984,6 +14001,11 @@ react-virtuoso@^1.9.0:
|
|||
react-app-polyfill "^1.0.6"
|
||||
resize-observer-polyfill "^1.5.1"
|
||||
|
||||
react-webcam@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-webcam/-/react-webcam-6.0.0.tgz#46dd9ba44cebe6bf3cc4ea2ff09d12243900abfa"
|
||||
integrity sha512-pw7067WYnDHRjAXYXrsLeig9/AAxCFDnnaEJzZ5ep6UZoYMqF4UNRtVkeTk0LotpwqT/c8vHisn/+QodNbUsQA==
|
||||
|
||||
react-window@^1.8.2:
|
||||
version "1.8.5"
|
||||
resolved "https://registry.npmjs.org/react-window/-/react-window-1.8.5.tgz"
|
||||
|
|
|
|||