feat(core): update session, security system and QR exchange

- Removed session creation and Lightning payment logic
- Refactored security system:
  * no more restrictions
  * all systems enabled on session creation
- Improved QR code exchange for mobile devices
This commit is contained in:
lockbitchat
2025-09-23 20:01:02 -04:00
parent 804b384271
commit 34094956b7
396 changed files with 126516 additions and 11881 deletions

16
node_modules/html5-qrcode/src/ui/scanner/base.d.ts generated vendored Normal file
View File

@@ -0,0 +1,16 @@
export declare class PublicUiElementIdAndClasses {
static ALL_ELEMENT_CLASS: string;
static CAMERA_PERMISSION_BUTTON_ID: string;
static CAMERA_START_BUTTON_ID: string;
static CAMERA_STOP_BUTTON_ID: string;
static TORCH_BUTTON_ID: string;
static CAMERA_SELECTION_SELECT_ID: string;
static FILE_SELECTION_BUTTON_ID: string;
static ZOOM_SLIDER_ID: string;
static SCAN_TYPE_CHANGE_ANCHOR_ID: string;
static TORCH_BUTTON_CLASS_TORCH_ON: string;
static TORCH_BUTTON_CLASS_TORCH_OFF: string;
}
export declare class BaseUiElementFactory {
static createElement<Type extends HTMLElement>(elementType: string, elementId: string): Type;
}

81
node_modules/html5-qrcode/src/ui/scanner/base.ts generated vendored Normal file
View File

@@ -0,0 +1,81 @@
/**
* @fileoverview
* Contains base classes for different UI elements used in the scanner.
*
* @author mebjas <minhazav@gmail.com>
*
* The word "QR Code" is registered trademark of DENSO WAVE INCORPORATED
* http://www.denso-wave.com/qrcode/faqpatent-e.html
*/
/**
* Id and classes of UI elements, for developers to configure the theme of
* end to end scanner using css.
*/
export class PublicUiElementIdAndClasses {
//#region Public list of element IDs for major UI elements.
/** Class name added to all major UI elements used in scanner. */
static ALL_ELEMENT_CLASS = "html5-qrcode-element";
/** Id of the camera permission button. */
static CAMERA_PERMISSION_BUTTON_ID = "html5-qrcode-button-camera-permission";
/** Id of the camera start button. */
static CAMERA_START_BUTTON_ID = "html5-qrcode-button-camera-start";
/** Id of the camera stop button. */
static CAMERA_STOP_BUTTON_ID = "html5-qrcode-button-camera-stop";
/** Id of the torch button. */
static TORCH_BUTTON_ID = "html5-qrcode-button-torch";
/** Id of the select element used for camera selection. */
static CAMERA_SELECTION_SELECT_ID = "html5-qrcode-select-camera";
/** Id of the button used for file selection. */
static FILE_SELECTION_BUTTON_ID = "html5-qrcode-button-file-selection";
/** Id of the range input for zoom. */
static ZOOM_SLIDER_ID = "html5-qrcode-input-range-zoom";
/**
* Id of the anchor {@code <a>} element used for swapping between file scan
* and camera scan.
*/
static SCAN_TYPE_CHANGE_ANCHOR_ID = "html5-qrcode-anchor-scan-type-change";
//#endregion
//#region List of classes for specific use-cases.
/** Torch button class when torch is ON. */
static TORCH_BUTTON_CLASS_TORCH_ON = "html5-qrcode-button-torch-on";
/** Torch button class when torch is OFF. */
static TORCH_BUTTON_CLASS_TORCH_OFF = "html5-qrcode-button-torch-off";
//#endregion
}
/**
* Factory class for creating different base UI elements used by the scanner.
*/
export class BaseUiElementFactory {
/**
* Creates {@link HTMLElement} of given {@param elementType}.
*
* @param elementType Type of element to create, example
*/
public static createElement<Type extends HTMLElement>(
elementType: string, elementId: string): Type {
let element: Type = <Type>(document.createElement(elementType));
element.id = elementId;
element.classList.add(PublicUiElementIdAndClasses.ALL_ELEMENT_CLASS);
if (elementType === "button") {
element.setAttribute("type", "button");
}
return element;
}
}

