diff --git a/how-to/detect-usb-devices/README.md b/how-to/detect-usb-devices/README.md new file mode 100644 index 00000000..54e2585f --- /dev/null +++ b/how-to/detect-usb-devices/README.md @@ -0,0 +1,45 @@ +# Detect USB Devices + +## How it Works + +The app.ts listens for button clicks and creates with a dynamic openfin window or from a manifest. + +The manifest is located in the config folder along with the manifest for this sample application + +## Get Started + +Follow the instructions below to get up and running. + +### Set up the project + +1. Install dependencies and do the initial build. Note that these examples assume you are in the sub-directory for the example. + +```shell +npm run setup +``` + +2. Build the project. + +```shell +npm run build +``` + +3. Start the test server in a new window. + +```shell +npm run start +``` + +4. Start the Platform application. + +```shell +npm run client +``` + +### What you will see + +1. The default platform provider window (as the manifest sets autoshow to true to help with development) + +## A note about this example + +This is an example of how to use OpenFin APIs to configure OpenFin Container. Its purpose is to provide an example and suggestions. **DO NOT** assume that it contains production-ready code. Please use this as a guide and provide feedback. Thanks! diff --git a/how-to/detect-usb-devices/client/src/app.ts b/how-to/detect-usb-devices/client/src/app.ts new file mode 100644 index 00000000..720cad66 --- /dev/null +++ b/how-to/detect-usb-devices/client/src/app.ts @@ -0,0 +1,84 @@ +import type OpenFin from "@openfin/core"; + +document.addEventListener("DOMContentLoaded", async () => { + try { + await initDom(); + } catch (error) { + console.error(error); + } +}); + +/** + * Initialize the DOM components. + */ +async function initDom(): Promise { + const btnOpenDynamicWindow = document.querySelector("#btn-open-dynamic-window"); + if (btnOpenDynamicWindow) { + btnOpenDynamicWindow.addEventListener("click", async (e: Event) => openDynamicApplicationWindow()); + } + + const btnCheckForDevices = document.querySelector("#btn-check-for-usb-devices"); + if (btnCheckForDevices) { + btnCheckForDevices.addEventListener("click", checkForUsbDevices); + } +} + +/** + * Checks to see what devices to connect. + */ +async function requestDevice(): Promise { + await navigator.usb.requestDevice({ filters: [{ vendorId: 0x1133 }] }); +} + + +/** + * Open a window using dynamic options. + * @returns The window. + */ +async function openDynamicApplicationWindow(): Promise { + const winOption = { + name: "child", + defaultWidth: 800, + defaultHeight: 800, + url: "https://developers.openfin.co/of-docs/docs/connect-usb-hid#find-all-connected-devices", + frame: true, + autoShow: true + }; + return fin.Window.create(winOption); +} + +/** + * Use the usb.getDevices method to get a list of connected devices. + */ +async function checkForUsbDevices(): Promise { + await requestDevice(); + // Get all connected USB devices the website has been granted access to. + // if ("usb" in navigator) { + // const usb = navigator.usb; + // await usb.getDevices() + // .then((devices) => { + // if (devices.length > 0) { + // console.log(`There are ${devices.length} detected USB devices.`); + // for (const device of devices) { + // console.log(device.productName); // "Arduino Micro" + // console.log(device.manufacturerName); // "Arduino LLC" + // } + // } else { + // console.log("There are no detected USB devices."); + // } + // return true; + // }) + // .catch((error) => { + // console.error(error); + // }); + // } else { + // console.log("There is no USB detection on the Navigator window object for this browser."); + // } +} + +/** + * + */ +// function addDeviceToList() { + +// } diff --git a/how-to/detect-usb-devices/client/tsconfig.json b/how-to/detect-usb-devices/client/tsconfig.json new file mode 100644 index 00000000..5c3dc1cc --- /dev/null +++ b/how-to/detect-usb-devices/client/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es2021", + "module": "ES2020", + "sourceMap": true, + "rootDir": "./src", + "outDir": "build", + "skipLibCheck": true, + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "types": ["./types/fin", "w3c-web-usb"] + }, + "include": ["./src/**/*.ts"] +} diff --git a/how-to/detect-usb-devices/client/types/fin.d.ts b/how-to/detect-usb-devices/client/types/fin.d.ts new file mode 100644 index 00000000..8936d86c --- /dev/null +++ b/how-to/detect-usb-devices/client/types/fin.d.ts @@ -0,0 +1,5 @@ +import type { fin as FinApi } from "@openfin/core"; + +declare global { + const fin: typeof FinApi; +} diff --git a/how-to/detect-usb-devices/client/webpack.config.js b/how-to/detect-usb-devices/client/webpack.config.js new file mode 100644 index 00000000..d0af750c --- /dev/null +++ b/how-to/detect-usb-devices/client/webpack.config.js @@ -0,0 +1,24 @@ +const path = require('path'); + +module.exports = [ + { + entry: './client/src/app.ts', + devtool: 'inline-source-map', + module: { + rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/ + } + ] + }, + resolve: { + extensions: ['.tsx', '.ts', '.js'] + }, + output: { + filename: 'app.bundle.js', + path: path.resolve(__dirname, '..', 'public', 'js') + } + } +]; diff --git a/how-to/detect-usb-devices/package.json b/how-to/detect-usb-devices/package.json new file mode 100644 index 00000000..8278e4e2 --- /dev/null +++ b/how-to/detect-usb-devices/package.json @@ -0,0 +1,25 @@ +{ + "name": "detect-usb-devices", + "version": "38.83.79", + "description": "A demonstration of how to check for connected usb devices", + "main": "index.js", + "scripts": { + "kill": "node ./scripts/kill.mjs", + "client": "node ./scripts/launch.mjs", + "build-client": "webpack build --config ./client/webpack.config.js --mode=development", + "build": "npm run build-client", + "start": "npx --yes http-server ./public -p 5050 -c-1", + "setup": "cd ../../ && npm install && cd how-to/create-window && npm run build" + }, + "author": "joe.ransegnola@here.io", + "license": "SEE LICENSE IN LICENSE.MD", + "devDependencies": { + "@openfin/core": "38.83.79", + "@openfin/node-adapter": "38.83.79", + "@types/w3c-web-usb": "^1.0.10", + "ts-loader": "^9.5.1", + "typescript": "^5.4.5", + "webpack": "^5.91.0", + "webpack-cli": "^5.1.4" + } +} diff --git a/how-to/detect-usb-devices/public/common/images/icon-blue.png b/how-to/detect-usb-devices/public/common/images/icon-blue.png new file mode 100644 index 00000000..fc784502 Binary files /dev/null and b/how-to/detect-usb-devices/public/common/images/icon-blue.png differ diff --git a/how-to/detect-usb-devices/public/common/style/app.css b/how-to/detect-usb-devices/public/common/style/app.css new file mode 100644 index 00000000..96c4673f --- /dev/null +++ b/how-to/detect-usb-devices/public/common/style/app.css @@ -0,0 +1,929 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter&display=swap'); + +:root { + --brand-background: var(--theme-background-primary, #1e1f23); + --brand-border: var(--theme-background4, #2f3136); + --brand-primary: var(--theme-brand-primary, #0a76d3); + --brand-text: var(--theme-text-default, #ffffff); + --brand-text-secondary: var(--theme-text-help, #c9cbd2); + --brand-input-background: var(--theme-background5, #383a40); + --brand-input-border: var(--theme-background6, #53565f); + --brand-input-border-highlight: var(--theme-input-focused, #c9cbd2); + --brand-table-header: var(--theme-background1, #111214); + --brand-table-header-text: var(--theme-text-default, #ffffff); + --brand-table-row-even: var(--theme-background3, #24262b); + --brand-table-row-odd: var(--theme-background4, #2f3136); + --brand-error: var(--theme-status-critical, #be1d1f); + --brand-success: var(--theme-status-success, #35c759); + + accent-color: var(--brand-primary); +} + +.theme-light { + --brand-background: var(--theme-background-primary, #fafbfe); + --brand-border: var(--theme-background4, #eceef1); + --brand-primary: var(--theme-brand-primary, #0a76d3); + --brand-text: var(--theme-text-default, #111214); + --brand-text-secondary: var(--theme-text-help, #2f3136); + --brand-input-background: var(--theme-background5, #dddfe4); + --brand-input-border: var(--theme-background6, #c9cbd2); + --brand-input-border-highlight: var(--theme-input-focused, #c9cbd2); + --brand-table-header: var(--theme-background1, #ffffff); + --brand-table-header-text: var(--theme-text-default, #111214); + --brand-table-row-even: var(--theme-background3, #eceef1); + --brand-table-row-odd: var(--theme-background4, #c9cbd2); + --brand-error: var(--theme-status-critical, #be1d1f); + --brand-success: var(--theme-status-success, #35c759); +} + +.primary { + color: var(--brand-primary); +} + +.error { + color: var(--brand-error); +} + +.success { + color: var(--brand-success); +} + +::selection { + background-color: var(--brand-primary); +} + +* { + font-family: Inter, 'Sans Serif', sans-serif; + box-sizing: border-box; +} + +html, +body { + height: 100%; +} + +body { + display: flex; + justify-content: stretch; + align-items: stretch; + overflow: hidden; + padding: 20px; + margin: 0; + background-color: var(--brand-background); + color: var(--brand-text); +} + +body.border { + padding: 20px; + margin: 0; + background-color: var(--brand-background); + color: var(--brand-text); + border: 1px solid #000000; + border-radius: 5px; + width: 99%; + height: 99%; +} + +body.border-light { + border-color: var(--brand-input-border-highlight); +} + +body.small { + padding: 15px; +} + +h1 { + font-size: 24px; + font-weight: 700; + line-height: 1; + margin-block-start: 0; + margin-block-end: 0; +} +h1.tag { + font-size: 12px; + font-weight: normal; + margin-top: 5px; + color: var(--brand-text-secondary); +} + +h2 { + font-size: 20px; + font-weight: 700; + line-height: 1; + margin-block-start: 0; + margin-block-end: 0; +} + +h3 { + font-size: 16px; + font-weight: 700; + line-height: 1; + margin-block-start: 0; + margin-block-end: 0; +} + +h4 { + font-size: 14px; + font-weight: 700; + line-height: 1; + margin-block-start: 0; + margin-block-end: 0; +} + +h5 { + font-size: 12px; + font-weight: 700; + line-height: 1; + margin-block-start: 0; + margin-block-end: 0; +} + +p { + color: var(--brand-text-secondary); + font-size: 12px; + margin-block-start: 0; + margin-block-end: 0; +} + +::-webkit-scrollbar { + width: 8px; + height: 8px; +} +::-webkit-scrollbar-track { + background: var(--brand-input-background); + border-radius: 5px; +} +::-webkit-scrollbar-thumb { + background: var(--brand-border); + border-radius: 5px; + border: 1px solid var(--brand-input-border); +} +::-webkit-scrollbar-thumb:hover { + border: 1px solid var(--brand-input-border-highlight); +} + +main { + min-height: 100px; +} + +a { + color: var(--brand-primary); + font-size: 12px; + font-weight: bold; + outline: none; + text-decoration: none; +} + +a:hover, +a:focus { + text-decoration: underline; +} + +button, +input[type='button'], +::-webkit-file-upload-button, +a.button, +footer a { + background-color: var(--brand-primary); + border: 2px solid var(--brand-primary); + color: #ffffff; + border-radius: 5px; + padding: 8px 20px; + text-align: left; + cursor: pointer; + font-size: 12px; + font-weight: bold; + text-decoration: none; + outline: 0; + white-space: nowrap; + transition-property: background-color, color, opacity; + transition-duration: 200ms; + transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1); +} + +a.button:hover, +footer a:hover { + text-decoration: none; +} + +a.button:focus, +a.button:hover, +input[type='button']:focus, +input[type='button']:hover, +input[type='file']:focus::-webkit-file-upload-button, +input[type='file']:hover::-webkit-file-upload-button, +::-webkit-file-upload-button:hover, +footer a:focus, +footer a:not(:disabled):hover, +button:not(:disabled):focus, +button:not(:disabled):hover { + background-image: linear-gradient(rgba(255, 255, 255, 0.1) 0 0); +} + +button.image { + width: 40px; + height: 40px; + padding: 0px; + display: flex; + align-items: center; + justify-content: center; +} +button.image > img { + width: 32px; + height: 32px; +} +button.secondary { + background-color: var(--brand-input-background); + color: var(--brand-text); + border-color: var(--brand-input-border); +} +button.secondary:not(:disabled):hover { + background-color: var(--brand-input-background); +} +button.plain { + border-color: transparent; + background-color: transparent; + color: var(--brand-primary); +} +button.plain:not(:disabled):not(:read-only):hover { + border-color: transparent; + background-color: transparent; +} + +button.small, +::-webkit-file-upload-button, +td button { + padding: 5px 10px; + font-size: 10px; +} +td button.plain { + width: auto; + height: auto; +} + +button.center { + text-align: center; +} + +::-webkit-file-upload-button { + margin-right: 10px; +} + +pre { + margin: 0; + padding: 5px; + border-radius: 5px; + font-size: 12px; + font-family: 'Courier New', Courier, monospace; + white-space: pre-wrap; + border: 1px solid var(--brand-input-border); + background-color: var(--brand-input-background); + width: 100%; + min-height: 20px; +} + +header { + padding-bottom: 10px; + border-bottom: 2px solid var(--brand-border); +} + +footer { + padding-top: 10px; + border-top: 1px solid var(--brand-border); +} + +fieldset { + display: flex; + flex-direction: column; + align-items: flex-start; + border: 0; + padding: 0; + font-size: 12px; + gap: 5px; +} + +fieldset.row { + flex-direction: row; + align-items: center; + gap: 10px; +} + +label { + font-size: 12px; + font-weight: 600; +} + +select, +input[type='text'], +input[type='url'], +input[type='email'], +input[type='password'], +input[type='file'], +input[type='number'], +input[type='date'], +input[type='time'], +input[type='datetime-local'], +input[type='month'], +input[type='week'], +input[type='color'], +textarea { + border-radius: 5px; + border: 1px solid var(--brand-input-border); + background-color: var(--brand-input-background); + font-size: 12px; + outline: 0; + color: var(--brand-text); + width: 50%; + min-width: 200px; + padding: 8px 12px; + box-shadow: rgb(0 0 0 / 25%) 0px 4px 4px; + transition-property: background-color, color, opacity; + transition-duration: 200ms; + transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1); +} + +textarea { + min-height: 64px; + resize: none; +} + +textarea.resizable { + resize: both; +} + +select.full, +input[type='text'].full, +input[type='email'].full, +input[type='url'].full, +input[type='password'].full, +input[type='file'].full, +input[type='number'].full, +input[type='date'].full, +input[type='time'].full, +input[type='datetime-local'].full, +input[type='month'].full, +input[type='week'].full, +input[type='range'].full, +textarea.full { + width: 95%; +} + +select:focus, +select:not(:disabled):hover, +input[type='text']:not(:read-only):focus, +input[type='text']:not(:disabled):not(:read-only):hover, +input[type='email']:not(:read-only):focus, +input[type='email']:not(:disabled):not(:read-only):hover, +input[type='url']:not(:read-only):focus, +input[type='url']:not(:disabled):not(:read-only):hover, +input[type='password']:not(:read-only):focus, +input[type='password']:not(:disabled):not(:read-only):hover, +input[type='file']:not(:read-only):focus, +input[type='file']:not(:disabled):not(:read-only):hover, +input[type='number']:not(:read-only):focus, +input[type='number']:not(:disabled):not(:read-only):hover, +input[type='date']:not(:read-only):focus, +input[type='date']:not(:disabled):not(:read-only):hover, +input[type='time']:not(:read-only):focus, +input[type='time']:not(:disabled):not(:read-only):hover, +input[type='datetime-local']:not(:read-only):focus, +input[type='datetime-local']:not(:disabled):not(:read-only):hover, +input[type='month']:not(:read-only):focus, +input[type='month']:not(:disabled):not(:read-only):hover, +input[type='week']:not(:read-only):focus, +input[type='week']:not(:disabled):not(:read-only):hover, +textarea:not(:read-only):focus, +textarea:not(:disabled):not(:read-only):hover, +input[type='checkbox']:focus, +input[type='checkbox']:not(:disabled):hover, +input[type='radio']:focus, +input[type='radio']:not(:disabled):hover, +input[type='color']:focus, +input[type='color']:not(:disabled):hover { + border-color: var(--brand-input-border-highlight); +} + +input[type='date'], +input[type='time'], +input[type='datetime-local'], +input[type='month'], +input[type='week'] { + width: 200px; + min-width: auto; +} + +.theme-dark ::-webkit-calendar-picker-indicator { + filter: invert(1); +} + +::-webkit-datetime-edit-day-field:focus, +::-webkit-datetime-edit-month-field:focus, +::-webkit-datetime-edit-year-field:focus, +::-webkit-datetime-edit-hour-field:focus, +::-webkit-datetime-edit-minute-field:focus, +::-webkit-datetime-edit-week-field:focus { + background-color: var(--brand-primary); + color: #ffffff; +} + +::-webkit-outer-spin-button, +::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type='file'] { + padding: 4px 12px; +} + +select { + appearance: none; +} + +select:not([multiple]):not(:disabled) { + appearance: none; + background-image: url("data:image/svg+xml;utf8,"); + background-repeat: no-repeat; + background-size: 12px; + background-position: calc(100% - 12px) 12px; + background-color: var(--brand-input-background); +} + +.theme-dark select:not([multiple]):not(:disabled) { + background-image: url("data:image/svg+xml;utf8,"); +} + +option { + color: var(--brand-text); + padding: 6px; + margin-bottom: 1px; +} + +option:hover { + background: var(--brand-input-border) + linear-gradient(0deg, var(--brand-input-border) 0%, var(--brand-input-border) 100%); + border-radius: 5px; +} + +option:checked { + background: var(--brand-primary) linear-gradient(0deg, var(--brand-primary) 0%, var(--brand-primary) 100%); + color: #ffffff; + border-radius: 5px; +} + +input[type='range'] { + -webkit-appearance: none; + height: 20px; + width: 200px; + background: transparent; + outline: none; +} + +input[type='range']::-webkit-slider-runnable-track { + -webkit-appearance: none; + height: 10px; + border: 1px solid var(--brand-input-border); + background-color: var(--brand-input-background); + border-radius: 5px; + box-shadow: rgb(0 0 0 / 25%) 0px 4px 4px; + transition-property: background-color, color, opacity; + transition-duration: 200ms; + transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1); +} + +input[type='range']::-webkit-slider-thumb { + -webkit-appearance: none; + height: 20px; + width: 20px; + border-radius: 5px; + margin-top: -6px; + border: 1px solid var(--brand-input-border); + background: var(--brand-input-background); + cursor: pointer; + box-shadow: rgb(0 0 0 / 25%) 0px 4px 4px; + transition-property: background-color, color, opacity; + transition-duration: 200ms; + transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1); +} + +input[type='range']:hover::-webkit-slider-runnable-track, +input[type='range']:focus::-webkit-slider-runnable-track, +input[type='range']:hover::-webkit-slider-thumb, +input[type='range']:focus::-webkit-slider-thumb { + border-color: var(--brand-input-border-highlight); +} + +button:disabled, +select:disabled, +input:disabled, +textarea:disabled, +footer a:disabled, +a[disabled] { + opacity: 0.3; + cursor: default; + pointer-events: none; + resize: none; +} + +select:invalid, +input:invalid, +textarea:invalid { + border-color: var(--brand-error); +} + +fieldset > span { + margin-left: 10px; + width: 50px; +} + +input[type='text'].large, +input[type='password'].large, +input[type='file'].large, +input[type='email'].large, +input[type='url'].large, +input[type='date'].large, +input[type='time'].large { + font-size: 20px; + padding: 20px 10px; +} + +input[type='text'].center, +input[type='password'].center, +input[type='email'].center, +input[type='url'].center { + text-align: center; +} + +input[type='checkbox'], +input[type='radio'] { + appearance: none; + border: 1px solid var(--brand-input-border); + background-color: var(--brand-input-background); + color: var(--brand-input-text); + outline: 0; + width: 20px; + height: 20px; + margin: 2px; + box-shadow: rgb(0 0 0 / 25%) 0px 4px 4px; + transition-property: background-color, color, opacity; + transition-duration: 200ms; + transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1); +} + +input[type='radio'] { + border-radius: 50%; +} + +input[type='checkbox']::before, +input[type='radio']::before { + content: ''; + display: none; + width: 16px; + height: 16px; + margin: 1px; + background-color: var(--brand-text); + opacity: 0.8; +} + +input[type='checkbox']:checked::before { + display: inline-block; + clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); +} +input[type='radio']:checked::before { + display: inline-block; + clip-path: circle(35% at 52% 50%); +} + +input[type='checkbox'] + label, +input[type='radio'] + label { + white-space: nowrap; +} + +input[type='color'] { + width: 100px; + min-width: auto; + padding: 5px; +} + +input[type='color']::-webkit-color-swatch-wrapper { + padding: 0; + background: transparent; +} + +input[type='color']::-webkit-color-swatch { + border-radius: 5px; + border: none; +} + +/* This disables the auto zoom on iDevices */ +@supports (-webkit-touch-callout: none) { + select, + input[type='text'], + input[type='url'], + input[type='email'], + input[type='password'], + input[type='file'], + input[type='number'], + input[type='date'], + input[type='time'], + input[type='datetime-local'], + input[type='month'], + input[type='week'], + input[type='color'], + textarea { + font-size: 16px; + } +} + +hr { + width: 100%; + border: 0; + border-bottom: 1px solid var(--brand-input-border); +} + +ul { + font-size: 12px; + display: flex; + flex-direction: column; + gap: 10px; + list-style-type: none; +} + +li { + font-size: 12px; + position: relative; + padding: 0 0 10px; +} + +li::before { + content: ' '; + display: inline-block; + background-color: var(--brand-primary); + width: 8px; + height: 8px; + border-radius: 2px; + position: absolute; + left: -13px; + top: 3px; +} + +em { + font-size: 12px; +} + +.row { + display: flex; + flex-direction: row; +} + +.col { + display: flex; + flex-direction: column; +} + +.fill { + flex: 1; +} + +.fill_2 { + flex: 2; +} + +.scroll { + overflow: auto; +} + +.scroll-vertical { + overflow-y: auto; +} + +.scroll-horizontal { + overflow-x: auto; +} + +.overflow-hidden { + overflow: hidden; +} + +.middle { + align-items: center; +} + +.bottom { + align-items: flex-end; +} + +.spread { + justify-content: space-between; +} + +.around { + justify-content: space-around; +} + +.left { + display: flex; + align-items: flex-start; +} + +.center { + display: flex; + justify-content: center; +} + +.right { + display: flex; + justify-content: flex-end; +} + +.wrap { + flex-wrap: wrap; +} + +.gap5 { + gap: 5px; +} + +.gap10 { + gap: 10px; +} + +.gap20 { + gap: 20px; +} + +.gap40 { + gap: 40px; +} + +.pad10 { + padding: 10px; +} + +.pad20 { + padding: 20px; +} + +.pad0 { + padding: 0px; +} + +.table { + display: flex; + flex: 1; + flex-direction: column; + font-size: 10px; +} + +table { + width: 100%; + font-size: 10px; + border: 0; + border-collapse: collapse; +} + +table, +.table { + border: var(--brand-input-border) 1px solid; +} + +.table-row { + display: flex; + flex: 1; + flex-direction: row; +} + +table > tr:first-child, +thead > tr, +.table-row.header { + background-color: var(--brand-table-header); +} + +table > tr:nth-child(even), +.table > div:nth-child(even), +tbody > tr:nth-child(odd) { + background-color: var(--brand-table-row-even); +} + +table > tr:nth-child(odd), +.table > div:not(:first-child):nth-child(odd), +tbody > tr:nth-child(even) { + background-color: var(--brand-table-row-odd); +} + +th, +.table-row.header > div { + color: var(--brand-table-header-text); + font-weight: bold; + padding: 10px 5px; + text-align: left; + word-break: break-word; +} + +.table-row > div { + flex: 1; +} + +td, +.table-row:not(.header) > div { + padding: 5px; + word-break: break-word; + color: var(--brand-text); +} + +.table-row:not(.header) > div { + display: flex; + align-items: center; +} + +@media screen and (max-width: 736px) { + .table-row { + flex-direction: column; + } + + .table-row.header { + display: none; + } + + .table-row:not(.header) > div { + display: flex; + align-items: center; + flex: 1; + gap: 10px; + } + + .table-row:not(.header) > div:before { + content: attr(data-name); + font-weight: bold; + white-space: nowrap; + width: 20%; + } + + .table-row > div.right { + justify-content: flex-start; + } + + .table-row > div.center { + justify-content: flex-start; + } +} + +.border { + border: 1px solid var(--brand-input-border); + border-radius: 5px; +} + +.drag { + user-select: none; + -webkit-app-region: drag; +} + +.no-drag { + -webkit-app-region: none; +} + +.form { + background-color: var(--brand-table-header); + border: var(--brand-input-border) 1px solid; + border-radius: 5px; + padding: 10px; + font-size: 12px; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 10px; + background-color: var(--brand-table-header); + border: var(--brand-input-border) 1px solid; + border-radius: 5px; + padding: 10px; +} + +.form-group.large { + gap: 20px; +} + +.width-full { + width: 100%; +} + +.max-width-full { + max-width: 100%; +} + +.width-responsive { + max-width: fit-content; +} + +.hidden { + display: none; +} + +.pointer { + cursor: pointer; +} + +.nowrap { + white-space: nowrap; +} diff --git a/how-to/detect-usb-devices/public/favicon.ico b/how-to/detect-usb-devices/public/favicon.ico new file mode 100644 index 00000000..0b6e09e6 Binary files /dev/null and b/how-to/detect-usb-devices/public/favicon.ico differ diff --git a/how-to/detect-usb-devices/public/html/app.html b/how-to/detect-usb-devices/public/html/app.html new file mode 100644 index 00000000..641ab867 --- /dev/null +++ b/how-to/detect-usb-devices/public/html/app.html @@ -0,0 +1,35 @@ + + + + + + + USB Device Detection + + + + + +
+
+

