Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion run/input/UPSTREAM_COMMIT
Original file line number Diff line number Diff line change
@@ -1 +1 @@
df978ccebf3bef0d72ac2a03c08f7d2f9a230f87
504ece74ae6cd2b5b033c540bb2e1f1910c829d5
64 changes: 56 additions & 8 deletions run/input/core/app/z2ui5/webapp/cc/CameraPicture.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ sap.ui.define(
"sap/ui/core/Control",
"sap/m/Dialog",
"sap/m/Button",
"sap/m/Text",
"sap/ui/core/HTML",
"z2ui5/core/Lib",
],
(Control, Dialog, Button, HTML, Lib) => {
(Control, Dialog, Button, Text, HTML, Lib) => {
"use strict";
// Camera button: opens a dialog with the live camera stream, captures
// a photo on demand and hands it to the backend as a base64 JPEG in
Expand Down Expand Up @@ -47,18 +48,28 @@ sap.ui.define(
},
},

// Returns true when a photo was taken, false when it could not be (so
// the caller can keep the dialog open and let the status line explain).
capture() {
const video = document.getElementById(`${this.getId()}-video`);
const canvas = document.getElementById(`${this.getId()}-canvas`);
if (!video || !canvas) return;
if (!video || !canvas) return false;

const videoWidth = video.videoWidth;
const videoHeight = video.videoHeight;
// The camera may not have delivered a frame yet (Capture pressed too
// early, or the stream failed): videoWidth/Height are 0, which would
// make the canvas 0-sized and drawImage() throw an InvalidStateError.
if (!videoWidth || !videoHeight) {
this._setStatus("Camera not ready yet - no frame captured.");
Lib.logError("CameraPicture: camera not ready, no frame to capture");
return false;
}
canvas.width = videoWidth;
canvas.height = videoHeight;

const ctx = canvas.getContext("2d", _CTX_2D_OPTS);
if (!ctx) return;
if (!ctx) return false;
ctx.drawImage(video, 0, 0, videoWidth, videoHeight);

// Full-resolution JPEG (quality 0.85) for the value, plus a small
Expand All @@ -67,8 +78,9 @@ sap.ui.define(
try {
resultb64 = canvas.toDataURL("image/jpeg", 0.85);
} catch (e) {
this._setStatus("Could not read the captured image.");
Lib.logError("CameraPicture: canvas toDataURL failed", e);
return;
return false;
}

const thumbH = videoWidth
Expand All @@ -87,11 +99,12 @@ sap.ui.define(
Lib.logError("CameraPicture: thumb toDataURL failed", e);
}

if (Lib.isDestroyed(this)) return;
if (Lib.isDestroyed(this)) return false;
this.setProperty("value", resultb64);
this.setProperty("thumbnail", thumbB64);
this.fireOnPhoto({ photo: resultb64 });
this._stopCamera();
return true;
},

_stopCamera() {
Expand All @@ -107,6 +120,11 @@ sap.ui.define(
onPicture() {
if (this._oScanDialog?.isOpen()) return;
if (!this._oScanDialog) {
// Visible status line inside the dialog so the user is told what is
// happening (starting, ready, or why the camera stays black).
this._oStatus = new Text().addStyleClass(
"sapUiSmallMarginBegin sapUiSmallMarginTop",
);
this._oScanDialog = new Dialog({
title: "Device Photo Function",
contentWidth: "640px",
Expand All @@ -116,15 +134,21 @@ sap.ui.define(
stretch: true,
afterClose: () => this._stopCamera(),
content: [
this._oStatus,
new HTML({
id: `${this.getId()}PictureContainer`,
content: `<video style="width:100%;height:100%;object-fit:contain;"${this.getAutoplay() ? " autoplay" : ""} id="${this.getId()}-video">`,
// playsinline + muted are required for autoplay of a live
// stream on iOS/Safari; min-height keeps the preview visible
// even if the dialog content box does not give it a height;
// the tag must be explicitly closed or the parser mangles it.
content: `<video style="width:100%;height:100%;min-height:60vh;object-fit:contain;background:#000;" playsinline muted${this.getAutoplay() ? " autoplay" : ""} id="${this.getId()}-video"></video>`,
}),
new Button({
text: "Capture",
// Keep the dialog open when the capture failed so the status
// line stays visible to explain why.
press: () => {
this.capture();
this._oScanDialog.close();
if (this.capture()) this._oScanDialog.close();
},
}),
new HTML({
Expand All @@ -141,10 +165,13 @@ sap.ui.define(
});
}

this._setStatus("Starting camera...");

this._oScanDialog.attachEventOnce("afterOpen", async () => {
if (Lib.isDestroyed(this)) return;
const video = document.getElementById(`${this.getId()}-video`);
if (!video) {
this._setStatus("Camera preview element not found.");
Lib.logError(
"CameraPicture: video element not found after dialog open",
);
Expand All @@ -161,6 +188,9 @@ sap.ui.define(
try {
const md = navigator.mediaDevices;
if (!md?.getUserMedia) {
this._setStatus(
"Camera not available - a secure (HTTPS) connection is required.",
);
Lib.logError("CameraPicture: mediaDevices API not available");
return;
}
Expand All @@ -177,13 +207,31 @@ sap.ui.define(
}
this._stream = stream;
video.srcObject = stream;
this._setStatus("Camera ready - press Capture to take a photo.");
// Some browsers do not honour the autoplay attribute for a
// srcObject stream - start playback explicitly.
try {
await video.play();
} catch (e) {
Lib.logError("CameraPicture: video.play() failed", e);
}
} catch (error) {
this._setStatus(
`Camera unavailable: ${error.name || "Error"} - ${error.message}`,
);
Lib.logError("CameraPicture: getUserMedia failed", error);
}
});
this._oScanDialog.open();
},

// Update the status line inside the camera dialog, if it exists.
_setStatus(message) {
if (this._oStatus && !Lib.isDestroyed(this._oStatus)) {
this._oStatus.setText(message);
}
},

exit() {
this._stopCamera();
if (this._oButton) this._oButton.destroy();
Expand Down
8 changes: 7 additions & 1 deletion run/input/core/app/z2ui5/webapp/cc/CameraSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,14 @@ sap.ui.define(
// ComboBox pre-filled with the device's cameras (video inputs) so the
// user can pick which one the CameraPicture control should use.
return ComboBox.extend("z2ui5.cc.CameraSelector", {
async init() {
// init() is a UI5 lifecycle listener and must not return a value, so it
// cannot be async - kick off the (async) device enumeration separately.
init() {
ComboBox.prototype.init.call(this);
this._loadCameras();
},

async _loadCameras() {
try {
const md = navigator.mediaDevices;
if (!md?.enumerateDevices) return;
Expand Down
24 changes: 22 additions & 2 deletions run/input/core/app/z2ui5/webapp/cc/Geolocation.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ sap.ui.define(["sap/ui/core/Control", "z2ui5/core/Lib"], (Control, Lib) => {
allowPreventDefault: true,
parameters: {},
},
// Fired when the position could not be read, so a backend can
// react. The control never surfaces any UI itself - handling is
// delegated entirely to whoever binds this event.
error: {
parameters: {
code: { type: "string" },
message: { type: "string" },
},
},
},
},

Expand All @@ -71,6 +80,18 @@ sap.ui.define(["sap/ui/core/Control", "z2ui5/core/Lib"], (Control, Lib) => {
this.fireFinished();
},

// Reading the position failed (1 = permission denied, 2 = position
// unavailable, 3 = timeout). Log it and fire the `error` event so a
// backend can handle it; the control never surfaces UI on its own.
callbackError(error) {
if (Lib.isDestroyed(this)) return;
Lib.logError(`Geolocation error (${error.code}): ${error.message}`);
this.fireError({
code: String(error.code),
message: error.message,
});
},

init() {
this._pendingGeolocate = true;
},
Expand All @@ -86,8 +107,7 @@ sap.ui.define(["sap/ui/core/Control", "z2ui5/core/Lib"], (Control, Lib) => {
if (!navigator.geolocation) return;
navigator.geolocation.getCurrentPosition(
this.callbackPosition.bind(this),
(error) =>
Lib.logError(`Geolocation error (${error.code}): ${error.message}`),
this.callbackError.bind(this),
{
enableHighAccuracy: this.getProperty("enableHighAccuracy"),
// Guard against an empty or non-numeric property - NaN or 0
Expand Down
20 changes: 9 additions & 11 deletions run/input/core/app/z2ui5/webapp/cc/SmartMultiInputExt.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,15 @@ sap.ui.define(
init() {
this._setControlBound = this.setControl.bind(this);
this._oInput = null;
this._oPendingInnerControlsCreated = null;
this._aPendingInnerControlsCreated = [];
this._bInnerControlsCreated = false;
Lib.registerCallback("onAfterRendering", this._setControlBound);
},
exit() {
Lib.unregisterCallback("onAfterRendering", this._setControlBound);
// Resolve any still-pending promise so awaiters don't hang.
if (this._oPendingInnerControlsCreated) {
this._oPendingInnerControlsCreated(null);
}
this._oPendingInnerControlsCreated = null;
// Resolve any still-pending promises so awaiters don't hang.
this._aPendingInnerControlsCreated.forEach((resolve) => resolve(null));
this._aPendingInnerControlsCreated = [];
},

onTokenUpdate(oEvent) {
Expand Down Expand Up @@ -125,17 +123,17 @@ sap.ui.define(
if (this._bInnerControlsCreated) {
resolve(this._oInput);
} else {
this._oPendingInnerControlsCreated = resolve;
this._aPendingInnerControlsCreated.push(resolve);
}
});
},
onInnerControlsCreated(oEvent) {
this._oInput = oEvent.getSource();
if (this._oPendingInnerControlsCreated) {
this._oPendingInnerControlsCreated(this._oInput);
}
this._oPendingInnerControlsCreated = null;
this._bInnerControlsCreated = true;
this._aPendingInnerControlsCreated.forEach((resolve) =>
resolve(this._oInput),
);
this._aPendingInnerControlsCreated = [];
},
});
},
Expand Down
53 changes: 24 additions & 29 deletions run/input/core/app/z2ui5/webapp/controller/View1.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ sap.ui.define(
"z2ui5/core/Server",
"sap/ui/model/odata/v2/ODataModel",
"sap/ui/core/routing/HashChanger",
"sap/ui/core/Element",
"z2ui5/core/Lib",
"z2ui5/core/FrontendAction",
"z2ui5/core/ViewSlots",
Expand All @@ -32,7 +31,6 @@ sap.ui.define(
Server,
ODataModel,
HashChanger,
Element,
Lib,
FrontendAction,
ViewSlots,
Expand Down Expand Up @@ -128,6 +126,8 @@ sap.ui.define(

if (S_POPUP?.CHECK_DESTROY) this.destroyPopup();
if (S_POPOVER?.CHECK_DESTROY) this.destroyPopover();
if (S_VIEW_NEST?.CHECK_DESTROY) this.destroyNestView();
if (S_VIEW_NEST2?.CHECK_DESTROY) this.destroyNestView2();

if (S_POPUP?.XML) {
this.destroyPopup();
Expand Down Expand Up @@ -205,6 +205,9 @@ sap.ui.define(
return;
}
oFragment.setModel(oModel);
// Share the one device model (created once in Component.js, never
// destroyed) so {device>...} bindings work in popups too.
oFragment.setModel(AppState.state.oDeviceModel, "device");
ViewSlots.setView("POPUP", oFragment);
oFragment.open();
},
Expand All @@ -229,30 +232,12 @@ sap.ui.define(
return;
}
oFragment.setModel(oModel);
// Shared device model (see displayFragment) - for popovers too.
oFragment.setModel(AppState.state.oDeviceModel, "device");

// Find the control to attach the popover to. We search the main
// view first, then any open popup / nested views, then the global
// UI5 control registry as a last resort.
let oControl =
ViewSlots.byId("MAIN", openById) ||
ViewSlots.byId("POPUP", openById) ||
ViewSlots.byId("NEST", openById) ||
ViewSlots.byId("NEST2", openById);
if (!oControl) {
if (Element.getElementById) {
oControl = Element.getElementById(openById);
} else {
/* ui5lint-disable no-globals, no-deprecated-api --
deliberate fallback for UI5 releases that do not provide
Element.getElementById yet (added in 1.119); the modern
API is used in the branch above. */
if (sap.ui.getCore) {
const core = sap.ui.getCore();
if (core?.byId) oControl = core.byId(openById);
}
/* ui5lint-enable no-globals, no-deprecated-api */
}
}
// Find the control to attach the popover to: any open slot first,
// then the global UI5 control registry as a last resort.
const oControl = ViewSlots.resolveById(openById);

if (!oControl) {
Lib.logError(
Expand Down Expand Up @@ -280,6 +265,8 @@ sap.ui.define(
return;
}
oView.setModel(oModel);
// Shared device model (see displayFragment) - for nested views too.
oView.setModel(AppState.state.oDeviceModel, "device");

const nestParams = AppState.state.oResponse?.PARAMS?.[paramKey];
if (!nestParams) {
Expand All @@ -298,10 +285,18 @@ sap.ui.define(
return;
}

try {
oParent[METHOD_DESTROY]();
} catch (e) {
Lib.logError("displayNestedView: parent destroy method failed", e);
// METHOD_DESTROY is optional: only call it when the app asked for a
// parent teardown method. An empty value used to reach oParent[""]()
// and throw on every render (e.g. app 065 passes only method_insert).
if (METHOD_DESTROY) {
try {
oParent[METHOD_DESTROY]();
} catch (e) {
Lib.logError(
`displayNestedView: parent destroy method '${METHOD_DESTROY}' failed`,
e,
);
}
}
try {
oParent[METHOD_INSERT](oView);
Expand Down
8 changes: 6 additions & 2 deletions run/input/core/app/z2ui5/webapp/core/DebugTool.fragment.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@
>
<items>
<IconTabFilter
text="Config"
key="CONFIG"
text="Log"
key="LOG"
/>
<IconTabFilter
text="System"
key="SYSTEM"
enabled="true"
/>
<IconTabFilter
Expand Down
Loading