View File

@@ -0,0 +1,17 @@
import { CameraDevice } from "../../camera/core";
export declare class CameraSelectionUi {
private readonly selectElement;
private readonly options;
private readonly cameras;
private constructor();
private render;
disable(): void;
isDisabled(): boolean;
enable(): void;
getValue(): string;
hasValue(value: string): boolean;
setValue(value: string): void;
hasSingleItem(): boolean;
numCameras(): number;
static create(parentElement: HTMLElement, cameras: Array<CameraDevice>): CameraSelectionUi;
}

View File

@@ -0,0 +1,129 @@
/**
* @fileoverview
* File for camera selection UI.
*
* @author mebjas <minhazav@gmail.com>
*
* The word "QR Code" is registered trademark of DENSO WAVE INCORPORATED
* http://www.denso-wave.com/qrcode/faqpatent-e.html
*/
import { CameraDevice } from "../../camera/core";
import {
BaseUiElementFactory,
PublicUiElementIdAndClasses
} from "./base";
import {
Html5QrcodeScannerStrings
} from "../../strings";
/** Class for rendering and handling camera selection UI. */
export class CameraSelectionUi {
private readonly selectElement: HTMLSelectElement;
private readonly options: Array<HTMLOptionElement>;
private readonly cameras: Array<CameraDevice>;
private constructor(cameras: Array<CameraDevice>) {
this.selectElement = BaseUiElementFactory
.createElement<HTMLSelectElement>(
"select",
PublicUiElementIdAndClasses.CAMERA_SELECTION_SELECT_ID);
this.cameras = cameras;
this.options = [];
}
/*eslint complexity: ["error", 10]*/
private render(
parentElement: HTMLElement) {
const cameraSelectionContainer = document.createElement("span");
cameraSelectionContainer.style.marginRight = "10px";
const numCameras = this.cameras.length;
if (numCameras === 0) {
throw new Error("No cameras found");
}
if (numCameras === 1) {
// If only one camera is found, don't show camera selection.
cameraSelectionContainer.style.display = "none";
} else {
// Otherwise, show the number of cameras found as well.
const selectCameraString = Html5QrcodeScannerStrings.selectCamera();
cameraSelectionContainer.innerText
= `${selectCameraString} (${this.cameras.length}) `;
}
let anonymousCameraId = 1;
for (const camera of this.cameras) {
const value = camera.id;
let name = camera.label == null ? value : camera.label;
// If no name is returned by the browser, replace it with custom
// camera label with a count.
if (!name || name === "") {
name = [
Html5QrcodeScannerStrings.anonymousCameraPrefix(),
anonymousCameraId++
].join(" ");
}
const option = document.createElement("option");
option.value = value;
option.innerText = name;
this.options.push(option);
this.selectElement.appendChild(option);
}
cameraSelectionContainer.appendChild(this.selectElement);
parentElement.appendChild(cameraSelectionContainer);
}
//#region Public APIs
public disable() {
this.selectElement.disabled = true;
}
public isDisabled() {
return this.selectElement.disabled === true;
}
public enable() {
this.selectElement.disabled = false;
}
public getValue(): string {
return this.selectElement.value;
}
public hasValue(value: string): boolean {
for (const option of this.options) {
if (option.value === value) {
return true;
}
}
return false;
}
public setValue(value: string) {
if (!this.hasValue(value)) {
throw new Error(`${value} is not present in the camera list.`);
}
this.selectElement.value = value;
}
public hasSingleItem() {
return this.cameras.length === 1;
}
public numCameras() {
return this.cameras.length;
}
//#endregion
/** Creates instance of {@link CameraSelectionUi} and renders it. */
public static create(
parentElement: HTMLElement,
cameras: Array<CameraDevice>): CameraSelectionUi {
let cameraSelectUi = new CameraSelectionUi(cameras);
cameraSelectUi.render(parentElement);
return cameraSelectUi;
}
}

View File

