Skip to content
Snippets Groups Projects
Verified Commit cca98f29 authored by Volker Schukai's avatar Volker Schukai :alien:
Browse files

feat: new camera capture control #295

parent feb6bf24
Branches
Tags
No related merge requests found
......@@ -164,11 +164,11 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1738843498,
"narHash": "sha256-7x+Q4xgFj9UxZZO9aUDCR8h4vyYut4zPUvfj3i+jBHE=",
"lastModified": 1739923778,
"narHash": "sha256-BqUY8tz0AQ4to2Z4+uaKczh81zsGZSYxjgvtw+fvIfM=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "f5a32fa27df91dfc4b762671a0e0a859a8a0058f",
"rev": "36864ed72f234b9540da4cf7a0c49e351d30d3f1",
"type": "github"
},
"original": {
......
/**
* Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
* Node module: @schukai/monster
*
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
*
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
* For more information about purchasing a commercial license, please contact schukai GmbH.
*
* SPDX-License-Identifier: AGPL-3.0
*/
import {instanceSymbol} from "../../constants.mjs";
import {
ATTRIBUTE_ROLE,
} from "../../dom/constants.mjs";
import {CustomElement} from "../../dom/customelement.mjs";
import {
assembleMethodSymbol,
registerCustomElement,
} from "../../dom/customelement.mjs";
import {CameraCaptureStyleSheet} from "./stylesheet/camera-capture.mjs";
import "../form/button.mjs";
import "../state/state.mjs";
import {getLocaleOfDocument} from "../../dom/locale.mjs";
import {addErrorAttribute} from "../../dom/error.mjs";
import {Queue} from "../../types/queue.mjs";
export {CameraCapture};
/**
* @private
* @type {symbol}
*/
const controlElementSymbol = Symbol("copyElement");
/**
* @private
* @type {symbol}
*/
const videoElementSymbol = Symbol("videoElement");
/**
* @private
* @type {symbol}
*/
const takePictureButtonElementSymbol = Symbol("takePictureButtonElement");
/**
* @private
* @type {symbol}
*/
const canvasElementSymbol = Symbol("canvasElement");
/**
* @private
* @type {symbol}
*/
const queueSymbol = Symbol("queue");
/**
* @private
* @type {symbol}
*/
const emptyHistoryStateElementSymbol = Symbol("emptyHistoryStateElement");
/**
* This is a camera capture component.
*
* @fragments /fragments/components/content/camera-capture/
*
* @example /examples/components/content/camera-capture/
*
* @since 3.111.0
* @copyright schukai GmbH
* @summary A simple but powerful camera capture component. It can be used to capture images from the camera.
*/
class CameraCapture extends CustomElement {
/**
* Constructor for the CameraCapture class.
* Calls the parent class constructor.
*/
constructor() {
super();
this[queueSymbol] = new Queue();
}
/**
* This method is called by the `instanceof` operator.
* @return {symbol}
*/
static get [instanceSymbol]() {
return Symbol.for("@schukai/monster/components/content/camera-capture@instance");
}
/**
*
* @return {Components.Content.Copy
*/
[assembleMethodSymbol]() {
super[assembleMethodSymbol]();
initControlReferences.call(this);
initEventHandler.call(this);
initCameraControl.call(this);
return this;
}
/**
* This method is called when the element is connected to the dom.
*
* @return {void}
*/
connectedCallback() {
super.connectedCallback();
// const document = getDocument();
//
// for (const [, type] of Object.entries(["click", "touch"])) {
// // close on outside ui-events
// document.addEventListener(type, this[closeEventHandler]);
// }
//
// updatePopper.call(this);
// attachResizeObserver.call(this);
}
/**
* This method is called when the element is disconnected from the dom.
*
* @return {void}
*/
disconnectedCallback() {
super.disconnectedCallback();
// // close on outside ui-events
// for (const [, type] of Object.entries(["click", "touch"])) {
// document.removeEventListener(type, this[closeEventHandler]);
// }
}
/**
* To set the options via the HTML Tag, the attribute `data-monster-options` must be used.
* @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
*
* The individual configuration values can be found in the table.
*
* @property {Object} templates Template definitions
* @property {string} templates.main Main template
* @property {Object} actions Callbacks
* @property {string} actions.click="throw Error" Callback when clicked
* @property {Object} features Features
* @property {boolean} features.stripTags=true Strip tags from the copied text
* @property {boolean} features.preventOpenEventSent=false Prevent open event from being sent
* @property {Object} popper Popper configuration
* @property {string} popper.placement="top" Popper placement
* @property {string[]} popper.middleware=["autoPlacement", "shift", "offset:15", "arrow"] Popper middleware
* @property {boolean} disabled=false Disabled state
*/
get defaults() {
return Object.assign({}, super.defaults, {
templates: {
main: getTemplate(),
},
disabled: false,
features: {},
labels: getTranslations(),
});
}
/**
* @return {string}
*/
static getTag() {
return "monster-camera-capture";
}
/**
* @return {CSSStyleSheet[]}
*/
static getCSSStyleSheet() {
return [CameraCaptureStyleSheet];
}
/**
* Retrieve the next image from the queue.
* If the queue is empty, it returns `null`.
* @returns {string|null}
*/
getNextImage() {
if (!this[queueSymbol].isEmpty()) {
const next = this[queueSymbol].poll();
if (!next) {
return null;
}
return next;
}
return null;
}
/**
* Capture an image from the camera and add it to the queue.
* The image is returned as a data URL.
* @returns {string}
*/
capture() {
this[canvasElementSymbol].width = this[videoElementSymbol].videoWidth;
this[canvasElementSymbol].height = this[videoElementSymbol].videoHeight;
const ctx = this[canvasElementSymbol].getContext('2d');
ctx.drawImage(this[videoElementSymbol], 0, 0, this[canvasElementSymbol].width, this[canvasElementSymbol].height);
const dataURL = this[canvasElementSymbol].toDataURL('image/png');
this[queueSymbol].add(dataURL);
return dataURL;
}
}
function initCameraControl() {
const self = this;
if (!navigator || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
addErrorAttribute(self, "Browser not supported");
return;
}
navigator.mediaDevices.getUserMedia({video: true})
.then(function (stream) {
self[takePictureButtonElementSymbol].style.display = "block";
self[videoElementSymbol].style.display = "block";
self[emptyHistoryStateElementSymbol].style.display = "none";
self[videoElementSymbol].srcObject = stream;
})
.catch(function (e) {
addErrorAttribute(self, e);
});
}
/**
* @private
* @returns {{takePicture: string}}
*/
function getTranslations() {
const locale = getLocaleOfDocument();
switch (locale.language) {
case "de":
return {
takePicture: "Bild aufnehmen",
cameraNotSupportedOrNotAllowed: "Die Kamera wird nicht unterstützt oder die Berechtigung wurde nicht erteilt.",
};
case "es":
return {
takePicture: "Tomar una foto",
cameraNotSupportedOrNotAllowed: "La cámara no es compatible o no se ha otorgado permiso.",
}
case "zh":
return {
takePicture: "拍照",
cameraNotSupportedOrNotAllowed: "相机不受支持或未授予权限。",
}
case "hi":
return {
takePicture: "तस्वीर खींचें",
cameraNotSupportedOrNotAllowed: "कैमरा समर्थित नहीं है या अनुमति नहीं दी गई है।",
}
case "bn":
return {
takePicture: "ছবি তুলুন",
cameraNotSupportedOrNotAllowed: "ক্যামেরা সমর্থিত নয় বা অনুমতি দেয়া হয়নি।",
}
case "pt":
return {
takePicture: "Tirar uma foto",
cameraNotSupportedOrNotAllowed: "A câmera não é suportada ou a permissão não foi concedida.",
}
case "ru":
return {
takePicture: "Сделать фото",
cameraNotSupportedOrNotAllowed: "Камера не поддерживается или разрешение не предоставлено.",
}
case "ja":
return {
takePicture: "写真を撮る",
cameraNotSupportedOrNotAllowed: "カメラがサポートされていないか、許可が付与されていません。",
}
case "pa":
return {
takePicture: "ਤਸਵੀਰ ਖਿੱਚੋ",
cameraNotSupportedOrNotAllowed: "ਕੈਮਰਾ ਦਾ ਸਮਰਥਨ ਨਹੀਂ ਹੈ ਜਾਂ ਅਨੁਮਤੀ ਨਹੀਂ ਦਿੱਤੀ ਗਈ ਹੈ।",
}
case "mr":
return {
takePicture: "फोटो घ्या",
cameraNotSupportedOrNotAllowed: "कॅमेरा समर्थित नाही किंवा परवानगी दिलेली नाही.",
}
case "fr":
return {
takePicture: "Prendre une photo",
cameraNotSupportedOrNotAllowed: "La caméra n'est pas prise en charge ou l'autorisation n'a pas été accordée.",
}
case "it":
return {
takePicture: "Scattare una foto",
cameraNotSupportedOrNotAllowed: "La fotocamera non è supportata o l'autorizzazione non è stata concessa.",
}
case "nl":
return {
takePicture: "Maak een foto",
cameraNotSupportedOrNotAllowed: "De camera wordt niet ondersteund of er is geen toestemming verleend.",
}
case "sv":
return {
takePicture: "Ta ett foto",
cameraNotSupportedOrNotAllowed: "Kameran stöds inte eller tillståndet har inte beviljats.",
}
case "pl":
return {
takePicture: "Zrób zdjęcie",
cameraNotSupportedOrNotAllowed: "Kamera nie jest obsługiwana lub nie udzielono zgody.",
}
case "da":
return {
takePicture: "Tag et billede",
cameraNotSupportedOrNotAllowed: "Kameraen understøttes ikke eller tilladelsen er ikke givet.",
}
case "fi":
return {
takePicture: "Ota kuva",
cameraNotSupportedOrNotAllowed: "Kameraa ei tueta tai lupaa ei ole myönnetty.",
}
case "no":
return {
takePicture: "Ta et bilde",
cameraNotSupportedOrNotAllowed: "Kameraen støttes ikke eller tillatelsen er ikke gitt.",
}
case "cs":
return {
takePicture: "Vyfotit",
cameraNotSupportedOrNotAllowed: "Fotoaparát není podporován nebo povolení nebylo uděleno.",
}
case "en":
default:
return {
takePicture: "Take a picture",
cameraNotSupportedOrNotAllowed: "The camera is not supported or permission has not been granted.",
};
}
}
/**
* @private
* @return {initEventHandler}
*/
function initEventHandler() {
const self = this;
this[takePictureButtonElementSymbol].setOption("actions.click", function () {
self.capture();
});
return this;
}
/**
* @private
* @return {void}
*/
function initControlReferences() {
this[controlElementSymbol] = this.shadowRoot.querySelector(
`[${ATTRIBUTE_ROLE}="control"]`,
);
this[takePictureButtonElementSymbol] = this.shadowRoot.querySelector(
`[data-monster-role="takePicture"]`,
);
this[videoElementSymbol] = this.shadowRoot.querySelector(
`video`,
);
this[canvasElementSymbol] = this.shadowRoot.querySelector(
`canvas`,
);
// data-monster-role="emptyHistoryState"
this[emptyHistoryStateElementSymbol] = this.shadowRoot.querySelector(
`[data-monster-role="emptyHistoryState"]`,
);
}
/**
* @private
* @return {string}
*/
function getTemplate() {
// language=HTML
return `
<div data-monster-role="control" part="control">
<monster-state data-monster-role="emptyHistoryState">
<svg slot="visual" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="350"
height="350" viewBox="0 0 92.604 92.604">
<defs>
<linearGradient id="a" x1="336.587" x2="372.879" y1="218.625" y2="218.625"
gradientUnits="userSpaceOnUse" spreadMethod="pad">
<stop offset="0" style="stop-color:#fefe00"/>
<stop offset="1" style="stop-color:#ff43c6"/>
</linearGradient>
<linearGradient xlink:href="#a" id="p" x1="-2894.157" x2="-805.215" y1="-4285.143"
y2="-2196.201" gradientTransform="translate(135.207 194.415)scale(.04433)"
gradientUnits="userSpaceOnUse" spreadMethod="pad"/>
<linearGradient xlink:href="#a" id="q" x1="-2894.157" x2="-805.215" y1="-4285.143"
y2="-2196.201" gradientTransform="translate(135.207 194.415)scale(.04433)"
gradientUnits="userSpaceOnUse" spreadMethod="pad"/>
<linearGradient xlink:href="#a" id="o" x1="-2894.157" x2="-805.215" y1="-4285.143"
y2="-2196.201" gradientTransform="translate(135.207 194.415)scale(.04433)"
gradientUnits="userSpaceOnUse" spreadMethod="pad"/>
</defs>
<g style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:url(#o);fill-opacity:1;stroke-width:.0440952;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000;stop-opacity:1">
<path d="M8.983 34.64a9.33 9.33 0 0 1 9.33-9.33H74.29a9.33 9.33 0 0 1 9.33 9.33v13.995a9.33 9.33 0 0 1-9.33 9.33h-22.07c.673.755 1.54 1.51 2.478 2.215a32.7 32.7 0 0 0 4.23 2.66l.066.027.014.01a2.332 2.332 0 0 1-1.045 4.417H34.64a2.332 2.332 0 0 1-1.045-4.417l.014-.01.065-.033a23 23 0 0 0 1.25-.69 33 33 0 0 0 2.981-1.965c.933-.7 1.806-1.46 2.477-2.215h-22.07a9.33 9.33 0 0 1-9.329-9.33Zm9.33-4.665a4.665 4.665 0 0 0-4.665 4.665v13.995a4.665 4.665 0 0 0 4.665 4.664H74.29a4.665 4.665 0 0 0 4.665-4.664V34.64a4.665 4.665 0 0 0-4.665-4.665z"
style="font-variation-settings:normal;vector-effect:none;fill:url(#p);fill-opacity:1;stroke-width:.0440952;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000;stop-opacity:1"/>
<path d="M46.302 36.972a4.665 4.665 0 1 0 0 9.33 4.665 4.665 0 0 0 0-9.33m-9.33 4.665a9.33 9.33 0 1 1 18.66 0 9.33 9.33 0 0 1-18.66 0m32.655 0a2.332 2.332 0 1 1-4.665 0 2.332 2.332 0 0 1 4.665 0"
style="font-variation-settings:normal;vector-effect:none;fill:url(#q);fill-opacity:1;stroke-width:.0440952;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000;stop-opacity:1"/>
</g>
</svg>
<p data-monster-replace="path:labels.cameraNotSupportedOrNotAllowed | default:there was an error:string"></p>
</monster-state>
<video autoplay style="display:none"></video>
<canvas style="display:none;"></canvas>
<div>
<monster-button part="takePictureButton" style="display:none"
data-monster-role="takePicture" data-monster-attributes="classes.takePictureButton"
data-monster-replace="path:labels.takePicture"></monster-button>
</div>
</div>`;
}
registerCustomElement(CameraCapture);
@import "../../style/control.pcss";
@import "../../style/property.pcss";
@import "../../style/floating-ui.pcss";
[data-monster-role="control"] {
display: flex;
justify-content: space-between;
margin: 0;
padding: 0;
flex-direction: column;
box-shadow: var(--monster-box-shadow-1);
background-color: var(--monster-color-primary-4);
border-color: var(--monster-bg-color-primary-4);
border-radius: var(--monster-border-radius);
border-style: var(--monster-border-style);
border-width: var(--monster-border-width);
color: var(--monster-bg-color-primary-4);
}
video, canvas {
max-width: 100%;
max-height: 100%;
display: block;
}
monster-button::part(button) {
border: none;
}
monster-state {
}
......@@ -156,7 +156,7 @@ function getMonsterVersion() {
}
/** don't touch, replaced by make with package.json version */
monsterVersion = new Version("3.110.2");
monsterVersion = new Version("3.110.4");
return monsterVersion;
}
......@@ -7,7 +7,7 @@ describe('Monster', function () {
let monsterVersion
/** don´t touch, replaced by make with package.json version */
monsterVersion = new Version("3.110.2")
monsterVersion = new Version("3.110.4")
let m = getMonsterVersion();
......
......@@ -9,8 +9,8 @@
</head>
<body>
<div id="headline" style="display: flex;align-items: center;justify-content: center;flex-direction: column;">
<h1 style='margin-bottom: 0.1em;'>Monster 3.110.2</h1>
<div id="lastupdate" style='font-size:0.7em'>last update Mi 19. Feb 22:20:07 CET 2025</div>
<h1 style='margin-bottom: 0.1em;'>Monster 3.110.4</h1>
<div id="lastupdate" style='font-size:0.7em'>last update Sa 22. Feb 23:00:11 CET 2025</div>
</div>
<div id="mocha-errors"
style="color: red;font-weight: bold;display: flex;align-items: center;justify-content: center;flex-direction: column;margin:20px;"></div>
......
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment