├── .editorconfig
├── .env
├── .env.development.local
├── .github
└── workflows
│ ├── develop.yml
│ └── master.yml
├── .gitignore
├── .nvmrc
├── .prettierrc
├── .unimportedrc.json
├── .vscode
├── extensions.json
└── settings.json
├── LICENSE
├── README.md
├── codegen.yml
├── craco.config.js
├── package-lock.json
├── package.json
├── public
├── close.png
├── defaultFavicon.ico
├── favicon.ico
├── fonts
│ ├── large.fnt
│ ├── large.png
│ ├── medium.fnt
│ ├── medium.png
│ ├── small.fnt
│ ├── small.png
│ ├── smaller.fnt
│ └── smaller.png
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src-tauri
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── benches.rs
├── build.rs
├── icons
│ ├── 128x128.png
│ ├── 128x128@2x.png
│ ├── 32x32.png
│ ├── icon.icns
│ ├── icon.ico
│ └── macos_template.png
├── src
│ ├── assets
│ │ ├── linux_active_window.sh
│ │ └── macos_active_window.as
│ ├── lib
│ │ ├── mod.rs
│ │ ├── os.rs
│ │ ├── serial.rs
│ │ └── system
│ │ │ ├── mod.rs
│ │ │ ├── nix.rs
│ │ │ └── win.rs
│ ├── main.rs
│ └── modules
│ │ ├── commands.rs
│ │ ├── event_handlers.rs
│ │ ├── mod.rs
│ │ ├── plugins.rs
│ │ ├── state.rs
│ │ ├── threads.rs
│ │ └── window.rs
├── tauri.conf.json
└── tauri.macos.conf.json
├── src
├── App.tsx
├── Body.tsx
├── CustomAlert.tsx
├── ModalBody.tsx
├── containers
│ ├── About.tsx
│ ├── BackButtonLiveData.tsx
│ ├── Collection
│ │ ├── Collections.tsx
│ │ ├── Menu.tsx
│ │ ├── Settings
│ │ │ ├── AutoPageSwitcher.tsx
│ │ │ ├── General.tsx
│ │ │ └── Modal.tsx
│ │ └── index.tsx
│ ├── ContentBody.tsx
│ ├── DefaultBackButtonSettings.tsx
│ ├── DeprecatedInfoModal.tsx
│ ├── DeveloperSettings.tsx
│ ├── Device.tsx
│ ├── DisplayButton
│ │ ├── ButtonSettings
│ │ │ ├── Action.tsx
│ │ │ ├── ChangePage.tsx
│ │ │ ├── FreeDeckSettings.tsx
│ │ │ ├── Hotkeys.tsx
│ │ │ ├── LeavePage.tsx
│ │ │ ├── SpecialKeys.tsx
│ │ │ ├── Text.tsx
│ │ │ └── index.tsx
│ │ ├── DisplayButtonSettingsModal.tsx
│ │ ├── DisplaySettings
│ │ │ ├── DropDisplay.tsx
│ │ │ ├── ImageSettings.tsx
│ │ │ └── index.tsx
│ │ ├── LiveDataSettings
│ │ │ └── index.tsx
│ │ └── index.tsx
│ ├── Displays.tsx
│ ├── FDHub
│ │ ├── Home.tsx
│ │ ├── PageDetails.tsx
│ │ ├── Search.tsx
│ │ ├── components
│ │ │ └── HubPage.tsx
│ │ └── index.tsx
│ ├── FirstTime.tsx
│ ├── GeneralSettingsModal.tsx
│ ├── Header.tsx
│ ├── HelpModal.tsx
│ ├── Login.tsx
│ ├── LoginButton.tsx
│ ├── Page
│ │ ├── Menu.tsx
│ │ ├── Pages.tsx
│ │ ├── Publish.tsx
│ │ ├── Settings
│ │ │ ├── AutoPageSwitcher.tsx
│ │ │ ├── General.tsx
│ │ │ └── Modal.tsx
│ │ └── index.tsx
│ └── _Boilerplate.tsx
├── definitions
│ ├── back.png
│ ├── defaultBackImage.ts
│ ├── defaultPage.ts
│ ├── emptyConvertedImage.ts
│ ├── floyd-steinberg.d.ts
│ ├── fonts.ts
│ ├── headers.ts
│ ├── iconSizes.ts
│ ├── keys.ts
│ └── modes.ts
├── generated
│ ├── button.ts
│ ├── config.ts
│ ├── display.ts
│ ├── index.ts
│ └── types-and-hooks.ts
├── graphql
│ └── queries.gql
├── index.tsx
├── lib
│ ├── components
│ │ ├── ActionPreview.tsx
│ │ ├── Alert.tsx
│ │ ├── Anchor.tsx
│ │ ├── Avatar.tsx
│ │ ├── Button.tsx
│ │ ├── Confirm.tsx
│ │ ├── CtrlDuo.tsx
│ │ ├── Divider.tsx
│ │ ├── ImagePreview.tsx
│ │ ├── LabelValue.tsx
│ │ ├── Menu.tsx
│ │ ├── Modal.tsx
│ │ ├── Row.tsx
│ │ ├── ScrollListContainer.tsx
│ │ ├── SelectInput.tsx
│ │ ├── Slider.tsx
│ │ ├── Switch.tsx
│ │ ├── TabView.tsx
│ │ ├── TextArea.tsx
│ │ ├── TextInput.tsx
│ │ ├── Title.tsx
│ │ ├── TitleInput.tsx
│ │ └── Window.tsx
│ ├── configFile
│ │ ├── consts.ts
│ │ ├── createBody.ts
│ │ ├── createBuffer.ts
│ │ ├── createFooter.ts
│ │ ├── createHeader.ts
│ │ ├── loadConfigFile.ts
│ │ ├── parseConfig.ts
│ │ └── ssd1306.ts
│ ├── file
│ │ ├── download.ts
│ │ ├── fileToImage.ts
│ │ └── handleFileSelect.ts
│ ├── hooks
│ │ ├── once.tsx
│ │ └── startup
│ │ │ ├── index.tsx
│ │ │ ├── liveData.tsx
│ │ │ ├── pageSwitcher.tsx
│ │ │ ├── persistentConfig.tsx
│ │ │ └── serialCommand.tsx
│ ├── image
│ │ ├── base64Encode.ts
│ │ ├── colorToMonoBitmap.ts
│ │ └── composeImage.ts
│ ├── localisation
│ │ └── keyboard.ts
│ ├── misc
│ │ ├── createToast.tsx
│ │ ├── eventListeners.tsx
│ │ ├── scrollToPage.tsx
│ │ └── util.ts
│ └── serial
│ │ ├── commands.ts
│ │ ├── fdSerialApi.ts
│ │ ├── index.ts
│ │ ├── tauri-serial.ts
│ │ └── web-serial.ts
├── react-app-env.d.ts
├── reportWebVitals.ts
├── schemas
│ ├── button.ts
│ ├── config.ts
│ └── display.ts
├── scripts
│ └── joiTypes.ts
├── service-worker.ts
├── serviceWorkerRegistration.ts
├── states
│ ├── appState.ts
│ ├── configState.ts
│ └── interfaces.ts
└── tailwind.css
├── syncVersions.js
├── tailwind.config.js
├── tsconfig.common.json
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root=true
2 | [*.{ts,tsx,jsx,js}]
3 | indent_size = 2
4 | indent_style = space
5 |
6 | [*.rs]
7 | indent_size = 4
8 | indent_style = space
9 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_API_URL=https://fddevapi.gosewis.ch
2 | REACT_APP_ENABLE_API=false
3 | REACT_APP_API_COMING_SOON=false
--------------------------------------------------------------------------------
/.env.development.local:
--------------------------------------------------------------------------------
1 | REACT_APP_API_URL=http://localhost:5000
2 | REACT_APP_ENABLE_API=false
3 | REACT_APP_API_COMING_SOON=false
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | #.env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | .eslintcache
26 |
27 | .vscode/arduino.json
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 16.13.0
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "useTabs": false,
4 | "printWidth": 80
5 | }
6 |
--------------------------------------------------------------------------------
/.unimportedrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "ignorePatterns": [
3 | "**/node_modules/**",
4 | "**/*.tests.{js,jsx,ts,tsx}",
5 | "**/*.test.{js,jsx,ts,tsx}",
6 | "**/*.spec.{js,jsx,ts,tsx}",
7 | "**/tests/**",
8 | "**/__tests__/**",
9 | "**/*.d.ts",
10 | "**/generated/types-and-hooks.ts"
11 | ],
12 | "ignoreUnimported": [
13 | "src/containers/_Boilerplate.tsx",
14 | "src/service-worker.ts"
15 | ],
16 | "ignoreUnused": [],
17 | "ignoreUnresolved": []
18 | }
19 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
4 |
5 | // List of extensions which should be recommended for users of this workspace.
6 | "recommendations": [
7 | "esbenp.prettier-vscode",
8 | "rust-lang.rust-analyzer",
9 | "amatiasq.sort-imports"
10 | ],
11 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace.
12 | "unwantedRecommendations": [
13 |
14 | ]
15 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | // "eslint.run": "onSave",
3 | "sort-imports.on-save": true,
4 | "sort-imports.default-sort-style": "module",
5 | "typescript.preferences.quoteStyle": "double",
6 | "editor.formatOnSave": true,
7 | "rust-analyzer.checkOnSave": {
8 | "command": "clippy",
9 | "enable": true
10 | }
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/codegen.yml:
--------------------------------------------------------------------------------
1 | overwrite: true
2 | schema: "http://localhost:5000/graphql"
3 | documents: "./src/graphql/**/*.gql"
4 | generates:
5 | src/generated/types-and-hooks.ts:
6 | config:
7 | maybeValue: T | null | undefined
8 | inputMaybeValue: T | null | undefined
9 | avoidOptionals: false
10 | plugins:
11 | - typescript
12 | - typescript-operations
13 | - typescript-react-apollo
14 |
--------------------------------------------------------------------------------
/craco.config.js:
--------------------------------------------------------------------------------
1 | // craco.config.js
2 | module.exports = {
3 | style: {
4 | postcss: {
5 | plugins: [require("tailwindcss"), require("autoprefixer")],
6 | },
7 | },
8 | devServer: {
9 | open: false,
10 | // hot: false,
11 | // liveReload: true,
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "freedeck-configurator",
3 | "version": "2.13.5",
4 | "configFileVersion": "1.3.0",
5 | "minFWVersion": "2.7.0",
6 | "private": true,
7 | "homepage": "./",
8 | "type": "commonjs",
9 | "dependencies": {
10 | "@apollo/client": "3.3.15",
11 | "@bitovi/use-simple-reducer": "^0.0.1",
12 | "@headlessui/react": "^1.4.1",
13 | "@heroicons/react": "^2.0.11",
14 | "@tauri-apps/api": "^1.0.2",
15 | "clsx": "^1.1.1",
16 | "floyd-steinberg": "^1.0.6",
17 | "jimp": "^0.16.0",
18 | "joi": "^17.5.0",
19 | "joi-to-typescript": "^2.3.0",
20 | "lodash": "^4.17.20",
21 | "pako": "^2.0.4",
22 | "preval.macro": "^5.0.0",
23 | "react": "^17.0.2",
24 | "react-dnd": "^16.0.1",
25 | "react-dnd-html5-backend": "^16.0.1",
26 | "react-dom": "^17.0.2",
27 | "react-dropzone": "^10.2.2",
28 | "react-hot-toast": "^2.1.1",
29 | "react-router-dom": "^6.0.2",
30 | "react-terminal-logger": "^1.3.8",
31 | "uuid": "^8.3.2",
32 | "web-vitals": "^2.1.2",
33 | "worker-interval": "^1.0.6"
34 | },
35 | "overrides": {
36 | "react-error-overlay": "6.0.9"
37 | },
38 | "scripts": {
39 | "start": "TAILWIND_MODE=watch craco start",
40 | "build": "craco build",
41 | "check:clean": "unimported; ts-unused-exports ./tsconfig.json --allowUnusedTypes --ignoreFiles=\"generated/.*|_Boilerplate.*\"",
42 | "eject": "react-scripts eject",
43 | "codegen": "graphql-codegen --config codegen.yml",
44 | "typegen": "pwd; ts-node -P tsconfig.common.json src/scripts/joiTypes.ts",
45 | "tauri:dev": "tauri dev",
46 | "tauri:build": "tauri build",
47 | "prebuild": "npm i"
48 | },
49 | "eslintConfig": {
50 | "extends": "react-app"
51 | },
52 | "browserslist": {
53 | "production": [
54 | ">0.2%",
55 | "not dead",
56 | "not op_mini all"
57 | ],
58 | "development": [
59 | "last 1 chrome version",
60 | "last 1 firefox version",
61 | "last 1 safari version"
62 | ]
63 | },
64 | "devDependencies": {
65 | "@craco/craco": "^6.4.3",
66 | "@graphql-codegen/cli": "1.21.4",
67 | "@graphql-codegen/introspection": "1.18.2",
68 | "@graphql-codegen/typescript": "1.22.0",
69 | "@graphql-codegen/typescript-operations": "^1.17.16",
70 | "@graphql-codegen/typescript-react-apollo": "^2.2.4",
71 | "@graphql-codegen/typescript-react-query": "^3.0.3",
72 | "@tauri-apps/cli": "^1.0.5",
73 | "@types/lodash": "^4.14.160",
74 | "@types/node": "^12.12.29",
75 | "@types/pako": "^2.0.0",
76 | "@types/preval.macro": "^3.0.0",
77 | "@types/react": "^17.0.2",
78 | "@types/react-dom": "^17.0.2",
79 | "@types/uuid": "^8.3.3",
80 | "@types/w3c-web-serial": "^1.0.2",
81 | "autoprefixer": "^9.8.8",
82 | "postcss": "^7.0.39",
83 | "react-scripts": "^4.0.3",
84 | "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.16",
85 | "ts-node": "^10.4.0",
86 | "ts-unused-exports": "^7.0.3",
87 | "typescript": "^4.5.2",
88 | "unimported": "^1.19.1"
89 | },
90 | "optionalDependencies": {
91 | "@tauri-apps/cli-darwin-x64": "^1.0.4",
92 | "@tauri-apps/cli-win32-x64-msvc": "^1.0.4"
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/public/close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FreeYourStream/freedeck-configurator/792f85a1944115e127bae20ef3fab1907e5ffef8/public/close.png
--------------------------------------------------------------------------------
/public/defaultFavicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FreeYourStream/freedeck-configurator/792f85a1944115e127bae20ef3fab1907e5ffef8/public/defaultFavicon.ico
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FreeYourStream/freedeck-configurator/792f85a1944115e127bae20ef3fab1907e5ffef8/public/favicon.ico
--------------------------------------------------------------------------------
/public/fonts/large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FreeYourStream/freedeck-configurator/792f85a1944115e127bae20ef3fab1907e5ffef8/public/fonts/large.png
--------------------------------------------------------------------------------
/public/fonts/medium.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FreeYourStream/freedeck-configurator/792f85a1944115e127bae20ef3fab1907e5ffef8/public/fonts/medium.png
--------------------------------------------------------------------------------
/public/fonts/small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FreeYourStream/freedeck-configurator/792f85a1944115e127bae20ef3fab1907e5ffef8/public/fonts/small.png
--------------------------------------------------------------------------------
/public/fonts/smaller.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FreeYourStream/freedeck-configurator/792f85a1944115e127bae20ef3fab1907e5ffef8/public/fonts/smaller.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
13 |
17 |
18 |
19 |
20 |
21 |
31 |
32 |
33 |
34 |
38 |
42 |
46 |
47 |
56 | FreeDeck Configurator
57 |
58 |
59 |
60 |
61 |
62 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FreeYourStream/freedeck-configurator/792f85a1944115e127bae20ef3fab1907e5ffef8/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FreeYourStream/freedeck-configurator/792f85a1944115e127bae20ef3fab1907e5ffef8/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "FreeDeck Configurator",
3 | "name": "FreeDeck Configurator",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src-tauri/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
--------------------------------------------------------------------------------
/src-tauri/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "freedeck_configurator"
3 | version = "2.13.5"
4 | description = "A Tauri App"
5 | authors = ["you"]
6 | license = ""
7 | repository = ""
8 | default-run = "freedeck_configurator"
9 | edition = "2021"
10 | rust-version = "1.57"
11 |
12 | [lib]
13 | name = "fd_lib"
14 | path = "src/lib/mod.rs"
15 |
16 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
17 |
18 | [build-dependencies]
19 | tauri-build = { version = "1.0.4", features = [] }
20 |
21 | [dev-dependencies]
22 | criterion = { version = "0.3" }
23 |
24 | [dependencies]
25 | enigo = "0.0.14"
26 | reqwest = { version = "0.11", features = ["blocking", "json"] }
27 | serde = { version = "1.0", features = ["derive"] }
28 | serde_json = "1.0"
29 | serialport = "4.2.0"
30 | tauri = { version = "1.0.5", features = ["api-all", "system-tray", "updater"] }
31 | tauri-macros = "1.0.4"
32 | tiny_http = "0.11"
33 | sysinfo = "0.29.10"
34 | log = "0.4.17"
35 | anyhow = "1.0.64"
36 |
37 | [target.'cfg(windows)'.dependencies]
38 | winapi = { version = "0.3.9", features = ["winuser"] }
39 | wmi = "0.11.2"
40 |
41 | [target.'cfg(target_os = "macos")'.dependencies]
42 | macos-app-nap = "0.0.1"
43 |
44 | [features]
45 | # by default Tauri runs in production mode
46 | # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
47 | default = ["custom-protocol"]
48 | # this feature is used used for production builds where `devPath` points to the filesystem
49 | # DO NOT remove this
50 | custom-protocol = ["tauri/custom-protocol"]
51 |
52 | [[bench]]
53 | path = "benches.rs"
54 | name = "benches"
55 | harness = false
56 |
57 | # [[bench]]
58 | # name = "serial"
59 | # harness = false
60 |
--------------------------------------------------------------------------------
/src-tauri/benches.rs:
--------------------------------------------------------------------------------
1 | extern crate criterion;
2 | use criterion::{criterion_group, criterion_main, Criterion};
3 |
4 | use fd_lib::system::SystemInfo;
5 |
6 | use fd_lib::os::get_current_window;
7 | use fd_lib::serial::FDSerial;
8 | fn criterion_benchmark(c: &mut Criterion) {
9 | c.bench_function("refresh serial ports", |b| {
10 | let mut serial = FDSerial::new();
11 | b.iter(|| serial.refresh_ports(0, |_ports| ()));
12 | });
13 | c.bench_function("get current window", |b| {
14 | b.iter(|| get_current_window(|path| Some(path.into())))
15 | });
16 | c.bench_function("get cpu temp", |b| {
17 | let mut system = SystemInfo::new().unwrap();
18 | b.iter(|| system.cpu_temp())
19 | });
20 | c.bench_function("get gpu temp", |b| {
21 | let mut system = SystemInfo::new().unwrap();
22 | b.iter(|| system.gpu_temp())
23 | });
24 | }
25 |
26 | criterion_group!(all, criterion_benchmark);
27 | criterion_main!(all);
28 |
--------------------------------------------------------------------------------
/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build();
3 | }
4 |
--------------------------------------------------------------------------------
/src-tauri/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FreeYourStream/freedeck-configurator/792f85a1944115e127bae20ef3fab1907e5ffef8/src-tauri/icons/128x128.png
--------------------------------------------------------------------------------
/src-tauri/icons/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FreeYourStream/freedeck-configurator/792f85a1944115e127bae20ef3fab1907e5ffef8/src-tauri/icons/128x128@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FreeYourStream/freedeck-configurator/792f85a1944115e127bae20ef3fab1907e5ffef8/src-tauri/icons/32x32.png
--------------------------------------------------------------------------------
/src-tauri/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FreeYourStream/freedeck-configurator/792f85a1944115e127bae20ef3fab1907e5ffef8/src-tauri/icons/icon.icns
--------------------------------------------------------------------------------
/src-tauri/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FreeYourStream/freedeck-configurator/792f85a1944115e127bae20ef3fab1907e5ffef8/src-tauri/icons/icon.ico
--------------------------------------------------------------------------------
/src-tauri/icons/macos_template.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FreeYourStream/freedeck-configurator/792f85a1944115e127bae20ef3fab1907e5ffef8/src-tauri/icons/macos_template.png
--------------------------------------------------------------------------------
/src-tauri/src/assets/linux_active_window.sh:
--------------------------------------------------------------------------------
1 | xprop -id $(xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2) _NET_WM_NAME 2>/dev/null | awk -F= '{print($2)}'
--------------------------------------------------------------------------------
/src-tauri/src/assets/macos_active_window.as:
--------------------------------------------------------------------------------
1 | global frontApp, frontAppName, windowTitle
2 | set windowTitle to ""
3 | tell application "System Events"
4 | set frontApp to first application process whose frontmost is true
5 | set frontAppName to name of frontApp
6 | tell process frontAppName
7 | tell (1st window whose value of attribute "AXMain" is true)
8 | set windowTitle to value of attribute "AXTitle"
9 | end tell
10 | end tell
11 | end tell
12 | return windowTitle & " - " & frontAppName
13 |
--------------------------------------------------------------------------------
/src-tauri/src/lib/mod.rs:
--------------------------------------------------------------------------------
1 | #![warn(clippy::unwrap_used, clippy::print_stdout)]
2 | pub mod os;
3 | pub mod serial;
4 | pub mod system;
5 |
--------------------------------------------------------------------------------
/src-tauri/src/lib/os.rs:
--------------------------------------------------------------------------------
1 | use anyhow::{anyhow, Result};
2 | use std::path::PathBuf;
3 | #[cfg(target_os = "macos")]
4 | pub fn get_current_window Option>(
5 | resolve_resource: F,
6 | ) -> Result {
7 | use std::process::Command;
8 | let script_location = resolve_resource("./src/assets/macos_active_window.as").unwrap();
9 | let mut command = Command::new("sh");
10 |
11 | command.arg("-c").arg(format!(
12 | "osascript {}",
13 | script_location.as_path().to_str().unwrap()
14 | ));
15 |
16 | let output = command.output().unwrap();
17 | let result = String::from_utf8(output.stdout).unwrap();
18 | let success = result.trim().len() > 0;
19 |
20 | if success {
21 | return Ok(result);
22 | }
23 | Err(anyhow!("failed to get active window, remove and readd freedeck-configurator to accessibility list?"))
24 | }
25 |
26 | #[cfg(target_os = "linux")]
27 | pub fn get_current_window Option>(
28 | _resolve_resource: F,
29 | ) -> Result {
30 | use std::process::Command;
31 |
32 | let mut command = Command::new("sh");
33 | command
34 | .arg("-c")
35 | .arg(include_str!("../assets/linux_active_window.sh"));
36 |
37 | let output = command.output()?;
38 | let result = String::from_utf8(output.stdout)?;
39 | let success = !result.trim().is_empty();
40 |
41 | if !success {
42 | return Err(anyhow!("failed to get active window"));
43 | }
44 | Ok(result)
45 | }
46 |
47 | #[cfg(target_os = "windows")]
48 | pub fn get_current_window Option>(
49 | _resolve_resource: F,
50 | ) -> Result {
51 | use std::{ffi::OsString, os::windows::prelude::OsStringExt};
52 | use winapi::um::winuser::{GetForegroundWindow, GetWindowTextW};
53 | unsafe {
54 | let window = GetForegroundWindow();
55 | let mut text: [u16; 512] = [0; 512];
56 | let _result: usize = GetWindowTextW(window, text.as_mut_ptr(), text.len() as i32) as usize;
57 | let (short, _) = text.split_at(_result);
58 | let result = OsString::from_wide(short)
59 | .to_str()
60 | .expect("os string conversion error")
61 | .to_string();
62 | let success = !result.trim().is_empty();
63 | if success {
64 | return Ok(OsString::from_wide(short)
65 | .to_str()
66 | .expect("os string conversion error")
67 | .to_string());
68 | }
69 | }
70 | Err(anyhow!("windows did an oopsy"))
71 | }
72 |
--------------------------------------------------------------------------------
/src-tauri/src/lib/system/mod.rs:
--------------------------------------------------------------------------------
1 | #[cfg(target_family = "windows")]
2 | pub mod win;
3 | #[cfg(target_family = "windows")]
4 | pub use win::SystemInfo;
5 |
6 | #[cfg(not(target_os = "windows"))]
7 | pub mod nix;
8 | #[cfg(not(target_os = "windows"))]
9 | pub use nix::SystemInfo;
10 |
--------------------------------------------------------------------------------
/src-tauri/src/lib/system/nix.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Result;
2 | use sysinfo::{Component, ComponentExt, System, SystemExt};
3 | pub struct SystemInfo {
4 | sys: System,
5 | }
6 |
7 | impl SystemInfo {
8 | pub fn new() -> Result {
9 | let sys = System::new_all();
10 | Ok(SystemInfo { sys })
11 | }
12 | pub fn cpu_temp(&mut self) -> f32 {
13 | let cpu: Option<&mut Component> = self.sys.components_mut().iter_mut().find(|c| {
14 | ["Tdie", "Tctl", "Package id 0", "Computer", "PECI CPU"].contains(&c.label())
15 | });
16 | match cpu {
17 | Some(c) => {
18 | c.refresh();
19 | c.temperature()
20 | }
21 | None => 0.0,
22 | }
23 | }
24 | pub fn gpu_temp(&mut self) -> f32 {
25 | let gpu: Option<&mut Component> = self
26 | .sys
27 | .components_mut()
28 | .iter_mut()
29 | .find(|c| ["edge", "GPU"].contains(&c.label()));
30 | match gpu {
31 | Some(g) => {
32 | g.refresh();
33 | g.temperature()
34 | }
35 | None => 0.0,
36 | }
37 | }
38 | pub fn list_sensors(&mut self) -> Vec {
39 | self.sys.refresh_components_list();
40 | self.sys.refresh_components();
41 | let sensors = self
42 | .sys
43 | .components()
44 | .iter()
45 | .map(|c| c.label().to_string())
46 | .collect();
47 | println!("{:?}", sensors);
48 | sensors
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src-tauri/src/lib/system/win.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Result;
2 | use std::collections::HashMap;
3 | use wmi::*;
4 |
5 | // struct SaveCOM(COMLibrary);
6 | // unsafe impl Send for SaveCOM {}
7 |
8 | pub struct SystemInfo {
9 | sys: WMIConnection,
10 | }
11 |
12 | impl SystemInfo {
13 | pub fn new() -> Result {
14 | let lib = COMLibrary::new()?;
15 | let connection = WMIConnection::with_namespace_path("root\\OpenHardwareMonitor", lib)?;
16 | Ok(SystemInfo { sys: connection })
17 | }
18 | pub fn cpu_temp(&mut self) -> f32 {
19 | let cpu_results: Result>, WMIError> =
20 | self.sys.raw_query("SELECT * FROM Sensor where Identifier = '/amdcpu/0/temperature/0' or Identifier = '/intelcpu/0/temperature/0' AND SensorType = 'Temperature'");
21 |
22 | let cpu_results = match cpu_results {
23 | Ok(result) => result,
24 | Err(_e) => return 0.0,
25 | };
26 |
27 | if cpu_results.is_empty() {
28 | 0.0
29 | } else {
30 | let cpu = &cpu_results[0];
31 | if let Some(Variant::R4(value)) = cpu.get("Value") {
32 | value.to_owned()
33 | } else {
34 | 0.0
35 | }
36 | }
37 | }
38 | pub fn gpu_temp(&mut self) -> f32 {
39 | let gpu_results: Result>, WMIError> =
40 | self.sys.raw_query("SELECT * FROM Sensor where Identifier = '/atigpu/0/temperature/0' or Identifier = '/nvidiagpu/0/temperature/0' AND SensorType = 'Temperature'");
41 |
42 | let gpu_results = match gpu_results {
43 | Ok(result) => result,
44 | Err(_e) => return 0.0,
45 | };
46 |
47 | if gpu_results.is_empty() {
48 | 0.0
49 | } else {
50 | let cpu = &gpu_results[0];
51 | if let Some(Variant::R4(value)) = cpu.get("Value") {
52 | value.to_owned()
53 | } else {
54 | 0.0
55 | }
56 | }
57 | }
58 | pub fn list_sensors(&mut self) -> Vec {
59 | let sensor_list_result: Vec> = match self
60 | .sys
61 | .raw_query("SELECT * FROM Sensor where SensorType='Temperature'")
62 | {
63 | Ok(result) => result,
64 | Err(e) => return vec![e.to_string()],
65 | };
66 |
67 | sensor_list_result
68 | .iter()
69 | .filter_map(|s| match s.get("Identifier") {
70 | Some(Variant::String(value)) => Some(value.clone()),
71 | _ => None,
72 | })
73 | .collect()
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src-tauri/src/main.rs:
--------------------------------------------------------------------------------
1 | #![warn(clippy::unwrap_used, clippy::print_stdout)]
2 | #![cfg_attr(
3 | all(not(debug_assertions), target_os = "windows"),
4 | windows_subsystem = "windows"
5 | )]
6 | mod modules;
7 |
8 | use std::sync::{Arc, Mutex};
9 |
10 | use fd_lib::serial::FDSerial;
11 | use modules::{commands, event_handlers, plugins::single_instance, state::FDState, threads};
12 |
13 | use tauri::{CustomMenuItem, Manager, SystemTray, SystemTrayMenu, SystemTrayMenuItem};
14 |
15 | use tauri_macros::generate_handler;
16 |
17 | fn main() {
18 | #[cfg(target_os = "macos")]
19 | macos_app_nap::prevent();
20 |
21 | let quit = CustomMenuItem::new("quit".to_string(), "Quit");
22 | let show = CustomMenuItem::new("show".to_string(), "Show");
23 | let updates = CustomMenuItem::new("updates".to_string(), "Check for updates");
24 | let mut aps_enabled = CustomMenuItem::new("aps".to_string(), "Auto page-switcher");
25 | aps_enabled.selected = true;
26 |
27 | let tray_menu = SystemTrayMenu::new()
28 | .add_item(show)
29 | .add_item(aps_enabled)
30 | .add_item(updates)
31 | .add_native_item(SystemTrayMenuItem::Separator)
32 | .add_item(quit);
33 | let tray = SystemTray::new().with_menu(tray_menu);
34 |
35 | let state = Arc::new(Mutex::new(FDState {
36 | serial: FDSerial::new(),
37 | current_window: "".to_string(),
38 | sensors: Vec::new(),
39 | }));
40 |
41 | #[allow(unused_mut)] // needed for macos
42 | let mut app = tauri::Builder::default()
43 | .plugin(single_instance::init())
44 | .system_tray(tray)
45 | .on_system_tray_event(event_handlers::handle_tray_event)
46 | .manage(state.clone())
47 | .invoke_handler(generate_handler!(
48 | commands::get_ports,
49 | commands::open,
50 | commands::close,
51 | commands::flush,
52 | commands::write,
53 | commands::read,
54 | commands::read_line,
55 | commands::get_current_window,
56 | commands::set_aps_state,
57 | commands::press_keys,
58 | commands::list_sensors
59 | ))
60 | .build(tauri::generate_context!())
61 | .expect("error while running tauri application");
62 |
63 | #[cfg(target_os = "linux")]
64 | if app.handle().env().appimage.is_none() {
65 | app.tray_handle()
66 | .get_item("updates")
67 | .set_enabled(false)
68 | .expect("failed to disable update menu item");
69 | app.tray_handle()
70 | .get_item("updates")
71 | .set_title("Platform does not support updates")
72 | .expect("failed to set update menu item title");
73 | }
74 |
75 | #[cfg(target_os = "macos")]
76 | app.set_activation_policy(tauri::ActivationPolicy::Regular);
77 |
78 | let ports_join = threads::ports_thread(&app.handle(), &state);
79 | let read_join = threads::read_thread(&app.handle(), &state);
80 | let current_window_join = threads::current_window_thread(&app.handle(), &state);
81 | let system_temps_join = threads::system_temps_thread(&app.handle(), &state);
82 |
83 | app.run(event_handlers::handle_tauri_event);
84 |
85 | ports_join.join().expect("ports_join failed");
86 | read_join.join().expect("read_join failed");
87 | current_window_join
88 | .join()
89 | .expect("current_window_join failed");
90 | system_temps_join.join().expect("system_temps_join failed");
91 | }
92 |
--------------------------------------------------------------------------------
/src-tauri/src/modules/commands.rs:
--------------------------------------------------------------------------------
1 | use super::state::FDState;
2 | use enigo::{Enigo, KeyboardControllable};
3 | use std::sync::{Arc, Mutex};
4 | use tauri::{Manager, State, Window};
5 | use tauri_macros::command;
6 |
7 | #[command]
8 | pub fn get_ports(state: State>>) -> Result, String> {
9 | let state = state.lock().expect("mutex poisened");
10 | Ok(state
11 | .serial
12 | .get_ports()
13 | .map_err(|e| e.to_string())?
14 | .iter()
15 | .map(|p| p.into())
16 | .collect())
17 | }
18 |
19 | #[command]
20 | pub fn open(state: State>>, path: String, baud_rate: u32) -> Result<(), String> {
21 | let mut state = state.lock().expect("mutex poisened");
22 | state
23 | .serial
24 | .connect(path, baud_rate)
25 | .map_err(|e| e.to_string())
26 | }
27 | #[command]
28 | pub fn close(state: State>>) {
29 | let mut state = state.lock().expect("mutex poisened");
30 | state.serial.disconnect();
31 | }
32 | #[command]
33 | pub fn flush(state: State>>) {
34 | let mut state = state.lock().expect("mutex poisened");
35 | state.serial.data = Vec::new();
36 | }
37 | #[command]
38 | pub fn write(state: State>>, data: Vec) -> Result<(), String> {
39 | let mut state = state.lock().expect("mutex poisened");
40 | state.serial.write(data).map_err(|e| e.to_string())
41 | }
42 |
43 | #[command]
44 | pub fn read(state: State>>) -> Vec {
45 | let mut state = state.lock().expect("mutex poisened");
46 | state.serial.read()
47 | }
48 |
49 | #[command]
50 | pub fn read_line(state: State>>) -> Result, String> {
51 | let mut state = state.lock().expect("mutex poisened");
52 | state.serial.read_line().map_err(|e| e.to_string())
53 | }
54 |
55 | #[command]
56 | pub fn press_keys(_state: State>>, key_string: String) {
57 | let mut enigo = Enigo::new();
58 | enigo.key_sequence(&key_string);
59 | }
60 |
61 | #[command]
62 | pub fn get_current_window(state: State>>) -> String {
63 | let state = state.lock().expect("mutex poisened");
64 | state.current_window.clone()
65 | }
66 |
67 | #[command]
68 | pub fn list_sensors(state: State>>) -> Vec {
69 | let state = state.lock().expect("mutex poisened");
70 | state.sensors.clone()
71 | }
72 |
73 | #[command]
74 | pub fn set_aps_state(window: Window, aps_state: bool) -> Result<(), String> {
75 | let aps_item = window.app_handle().tray_handle().get_item("aps");
76 | aps_item.set_selected(aps_state).map_err(|e| e.to_string())
77 | }
78 |
--------------------------------------------------------------------------------
/src-tauri/src/modules/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod commands;
2 | pub mod event_handlers;
3 | pub mod plugins;
4 | pub mod state;
5 | pub mod threads;
6 | pub mod window;
7 |
--------------------------------------------------------------------------------
/src-tauri/src/modules/plugins.rs:
--------------------------------------------------------------------------------
1 | pub mod single_instance {
2 |
3 | use crate::modules::window;
4 |
5 | use log::info;
6 | use std::thread;
7 | use tauri::{
8 | plugin::{Builder, TauriPlugin},
9 | AppHandle, Manager, Runtime,
10 | };
11 | use tiny_http::{Response, Server};
12 |
13 | pub fn start_server(app_handle: AppHandle) {
14 | thread::spawn(move || {
15 | let server = match Server::http("localhost:57891") {
16 | Ok(server) => server,
17 | Err(e) => {
18 | println!("Error starting server on port 57891: {}", e);
19 | return;
20 | }
21 | };
22 | for request in server.incoming_requests() {
23 | let response = Response::from_string("ok");
24 | match request.respond(response) {
25 | Ok(_) => println!("responded to request"),
26 | Err(e) => println!("failed to respond to request: {:?}", e),
27 | }
28 | let window = app_handle
29 | .get_window("main")
30 | .expect("main window not found");
31 | window::show(window);
32 | }
33 | });
34 | }
35 |
36 | /// Initializes the plugin.
37 | #[must_use]
38 | pub fn init() -> TauriPlugin {
39 | Builder::new("single-instance")
40 | .setup(|app_handle| {
41 | println!("Starting server...");
42 | match reqwest::blocking::get("http://localhost:57891/") {
43 | Ok(result) => {
44 | info!(
45 | "App already running: {}",
46 | result
47 | .text()
48 | .unwrap_or_else(|e| format!("no response because of error: {}", e))
49 | );
50 | app_handle.exit(1);
51 | }
52 | Err(_) => start_server(app_handle.clone()),
53 | }
54 | Ok(())
55 | })
56 | .build()
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src-tauri/src/modules/state.rs:
--------------------------------------------------------------------------------
1 | use fd_lib::serial::FDSerial;
2 |
3 | pub struct FDState {
4 | pub serial: FDSerial,
5 | pub current_window: String,
6 | pub sensors: Vec,
7 | }
8 |
--------------------------------------------------------------------------------
/src-tauri/src/modules/window.rs:
--------------------------------------------------------------------------------
1 | use log::error;
2 | use tauri::{Runtime, Window};
3 |
4 | pub fn show(window: Window) {
5 | window
6 | .hide()
7 | .unwrap_or_else(|e| error!("Error hiding window: {}", e));
8 | window
9 | .set_focus()
10 | .unwrap_or_else(|e| error!("Error setting focus: {}", e));
11 | window
12 | .show()
13 | .unwrap_or_else(|e| error!("Error showing window: {}", e));
14 | window
15 | .unminimize()
16 | .unwrap_or_else(|e| error!("Error unminimizing window: {}", e));
17 | }
18 |
--------------------------------------------------------------------------------
/src-tauri/tauri.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../node_modules/@tauri-apps/cli/schema.json",
3 | "build": {
4 | "beforeBuildCommand": "npm run build",
5 | "beforeDevCommand": "npm run start",
6 | "devPath": "http://localhost:3000",
7 | "distDir": "../build"
8 | },
9 | "package": {
10 | "productName": "freedeck-configurator",
11 | "version": "2.13.5"
12 | },
13 | "tauri": {
14 | "allowlist": {
15 | "all": true
16 | },
17 | "bundle": {
18 | "active": true,
19 | "category": "DeveloperTool",
20 | "copyright": "",
21 | "deb": {
22 | "depends": []
23 | },
24 | "externalBin": [],
25 | "icon": [
26 | "icons/32x32.png",
27 | "icons/128x128.png",
28 | "icons/128x128@2x.png",
29 | "icons/icon.icns",
30 | "icons/icon.ico"
31 | ],
32 | "identifier": "com.freeyourstream.freedeck-configurator",
33 | "longDescription": "",
34 | "macOS": {
35 | "entitlements": null,
36 | "exceptionDomain": "",
37 | "frameworks": [],
38 | "providerShortName": null,
39 | "signingIdentity": null
40 | },
41 | "resources": [
42 | "src/assets/macos_active_window.as"
43 | ],
44 | "shortDescription": "",
45 | "targets": "all",
46 | "windows": {
47 | "certificateThumbprint": null,
48 | "digestAlgorithm": "sha256",
49 | "timestampUrl": ""
50 | }
51 | },
52 | "security": {
53 | "csp": null
54 | },
55 | "systemTray": {
56 | "iconPath": "icons/32x32.png"
57 | },
58 | "updater": {
59 | "active": true,
60 | "dialog": false,
61 | "endpoints": [
62 | "https://fdconfig.freeyourstream.com/updater.json"
63 | ],
64 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDlFNTk1ODc4NzcwQUYyNgpSV1FtcjNDSGg1WGxDZThFbmVIdGh0YTdoV21xTkZ6ODRHSmNXR0JrQ0V5aGJyZVJJS3M1NE5wVQo="
65 | },
66 | "windows": [
67 | {
68 | "fullscreen": false,
69 | "height": 768,
70 | "minHeight": 768,
71 | "resizable": true,
72 | "title": "FreeDeck Configurator",
73 | "width": 1366,
74 | "minWidth": 1366,
75 | "fileDropEnabled": false
76 | }
77 | ]
78 | }
79 | }
--------------------------------------------------------------------------------
/src-tauri/tauri.macos.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../node_modules/@tauri-apps/cli/schema.json",
3 | "tauri": {
4 | "systemTray": {
5 | "iconPath": "icons/macos_template.png",
6 | "iconAsTemplate": true
7 | }
8 | }
9 | }
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useSimpleReducer } from "@bitovi/use-simple-reducer";
2 | import React, { useRef } from "react";
3 | import { DndProvider } from "react-dnd";
4 | import { HTML5Backend } from "react-dnd-html5-backend";
5 | import { HashRouter } from "react-router-dom";
6 |
7 | import { Body } from "./Body";
8 | import { Config } from "./generated";
9 | import { useOnce } from "./lib/hooks/once";
10 | import { useBackgroundTasks } from "./lib/hooks/startup";
11 | import { AddEventListeners } from "./lib/misc/eventListeners";
12 | import { ModalBody } from "./ModalBody";
13 | import {
14 | AppDispatchContext,
15 | AppState,
16 | AppStateContext,
17 | IAppReducer,
18 | appReducer,
19 | } from "./states/appState";
20 | import {
21 | ConfigDispatchContext,
22 | ConfigStateContext,
23 | IConfigReducer,
24 | configReducer,
25 | } from "./states/configState";
26 |
27 | export type RefState = { deck: AppState["deck"]; system: AppState["system"] };
28 | export type StateRef = React.MutableRefObject;
29 |
30 | const App: React.FC<{
31 | defaultConfigState: Config;
32 | defaultAppState: AppState;
33 | }> = ({ defaultConfigState, defaultAppState }) => {
34 | const [configState, configDispatch] = useSimpleReducer<
35 | Config,
36 | IConfigReducer
37 | >(defaultConfigState, configReducer);
38 |
39 | const [appState, appDispatch] = useSimpleReducer(
40 | defaultAppState,
41 | appReducer
42 | );
43 |
44 | const refState = useRef({
45 | deck: appState.deck,
46 | system: appState.system,
47 | });
48 | useOnce(appState, appDispatch, refState);
49 | useBackgroundTasks(
50 | configState,
51 | configDispatch,
52 | appState,
53 | appDispatch,
54 | refState
55 | );
56 | AddEventListeners({ appDispatchContext: appDispatch });
57 |
58 | return (
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | );
74 | };
75 |
76 | export default App;
77 |
--------------------------------------------------------------------------------
/src/Body.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | PlusCircleIcon,
3 | QuestionMarkCircleIcon,
4 | } from "@heroicons/react/24/outline";
5 | import { useContext } from "react";
6 | import { useDrop } from "react-dnd";
7 | import { Toaster } from "react-hot-toast";
8 | import { useNavigate } from "react-router-dom";
9 |
10 | import { Collections } from "./containers/Collection/Collections";
11 | import { ContentBody } from "./containers/ContentBody";
12 | import { FirstPage } from "./containers/FirstTime";
13 | import { Header } from "./containers/Header";
14 | import { Pages } from "./containers/Page/Pages";
15 | import { FDButton } from "./lib/components/Button";
16 | import {
17 | ConfigDispatchContext,
18 | ConfigStateContext,
19 | } from "./states/configState";
20 |
21 | export const Body = () => {
22 | const nav = useNavigate();
23 | const configState = useContext(ConfigStateContext);
24 | const { createCollection, addPage, setPageCollection } = useContext(
25 | ConfigDispatchContext
26 | );
27 | const [, drop] = useDrop<{ pageId: string; collectionId: string }>({
28 | accept: "page",
29 | drop: (item, monitor) => {
30 | if (!!monitor.getItem().collectionId && monitor.isOver()) {
31 | setPageCollection({
32 | pageId: monitor.getItem().pageId,
33 | collectionId: monitor.getItem().collectionId,
34 | });
35 | }
36 | },
37 | });
38 | return (
39 |
40 |
41 |
42 | {!!Object.values(configState.pages.byId).filter(
43 | (p) => !p.isInCollection
44 | ).length && }
45 | {!Object.values(configState.pages.sorted).length && }
46 | {!!Object.keys(configState.collections.sorted).length &&
47 | !!configState.pages.sorted.length && }
48 |
49 |
50 |
51 | }
53 | className="mr-4"
54 | size={3}
55 | type="primary"
56 | onClick={() => nav("/help")}
57 | >
58 | Help
59 |
60 |
61 | {!!Object.keys(configState.pages.sorted).length && (
62 |
63 | }
65 | className="mr-4"
66 | size={3}
67 | type="primary"
68 | onClick={() => createCollection({})}
69 | >
70 | Add Collection
71 |
72 | }
74 | size={3}
75 | type="primary"
76 | onClick={() => addPage({})}
77 | >
78 | Add Page
79 |
80 |
81 | )}
82 |
83 | );
84 | };
85 |
--------------------------------------------------------------------------------
/src/CustomAlert.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 |
3 | import { Alert } from "./lib/components/Alert";
4 | import { Confirm } from "./lib/components/Confirm";
5 | import { AppDispatchContext, AppStateContext } from "./states/appState";
6 |
7 | export const CustomAlert = () => {
8 | const appState = useContext(AppStateContext);
9 | const { closeAlert, openAlert, closeConfirm, openConfirm } =
10 | useContext(AppDispatchContext);
11 | window.advancedAlert = (title, text) => openAlert({ title, text });
12 | window.advancedConfirm = (title, text, onAccept) =>
13 | openConfirm({ title, text, onAccept });
14 |
15 | return (
16 | <>
17 | closeAlert(undefined)}
22 | />
23 | closeConfirm({ value })}
28 | />
29 | >
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/ModalBody.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { Route, Routes, useNavigate } from "react-router-dom";
3 |
4 | import { CollectionSettingsModal } from "./containers/Collection/Settings/Modal";
5 | import { DeprecatedInfoModal } from "./containers/DeprecatedInfoModal";
6 | import { DBSettingsModal } from "./containers/DisplayButton/DisplayButtonSettingsModal";
7 | import { FDHub } from "./containers/FDHub";
8 | import { GlobalSettings } from "./containers/GeneralSettingsModal";
9 | import { HelpModal } from "./containers/HelpModal";
10 | import { LoginModal } from "./containers/Login";
11 | import { PublishPage } from "./containers/Page/Publish";
12 | import { PageSettingsModal } from "./containers/Page/Settings/Modal";
13 | import { CustomAlert } from "./CustomAlert";
14 |
15 | export const ModalBody = () => {
16 | const nav = useNavigate();
17 | useEffect(() => {
18 | if (localStorage.getItem("dontShow128Info") === null) {
19 | nav("/deprecated-info");
20 | }
21 | // eslint-disable-next-line react-hooks/exhaustive-deps
22 | }, []);
23 | return (
24 |
25 | } />
26 | } />
27 | } />
28 | }
31 | />
32 | } />
33 | }
36 | />
37 | } />
38 | {process.env.REACT_APP_ENABLE_API === "true" && (
39 | <>
40 | } />
41 | } />
42 | } />
43 | >
44 | )}
45 |
46 | );
47 | };
48 |
--------------------------------------------------------------------------------
/src/containers/About.tsx:
--------------------------------------------------------------------------------
1 | import preval from "preval.macro";
2 | import React from "react";
3 |
4 | import packageJson from "../../package.json";
5 | import { Label, Value } from "../lib/components/LabelValue";
6 | import { Row } from "../lib/components/Row";
7 | import { ScrollListContainer } from "../lib/components/ScrollListContainer";
8 | import { TitleBox } from "../lib/components/Title";
9 |
10 | export const About: React.FC = () => {
11 | return (
12 |
13 |
14 |
15 |
16 | {packageJson.version}
17 |
18 |
19 |
20 | {preval`module.exports = new Date().getTime()`}
21 |
22 |
23 |
24 | {preval`module.exports = require('child_process').execSync("git rev-parse --short HEAD").toString()`}
25 |
26 |
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/src/containers/BackButtonLiveData.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { LiveDataSettingsContainer } from "./DisplayButton/LiveDataSettings";
4 |
5 | export const BackButtonLiveData: React.FC<{}> = () => {
6 | return ;
7 | };
8 |
--------------------------------------------------------------------------------
/src/containers/Collection/Collections.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 |
3 | import { TitleBox } from "../../lib/components/Title";
4 | import { ConfigStateContext } from "../../states/configState";
5 | import { Collection } from ".";
6 |
7 | export const Collections: React.FC<{ className?: string }> = ({
8 | children,
9 | className,
10 | }) => {
11 | const configState = useContext(ConfigStateContext);
12 | return (
13 |
14 |
15 |
16 | {Object.entries(configState.collections.byId).map(
17 | ([id, collection]) => (
18 |
19 | )
20 | )}
21 |
22 |
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/src/containers/Collection/Menu.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Bars3Icon,
3 | CogIcon,
4 | ShareIcon,
5 | TrashIcon,
6 | } from "@heroicons/react/24/outline";
7 | import c from "clsx";
8 | import React, { useContext } from "react";
9 | import { useNavigate } from "react-router";
10 |
11 | import { iconSize } from "../../definitions/iconSizes";
12 | import { CtrlDuo } from "../../lib/components/CtrlDuo";
13 | import { FDMenu } from "../../lib/components/Menu";
14 | import { AppDispatchContext } from "../../states/appState";
15 | import { ConfigDispatchContext } from "../../states/configState";
16 |
17 | // import { PublishPage } from "./Publish";
18 |
19 | export const CollectionMenu: React.FC<{ collectionId: string }> = ({
20 | collectionId,
21 | }) => {
22 | const nav = useNavigate();
23 | const configDispatch = useContext(ConfigDispatchContext);
24 | const appDispatch = useContext(AppDispatchContext);
25 | return (
26 |
27 | {/* {process.env.REACT_APP_ENABLE_API === "true" && (
28 |
setPublishOpen(val)}
31 | collectionId={collectionId}
32 | />
33 | )} */}
34 |
35 |
36 | ,
42 | onClick: () => {
43 | nav(`/collection/${collectionId}`);
44 | },
45 | },
46 | {
47 | title: "Delete",
48 | prefix: ,
49 | onClick: () =>
50 | appDispatch.openConfirm({
51 | title: "Delete this collection?",
52 | text: "Do you want to delete this collection? It will be gone forever",
53 | onAccept: () =>
54 | configDispatch.deleteCollection({ collectionId }),
55 | }),
56 | },
57 | {
58 | title: "Publish",
59 | prefix: ,
60 | disabled: true || process.env.REACT_APP_ENABLE_API !== "true",
61 | onClick: () => nav(`/publishCollection/${collectionId}`),
62 | },
63 | ]}
64 | >
65 |
66 |
67 | {
69 | configDispatch.deleteCollection({ collectionId });
70 | }}
71 | className="w-9 h-9 p-1.5 rounded-full bg-danger-600 hover:bg-danger-400"
72 | />
73 |
74 |
75 | );
76 | };
77 |
--------------------------------------------------------------------------------
/src/containers/Collection/Settings/AutoPageSwitcher.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 |
3 | import { Label } from "../../../lib/components/LabelValue";
4 | import { Row } from "../../../lib/components/Row";
5 | import { FDSwitch } from "../../../lib/components/Switch";
6 | import { TextArea } from "../../../lib/components/TextArea";
7 | import { TitleBox } from "../../../lib/components/Title";
8 | import {
9 | ConfigDispatchContext,
10 | ConfigStateContext,
11 | } from "../../../states/configState";
12 |
13 | export const AutoPageSwitcherSettings: React.FC<{ collectionId: string }> = ({
14 | collectionId,
15 | }) => {
16 | const configState = useContext(ConfigStateContext);
17 | const { setUseCollectionName } = useContext(ConfigDispatchContext);
18 | const { changeCollectionWindowName } = useContext(ConfigDispatchContext);
19 | const collection = configState.collections.byId[collectionId];
20 |
21 | return (
22 |
23 |
24 |
25 |
26 | setUseCollectionName({ collectionId, value })}
28 | value={collection.useCollectionNameAsWindowName}
29 | />
30 |
31 |
32 |
35 |
49 |
50 |
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/src/containers/Collection/Settings/General.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 |
3 | import { Label } from "../../../lib/components/LabelValue";
4 | import { Row } from "../../../lib/components/Row";
5 | import { TextInput } from "../../../lib/components/TextInput";
6 | import { TitleBox } from "../../../lib/components/Title";
7 | import {
8 | ConfigDispatchContext,
9 | ConfigStateContext,
10 | } from "../../../states/configState";
11 |
12 | export const CollectionGeneralSettings: React.FC<{ collectionId: string }> = ({
13 | collectionId,
14 | }) => {
15 | const configState = useContext(ConfigStateContext);
16 | const { renameCollection } = useContext(ConfigDispatchContext);
17 | const collection = configState.collections.byId[collectionId];
18 |
19 | return (
20 |
21 |
22 |
23 |
24 | renameCollection({ collectionId, name })}
29 | />
30 |
31 |
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/containers/Collection/Settings/Modal.tsx:
--------------------------------------------------------------------------------
1 | import { CircleStackIcon, CogIcon } from "@heroicons/react/24/outline";
2 | import React, { useContext } from "react";
3 | import { useNavigate, useParams } from "react-router-dom";
4 |
5 | import { iconSize } from "../../../definitions/iconSizes";
6 | import { TabView } from "../../../lib/components/TabView";
7 | import { FDWindow } from "../../../lib/components/Window";
8 | import { getCollectionName } from "../../../lib/misc/util";
9 | import { ConfigStateContext } from "../../../states/configState";
10 | import { AutoPageSwitcherSettings } from "./AutoPageSwitcher";
11 | import { CollectionGeneralSettings } from "./General";
12 |
13 | export const CollectionSettingsModal: React.FC = () => {
14 | const params = useParams();
15 | const nav = useNavigate();
16 | const configState = useContext(ConfigStateContext);
17 | const { collectionId } = params;
18 |
19 | if (collectionId === undefined) return <>>;
20 |
21 | const collection = configState.collections.byId[collectionId];
22 |
23 | if (collection === undefined) return <>>;
24 | return (
25 | nav("/")}
30 | >
31 | ,
37 | prefix: ,
38 | },
39 | {
40 | title: "Auto Page Switcher",
41 | content: ,
42 | prefix: ,
43 | },
44 | ]}
45 | />
46 |
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/src/containers/Collection/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import { useDrop } from "react-dnd";
3 |
4 | import { Label } from "../../lib/components/LabelValue";
5 | import { TitleInput } from "../../lib/components/TitleInput";
6 | import {
7 | ConfigDispatchContext,
8 | ConfigStateContext,
9 | } from "../../states/configState";
10 | import { Page } from "../Page";
11 | import { CollectionMenu } from "./Menu";
12 |
13 | export const Collection: React.FC<{ collectionId: string }> = ({
14 | collectionId,
15 | }) => {
16 | const configState = useContext(ConfigStateContext);
17 | const { renameCollection, setPageCollection } = useContext(
18 | ConfigDispatchContext
19 | );
20 | const collection = configState.collections.byId[collectionId];
21 | const [{ targetCollectionId }, drop] = useDrop<
22 | { pageId: string },
23 | {},
24 | { targetCollectionId: string }
25 | >({
26 | accept: "page",
27 | drop: (item, monitor): any => {
28 | if (!collection.pages.find((p) => p === monitor.getItem().pageId))
29 | setPageCollection({
30 | pageId: monitor.getItem().pageId,
31 | collectionId: targetCollectionId,
32 | });
33 | return {};
34 | },
35 | collect: () => ({
36 | targetCollectionId: collectionId,
37 | }),
38 | });
39 | return (
40 |
44 |
45 | renameCollection({ collectionId, name })}
47 | value={collection.name}
48 | placeholder={`${collectionId.slice(-4)} - Click to edit`}
49 | />
50 |
51 |
52 |
53 | {collection.pages.length ? (
54 | collection.pages.map((pageId) => (
55 |
56 | ))
57 | ) : (
58 |
59 |
62 |
63 | )}
64 |
65 |
66 | );
67 | };
68 |
--------------------------------------------------------------------------------
/src/containers/ContentBody.tsx:
--------------------------------------------------------------------------------
1 | import c from "clsx";
2 | import React from "react";
3 |
4 | export const ContentBody: React.FC = (props) => {
5 | return (
6 |
12 | {props.children}
13 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/src/containers/DefaultBackButtonSettings.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { DisplaySettingsContainer } from "./DisplayButton/DisplaySettings";
4 |
5 | export const DefaultBackButtonSettings: React.FC<{}> = () => {
6 | return ;
7 | };
8 |
--------------------------------------------------------------------------------
/src/containers/DeprecatedInfoModal.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useNavigate } from "react-router";
3 |
4 | import { FDWindow } from "../lib/components/Window";
5 |
6 | export const DeprecatedInfoModal: React.FC<{}> = () => {
7 | const nav = useNavigate();
8 |
9 | return (
10 | {
14 | nav("/");
15 | localStorage.setItem("dontShow128Info", "true");
16 | }}
17 | title="Deprecation Info"
18 | >
19 |
20 |
Attention!
21 |
The config format changed slightly.
22 |
You need to update the firmware of your FreeDeck.
23 |
Then just save the config once again and everything is fine.
24 |
The update allows for storing 60 keys instead of 7.
25 |
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/src/containers/DeveloperSettings.tsx:
--------------------------------------------------------------------------------
1 | import { invoke } from "@tauri-apps/api";
2 | import React, { useContext } from "react";
3 |
4 | import { FDButton } from "../lib/components/Button";
5 | import { Label } from "../lib/components/LabelValue";
6 | import { Row } from "../lib/components/Row";
7 | import { ScrollListContainer } from "../lib/components/ScrollListContainer";
8 | import { TextInput } from "../lib/components/TextInput";
9 | import { TitleBox } from "../lib/components/Title";
10 | import { createToast } from "../lib/misc/createToast";
11 | import { AppDispatchContext, AppStateContext } from "../states/appState";
12 |
13 | export const DeveloperSettings: React.FC = () => {
14 | const { serialApi, devLog } = useContext(AppStateContext);
15 | const appDispatch = useContext(AppDispatchContext);
16 | return (
17 |
18 |
19 |
20 |
21 | serialApi?.writeToScreen(val)} />
22 |
23 |
24 |
25 |
26 | createToast({ title: "Title", text: "Text" })}
28 | >
29 | Toast!
30 |
31 |
32 |
33 |
34 |
36 | invoke("get_current_window").then((value) =>
37 | appDispatch.openAlert({ text: value, title: "debug" })
38 | )
39 | }
40 | >
41 | invoke
42 |
43 |
44 |
45 |
46 |
48 | invoke("list_sensors").then((value) =>
49 | appDispatch.openAlert({
50 | text: value.join(", "),
51 | title: "sensors",
52 | })
53 | )
54 | }
55 | >
56 | invoke
57 |
58 |
59 | {JSON.stringify(devLog, undefined, 2)}
60 |
61 |
62 | );
63 | };
64 |
--------------------------------------------------------------------------------
/src/containers/DisplayButton/ButtonSettings/ChangePage.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ArrowLeftOnRectangleIcon,
3 | PlusCircleIcon,
4 | } from "@heroicons/react/24/outline";
5 | import React, { useContext } from "react";
6 |
7 | import { EAction } from "../../../definitions/modes";
8 | import { ButtonSetting, Pages } from "../../../generated";
9 | import { FDButton } from "../../../lib/components/Button";
10 | import { Label } from "../../../lib/components/LabelValue";
11 | import { Row } from "../../../lib/components/Row";
12 | import { FDSelect } from "../../../lib/components/SelectInput";
13 | import { scrollToPage } from "../../../lib/misc/scrollToPage";
14 | import { getPageName } from "../../../lib/misc/util";
15 | import { ConfigDispatchContext } from "../../../states/configState";
16 |
17 | export const ChangePage: React.FC<{
18 | secondary: boolean;
19 | values: ButtonSetting["values"];
20 | previousPage: string;
21 | pages: Pages;
22 | previousDisplay: number;
23 | setValues: (values: ButtonSetting["values"]) => void;
24 | }> = ({
25 | values,
26 | previousPage,
27 | pages,
28 | previousDisplay,
29 | setValues,
30 | secondary,
31 | }) => {
32 | const configDispatch = useContext(ConfigDispatchContext);
33 | return (
34 | <>
35 | {pages.sorted.length ? (
36 |
37 |
38 |
42 | setValues({ ...values, [EAction.changePage]: value })
43 | }
44 | options={[
45 | { text: "Select Page", value: "" },
46 | ...pages.sorted.map((id) => {
47 | const page = pages.byId[id];
48 | return { value: id, text: `Go to ${getPageName(id, page)}` };
49 | }),
50 | ]}
51 | />
52 |
53 | ) : null}
54 |
55 | {["", undefined].includes(values[EAction.changePage]) ? (
56 | }
58 | size={2}
59 | onClick={async () =>
60 | await configDispatch.addPage({
61 | previousPage,
62 | previousDisplay,
63 | secondary,
64 | })
65 | }
66 | >
67 | Add Page
68 |
69 | ) : (
70 | }
72 | size={2}
73 | onClick={() => scrollToPage(values[EAction.changePage]!)}
74 | >
75 | Scroll To{" "}
76 | {getPageName(
77 | values[EAction.changePage]!,
78 | pages.byId[values[EAction.changePage]!]
79 | )}
80 |
81 | )}
82 |
83 | >
84 | );
85 | };
86 |
--------------------------------------------------------------------------------
/src/containers/DisplayButton/ButtonSettings/FreeDeckSettings.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { EAction, FDSettings } from "../../../definitions/modes";
4 | import { ButtonSetting } from "../../../generated";
5 | import { Label, Value } from "../../../lib/components/LabelValue";
6 | import { Row } from "../../../lib/components/Row";
7 | import { FDSelect } from "../../../lib/components/SelectInput";
8 | import { FDSlider } from "../../../lib/components/Slider";
9 |
10 | export const FreeDeckSettings: React.FC<{
11 | values: ButtonSetting["values"];
12 | setValues: (values: ButtonSetting["values"]) => void;
13 | }> = ({ values, setValues }) => {
14 | return (
15 | <>
16 |
17 |
18 |
22 | setValues({
23 | ...values,
24 | [EAction.settings]: {
25 | ...values[EAction.settings],
26 | setting: value,
27 | },
28 | })
29 | }
30 | options={[
31 | { text: "Select Setting", value: -1 },
32 | {
33 | text: "Decrease Brightness",
34 | value: FDSettings.change_brightness,
35 | },
36 | {
37 | text: "Increase Brightness",
38 | value: FDSettings.change_brightness,
39 | },
40 | { text: "Set Brightness", value: FDSettings.absolute_brightness },
41 | ]}
42 | />
43 |
44 | {values[EAction.settings].setting === FDSettings.absolute_brightness && (
45 |
46 |
51 | setValues({
52 | ...values,
53 | [EAction.settings]: {
54 | ...values[EAction.settings],
55 | value: e.target.valueAsNumber,
56 | },
57 | })
58 | }
59 | />
60 |
61 | {(((values[EAction.settings].value || 128) / 255) * 100).toFixed(0)}
62 | %
63 |
64 |
65 | )}
66 | >
67 | );
68 | };
69 |
--------------------------------------------------------------------------------
/src/containers/DisplayButton/ButtonSettings/Hotkeys.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { keys } from "../../../definitions/keys";
4 | import { EAction } from "../../../definitions/modes";
5 | import { ButtonSetting } from "../../../generated/button";
6 | import { FDButton } from "../../../lib/components/Button";
7 | import { Label } from "../../../lib/components/LabelValue";
8 | import { Row } from "../../../lib/components/Row";
9 | import { FDSelect } from "../../../lib/components/SelectInput";
10 | import { ROW_SIZE } from "../../../lib/configFile/consts";
11 | import { useTranslateKeyboardLayout } from "../../../lib/localisation/keyboard";
12 |
13 | const HotkeyKeys: React.FC<{
14 | values: number[];
15 | setKeys: (keys: number[]) => void;
16 | }> = ({ values, setKeys }) => {
17 | const translatedKeys = useTranslateKeyboardLayout(values);
18 | return (
19 |
20 | {translatedKeys.map((key, index) => (
21 | {
25 | const newKeys = [...values];
26 | newKeys.splice(index, 1);
27 | setKeys(newKeys.slice(0, 7));
28 | }}
29 | >
30 | {!!(
31 | translatedKeys.slice(0, index + 1).filter((k) => k === key).length %
32 | 2
33 | )
34 | ? key
35 | : `^${key}`}
36 |
37 | ))}
38 |
39 | );
40 | };
41 |
42 | export const Hotkeys: React.FC<{
43 | values: ButtonSetting["values"];
44 | setValues: (values: ButtonSetting["values"]) => void;
45 | }> = ({ setValues, values }) => {
46 | const setKeys = (newValues: number[]) =>
47 | newValues.length <= ROW_SIZE / 2 - 4 &&
48 | setValues({
49 | ...values,
50 | [EAction.hotkeys]: newValues,
51 | });
52 | const onHotKey = (e: React.KeyboardEvent, lengthLimit = 7) => {
53 | if (e.repeat) return;
54 | const key = Object.keys(keys).find(
55 | (key) => keys[key]?.js === e.nativeEvent.code
56 | );
57 | if (!key) return;
58 | //ignore backspace
59 | if (keys[key]!.hid === 42 && values[EAction.hotkeys].length > 0) {
60 | setKeys([
61 | ...values[EAction.hotkeys].slice(0, values[EAction.hotkeys].length - 1),
62 | ]);
63 | } else setKeys([...values[EAction.hotkeys], keys[key]!.hid]);
64 | };
65 |
66 | return (
67 | <>
68 |
69 |
70 | {
74 | if (
75 | !!values[EAction.hotkeys].find((k) => k === value) &&
76 | value >= 0xe0
77 | )
78 | return;
79 | setKeys([...values[EAction.hotkeys], parseInt(value)]);
80 | }}
81 | options={[
82 | { text: "Choose key", value: 0 },
83 | ...Object.keys(keys).map((keyName) => ({
84 | text: keyName,
85 | value: keys[keyName]?.hid,
86 | })),
87 | ]}
88 | />
89 |
90 |
91 | onHotKey(e)}
95 | >
96 | Click/Focus to scan
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | >
105 | );
106 | };
107 |
--------------------------------------------------------------------------------
/src/containers/DisplayButton/ButtonSettings/LeavePage.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { ButtonSetting, Pages } from "../../../generated";
4 | import { Label } from "../../../lib/components/LabelValue";
5 | import { Row } from "../../../lib/components/Row";
6 | import { FDSelect } from "../../../lib/components/SelectInput";
7 | import { FDSwitch } from "../../../lib/components/Switch";
8 | import { getPageName } from "../../../lib/misc/util";
9 |
10 | export const LeavePage: React.FC<{
11 | value: ButtonSetting["leavePage"];
12 | pages: Pages;
13 | primary: boolean;
14 | setValue: (value: ButtonSetting["leavePage"]) => void;
15 | }> = ({ value, pages, setValue, primary }) => {
16 | const startPage = pages.sorted[0];
17 | return (
18 | <>
19 | {pages.sorted.length ? (
20 | <>
21 |
22 |
23 |
26 | setValue({ enabled, pageId: value.pageId })
27 | }
28 | />
29 |
30 | {value.enabled && (
31 |
32 |
33 | setValue({ enabled: true, pageId: value })}
37 | options={[
38 | { text: "Select Page", value: "" },
39 | ...pages.sorted.map((id) => {
40 | const page = pages.byId[id];
41 | return {
42 | value: id,
43 | text: `Go to ${getPageName(id, page)}`,
44 | };
45 | }),
46 | ]}
47 | />
48 |
49 | )}
50 | >
51 | ) : null}
52 | >
53 | );
54 | };
55 |
--------------------------------------------------------------------------------
/src/containers/DisplayButton/ButtonSettings/SpecialKeys.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { EMediaKeys, MediaKeys } from "../../../definitions/keys";
4 | import { EAction } from "../../../definitions/modes";
5 | import { ButtonSetting } from "../../../generated/button";
6 | import { Label } from "../../../lib/components/LabelValue";
7 | import { Row } from "../../../lib/components/Row";
8 | import { FDSelect } from "../../../lib/components/SelectInput";
9 |
10 | export const SpecialKeys: React.FC<{
11 | values: ButtonSetting["values"];
12 | setValues: (values: ButtonSetting["values"]) => void;
13 | }> = ({ values, setValues }) => {
14 | return (
15 |
16 |
17 |
21 | setValues({ ...values, [EAction.special_keys]: parseInt(value) })
22 | }
23 | options={[
24 | { text: "Choose key", value: 0 },
25 | // @ts-ignore
26 | ...MediaKeys.map((key) => ({ value: EMediaKeys[key], text: key })),
27 | ]}
28 | />
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/containers/DisplayButton/ButtonSettings/Text.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { EAction } from "../../../definitions/modes";
4 | import { ButtonSetting } from "../../../generated/button";
5 |
6 | export const Text: React.FC<{
7 | values: ButtonSetting["values"];
8 | setValues: (values: ButtonSetting["values"]) => void;
9 | }> = ({ values, setValues }) => {
10 | const setKeys = (newValues: string) =>
11 | setValues({
12 | ...values,
13 | [EAction.text]: newValues,
14 | });
15 | const onKey = (
16 | e: React.ChangeEvent,
17 | lengthLimit = 7
18 | ) => {
19 | setKeys(e.target.value);
20 | };
21 | return (
22 | <>
23 |