@@ -0,0 +1,16 @@
export type OnCameraZoomValueChangeCallback = (zoomValue: number) => void;
export declare class CameraZoomUi {
private zoomElementContainer;
private rangeInput;
private rangeText;
private onChangeCallback;
private constructor();
private render;
private onValueChange;
setValues(minValue: number, maxValue: number, defaultValue: number, step: number): void;
show(): void;
hide(): void;
setOnCameraZoomValueChangeCallback(onChangeCallback: OnCameraZoomValueChangeCallback): void;
removeOnCameraZoomValueChangeCallback(): void;
static create(parentElement: HTMLElement, renderOnCreate: boolean): CameraZoomUi;
}

View File

@@ -0,0 +1,126 @@
/**
* @fileoverview
* File for camera zooming UI.
*
* @author mebjas <minhazav@gmail.com>
*
* The word "QR Code" is registered trademark of DENSO WAVE INCORPORATED
* http://www.denso-wave.com/qrcode/faqpatent-e.html
*/
import {
BaseUiElementFactory,
PublicUiElementIdAndClasses
} from "./base";
import { Html5QrcodeScannerStrings } from "../../strings";
/** Callback when zoom value changes with the slider UI. */
export type OnCameraZoomValueChangeCallback = (zoomValue: number) => void;
/** Class for creating and managing zoom slider UI. */
export class CameraZoomUi {
private zoomElementContainer: HTMLDivElement;
private rangeInput: HTMLInputElement;
private rangeText: HTMLSpanElement;
private onChangeCallback: OnCameraZoomValueChangeCallback | null = null;
private constructor() {
this.zoomElementContainer = document.createElement("div");
this.rangeInput = BaseUiElementFactory.createElement<HTMLInputElement>(
"input", PublicUiElementIdAndClasses.ZOOM_SLIDER_ID);
this.rangeInput.type = "range";
this.rangeText = document.createElement("span");
// default values.
this.rangeInput.min = "1";
this.rangeInput.max = "5";
this.rangeInput.value = "1";
this.rangeInput.step = "0.1";
}
private render(
parentElement: HTMLElement,
renderOnCreate: boolean) {
// Style for the range slider.
this.zoomElementContainer.style.display
= renderOnCreate ? "block" : "none";
this.zoomElementContainer.style.padding = "5px 10px";
this.zoomElementContainer.style.textAlign = "center";
parentElement.appendChild(this.zoomElementContainer);
this.rangeInput.style.display = "inline-block";
this.rangeInput.style.width = "50%";
this.rangeInput.style.height = "5px";
this.rangeInput.style.background = "#d3d3d3";
this.rangeInput.style.outline = "none";
this.rangeInput.style.opacity = "0.7";
let zoomString = Html5QrcodeScannerStrings.zoom();
this.rangeText.innerText = `${this.rangeInput.value}x ${zoomString}`;
this.rangeText.style.marginRight = "10px";
// Bind values.
let $this = this;
this.rangeInput.addEventListener("input", () => $this.onValueChange());
this.rangeInput.addEventListener("change", () => $this.onValueChange());
this.zoomElementContainer.appendChild(this.rangeInput);
this.zoomElementContainer.appendChild(this.rangeText);
}
private onValueChange() {
let zoomString = Html5QrcodeScannerStrings.zoom();
this.rangeText.innerText = `${this.rangeInput.value}x ${zoomString}`;
if (this.onChangeCallback) {
this.onChangeCallback(parseFloat(this.rangeInput.value));
}
}
//#region Public APIs
public setValues(
minValue: number,
maxValue: number,
defaultValue: number,
step: number) {
this.rangeInput.min = minValue.toString();
this.rangeInput.max = maxValue.toString();
this.rangeInput.step = step.toString();
this.rangeInput.value = defaultValue.toString();
this.onValueChange();
}
public show() {
this.zoomElementContainer.style.display = "block";
}
public hide() {
this.zoomElementContainer.style.display = "none";
}
public setOnCameraZoomValueChangeCallback(
onChangeCallback: OnCameraZoomValueChangeCallback) {
this.onChangeCallback = onChangeCallback;
}
public removeOnCameraZoomValueChangeCallback() {
this.onChangeCallback = null;
}
//#endregion
/**
* Creates and renders the zoom slider if {@code renderOnCreate} is
* {@code true}.
*/
public static create(
parentElement: HTMLElement,
renderOnCreate: boolean): CameraZoomUi {
let cameraZoomUi = new CameraZoomUi();
cameraZoomUi.render(parentElement, renderOnCreate);
return cameraZoomUi;
}
}

