From d572b0651582b753d2d5f4061be117a267ad9667 Mon Sep 17 00:00:00 2001 From: drbeat Date: Wed, 6 Dec 2023 17:20:16 +0100 Subject: [PATCH] add react structure --- .eslintrc.cjs | 7 +- frontend/index.html | 23 + frontend/public/css/basicui.css | 16 - frontend/src/actions/root.js | 92 +++ frontend/src/actions/service.js | 67 ++ frontend/src/actions/servicecalls.js | 541 +++++++++++++ frontend/src/actions/settings.js | 101 +++ .../assets/icon_lightModeBlack.png | Bin .../assets/icon_lightModeBlue.png | Bin .../assets/icon_lightModeDark.png | Bin frontend/src/components/Navigation.jsx | 24 + frontend/src/components/Status.jsx | 31 + frontend/src/components/ThemeSwitch.jsx | 27 + frontend/src/config/store.js | 46 ++ frontend/src/config/view.js | 0 frontend/{public => src}/index.html | 0 frontend/src/index.jsx | 7 + frontend/src/ui.js | 102 --- frontend/src/view/About.jsx | 87 ++ frontend/src/view/App.jsx | 47 ++ frontend/src/view/Logs.jsx | 37 + frontend/src/view/Service.jsx | 26 + frontend/src/view/Settings.jsx | 213 +++++ package-lock.json | 750 +++++++++++++----- package.json | 9 +- vite.config.js | 49 +- 26 files changed, 1951 insertions(+), 351 deletions(-) create mode 100644 frontend/index.html create mode 100644 frontend/src/actions/root.js create mode 100644 frontend/src/actions/service.js create mode 100644 frontend/src/actions/servicecalls.js create mode 100644 frontend/src/actions/settings.js rename frontend/{public => src}/assets/icon_lightModeBlack.png (100%) rename frontend/{public => src}/assets/icon_lightModeBlue.png (100%) rename frontend/{public => src}/assets/icon_lightModeDark.png (100%) create mode 100644 frontend/src/components/Navigation.jsx create mode 100644 frontend/src/components/Status.jsx create mode 100644 frontend/src/components/ThemeSwitch.jsx create mode 100644 frontend/src/config/store.js create mode 100644 frontend/src/config/view.js rename frontend/{public => src}/index.html (100%) create mode 100644 frontend/src/index.jsx create mode 100644 frontend/src/view/About.jsx create mode 100644 frontend/src/view/App.jsx create mode 100644 frontend/src/view/Logs.jsx create mode 100644 frontend/src/view/Service.jsx create mode 100644 frontend/src/view/Settings.jsx diff --git a/.eslintrc.cjs b/.eslintrc.cjs index e95b487..c4836a2 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -3,8 +3,9 @@ module.exports = { browser: true, }, extends: [ - 'airbnb-base', - 'plugin:compat/recommended' + 'airbnb', + 'eslint:recommended', + 'plugin:react/recommended', ], overrides: [ ], @@ -16,6 +17,6 @@ module.exports = { 'no-console': 0, 'no-bitwise': 0, 'no-await-in-loop': 0, - 'no-constant-condition': 0 + 'no-constant-condition': 0, }, }; diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..87550d9 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,23 @@ + + + + + + PicCap - Hyperion Sender App + + + + + + + + + + + +
+ + + + + \ No newline at end of file diff --git a/frontend/public/css/basicui.css b/frontend/public/css/basicui.css index d62fea9..977884a 100644 --- a/frontend/public/css/basicui.css +++ b/frontend/public/css/basicui.css @@ -102,22 +102,6 @@ nav{ transition: all 0.3s ease; } -.service { - display: block; -} - -.settings { - display: none; -} - -.logs { - display: none; -} - -.about { - display: none; -} - .status { position: fixed; bottom: 0; diff --git a/frontend/src/actions/root.js b/frontend/src/actions/root.js new file mode 100644 index 0000000..1e22601 --- /dev/null +++ b/frontend/src/actions/root.js @@ -0,0 +1,92 @@ +let checkRootStatusIntervalID = null; +const onHBExec = (result, logStore) => { + if (result.returnValue) { + logStore.setPiccapLog(`HBChannel exec returned. stdout: ${result.stdoutString} stderr: ${result.stderrString}`); + } else { + logStore.setPiccapLog(`HBChannel exec failed! Code: ${result.errorCode}`); + } +}; + +const makeServiceRoot = (logStore) => { + logStore.setPiccapLog('Rooting..'); + logStore.setInfoState('Rooting app and service..'); + logStore.setPiccapLog('Calling HBChannel exec to elevate app and service'); + /* eslint-disable no-undef */ + webOS.service.request( + 'luna://org.webosbrew.hbchannel.service', + { + method: 'exec', + parameters: { + command: '/media/developer/apps/usr/palm/services/org.webosbrew.hbchannel.service/elevate-service org.webosbrew.piccap; /media/developer/apps/usr/palm/services/org.webosbrew.hbchannel.service/elevate-service org.webosbrew.piccap.service', + }, + onSuccess: (result) => { + onHBExec(result, logStore); + + logStore.setPiccapLog('Elevation completed - killing service process..'); + logStore.setInfoState('Killing service..'); + killHyperion(); + }, + onFailure: (result) => onHBExec(result, logStore), + }, + ); + /* eslint-enable no-undef */ + + logStore.setInfoState('Finished making root processing'); +}; + +const onCheckRootStatus = (result, logStore, rootStore) => { + if (result.returnValue === true) { + if (result.elevated) { + logStore.setPiccapLog('PicCap-Service returned rooted!'); + document.getElementById('txtInfoState').innerHTML = 'Running as root'; + rootStore.setRoot(true); + clearInterval(checkRootStatusIntervalID); + rootStore.setRootingInProgress(false); + } else { + if (!rootStore.rootingInProgress) { + logStore.setPiccapLog('Rooting not in progress yet.'); + makeServiceRoot(); + rootStore.setRootingInProgress(true); + } + logStore.setPiccapLog('PicCap-Service returned not rooted yet! Will check again soon.'); + document.getElementById('txtInfoState').innerHTML = 'Not running as root. Service elevation in progress..'; + } + } else { + logStore.setPiccapLog(`Getting root-status from PicCap-Service failed! Will try again. Code: ${result.errorCode}`); + document.getElementById('txtInfoState').innerHTML = 'PicCap-Service status failed!'; + } +}; + +const checkRoot = (logStore, rootStore) => { + logStore.setStatus('Processing root check'); + logStore.setPiccapLog('Starting loop for PicCap-Service to get root-status'); + + let firstInterval = true; + checkRootStatusIntervalID = window.setInterval(() => { + logStore.setPiccapLog('Calling PicCap-Service to get root-status'); + document.getElementById('txtInfoState').innerHTML = 'Checking root status'; + /* eslint-disable no-undef */ + webOS.service.request( + 'luna://org.webosbrew.piccap.service', + { + method: 'status', + parameters: {}, + onSuccess: (result) => onCheckRootStatus(result, logStore, rootStore), + onFailure: (result) => onCheckRootStatus(result, logStore, rootStore), + }, + ); + /* eslint-enable no-undef */ + + if (!rootStore.rootingInProgress && !rootStore.root && !firstInterval) { + logStore.setPiccapLog('Not rooted and rooting not in progress yet.'); + makeServiceRoot(logStore); + rootStore.setRootingInProgress(true); + } + firstInterval = false; + }, 3000); +}; + +export { + checkRoot, + onHBExec, +}; diff --git a/frontend/src/actions/service.js b/frontend/src/actions/service.js new file mode 100644 index 0000000..4456db2 --- /dev/null +++ b/frontend/src/actions/service.js @@ -0,0 +1,67 @@ +import { getSettings } from './settings'; + +const onServiceCallback = (result, logStore) => { + if (result.returnValue) { + logStore.setPiccapLog('Servicecall returned successfully.'); + logStore.setInfoState('Servicecall success!'); + } else { + logStore.setPiccapLog(`Servicecall failed! Code: ${result.errorCode}`); + logStore.setInfoState('Servicecall failed!'); + } +}; + +const serviceStart = (logStore) => { + logStore.setPiccapLog('Start clicked'); + try { + logStore.setStatus('Starting service...'); + logStore.setInfoState('Sending start command'); + /* eslint-disable no-undef */ + webOS.service.request( + 'luna://org.webosbrew.piccap.service', + { + method: 'start', + parameters: {}, + onSuccess: (result) => onServiceCallback(result, logStore), + onFailure: (result) => onServiceCallback(result, logStore), + }, + ); + /* eslint-enable no-undef */ + } catch (err) { + logStore.setStatus(`Failed: ${JSON.stringify(err)}`); + } + + logStore.setInfoState('Start command send'); +}; + +const serviceStop = (logStore) => { + logStore.setPiccapLog('Stop clicked'); + try { + logStore.setStatus('Stopping service...'); + logStore.setInfoState('Sending stop command'); + /* eslint-disable no-undef */ + webOS.service.request( + 'luna://org.webosbrew.piccap.service', + { + method: 'stop', + parameters: {}, + onSuccess: (result) => onServiceCallback(result, logStore), + onFailure: (result) => onServiceCallback(result, logStore), + }, + ); + /* eslint-enable no-undef */ + } catch (err) { + logStore.setStatus(`Failed: ${JSON.stringify(err)}`); + } + logStore.setInfoState('Stop command send'); +}; + +const serviceReload = (logStore) => { + logStore.setPiccapLog('Reload clicked'); + getSettings(logStore); +}; + +export { + serviceStart, + serviceStop, + serviceReload, +}; diff --git a/frontend/src/actions/servicecalls.js b/frontend/src/actions/servicecalls.js new file mode 100644 index 0000000..b664847 --- /dev/null +++ b/frontend/src/actions/servicecalls.js @@ -0,0 +1,541 @@ +const availableQuirks = { + // DILE_VT + QUIRK_DILE_VT_CREATE_EX: '0x1', + QUIRK_DILE_VT_NO_FREEZE_CAPTURE: '0x2', + QUIRK_DILE_VT_DUMP_LOCATION_2: '0x4', + // vtCapture + QUIRK_VTCAPTURE_FORCE_CAPTURE: '0x100', +}; + +let isRoot = false; +let rootingInProgress = false; + +function logIt(message) { + const textareaConsoleLog = document.getElementById('textareaConsoleLog'); + console.log(message); + textareaConsoleLog.value += `${message}\n`; +} + +function onHBExec(result) { + if (result.returnValue === true) { + logIt(`HBChannel exec returned. stdout: ${result.stdoutString} stderr: ${result.stderrString}`); + } else { + logIt(`HBChannel exec failed! Code: ${result.errorCode}`); + } +} + +function killHyperion() { + document.getElementById('txtInfoState').innerHTML = 'Killing service..'; + logIt('Calling HBChannel exec to kill hyperion-webos'); + /* eslint-disable no-undef */ + webOS.service.request( + 'luna://org.webosbrew.hbchannel.service', + { + method: 'exec', + parameters: { + command: 'kill -9 $(pidof hyperion-webos)', + }, + onSuccess: onHBExec, + onFailure: onHBExec, + }, + ); + /* eslint-enable no-undef */ +} + +function makeServiceRoot() { + logIt('Rooting..'); + document.getElementById('txtInfoState').innerHTML = 'Rooting app and service..'; + logIt('Calling HBChannel exec to elevate app and service'); + /* eslint-disable no-undef */ + webOS.service.request( + 'luna://org.webosbrew.hbchannel.service', + { + method: 'exec', + parameters: { + command: '/media/developer/apps/usr/palm/services/org.webosbrew.hbchannel.service/elevate-service org.webosbrew.piccap; /media/developer/apps/usr/palm/services/org.webosbrew.hbchannel.service/elevate-service org.webosbrew.piccap.service', + }, + onSuccess(result) { + onHBExec(result); + + logIt('Elevation completed - killing service process..'); + document.getElementById('txtInfoState').innerHTML = 'Killing service..'; + killHyperion(); + }, + onFailure: onHBExec, + }, + ); + /* eslint-enable no-undef */ + + document.getElementById('txtInfoState').innerHTML = 'Finished making root processing'; +} + +let checkRootStatusIntervalID = null; +function onCheckRootStatus(result) { + if (result.returnValue === true) { + if (result.elevated) { + logIt('PicCap-Service returned rooted!'); + document.getElementById('txtInfoState').innerHTML = 'Running as root'; + isRoot = true; + clearInterval(checkRootStatusIntervalID); + rootingInProgress = false; + } else { + if (rootingInProgress === false) { + logIt('Rooting not in progress yet.'); + makeServiceRoot(); + rootingInProgress = true; + } + logIt('PicCap-Service returned not rooted yet! Will check again soon.'); + document.getElementById('txtInfoState').innerHTML = 'Not running as root. Service elevation in progress..'; + } + } else { + logIt(`Getting root-status from PicCap-Service failed! Will try again. Code: ${result.errorCode}`); + document.getElementById('txtInfoState').innerHTML = 'PicCap-Service status failed!'; + } +} + +function checkRoot() { + document.getElementById('txtServiceStatus').innerHTML = 'Processing root check'; + logIt('Starting loop for PicCap-Service to get root-status'); + + let firstInterval = true; + checkRootStatusIntervalID = window.setInterval(() => { + logIt('Calling PicCap-Service to get root-status'); + document.getElementById('txtInfoState').innerHTML = 'Checking root status'; + /* eslint-disable no-undef */ + webOS.service.request( + 'luna://org.webosbrew.piccap.service', + { + method: 'status', + parameters: {}, + onSuccess: onCheckRootStatus, + onFailure: onCheckRootStatus, + }, + ); + /* eslint-enable no-undef */ + + if (rootingInProgress === false && isRoot === false && firstInterval === false) { + logIt('Not rooted and rooting not in progress yet.'); + makeServiceRoot(); + rootingInProgress = true; + } + firstInterval = false; + }, 3000); +} + +function getStatus() { + document.getElementById('txtInfoState').innerHTML = 'Getting status info..'; + /* eslint-disable no-undef */ + webOS.service.request( + 'luna://org.webosbrew.piccap.service', + { + method: 'status', + parameters: {}, + onSuccess(result) { + if (result.returnValue === true) { + document.getElementById('txtServiceVersion').innerHTML = result.version; + document.getElementById('txtServiceStatus').innerHTML = result.isRunning ? 'Capturing' : 'Not capturing'; + + document.getElementById('txtInfoReceiver').innerHTML = result.connected ? 'Connected' : 'Disconnected'; + document.getElementById('txtInfoVideo').innerHTML = result.videoRunning ? `Capturing with ${result.videoBackend}` : 'Not capturing'; + document.getElementById('txtInfoUI').innerHTML = result.uiRunning ? `Capturing with ${result.uiBackend}` : 'Not capturing'; + document.getElementById('txtInfoFPS').innerHTML = result.framerate.toFixed(2); /* Round to 2 decimal points */ + + document.getElementById('txtInfoState').innerHTML = 'Status info refreshed'; + } else { + logIt('Getting status info from PicCap-Service failed! Return value false!'); + document.getElementById('txtInfoState').innerHTML = 'Getting status info failed!'; + } + }, + onFailure(result) { + logIt(`Getting status info from PicCap-Service failed! Code: ${result.errorCode}`); + document.getElementById('txtInfoState').innerHTML = 'Getting status info failed!'; + }, + }, + ); + /* eslint-enable no-undef */ +} + +function getSettings() { + document.getElementById('txtInfoState').innerHTML = 'Loading settings..'; + /* eslint-disable no-undef */ + webOS.service.request( + 'luna://org.webosbrew.piccap.service', + { + method: 'getSettings', + parameters: {}, + onSuccess(result) { + if (result.returnValue === true) { + document.getElementById('selectSettingsVideoBackend').value = result.novideo === true ? 'disabled' : result.backend || 'auto'; + document.getElementById('selectSettingsGraphicalBackend').value = result.nogui === true ? 'disabled' : result.uibackend || 'auto'; + + document.getElementById('checkSettingsLocalSocket').checked = result['unix-socket']; + socketCheckChanged(document.getElementById('checkSettingsLocalSocket')); + + if (result.address.includes('/')) { + switch (result.address) { + case '/tmp/hyperhdr-domain': + document.getElementById('selectSettingsSocket').value = 'hyperhdr'; + break; + default: + document.getElementById('selectSettingsSocket').value = 'manual'; + document.getElementById('txtInputSettingsAddress').value = result.address; + } + document.getElementById('txtInputSettingsAddress').value = '127.0.0.1'; + socketSelectChanged(document.getElementById('selectSettingsSocket')); + } else { + document.getElementById('txtInputSettingsAddress').value = result.address || '127.0.0.1'; + } + + document.getElementById('txtInputSettingsPort').value = result.port; + document.getElementById('txtInputSettingsPriority').value = result.priority; + + document.getElementById('txtInputSettingsFPS').value = result.fps; + + // Process Height/Width for easier selection + switch (result.width * result.height) { + case 57600: + document.getElementById('selectSettingsResolution').value = '320x180'; + break; + case 36864: + document.getElementById('selectSettingsResolution').value = '256x144'; + break; + case 20736: + document.getElementById('selectSettingsResolution').value = '192x108'; + break; + case 9984: + document.getElementById('selectSettingsResolution').value = '128x78'; + break; + default: + document.getElementById('selectSettingsResolution').value = 'manual'; + document.getElementById('txtInputSettingsWidth').value = result.width; + document.getElementById('txtInputSettingsHeight').value = result.height; + break; + } + + Object.keys(availableQuirks).forEach((quirk) => { + logIt(`Processing: ${quirk}`); + const quirkval = availableQuirks[quirk]; + /* eslint-disable eqeqeq */ + if ((result.quirks & quirkval) == quirkval) { + logIt(`Quirk ${quirk} enabled!`); + document.getElementById(`checkSettings${quirk}`).checked = true; + } + /* eslint-enable eqeqeq */ + }); + + document.getElementById('checkSettingsVSync').checked = result.vsync; + document.getElementById('checkSettingsAutostart').checked = result.autostart; + document.getElementById('checkSettingsNoHDR').checked = result.nohdr; + document.getElementById('checkSettingsNoPowerstate').checked = result.nopowerstate; + + logIt('Loading settings done!'); + document.getElementById('txtInfoState').innerHTML = 'Settings loaded'; + } else { + logIt('Getting settings from PicCap-Service failed! Return value false!'); + document.getElementById('txtInfoState').innerHTML = 'Getting settings failed!'; + } + }, + onFailure(result) { + logIt(`Getting settings from PicCap-Service failed! Code: ${result.errorCode}`); + document.getElementById('txtInfoState').innerHTML = 'Getting settings failed!'; + }, + }, + ); + /* eslint-enable no-undef */ +} + +window.restartHyperion = () => { + document.getElementById('txtInfoState').innerHTML = 'Killing hyperion.. Will be started again through status loop'; + killHyperion(); +}; + +function saveSettings(config) { + /* eslint-disable no-undef */ + webOS.service.request( + 'luna://org.webosbrew.piccap.service', + { + method: 'setSettings', + parameters: config, + onSuccess(result) { + if (result.returnValue === true) { + logIt('Saving settings success!'); + document.getElementById('txtInfoState').innerHTML = 'Save settings success!'; + getSettings(); + } else { + logIt('Save settings for PicCap-Service failed! Return value false!'); + document.getElementById('txtInfoState').innerHTML = 'Save settings failed!'; + } + }, + onFailure(result) { + logIt(`Save settings for PicCap-Service failed! Code: ${result.errorCode}`); + document.getElementById('txtInfoState').innerHTML = 'Sace settings failed!'; + }, + }, + ); + /* eslint-enable no-undef */ +} + +window.serviceResetSettings = () => { + document.getElementById('txtInfoState').innerHTML = 'Loading default settings..'; + const config = { + backend: 'auto', + uibackend: 'auto', + + novideo: false, + nogui: false, + + address: '', + port: 19400, + priority: 150, + + fps: 0, + + width: 320, + height: 180, + quirks: 0, + + vsync: true, + autostart: false, + }; + logIt(config); + + document.getElementById('txtInfoState').innerHTML = 'Sending default settings..'; + saveSettings(config); +}; + +window.serviceSaveSettings = () => { + document.getElementById('txtInfoState').innerHTML = 'Collecting settings..'; + + let quirkcalc = 0; + Object.keys(availableQuirks).forEach((quirk) => { + logIt(`Processing quirk: ${quirk}`); + const quirkval = availableQuirks[quirk]; + logIt(`Quirk val: ${quirkval}`); + if (document.getElementById(`checkSettings${quirk}`).checked === true) { + quirkcalc |= quirkval; + logIt(`Quirkcalc: ${quirkcalc}`); + } + }); + + let width; + let height; + switch (document.getElementById('selectSettingsResolution').value) { + case '320x180': + width = 320; + height = 180; + break; + case '256x144': + width = 256; + height = 144; + break; + case '192x108': + width = 192; + height = 108; + break; + case '128x78': + width = 128; + height = 78; + break; + case 'manual': + width = parseInt(document.getElementById('txtInputSettingsWidth').value, 10); + height = parseInt(document.getElementById('txtInputSettingsHeight').value, 10); + break; + default: + width = 320; + height = 180; + break; + } + + let address; + if (document.getElementById('checkSettingsLocalSocket').checked === true) { + switch (document.getElementById('selectSettingsSocket').value) { + case 'hyperhdr': + address = '/tmp/hyperhdr-domain'; + break; + case 'manual': + address = document.getElementById('txtInputSettingsSocketPath').value; + break; + default: + address = undefined; + logIt('Address wasnt found!'); + break; + } + } else { + address = document.getElementById('txtInputSettingsAddress').value; + } + + const config = { + backend: document.getElementById('selectSettingsVideoBackend').value === 'disabled' ? 'auto' : document.getElementById('selectSettingsVideoBackend').value, + uibackend: document.getElementById('selectSettingsGraphicalBackend').value === 'disabled' ? 'auto' : document.getElementById('selectSettingsGraphicalBackend').value, + + novideo: document.getElementById('selectSettingsVideoBackend').value === 'disabled', + nogui: document.getElementById('selectSettingsGraphicalBackend').value === 'disabled', + + 'unix-socket': document.getElementById('checkSettingsLocalSocket').checked, + address, + port: parseInt(document.getElementById('txtInputSettingsPort').value, 10) || undefined, + priority: parseInt(document.getElementById('txtInputSettingsPriority').value, 10) || undefined, + + fps: parseInt(document.getElementById('txtInputSettingsFPS').value, 10) || 0, + + width: width || undefined, + height: height || undefined, + quirks: quirkcalc, + + vsync: document.getElementById('checkSettingsVSync').checked, + autostart: document.getElementById('checkSettingsAutostart').checked, + nohdr: document.getElementById('checkSettingsNoHDR').checked, + nopowerstate: document.getElementById('checkSettingsNoPowerstate').checked, + + }; + + logIt(`Config: ${JSON.stringify(config)}`); + + document.getElementById('txtInfoState').innerHTML = 'Sending settings..'; + saveSettings(config); +}; + +window.reloadHyperionLog = () => { + logIt('Calling HBCHannel to get latest 200 hyperion-webos log lines.'); + /* eslint-disable no-undef */ + webOS.service.request( + 'luna://org.webosbrew.hbchannel.service', + { + method: 'exec', + parameters: { + command: 'grep hyperion-webos /var/log/messages | tail -n200', + }, + onSuccess(result) { + onHBExec(result); + const textareaHyperionLog = document.getElementById('textareaHyperionLog'); + textareaHyperionLog.value += `${result.stdoutString}\r\n`; + }, + onFailure: onHBExec, + }, + ); + /* eslint-enable no-undef */ +}; + +// Using this function to setup logging for now. +// Future start/stop of currently not implemented hyperion-webos log method. +window.startStopLogging = () => { + logIt('Setup logging using HBChannel'); + document.getElementById('txtInfoState').innerHTML = 'Calling HBChannel for log setup'; + /* eslint-disable no-undef */ + webOS.service.request( + 'luna://org.webosbrew.hbchannel.service', + { + method: 'exec', + parameters: { + command: '/media/developer/apps/usr/palm/services/org.webosbrew.piccap.service/setuplegacylogging.sh', + }, + onSuccess: onHBExec, + onFailure: onHBExec, + }, + ); + /* eslint-enable no-undef */ +/* + // Future Stuff + const btnLogStartStop = document.getElementById('btnLogStartStop'); + if (!loggingStarted) { + loggingStarted = true; + btnLogStartStop.innerHTML = 'Stop logging'; + } else { + loggingStarted = false; + btnLogStartStop.innerHTML = 'Start logging'; + } */ +}; + +function onServiceCallback(result) { + if (result.returnValue === true) { + logIt('Servicecall returned successfully.'); + document.getElementById('txtInfoState').innerHTML = 'Servicecall success!'; + } else { + logIt(`Servicecall failed! Code: ${result.errorCode}`); + document.getElementById('txtInfoState').innerHTML = 'Servicecall failed!'; + } +} + +window.serviceStart = () => { + logIt('Start clicked'); + try { + document.getElementById('txtServiceStatus').innerHTML = 'Starting service...'; + document.getElementById('txtInfoState').innerHTML = 'Sending start command'; + /* eslint-disable no-undef */ + webOS.service.request( + 'luna://org.webosbrew.piccap.service', + { + method: 'start', + parameters: {}, + onSuccess: onServiceCallback, + onFailure: onServiceCallback, + }, + ); + /* eslint-enable no-undef */ + } catch (err) { + document.getElementById('txtServiceStatus').innerHTML = `Failed: ${JSON.stringify(err)}`; + } + document.getElementById('txtInfoState').innerHTML = 'Start command send'; +}; + +window.serviceStop = () => { + logIt('Stop clicked'); + try { + document.getElementById('txtServiceStatus').innerHTML = 'Stopping service...'; + document.getElementById('txtInfoState').innerHTML = 'Sending stop command'; + /* eslint-disable no-undef */ + webOS.service.request( + 'luna://org.webosbrew.piccap.service', + { + method: 'stop', + parameters: {}, + onSuccess: onServiceCallback, + onFailure: onServiceCallback, + }, + ); + /* eslint-enable no-undef */ + } catch (err) { + document.getElementById('txtServiceStatus').innerHTML = `Failed: ${JSON.stringify(err)}`; + } + document.getElementById('txtInfoState').innerHTML = 'Stop command send'; +}; + +window.serviceReload = () => { + logIt('Reload clicked'); + getSettings(); +}; + +window.tvReboot = () => { + logIt('Trying to reboot TV using HBChannel..'); + document.getElementById('txtInfoState').innerHTML = 'Rebooting TV..'; + /* eslint-disable no-undef */ + webOS.service.request( + 'luna://org.webosbrew.hbchannel.service', + { + method: 'reboot', + parameters: {}, + onSuccess: onServiceCallback, + onFailure: onServiceCallback, + }, + ); + /* eslint-enable no-undef */ +}; + +window.addEventListener('load', () => { + logIt('Startup of PicCap...'); + checkRoot(); + + logIt('Starting load settings loop...'); + const getStatusIntervalID = window.setInterval(() => { + if (isRoot === true) { + logIt('Loading settings, we are rooted.'); + getSettings(); + clearInterval(getStatusIntervalID); + } + }, 3000); + + logIt('Starting status loop...'); + setInterval(() => { + getStatus(); + }, 4000); +}); diff --git a/frontend/src/actions/settings.js b/frontend/src/actions/settings.js new file mode 100644 index 0000000..861e589 --- /dev/null +++ b/frontend/src/actions/settings.js @@ -0,0 +1,101 @@ +const availableQuirks = Object.freeze({ + // DILE_VT + QUIRK_DILE_VT_CREATE_EX: '0x1', + QUIRK_DILE_VT_NO_FREEZE_CAPTURE: '0x2', + QUIRK_DILE_VT_DUMP_LOCATION_2: '0x4', + // vtCapture + QUIRK_VTCAPTURE_FORCE_CAPTURE: '0x100', +}); + +const getSettings = (logStore) => { + logStore.setInfoState('Loading settings..'); + /* eslint-disable no-undef */ + webOS.service.request( + 'luna://org.webosbrew.piccap.service', + { + method: 'getSettings', + parameters: {}, + onSuccess(result) { + if (result.returnValue === true) { + document.getElementById('selectSettingsVideoBackend').value = result.novideo === true ? 'disabled' : result.backend || 'auto'; + document.getElementById('selectSettingsGraphicalBackend').value = result.nogui === true ? 'disabled' : result.uibackend || 'auto'; + + document.getElementById('checkSettingsLocalSocket').checked = result['unix-socket']; + socketCheckChanged(document.getElementById('checkSettingsLocalSocket')); + + if (result.address.includes('/')) { + switch (result.address) { + case '/tmp/hyperhdr-domain': + document.getElementById('selectSettingsSocket').value = 'hyperhdr'; + break; + default: + document.getElementById('selectSettingsSocket').value = 'manual'; + document.getElementById('txtInputSettingsAddress').value = result.address; + } + document.getElementById('txtInputSettingsAddress').value = '127.0.0.1'; + socketSelectChanged(document.getElementById('selectSettingsSocket')); + } else { + document.getElementById('txtInputSettingsAddress').value = result.address || '127.0.0.1'; + } + + document.getElementById('txtInputSettingsPort').value = result.port; + document.getElementById('txtInputSettingsPriority').value = result.priority; + + document.getElementById('txtInputSettingsFPS').value = result.fps; + + // Process Height/Width for easier selection + switch (result.width * result.height) { + case 57600: + document.getElementById('selectSettingsResolution').value = '320x180'; + break; + case 36864: + document.getElementById('selectSettingsResolution').value = '256x144'; + break; + case 20736: + document.getElementById('selectSettingsResolution').value = '192x108'; + break; + case 9984: + document.getElementById('selectSettingsResolution').value = '128x78'; + break; + default: + document.getElementById('selectSettingsResolution').value = 'manual'; + document.getElementById('txtInputSettingsWidth').value = result.width; + document.getElementById('txtInputSettingsHeight').value = result.height; + break; + } + + Object.keys(availableQuirks).forEach((quirk) => { + logStore.setPiccapLog(`Processing: ${quirk}`); + const quirkval = availableQuirks[quirk]; + /* eslint-disable eqeqeq */ + if ((result.quirks & quirkval) == quirkval) { + logStore.setPiccapLog(`Quirk ${quirk} enabled!`); + document.getElementById(`checkSettings${quirk}`).checked = true; + } + /* eslint-enable eqeqeq */ + }); + + document.getElementById('checkSettingsVSync').checked = result.vsync; + document.getElementById('checkSettingsAutostart').checked = result.autostart; + document.getElementById('checkSettingsNoHDR').checked = result.nohdr; + document.getElementById('checkSettingsNoPowerstate').checked = result.nopowerstate; + + logStore.setPiccapLog('Loading settings done!'); + document.getElementById('txtInfoState').innerHTML = 'Settings loaded'; + } else { + logStore.setPiccapLog('Getting settings from PicCap-Service failed! Return value false!'); + document.getElementById('txtInfoState').innerHTML = 'Getting settings failed!'; + } + }, + onFailure(result) { + logStore.setPiccapLog(`Getting settings from PicCap-Service failed! Code: ${result.errorCode}`); + document.getElementById('txtInfoState').innerHTML = 'Getting settings failed!'; + }, + }, + ); + /* eslint-enable no-undef */ +}; + +export { + getSettings, +}; diff --git a/frontend/public/assets/icon_lightModeBlack.png b/frontend/src/assets/icon_lightModeBlack.png similarity index 100% rename from frontend/public/assets/icon_lightModeBlack.png rename to frontend/src/assets/icon_lightModeBlack.png diff --git a/frontend/public/assets/icon_lightModeBlue.png b/frontend/src/assets/icon_lightModeBlue.png similarity index 100% rename from frontend/public/assets/icon_lightModeBlue.png rename to frontend/src/assets/icon_lightModeBlue.png diff --git a/frontend/public/assets/icon_lightModeDark.png b/frontend/src/assets/icon_lightModeDark.png similarity index 100% rename from frontend/public/assets/icon_lightModeDark.png rename to frontend/src/assets/icon_lightModeDark.png diff --git a/frontend/src/components/Navigation.jsx b/frontend/src/components/Navigation.jsx new file mode 100644 index 0000000..ff939ae --- /dev/null +++ b/frontend/src/components/Navigation.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useView } from '../config/store'; + +function Navigation() { + const switchView = useView((state) => state.switchView); + return ( + + ); +} + +export default Navigation; diff --git a/frontend/src/components/Status.jsx b/frontend/src/components/Status.jsx new file mode 100644 index 0000000..c6f2a96 --- /dev/null +++ b/frontend/src/components/Status.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import ThemeSwitch from './ThemeSwitch'; +import { useLog } from '../config/store'; + +function Status() { + const infoState = useLog((state) => state.infoState); + + return ( +
+ +
+

State:

+

{infoState}

+ +

| Receiver:

+

n/a

+ +

| UI:

+

n/a

+ +

| Video:

+

n/a

+ +

| FPS:

+

n/a

+
+
+ ); +} + +export default Status; diff --git a/frontend/src/components/ThemeSwitch.jsx b/frontend/src/components/ThemeSwitch.jsx new file mode 100644 index 0000000..2a0ce63 --- /dev/null +++ b/frontend/src/components/ThemeSwitch.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { useTheme } from '../config/store'; + +import iconLightmode from '../assets/icon_lightModeBlue.png'; +import iconDarkmode from '../assets/icon_lightModeDark.png'; +import iconBlackmode from '../assets/icon_lightModeDark.png'; + +function ThemeSwitch() { + const mode = useTheme((state) => state.mode); + const switchMode = useTheme((state) => state.switchMode); + + return ( +
+ + + +
+ ); +} + +export default ThemeSwitch; diff --git a/frontend/src/config/store.js b/frontend/src/config/store.js new file mode 100644 index 0000000..9276541 --- /dev/null +++ b/frontend/src/config/store.js @@ -0,0 +1,46 @@ +import { create } from 'zustand'; +import zukeeper from 'zukeeper'; + +const useView = create(zukeeper((set) => ({ + view: 'service', + switchView: (newView) => set(() => ({ view: newView })), +}))); + +const useTheme = create(zukeeper((set) => ({ + mode: 'blue', + switchMode: (newMode) => set(() => ({ mode: newMode })), +}))); + +const useRoot = create(zukeeper((set) => ({ + root: false, + rootingInProgress: false, + setRoot: (newStatus) => set(() => ({ root: newStatus })), + setRootingInProgress: (newStatus) => set(() => ({ rootingInProgress: newStatus })), +}))); + +const useLog = create(zukeeper((set) => ({ + piccapLog: 'Logs from PicCap\n', + hyperionLog: 'Logs from hyperion-webos - Log gathering must be started and reloaded manually.\r\n', + logView: 'piccap', + status: 'Loading status..', + infoState: 'Loading..', + setPiccapLog: (message) => set((state) => ({ piccapLog: `${state.piccapLog}${message}\n` })), + setHyperionLog: (message) => set((state) => ({ hyperionLog: `${state.hyperionLog}${message}\r\n` })), + setLogView: (newView) => set(() => ({ logView: newView })), + setStatus: (newStatus) => set(() => ({ status: newStatus })), + setInfoState: (newStatus) => set(() => ({ infoState: newStatus })), +}))); + +window.store = { + ...useView, + ...useTheme, + ...useRoot, + ...useLog, +}; + +export { + useView, + useTheme, + useRoot, + useLog, +}; diff --git a/frontend/src/config/view.js b/frontend/src/config/view.js new file mode 100644 index 0000000..e69de29 diff --git a/frontend/public/index.html b/frontend/src/index.html similarity index 100% rename from frontend/public/index.html rename to frontend/src/index.html diff --git a/frontend/src/index.jsx b/frontend/src/index.jsx new file mode 100644 index 0000000..c52cb7c --- /dev/null +++ b/frontend/src/index.jsx @@ -0,0 +1,7 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import 'core-js/stable'; +import App from './view/App'; + +const root = createRoot(document.getElementById('root')); +root.render(); diff --git a/frontend/src/ui.js b/frontend/src/ui.js index 075997b..d748fa5 100644 --- a/frontend/src/ui.js +++ b/frontend/src/ui.js @@ -6,108 +6,6 @@ function logIt(message) { textareaConsoleLog.value += `${message}\n`; } -/* eslint-disable func-names */ -window.switchView = function (view) { - const service = document.getElementById('service'); - const settings = document.getElementById('settings'); - const logs = document.getElementById('logs'); - const about = document.getElementById('about'); - - const btnservice = document.getElementById('btnNavService'); - const btnsettings = document.getElementById('btnNavSettings'); - const btnlogs = document.getElementById('btnNavLogs'); - const btnabout = document.getElementById('btnNavAbout'); - - const settingItemsAdv = document.getElementById('settingItemsAdv'); - const settingItemsNormal = document.getElementById('settingItemsNormal'); - const btnAdvanced = document.getElementById('btnSettingsAdvanced'); - switch (view) { - case 'service': - service.style.display = 'block'; - btnservice.style.background = 'white'; - btnservice.style.color = 'black'; - - settings.style.display = 'none'; - btnsettings.style.background = null; - btnsettings.style.color = null; - - logs.style.display = 'none'; - btnlogs.style.background = null; - btnlogs.style.color = null; - - about.style.display = 'none'; - btnabout.style.background = null; - btnabout.style.color = null; - break; - case 'settings': - service.style.display = 'none'; - btnservice.style.background = null; - btnservice.style.color = null; - - settings.style.display = 'block'; - btnsettings.style.background = 'white'; - btnsettings.style.color = 'black'; - - logs.style.display = 'none'; - btnlogs.style.background = null; - btnlogs.style.color = null; - - about.style.display = 'none'; - btnabout.style.background = null; - btnabout.style.color = null; - - // Open non advanced page - btnAdvanced.style.background = null; - btnAdvanced.style.color = null; - settingItemsNormal.style.display = 'block'; - settingItemsAdv.style.display = 'none'; - break; - case 'logs': - service.style.display = 'none'; - btnservice.style.background = null; - btnservice.style.color = null; - - settings.style.display = 'none'; - btnsettings.style.background = null; - btnsettings.style.color = null; - - logs.style.display = 'block'; - btnlogs.style.background = 'white'; - btnlogs.style.color = 'black'; - - about.style.display = 'none'; - btnabout.style.background = null; - btnabout.style.color = null; - break; - case 'about': - service.style.display = 'none'; - btnservice.style.background = null; - btnservice.style.color = null; - - settings.style.display = 'none'; - btnsettings.style.background = null; - btnsettings.style.color = null; - - logs.style.display = 'none'; - btnlogs.style.background = null; - btnlogs.style.color = null; - - about.style.display = 'block'; - btnabout.style.background = 'white'; - btnabout.style.color = 'black'; - break; - default: - service.style.display = null; - btnservice.style.background = null; - btnservice.style.color = null; - - settings.style.display = null; - logs.style.display = null; - about.style.display = null; - break; - } -}; - window.resolutionChanged = function (elem) { document.getElementById('manualres').style.display = elem.value === 'manual' ? 'inline' : 'none'; }; diff --git a/frontend/src/view/About.jsx b/frontend/src/view/About.jsx new file mode 100644 index 0000000..8eb5c7d --- /dev/null +++ b/frontend/src/view/About.jsx @@ -0,0 +1,87 @@ +import React from 'react'; + +function About() { + return ( +
+
+

Some info about this project

+
+
+
+
    +
  • +

    + PicCap is the frontend app, which you have installed on your TV and you can see here, to make things as easy as possible. It ships and controls the seperated hyperion-webos background service, which controls the capture interfaces on your TV based on reverse engineering, proccesses the output and sends the resulting low quality image data to a receiver like Hyperion's flatbuffer server. +
    + On newer TVs there is no official way for capturing DRM-protected content like Netflix or Amazon. This restriction doesn't take place for content comming from an HDMI input. +
    + So currently as a workaround you can play your media using your PC, FireTV-Stick or Chromecast and still enjoy your LEDs. +
    + This app requires to be run as root and tries to do this at the first start using the Homebrew Channel. +

    +
  • +
+
    +
  • _______________________________________________

  • +
+
+
    +
  • +

    +
    + Feel free to raise an issue or pull request, or come to the OpenLG-Discord, if you have some questions. +

    +
  • +
+
+ +
+ +
+
    +
  • _______________________________________________

  • +
+
+
    +
  • +

    +
    + Some love to everyone who was, or still is involved into this project and of course the OpenLG-/Hyperion-Community! ♥ +

    +
  • +
+
+
+
    +
  • +

    hyperion-webos

    +
    +
  • +
  • +

    PicCap

    +
    +
  • +
+
+
+ ); +} + +export default About; diff --git a/frontend/src/view/App.jsx b/frontend/src/view/App.jsx new file mode 100644 index 0000000..b2972fe --- /dev/null +++ b/frontend/src/view/App.jsx @@ -0,0 +1,47 @@ +import React, { useEffect } from 'react'; +import { + useLog, useRoot, useView, +} from '../config/store'; +import Navigation from '../components/Navigation'; +import Service from './Service'; +import Settings from './Settings'; +import Status from '../components/Status'; +import Logs from './Logs'; +import About from './About'; +import { checkRoot } from '../actions/root'; + +function App() { + const view = useView((state) => state.view); + const rootStore = useRoot(); + const logStore = useLog(); + + useEffect(() => { + logStore.setPiccapLog('Startup of PicCap...'); + checkRoot(logStore, rootStore); + + logStore.setPiccapLog('Starting load settings loop...'); + const getStatusIntervalID = window.setInterval(() => { + if (rootStore.root === true) { + logStore.setPiccapLog('Loading settings, we are rooted.'); + // getSettings(); + clearInterval(getStatusIntervalID); + } + }, 3000); + }, [rootStore.root]); + + return ( +
+ +
+
+ {view === 'service' && } + {view === 'settings' && } + {view === 'logs' && } + {view === 'about' && } +
+ +
+ ); +} + +export default App; diff --git a/frontend/src/view/Logs.jsx b/frontend/src/view/Logs.jsx new file mode 100644 index 0000000..5624250 --- /dev/null +++ b/frontend/src/view/Logs.jsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { useLog } from '../config/store'; + +function Logs() { + const log = useLog(); + + return ( +
+
+

+ Some very simple experimental feature to collect logs. Setup logging is needed after a reboot. +
+ Will be reworked in newer versions. Press the load button to get last 200 log entries. +

+
+ + + + + +
+
+ {log.logView === 'piccap' && ( +
+