USB Device Detection

+

Demonstrate how to check for USB Devices using WebUSB API & OpenFin.

+
+
+ OpenFin +
+
+
+ + +
+ + +
+
+ +
    +
    +
    + + diff --git a/how-to/detect-usb-devices/public/manifest.fin.json b/how-to/detect-usb-devices/public/manifest.fin.json new file mode 100644 index 00000000..a721297c --- /dev/null +++ b/how-to/detect-usb-devices/public/manifest.fin.json @@ -0,0 +1,127 @@ +{ + "runtime": { + "arguments": "--v=1 --inspect", + "version": "38.126.83.79" + }, + "platform": { + "uuid": "how-to-detect-usb-devices", + "autoShow": false, + "icon": "http://localhost:5050/favicon.ico", + "permissions": { + "Application": { + "getFileDownloadLocation": true, + "setFileDownloadLocation": true + }, + "System": { + "setDomainSettings": true, + "registerCustomProtocol": true, + "unregisterCustomProtocol": true, + "getCustomProtocolState": true, + "readRegistryValue": true + }, + "webAPIs": ["notifications", "audio", "video", "hid", "usb"], + "devices": [ + { + "vendorId": 1133, + "productId": 2695 + } + ] + }, + "defaultViewOptions": { + "downloadShelf": { + "enabled": true + }, + "permissions": { + "Application": { + "getFileDownloadLocation": true, + "setFileDownloadLocation": true + }, + "System": { + "setDomainSettings": true, + "registerCustomProtocol": true, + "unregisterCustomProtocol": true, + "getCustomProtocolState": true, + "readRegistryValue": true + }, + "webAPIs": ["notifications", "audio", "video", "hid", "usb"], + "devices": [ + { + "vendorId": 1133, + "productId": 2695 + }, + { + "vendorId": 1452, + "productId": 5010 + } + ] + } + }, + "defaultWindowOptions": { + "downloadShelf": { + "enabled": true + }, + "permissions": { + "Application": { + "getFileDownloadLocation": true, + "setFileDownloadLocation": true + }, + "System": { + "setDomainSettings": true, + "registerCustomProtocol": true, + "unregisterCustomProtocol": true, + "getCustomProtocolState": true, + "readRegistryValue": true + }, + "webAPIs": ["notifications", "audio", "video", "hid", "usb"] + } + }, + "defaultDomainSettings": { + "rules": [ + { + "match": [""], + "options": { + "downloadSettings": { + "rules": [ + { + "match": [""], + "behavior": "no-prompt" + } + ] + } + } + } + ] + } + }, + "snapshot": { + "windows": [ + { + "layout": { + "content": [ + { + "type": "stack", + "id": "no-drop-target", + "content": [ + { + "type": "component", + "componentName": "view", + "componentState": { + "processAffinity": "ps_1", + "url": "http://localhost:5050/html/app.html", + "permissions": { + "webAPIs": ["hid", "usb"], + "devices": [{ "vendorId": 1133, "productId": 2140 }], + "System": { + "launchExternalProcess": true + } + } + } + } + ] + } + ] + } + } + ] + } +} diff --git a/how-to/detect-usb-devices/scripts/kill.mjs b/how-to/detect-usb-devices/scripts/kill.mjs new file mode 100644 index 00000000..634090b0 --- /dev/null +++ b/how-to/detect-usb-devices/scripts/kill.mjs @@ -0,0 +1,34 @@ +/** + * This script will kill all OpenFin tasks from running. + * Useful if you want to make sure you have nothing left running in the background. + */ +import { exec } from 'child_process'; + +console.log('Kill All OpenFin Tasks'); +console.log('======================'); +console.log(); + +console.log(`Platform: ${process.platform}`); + +const isWindows = process.platform.startsWith('win'); +console.log('Running on Windows:', isWindows); + +const cmd = isWindows + ? `cmd.exe /c taskkill /F /IM OpenFin.exe /T & cmd.exe /c taskkill /F /IM OpenFinRVM.exe /T` + : `pkill -9 OpenFin`; + +console.log(); +console.log('Command:', cmd); +console.log(); + +exec(cmd, (error, stdout, stderr) => { + if (error) { + console.log(error.message); + return; + } + if (stderr) { + console.log(stderr); + return; + } + console.log(stdout); +}); diff --git a/how-to/detect-usb-devices/scripts/launch.mjs b/how-to/detect-usb-devices/scripts/launch.mjs new file mode 100644 index 00000000..50444893 --- /dev/null +++ b/how-to/detect-usb-devices/scripts/launch.mjs @@ -0,0 +1,205 @@ +/* eslint-disable unicorn/no-process-exit */ +/** + * This script will launch an OpenFin application. + * It uses the OpenFin NodeJS adapter to launch the url specified on the command line. + * Pressing Ctrl+C/Command+C will terminate the application. + */ +import { connect, launch } from '@openfin/node-adapter'; +import { exec } from 'child_process'; +import { setDefaultResultOrder } from 'dns'; + +/** + * Run the process. + * @param manifestUrl The manifest to launch. + */ +async function run(manifestUrl) { + try { + if (process.platform === 'darwin') { + await launchFromFinsLink(manifestUrl); + } else { + await launchFromNodeAdapter(manifestUrl); + } + } catch (e) { + console.error(`Error: Connection failed`); + console.error(e.message); + } +} + +/** + * Launch the manifest using the node adapter. + * @param manifestUrl the url to launch + */ +async function launchFromNodeAdapter(manifestUrl) { + let quitRequested = false; + let quit; + + const fin = await connectAndGetFinAPI(manifestUrl); + + if (fin) { + const manifest = await fin.System.fetchManifest(manifestUrl); + + if (manifest.platform?.uuid !== undefined) { + quit = async () => { + try { + if (!quitRequested) { + quitRequested = true; + console.log('Calling platform quit'); + const platform = fin.Platform.wrapSync({ uuid: manifest.platform.uuid }); + await platform.quit(); + } + } catch (err) { + if (err.toString().includes('no longer connected')) { + console.log('Platform no longer connected'); + console.log('Exiting process'); + process.exit(); + } else { + console.error(err); + } + } + }; + console.log(`Wrapped target platform: ${manifest.platform.uuid}`); + } else { + quit = async () => { + try { + if (!quitRequested) { + quitRequested = true; + console.log('Calling application quit'); + const app = fin.Application.wrapSync({ uuid: manifest.startup_app.uuid }); + await app.quit(); + } + } catch (err) { + console.error(err); + } + }; + console.log(`Wrapped classic app: ${manifest.startup_app.uuid}`); + } + + // do something when app is closing + process.on('exit', async () => { + console.log('Process exit called'); + await quit(); + }); + + // catches ctrl+c event + process.on('SIGINT', async () => { + console.log('Ctrl + C called'); + await quit(); + }); + + console.log(`You successfully connected to the manifest: ${manifestUrl}`); + console.log(`Please wait while the sample loads.`); + console.log(); + console.log(`Close all platform windows to close the platform.`); + console.log(`Otherwise press Ctrl + C (Windows) or Command + C (Mac) to exit and close the sample.`); + console.log(); + } +} + +/** + * Launch the manifest using the node adapter. + * @param manifestUrl The manifest to launch. + */ +async function launchFromFinsLink(manifestUrl) { + console.log(); + console.log(`Launching manifest...`); + console.log(); + const finsLink = manifestUrl.replace('http', 'fin'); + const finsProcess = exec(`open ${finsLink}`); + + /** + * A simple message to inform the user how to quit the platform. + */ + function logQuitMessage() { + console.log(); + console.log('Please quit the platform using:'); + console.log(); + console.log('- Process Manager (https://start.openfin.co/pm)'); + console.log('- run npm run kill to kill all OpenFin processes.'); + console.log(); + console.log(); + } + + finsProcess.stdout.on('data', (data) => { + console.log(`stdout: ${data}`); + }); + + finsProcess.stderr.on('data', (data) => { + console.error(`stderr: ${data}`); + }); + + // do something when app is closing + process.on('exit', async () => { + console.log('Please wait for the OpenFin application to launch.'); + logQuitMessage(); + }); + + // catches ctrl+c event + process.on('SIGINT', async () => { + console.log('Ctrl + C called'); + logQuitMessage(); + }); + + finsProcess.on('exit', (code) => { + if (code === 0) { + console.log('Manifest launched successfully'); + } else { + console.log(`Child process exited with code ${code.toString()}`); + } + }); +} + +/** + * Launch the manifest using the node adapter. + * @param manifestUrl The manifest to launch. + * @param exitMethod The method to call if the adapter connection exits. + * @returns The fin object representing the connection. + */ +async function connectAndGetFinAPI(manifestUrl, exitMethod) { + try { + console.log(`Launching manifest...`); + console.log(); + + const port = await launch({ manifestUrl }); + + // We will use the port to connect from Node to determine when OpenFin exists. + const fin = await connect({ + uuid: `dev-connection-${Date.now()}`, // Supply an addressable Id for the connection + address: `ws://127.0.0.1:${port}`, // Connect to the given port. + nonPersistent: true // We want OpenFin to exit as our application exists. + }); + + // Once OpenFin exits we shut down the process. + fin.once('disconnected', () => { + console.log('Platform disconnected'); + console.log('Exiting process'); + process.exit(); + }); + + return fin; + } catch (e) { + console.error('Error: Failed launching manifest'); + console.error(e.message); + if (e.message.includes('Could not locate')) { + console.error('Is the web server running and the manifest JSON valid ?'); + } + } +} + +console.log('Launch Manifest'); +console.log('==============='); +console.log(); +console.log(`Platform: ${process.platform}`); + +const launchArgs = process.argv.slice(2); +const manifest = launchArgs.length > 0 ? launchArgs[0] : 'http://localhost:5050/manifest.fin.json'; +console.log(`Manifest: ${manifest}`); + +try { + setDefaultResultOrder('ipv4first'); +} catch { + // Early versions of node do not support this method, but those earlier versions + // also do not have the same issue with interface ordering, so it doesn't matter + // that it hasn't been called. +} + +run(manifest).catch((err) => console.error(err)); diff --git a/package-lock.json b/package-lock.json index 11a3c60f..7632406c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -97,6 +97,19 @@ "webpack-cli": "^5.1.4" } }, + "how-to/detect-usb-devices": { + "version": "38.83.79", + "license": "SEE LICENSE IN LICENSE.MD", + "devDependencies": { + "@openfin/core": "38.83.79", + "@openfin/node-adapter": "38.83.79", + "@types/w3c-web-usb": "^1.0.10", + "ts-loader": "^9.5.1", + "typescript": "^5.4.5", + "webpack": "^5.91.0", + "webpack-cli": "^5.1.4" + } + }, "how-to/integration-excel": { "version": "38.83.79", "license": "SEE LICENSE IN LICENSE.MD", @@ -2394,6 +2407,12 @@ "dev": true, "peer": true }, + "node_modules/@types/w3c-web-usb": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.10.tgz", + "integrity": "sha512-CHgUI5kTc/QLMP8hODUHhge0D4vx+9UiAwIGiT0sTy/B2XpdX1U5rJt6JSISgr6ikRT7vxV9EVAFeYZqUnl1gQ==", + "dev": true + }, "node_modules/@types/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz", @@ -4790,6 +4809,10 @@ "node": ">=8" } }, + "node_modules/detect-usb-devices": { + "resolved": "how-to/detect-usb-devices", + "link": true + }, "node_modules/diff": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",