View File

@@ -0,0 +1,19 @@
export type OnFileSelected = (file: File) => void;
export declare class FileSelectionUi {
private readonly fileBasedScanRegion;
private readonly fileScanInput;
private readonly fileSelectionButton;
private constructor();
hide(): void;
show(): void;
isShowing(): boolean;
resetValue(): void;
private createFileBasedScanRegion;
private fileBasedScanRegionDefaultBorder;
private fileBasedScanRegionActiveBorder;
private createDragAndDropMessage;
private setImageNameToButton;
private setInitialValueToButton;
private getFileScanInputId;
static create(parentElement: HTMLDivElement, showOnRender: boolean, onFileSelected: OnFileSelected): FileSelectionUi;
}

View File

@@ -0,0 +1,263 @@
/**
* @fileoverview
* File for file selection UI handling.
*
* @author mebjas <minhazav@gmail.com>
*
* The word "QR Code" is registered trademark of DENSO WAVE INCORPORATED
* http://www.denso-wave.com/qrcode/faqpatent-e.html
*/
import {Html5QrcodeScannerStrings} from "../../strings";
import {
BaseUiElementFactory,
PublicUiElementIdAndClasses
} from "./base";
/**
* Interface for callback when a file is selected by user using the button.
*/
export type OnFileSelected = (file: File) => void;
/** UI class for file selection handling. */
export class FileSelectionUi {
private readonly fileBasedScanRegion: HTMLDivElement;
private readonly fileScanInput: HTMLInputElement;
private readonly fileSelectionButton: HTMLButtonElement;
/** Creates object and renders. */
private constructor(
parentElement: HTMLDivElement,
showOnRender: boolean,
onFileSelected: OnFileSelected) {
this.fileBasedScanRegion = this.createFileBasedScanRegion();
this.fileBasedScanRegion.style.display
= showOnRender ? "block" : "none";
parentElement.appendChild(this.fileBasedScanRegion);
let fileScanLabel = document.createElement("label");
fileScanLabel.setAttribute("for", this.getFileScanInputId());
fileScanLabel.style.display = "inline-block";
this.fileBasedScanRegion.appendChild(fileScanLabel);
this.fileSelectionButton
= BaseUiElementFactory.createElement<HTMLButtonElement>(
"button",
PublicUiElementIdAndClasses.FILE_SELECTION_BUTTON_ID);
this.setInitialValueToButton();
// Bind click events with the label element.
this.fileSelectionButton.addEventListener("click", (_) => {
fileScanLabel.click();
});
fileScanLabel.append(this.fileSelectionButton);
this.fileScanInput
= BaseUiElementFactory.createElement<HTMLInputElement>(
"input", this.getFileScanInputId());
this.fileScanInput.type = "file";
this.fileScanInput.accept = "image/*";
this.fileScanInput.style.display = "none";
fileScanLabel.appendChild(this.fileScanInput);
let $this = this;
/*eslint complexity: ["error", 5]*/
this.fileScanInput.addEventListener("change", (e: Event) => {
if (e == null || e.target == null) {
return;
}
let target: HTMLInputElement = e.target as HTMLInputElement;
if (target.files && target.files.length === 0) {
return;
}
let fileList: FileList = target.files!;
const file: File = fileList[0];
let fileName = file.name;
$this.setImageNameToButton(fileName);
onFileSelected(file);
});
// Render drag and drop label
let dragAndDropMessage = this.createDragAndDropMessage();
this.fileBasedScanRegion.appendChild(dragAndDropMessage);
this.fileBasedScanRegion.addEventListener("dragenter", function(event) {
$this.fileBasedScanRegion.style.border
= $this.fileBasedScanRegionActiveBorder();
event.stopPropagation();
event.preventDefault();
});
this.fileBasedScanRegion.addEventListener("dragleave", function(event) {
$this.fileBasedScanRegion.style.border
= $this.fileBasedScanRegionDefaultBorder();
event.stopPropagation();
event.preventDefault();
});
this.fileBasedScanRegion.addEventListener("dragover", function(event) {
$this.fileBasedScanRegion.style.border
= $this.fileBasedScanRegionActiveBorder();
event.stopPropagation();
event.preventDefault();
});
/*eslint complexity: ["error", 10]*/
this.fileBasedScanRegion.addEventListener("drop", function(event) {
event.stopPropagation();
event.preventDefault();
$this.fileBasedScanRegion.style.border
= $this.fileBasedScanRegionDefaultBorder();
var dataTransfer = event.dataTransfer;
if (dataTransfer) {
let files = dataTransfer.files;
if (!files || files.length === 0) {
return;
}
let isAnyFileImage = false;
for (let i = 0; i < files.length; ++i) {
let file = files.item(i);
if (!file) {
continue;
}
let imageType = /image.*/;
// Only process images.
if (!file.type.match(imageType)) {
continue;
}
isAnyFileImage = true;
let fileName = file.name;
$this.setImageNameToButton(fileName);
onFileSelected(file);
dragAndDropMessage.innerText
= Html5QrcodeScannerStrings.dragAndDropMessage();
break;
}
// None of the files were images.
if (!isAnyFileImage) {
dragAndDropMessage.innerText
= Html5QrcodeScannerStrings
.dragAndDropMessageOnlyImages();
}
}
});
}
//#region Public APIs.
/** Hide the file selection UI. */
public hide() {
this.fileBasedScanRegion.style.display = "none";
this.fileScanInput.disabled = true;
}
/** Show the file selection UI. */
public show() {
this.fileBasedScanRegion.style.display = "block";
this.fileScanInput.disabled = false;
}
/** Returns {@code true} if UI container is displayed. */
public isShowing(): boolean {
return this.fileBasedScanRegion.style.display === "block";
}
/** Reset the file selection value */
public resetValue() {
this.fileScanInput.value = "";
this.setInitialValueToButton();
}
//#endregion
//#region private APIs
private createFileBasedScanRegion(): HTMLDivElement {
let fileBasedScanRegion = document.createElement("div");
fileBasedScanRegion.style.textAlign = "center";
fileBasedScanRegion.style.margin = "auto";
fileBasedScanRegion.style.width = "80%";
fileBasedScanRegion.style.maxWidth = "600px";
fileBasedScanRegion.style.border
= this.fileBasedScanRegionDefaultBorder();
fileBasedScanRegion.style.padding = "10px";
fileBasedScanRegion.style.marginBottom = "10px";
return fileBasedScanRegion;
}
private fileBasedScanRegionDefaultBorder() {
return "6px dashed #ebebeb";
}
/** Border when a file is being dragged over the file scan region. */
private fileBasedScanRegionActiveBorder() {
return "6px dashed rgb(153 151 151)";
}
private createDragAndDropMessage(): HTMLDivElement {
let dragAndDropMessage = document.createElement("div");
dragAndDropMessage.innerText
= Html5QrcodeScannerStrings.dragAndDropMessage();
dragAndDropMessage.style.fontWeight = "400";
return dragAndDropMessage;
}
private setImageNameToButton(imageFileName: string) {
const MAX_CHARS = 20;
if (imageFileName.length > MAX_CHARS) {
// Strip first 8
// Strip last 8
// Add 4 dots
let start8Chars = imageFileName.substring(0, 8);
let length = imageFileName.length;
let last8Chars = imageFileName.substring(length - 8, length);
imageFileName = `${start8Chars}....${last8Chars}`;
}
let newText = Html5QrcodeScannerStrings.fileSelectionChooseAnother()
+ " - "
+ imageFileName;
this.fileSelectionButton.innerText = newText;
}
private setInitialValueToButton() {
let initialText = Html5QrcodeScannerStrings.fileSelectionChooseImage()
+ " - "
+ Html5QrcodeScannerStrings.fileSelectionNoImageSelected();
this.fileSelectionButton.innerText = initialText;
}
private getFileScanInputId(): string {
return "html5-qrcode-private-filescan-input";
}
//#endregion
/**
* Creates a file selection UI and renders.
*
* @param parentElement parent div element to render the UI to.
* @param showOnRender if {@code true}, the UI will be shown upon render
* else hidden.
* @param onFileSelected callback to be called when file selection changes.
*
* @returns Instance of {@code FileSelectionUi}.
*/
public static create(
parentElement: HTMLDivElement,
showOnRender: boolean,
onFileSelected: OnFileSelected): FileSelectionUi {
let button = new FileSelectionUi(
parentElement, showOnRender, onFileSelected);
return button;
}
}

View File

@@ -0,0 +1,11 @@
import { Html5QrcodeScanType } from "../../core";
export declare class ScanTypeSelector {
private supportedScanTypes;
constructor(supportedScanTypes?: Array<Html5QrcodeScanType> | []);
getDefaultScanType(): Html5QrcodeScanType;
hasMoreThanOneScanType(): boolean;
isCameraScanRequired(): boolean;
static isCameraScanType(scanType: Html5QrcodeScanType): boolean;
static isFileScanType(scanType: Html5QrcodeScanType): boolean;
private validateAndReturnScanTypes;
}

View File

@@ -0,0 +1,94 @@
/**
* @fileoverview
* Util class to help with scan type selection in scanner class.
*
* @author mebjas <minhazav@gmail.com>
*
* The word "QR Code" is registered trademark of DENSO WAVE INCORPORATED
* http://www.denso-wave.com/qrcode/faqpatent-e.html
*/
import {
Html5QrcodeScanType,
Html5QrcodeConstants
} from "../../core";
/** Util class to help with scan type selection in scanner class. */
export class ScanTypeSelector {
private supportedScanTypes: Array<Html5QrcodeScanType>;
constructor(supportedScanTypes?: Array<Html5QrcodeScanType> | []) {
this.supportedScanTypes = this.validateAndReturnScanTypes(
supportedScanTypes);
}
/**
* Returns the default {@link Html5QrcodeScanType} scanner UI should be
* created with.
*/
public getDefaultScanType(): Html5QrcodeScanType {
return this.supportedScanTypes[0];
}
/**
* Returns {@code true} if more than one {@link Html5QrcodeScanType} are
* set.
*/
public hasMoreThanOneScanType(): boolean {
return this.supportedScanTypes.length > 1;
}
/** Returns {@code true} if camera scan is required at all. */
public isCameraScanRequired(): boolean {
for (const scanType of this.supportedScanTypes) {
if (ScanTypeSelector.isCameraScanType(scanType)) {
return true;
}
}
return false;
}
/** Returns {@code true} is {@code scanType} is camera based. */
public static isCameraScanType(scanType: Html5QrcodeScanType): boolean {
return scanType === Html5QrcodeScanType.SCAN_TYPE_CAMERA;
}
/** Returns {@code true} is {@code scanType} is file based. */
public static isFileScanType(scanType: Html5QrcodeScanType): boolean {
return scanType === Html5QrcodeScanType.SCAN_TYPE_FILE;
}
//#region Private methods.
/**
* Validates the input {@code supportedScanTypes}.
*
* Fails early if the config values is incorrectly set.
*/
private validateAndReturnScanTypes(
supportedScanTypes?:Array<Html5QrcodeScanType>):
Array<Html5QrcodeScanType> {
// If not set, use the default values and order.
if (!supportedScanTypes || supportedScanTypes.length === 0) {
return Html5QrcodeConstants.DEFAULT_SUPPORTED_SCAN_TYPE;
}
// Fail if more than expected number of values exist.
let maxExpectedValues
= Html5QrcodeConstants.DEFAULT_SUPPORTED_SCAN_TYPE.length;
if (supportedScanTypes.length > maxExpectedValues) {
throw `Max ${maxExpectedValues} values expected for `
+ "supportedScanTypes";
}
// Fail if any of the scan types are not supported.
for (const scanType of supportedScanTypes) {
if (!Html5QrcodeConstants.DEFAULT_SUPPORTED_SCAN_TYPE
.includes(scanType)) {
throw `Unsupported scan type ${scanType}`;
}
}
return supportedScanTypes;
}
//#endregion
}

View File

@@ -0,0 +1,28 @@
import { BooleanCameraCapability } from "../../camera/core";
export type OnTorchActionFailureCallback = (failureMessage: string) => void;
interface TorchButtonController {
disable(): void;
enable(): void;
setText(text: string): void;
}
export interface TorchButtonOptions {
display: string;
marginLeft: string;
}
export declare class TorchButton implements TorchButtonController {
private readonly torchButton;
private readonly onTorchActionFailureCallback;
private torchController;
private constructor();
private render;
updateTorchCapability(torchCapability: BooleanCameraCapability): void;
getTorchButton(): HTMLButtonElement;
hide(): void;
show(): void;
disable(): void;
enable(): void;
setText(text: string): void;
reset(): void;
static create(parentElement: HTMLElement, torchCapability: BooleanCameraCapability, torchButtonOptions: TorchButtonOptions, onTorchActionFailureCallback: OnTorchActionFailureCallback): TorchButton;
}
export {};

View File

@@ -0,0 +1,227 @@
/**
* @fileoverview
* File for torch related UI components and handling.
*
* @author mebjas <minhazav@gmail.com>
*
* The word "QR Code" is registered trademark of DENSO WAVE INCORPORATED
* http://www.denso-wave.com/qrcode/faqpatent-e.html
*/
import { BooleanCameraCapability } from "../../camera/core";
import { Html5QrcodeScannerStrings } from "../../strings";
import {
BaseUiElementFactory,
PublicUiElementIdAndClasses
} from "./base";
/**
* Interface for callback that will be called in case of torch action failures.
*/
export type OnTorchActionFailureCallback = (failureMessage: string) => void;
/** Interface for controlling torch button. */
interface TorchButtonController {
disable(): void;
enable(): void;
setText(text: string): void;
}
/** Controller class for handling torch / flash. */
class TorchController {
private readonly torchCapability: BooleanCameraCapability;
private readonly buttonController: TorchButtonController;
private readonly onTorchActionFailureCallback: OnTorchActionFailureCallback;
// Mutable states.
private isTorchOn: boolean = false;
constructor(
torchCapability: BooleanCameraCapability,
buttonController: TorchButtonController,
onTorchActionFailureCallback: OnTorchActionFailureCallback) {
this.torchCapability = torchCapability;
this.buttonController = buttonController;
this.onTorchActionFailureCallback = onTorchActionFailureCallback;
}
/** Returns {@code true} if torch is enabled. */
public isTorchEnabled(): boolean {
return this.isTorchOn;
}
/**
* Flips the state of the torch.
*
* <p> Turns torch On if current state is Off and vice-versa.
* <p> Modifies the UI state accordingly.
*
* @returns Promise that finishes when the async action is done.
*/
public async flipState(): Promise<void> {
this.buttonController.disable();
let isTorchOnExpected = !this.isTorchOn;
try {
await this.torchCapability.apply(isTorchOnExpected);
this.updateUiBasedOnLatestSettings(
this.torchCapability.value()!, isTorchOnExpected);
} catch (error) {
this.propagateFailure(isTorchOnExpected, error);
this.buttonController.enable();
}
}
private updateUiBasedOnLatestSettings(
isTorchOn: boolean,
isTorchOnExpected: boolean) {
if (isTorchOn === isTorchOnExpected) {
// Action succeeded, flip the state.
this.buttonController.setText(isTorchOnExpected
? Html5QrcodeScannerStrings.torchOffButton()
: Html5QrcodeScannerStrings.torchOnButton());
this.isTorchOn = isTorchOnExpected;
} else {
// Torch didn't get set as expected.
// Show warning.
this.propagateFailure(isTorchOnExpected);
}
this.buttonController.enable();
}
private propagateFailure(
isTorchOnExpected: boolean, error?: any) {
let errorMessage = isTorchOnExpected
? Html5QrcodeScannerStrings.torchOnFailedMessage()
: Html5QrcodeScannerStrings.torchOffFailedMessage();
if (error) {
errorMessage += "; Error = " + error;
}
this.onTorchActionFailureCallback(errorMessage);
}
/**
* Resets the state.
*
* <p>Note: Doesn't turn off the torch implicitly.
*/
public reset() {
this.isTorchOn = false;
}
}
/** Options for creating torch button. */
export interface TorchButtonOptions {
display: string;
marginLeft: string;
}
/** Helper class for creating Torch UI component. */
export class TorchButton implements TorchButtonController {
private readonly torchButton: HTMLButtonElement;
private readonly onTorchActionFailureCallback: OnTorchActionFailureCallback;
private torchController: TorchController;
private constructor(
torchCapability: BooleanCameraCapability,
onTorchActionFailureCallback: OnTorchActionFailureCallback) {
this.onTorchActionFailureCallback = onTorchActionFailureCallback;
this.torchButton
= BaseUiElementFactory.createElement<HTMLButtonElement>(
"button", PublicUiElementIdAndClasses.TORCH_BUTTON_ID);
this.torchController = new TorchController(
torchCapability,
/* buttonController= */ this,
onTorchActionFailureCallback);
}
private render(
parentElement: HTMLElement, torchButtonOptions: TorchButtonOptions) {
this.torchButton.innerText
= Html5QrcodeScannerStrings.torchOnButton();
this.torchButton.style.display = torchButtonOptions.display;
this.torchButton.style.marginLeft = torchButtonOptions.marginLeft;
let $this = this;
this.torchButton.addEventListener("click", async (_) => {
await $this.torchController.flipState();
if ($this.torchController.isTorchEnabled()) {
$this.torchButton.classList.remove(
PublicUiElementIdAndClasses.TORCH_BUTTON_CLASS_TORCH_OFF);
$this.torchButton.classList.add(
PublicUiElementIdAndClasses.TORCH_BUTTON_CLASS_TORCH_ON);
} else {
$this.torchButton.classList.remove(
PublicUiElementIdAndClasses.TORCH_BUTTON_CLASS_TORCH_ON);
$this.torchButton.classList.add(
PublicUiElementIdAndClasses.TORCH_BUTTON_CLASS_TORCH_OFF);
}
});
parentElement.appendChild(this.torchButton);
}
public updateTorchCapability(torchCapability: BooleanCameraCapability) {
this.torchController = new TorchController(
torchCapability,
/* buttonController= */ this,
this.onTorchActionFailureCallback);
}
/** Returns the torch button. */
public getTorchButton(): HTMLButtonElement {
return this.torchButton;
}
public hide() {
this.torchButton.style.display = "none";
}
public show() {
this.torchButton.style.display = "inline-block";
}
disable(): void {
this.torchButton.disabled = true;
}
enable(): void {
this.torchButton.disabled = false;
}
setText(text: string): void {
this.torchButton.innerText = text;
}
/**
* Resets the state.
*
* <p>Note: Doesn't turn off the torch implicitly.
*/
public reset() {
this.torchButton.innerText = Html5QrcodeScannerStrings.torchOnButton();
this.torchController.reset();
}
/**
* Factory method for creating torch button.
*
* @param parentElement parent HTML element to render torch button into
* @param torchCapability torch capability of the camera
* @param torchButtonOptions options for creating torch
* @param onTorchActionFailureCallback callback to be called in case of
* torch action failure.
*/
public static create(
parentElement: HTMLElement,
torchCapability: BooleanCameraCapability,
torchButtonOptions: TorchButtonOptions,
onTorchActionFailureCallback: OnTorchActionFailureCallback)
: TorchButton {
let button = new TorchButton(
torchCapability, onTorchActionFailureCallback);
button.render(parentElement, torchButtonOptions);
return button;
}
}