├── .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 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/lib/components/TextInput.tsx: -------------------------------------------------------------------------------- 1 | import c from "clsx"; 2 | import React from "react"; 3 | 4 | export const TextInput: React.FC<{ 5 | className?: string; 6 | disabled?: boolean; 7 | onChange: (value: string) => any; 8 | onEnter?: () => any; 9 | value?: string; 10 | placeholder?: string; 11 | }> = ({ 12 | className, 13 | children, 14 | onChange, 15 | onEnter, 16 | value, 17 | placeholder, 18 | disabled, 19 | }) => { 20 | return ( 21 | { 32 | onChange(e.currentTarget.value); 33 | }} 34 | onKeyDown={(e) => e.key === "Enter" && onEnter && onEnter()} 35 | value={value} 36 | > 37 | {children} 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/lib/components/Title.tsx: -------------------------------------------------------------------------------- 1 | import c from "clsx"; 2 | import React from "react"; 3 | 4 | export const TitleBox: React.FC<{ 5 | className?: string; 6 | title: string; 7 | center?: boolean; 8 | }> = ({ className, children, title, center = false }) => { 9 | return ( 10 |
16 |
17 | {title} 18 |
19 |
{children}
20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/lib/components/TitleInput.tsx: -------------------------------------------------------------------------------- 1 | import c from "clsx"; 2 | import React from "react"; 3 | 4 | export const TitleInput: React.FC<{ 5 | className?: string; 6 | disabled?: boolean; 7 | onChange: (value: string) => any; 8 | onEnter?: () => any; 9 | value?: string; 10 | placeholder?: string; 11 | }> = ({ 12 | className, 13 | children, 14 | onChange, 15 | onEnter, 16 | value, 17 | placeholder, 18 | disabled, 19 | }) => { 20 | return ( 21 | { 32 | onChange(e.currentTarget.value); 33 | }} 34 | onKeyDown={(e) => e.key === "Enter" && onEnter && onEnter()} 35 | value={value} 36 | > 37 | {children} 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/lib/components/Window.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog, Transition } from "@headlessui/react"; 2 | import { XCircleIcon } from "@heroicons/react/24/solid"; 3 | import c from "clsx"; 4 | import React, { Fragment } from "react"; 5 | 6 | import { CustomAlert } from "../../CustomAlert"; 7 | 8 | export const FDWindow: React.FC<{ 9 | className?: string; 10 | visible?: boolean; 11 | setClose: () => void; 12 | title?: string; 13 | }> = ({ visible, setClose, children, title, className }) => { 14 | return ( 15 | 16 | setClose()} 20 | > 21 | 30 | 31 | 32 | 41 |
47 |
53 | {title} 54 |
55 |
56 |
setClose()} 61 | > 62 | 63 |
64 |
65 | {children} 66 | 67 |
68 |
69 |
70 |
71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /src/lib/configFile/consts.ts: -------------------------------------------------------------------------------- 1 | export const ROW_SIZE = 128; 2 | export const TRANSMIT_BUFFER_SIZE = 128; 3 | -------------------------------------------------------------------------------- /src/lib/configFile/createBody.ts: -------------------------------------------------------------------------------- 1 | import { ActionValue, EAction, FDSettings } from "../../definitions/modes"; 2 | import { DisplayButton, Pages } from "../../generated"; 3 | import { ROW_SIZE } from "./consts"; 4 | import { optimizeForSSD1306 } from "./ssd1306"; 5 | 6 | const writeAction = ( 7 | buttonRows: Buffer, 8 | db: DisplayButton, 9 | pages: Pages, 10 | rowOffset: number, 11 | isSecondary: boolean 12 | ) => { 13 | const buttonAction = isSecondary ? db.button.secondary : db.button.primary; 14 | const dataOffset = rowOffset + (isSecondary ? ROW_SIZE / 2 : 0); 15 | buttonRows.writeUInt8( 16 | //@ts-ignore 17 | ActionValue[buttonAction.mode], 18 | dataOffset 19 | ); 20 | switch (buttonAction.mode) { 21 | case EAction.changePage: 22 | const pageIndex = pages.sorted.findIndex( 23 | (id) => id === buttonAction.values[EAction.changePage] 24 | ); 25 | buttonRows.writeUInt16LE(pageIndex, dataOffset + 1); 26 | break; 27 | case EAction.hotkeys: 28 | buttonAction.values[EAction.hotkeys].forEach((hotkey, index) => { 29 | buttonRows.writeUInt8(hotkey, dataOffset + index + 1); 30 | }); 31 | break; 32 | case EAction.special_keys: 33 | buttonRows.writeUInt8( 34 | buttonAction.values[EAction.special_keys], 35 | dataOffset + 1 36 | ); 37 | break; 38 | case EAction.settings: 39 | buttonRows.writeUInt8( 40 | buttonAction.values[EAction.settings].setting!, 41 | dataOffset + 1 42 | ); 43 | if ( 44 | buttonAction.values[EAction.settings].setting === 45 | FDSettings.absolute_brightness 46 | ) 47 | buttonRows.writeUInt8( 48 | buttonAction.values[EAction.settings].value!, 49 | dataOffset + 2 50 | ); 51 | 52 | break; 53 | } 54 | const pageIndex = pages.sorted.findIndex( 55 | (id) => 56 | id === db.button[isSecondary ? "secondary" : "primary"].leavePage.pageId 57 | ); 58 | buttonRows.writeUInt16LE(pageIndex + 1, dataOffset + ROW_SIZE / 2 - 2); 59 | 60 | return buttonRows; 61 | }; 62 | 63 | export const createButtonBody = (pages: Pages) => { 64 | const buttonRowCount = 65 | pages.sorted.length * pages.byId[pages.sorted[0]].displayButtons.length; 66 | let buttonRows = new Buffer(ROW_SIZE * buttonRowCount); 67 | pages.sorted.forEach((pageId, pageIndex) => { 68 | const page = pages.byId[pageId]; 69 | page.displayButtons.forEach((db, buttonIndex) => { 70 | const rowOffset = 71 | page.displayButtons.length * ROW_SIZE * pageIndex + 72 | buttonIndex * ROW_SIZE; 73 | buttonRows = writeAction(buttonRows, db, pages, rowOffset, false); 74 | buttonRows = writeAction(buttonRows, db, pages, rowOffset, true); 75 | }); 76 | }); 77 | return buttonRows; 78 | }; 79 | 80 | export const createImageBody = (pages: Pages) => { 81 | let imageBuffer = new Buffer(0); 82 | const bmpHeaderSize = 83 | pages.byId[ 84 | pages.sorted[0] 85 | ].displayButtons[0].display.convertedImage.readUInt32LE(10); 86 | console.log(bmpHeaderSize); 87 | pages.sorted.forEach((id) => { 88 | const page = pages.byId[id]; 89 | page.displayButtons.forEach((db) => { 90 | const hasLiveData = new Buffer(1); 91 | hasLiveData.writeUInt8( 92 | db.live.top !== "none" || db.live.bottom !== "none" ? 1 : 0, 93 | 0 94 | ); 95 | imageBuffer = Buffer.concat([ 96 | imageBuffer, 97 | hasLiveData, 98 | optimizeForSSD1306(db.display.convertedImage.slice(bmpHeaderSize)), 99 | ]); 100 | }); 101 | }); 102 | return imageBuffer; 103 | }; 104 | -------------------------------------------------------------------------------- /src/lib/configFile/createBuffer.ts: -------------------------------------------------------------------------------- 1 | import { Config } from "../../generated"; 2 | import { createButtonBody, createImageBody } from "./createBody"; 3 | import { createFooter } from "./createFooter"; 4 | import { createHeader } from "./createHeader"; 5 | export const createConfigBuffer = ( 6 | configState: Config, 7 | saveJson: boolean 8 | ): Buffer => { 9 | let buffer = Buffer.concat([ 10 | createHeader( 11 | configState.width, 12 | configState.height, 13 | configState.brightness, 14 | configState.screenSaverTimeout, 15 | configState.oledSpeed, 16 | configState.oledDelay, 17 | configState.preChargePeriod, 18 | Math.min(15, configState.clockFreq) * 16 + 19 | Math.min(15, configState.clockDiv), 20 | configState.saveJson, 21 | configState.pages.sorted.length 22 | ), 23 | createButtonBody(configState.pages), 24 | createImageBody(configState.pages), 25 | ]); 26 | if (configState.saveJson || saveJson) { 27 | buffer = Buffer.concat([buffer, createFooter(configState)]); 28 | } 29 | return buffer; 30 | }; 31 | -------------------------------------------------------------------------------- /src/lib/configFile/createFooter.ts: -------------------------------------------------------------------------------- 1 | import { cloneDeep } from "lodash"; 2 | import { deflate } from "pako"; 3 | 4 | import { Config } from "../../generated"; 5 | 6 | export const slimConfig = (state: Config) => { 7 | let slimState = cloneDeep(state); 8 | for (let i = 0; i < slimState.pages.sorted.length; i++) { 9 | const id = slimState.pages.sorted[i]; 10 | const page = slimState.pages.byId[id]; 11 | for (const button of page.displayButtons) { 12 | delete (button.display as any).convertedImage; 13 | delete (button.display as any).previewImage; 14 | } 15 | } 16 | return slimState; 17 | }; 18 | 19 | export const createFooter = (state: Config): Buffer => { 20 | //save this at the end of the config 21 | let slimState = slimConfig(state); 22 | let slimString = JSON.stringify(slimState); 23 | let deflated = deflate(slimString); 24 | return Buffer.from(deflated); 25 | }; 26 | -------------------------------------------------------------------------------- /src/lib/configFile/createHeader.ts: -------------------------------------------------------------------------------- 1 | import { ROW_SIZE } from "./consts"; 2 | 3 | /** 4 | * @param width number of displays horizontal. 5 | * @param height number of displays vertical. 6 | * @param brightness default brightness 0-255. 7 | * @param numberOfPages 0-65535. 8 | * @example ```txt 9 | * The header is 16 bytes long: 10 | * - 0: width 11 | * - 1: height 12 | * - 2-3: image data offset (numberOfPages * width * height + 1) 13 | * - 4: brightness 14 | * - 5-6: display timeout (0=always on) 15 | * - 7: oled speed (used for freedeck pico) 16 | * - 8: oled delay (used for freedeck ino) 17 | * - 9: oled preChargePeriod (to adjust coil whine) 18 | * - 10: oled refreshFrequency (to adjust coil whine and burn in) 19 | * - 11: save json (is json is saved in the config file) 20 | * ``` 21 | */ 22 | export const createHeader = ( 23 | width: number, 24 | height: number, 25 | brightness: number, 26 | screenTimeout: number, 27 | oledSpeed: number, 28 | oledDelay: number, 29 | preChargePeriod: number, 30 | refreshFrequency: number, 31 | saveJson: boolean, 32 | numberOfPages: number 33 | ): Buffer => { 34 | // const header = new Buffer(16); 35 | const header = Buffer.alloc(ROW_SIZE); 36 | 37 | // write width and height 38 | header.writeUInt8(width, 0); 39 | header.writeUInt8(height, 1); 40 | 41 | // check that we dont have more than 65535 pages 42 | if (Math.pow(2, 16) - 1 < numberOfPages) { 43 | alert("too many pages"); 44 | throw new Error("too many pages"); 45 | } 46 | // write the "row" offset where images begin 47 | // one row is 16 bytes, so we write the actual byte offset value divided by 16 48 | // we add 1 for the header row 49 | header.writeUInt16LE(numberOfPages * width * height + 1, 2); 50 | header.writeUInt8(brightness, 4); 51 | header.writeUInt16LE(screenTimeout, 5); 52 | header.writeUInt8(oledSpeed, 7); 53 | header.writeUInt8(oledDelay, 8); 54 | header.writeUInt8(preChargePeriod, 9); 55 | header.writeUInt8(refreshFrequency, 10); 56 | header.writeUInt8(saveJson ? 1 : 0, 11); 57 | return header; 58 | }; 59 | -------------------------------------------------------------------------------- /src/lib/configFile/loadConfigFile.ts: -------------------------------------------------------------------------------- 1 | import { Config } from "../../generated"; 2 | import { handleFileSelect } from "../file/handleFileSelect"; 3 | import { parseConfig } from "./parseConfig"; 4 | const isBuffer = (data: Buffer | FileList): data is Buffer => { 5 | return !!(data as Buffer).byteLength; 6 | }; 7 | export const loadConfigFile = async ( 8 | fileList: FileList | Buffer, 9 | setState: (newState: Config) => any 10 | ) => { 11 | const file = isBuffer(fileList) 12 | ? fileList 13 | : Buffer.from(await handleFileSelect(fileList[0])); 14 | 15 | try { 16 | const config = await parseConfig(file); 17 | setState(config); 18 | } catch (e: any) { 19 | window.advancedAlert("Invalid config File", (e as Error).message); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/lib/configFile/ssd1306.ts: -------------------------------------------------------------------------------- 1 | export const optimizeForSSD1306 = (buffer: Buffer) => { 2 | // let optimizedImage = new Buffer(0); 3 | let optimizedImage = Buffer.alloc(0); 4 | let b; 5 | 6 | let dst_mask; 7 | for (let y = 0; y < 8; y++) { 8 | // 8 lines of 8 pixels 9 | for (let j = 0; j < 8; j++) { 10 | // 8 sections of 16 columns = 128px width 11 | let s = j * 2 + y * 16 * 8; // source line j*2 because j 12 | // console.log("s", s); 13 | const ucTemp = new Array(16).fill(0); // start with all black 14 | for (let x = 0; x < 16; x += 8) { 15 | // block of 16x8 pixels 16 | dst_mask = 1; 17 | for (let q = 0; q < 8; q++) { 18 | // console.log("b", s + q * 16); 19 | b = buffer[s + q * 16]; 20 | for (let z = 0; z < 8; z++) { 21 | if (b & 0x80) { 22 | ucTemp[x + z] |= dst_mask; 23 | } 24 | b <<= 1; 25 | } // for z 26 | dst_mask <<= 1; 27 | } // for q 28 | s++; // next source uint8_t 29 | } // for x 30 | optimizedImage = Buffer.concat([optimizedImage, Buffer.from(ucTemp)]); 31 | //oledCachedWrite(ucTemp, 16); 32 | } // for j 33 | } 34 | return Buffer.from(optimizedImage); 35 | }; 36 | -------------------------------------------------------------------------------- /src/lib/file/download.ts: -------------------------------------------------------------------------------- 1 | import { writeBinaryFile } from "@tauri-apps/api/fs"; 2 | 3 | export const download = async (data: Buffer) => { 4 | if ("__TAURI_IPC__" in window) { 5 | // todo: fix this not downloading 6 | const { save } = await import("@tauri-apps/api/dialog"); 7 | const path = await save({ 8 | defaultPath: "config.bin", 9 | }); 10 | if (!path) return; 11 | await writeBinaryFile(path, data); 12 | } else { 13 | var a = document.createElement("a"); 14 | document.body.appendChild(a); 15 | const blob = new Blob([data], { type: "octet/stream" }); 16 | const url = window.URL.createObjectURL(blob); 17 | a.href = url; 18 | a.download = "config.bin"; 19 | a.click(); 20 | window.URL.revokeObjectURL(url); 21 | a.remove(); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/lib/file/fileToImage.ts: -------------------------------------------------------------------------------- 1 | import { handleFileSelect } from "./handleFileSelect"; 2 | export const stringToImage = async (input: string) => { 3 | const jimp = (await import("jimp")).default; 4 | const jimage = await jimp.read(input); 5 | const mime = jimage.getMIME(); 6 | const newMime = mime === "image/jpeg" ? mime : "image/gif"; 7 | const resizedBuffer = await jimage 8 | .quality(70) 9 | .scaleToFit(256, 128, "") 10 | .getBufferAsync(newMime); 11 | return resizedBuffer; 12 | }; 13 | export const fileToImage = async (file: File) => { 14 | const buffer = await handleFileSelect(file); 15 | const jimp = (await import("jimp")).default; 16 | const jimage = await jimp.read(Buffer.from(buffer)); 17 | const mime = jimage.getMIME(); 18 | const newMime = mime === "image/jpeg" ? mime : "image/gif"; 19 | const resizedBuffer = await jimage 20 | .quality(70) 21 | .scaleToFit(256, 128, "") 22 | .getBufferAsync(newMime); 23 | return resizedBuffer; 24 | }; 25 | -------------------------------------------------------------------------------- /src/lib/file/handleFileSelect.ts: -------------------------------------------------------------------------------- 1 | let fileReader: FileReader; 2 | export const handleFileSelect = (file: File): Promise => { 3 | return new Promise((resolve, reject) => { 4 | if (!file) return; 5 | fileReader = new FileReader(); 6 | fileReader.onloadend = () => { 7 | const content = fileReader.result as ArrayBuffer; 8 | if (!content?.byteLength) return; 9 | resolve(content as ArrayBuffer); 10 | }; 11 | fileReader.readAsArrayBuffer(file); 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /src/lib/hooks/once.tsx: -------------------------------------------------------------------------------- 1 | import { listen } from "@tauri-apps/api/event"; 2 | import { useEffect } from "react"; 3 | 4 | import { StateRef } from "../../App"; 5 | import { AppState, IAppDispatch } from "../../states/appState"; 6 | import { saveCurrentPage } from "../serial/commands"; 7 | 8 | export const useOnce = ( 9 | appState: AppState, 10 | appDispatch: IAppDispatch, 11 | stateRef: StateRef 12 | ) => { 13 | const serialApi = appState.serialApi; 14 | useEffect(() => { 15 | if ("__TAURI_IPC__" in window) { 16 | listen<{ cpuTemp: number; gpuTemp: number }>("system_temps", (event) => { 17 | appDispatch.setTemps(event.payload); 18 | }); 19 | } 20 | if (!serialApi) return; 21 | const portsId = serialApi.registerOnPortsChanged( 22 | (ports, connectedPortIndex) => { 23 | appDispatch.setAvailablePorts(ports); 24 | appDispatch.setConnectedPortIndex(connectedPortIndex); 25 | appDispatch.setDevLog({ 26 | path: "connectedPortIndex", 27 | data: connectedPortIndex, 28 | }); 29 | if (connectedPortIndex > -1) { 30 | serialApi 31 | .getHasJson() 32 | .then((hasJson) => appDispatch.setHasJson(hasJson)); 33 | serialApi.getCurrentPage().then((page) => { 34 | console.log(page); 35 | saveCurrentPage(page, appDispatch, stateRef); 36 | }); 37 | } 38 | } 39 | ); 40 | return () => { 41 | serialApi.clearOnPortsChanged(portsId); 42 | }; 43 | // eslint-disable-next-line react-hooks/exhaustive-deps 44 | }, []); 45 | }; 46 | -------------------------------------------------------------------------------- /src/lib/hooks/startup/index.tsx: -------------------------------------------------------------------------------- 1 | import { StateRef } from "../../../App"; 2 | import { Config } from "../../../generated"; 3 | import { AppState, IAppDispatch } from "../../../states/appState"; 4 | import { IConfigDispatch } from "../../../states/configState"; 5 | import { useLiveData } from "./liveData"; 6 | import { usePageSwitcher } from "./pageSwitcher"; 7 | import { usePersistentConfig } from "./persistentConfig"; 8 | import { useSerialCommand } from "./serialCommand"; 9 | 10 | export const useBackgroundTasks = ( 11 | configState: Config, 12 | configDispatch: IConfigDispatch, 13 | appState: AppState, 14 | appDispatch: IAppDispatch, 15 | refState: StateRef 16 | ) => { 17 | usePersistentConfig(configDispatch, appDispatch); 18 | usePageSwitcher(configState, appState); 19 | 20 | useSerialCommand(configState, appState, appDispatch, refState); 21 | useLiveData(configState, appState, refState); 22 | }; 23 | -------------------------------------------------------------------------------- /src/lib/hooks/startup/persistentConfig.tsx: -------------------------------------------------------------------------------- 1 | import { listen } from "@tauri-apps/api/event"; 2 | import { useEffect } from "react"; 3 | 4 | import { IAppDispatch } from "../../../states/appState"; 5 | import { IConfigDispatch } from "../../../states/configState"; 6 | import { convertCurrentConfig } from "../../configFile/parseConfig"; 7 | 8 | export const usePersistentConfig = ( 9 | configDispatch: IConfigDispatch, 10 | appDispatch: IAppDispatch 11 | ) => { 12 | useEffect(() => { 13 | const handleStartup = async () => { 14 | const config = localStorage.getItem("config"); 15 | 16 | if (config) { 17 | const parsed = JSON.parse(config); 18 | const converted = await convertCurrentConfig(parsed); 19 | configDispatch.setState(converted); 20 | } 21 | 22 | if (!(window as any).__TAURI_IPC__) return; 23 | 24 | const value = localStorage.getItem("autoPageSwitcherEnabled"); 25 | const autoPageSwitcherEnabled = value === null ? true : JSON.parse(value); 26 | 27 | appDispatch.toggleAutoPageSwitcher(autoPageSwitcherEnabled); 28 | 29 | await listen("toggle_aps", (data) => { 30 | appDispatch.toggleAutoPageSwitcher(undefined); 31 | }); 32 | }; 33 | 34 | handleStartup(); 35 | // eslint-disable-next-line react-hooks/exhaustive-deps 36 | }, []); 37 | }; 38 | -------------------------------------------------------------------------------- /src/lib/hooks/startup/serialCommand.tsx: -------------------------------------------------------------------------------- 1 | import { UnlistenFn, listen } from "@tauri-apps/api/event"; 2 | import { useEffect } from "react"; 3 | 4 | import { StateRef } from "../../../App"; 5 | import { Config } from "../../../generated"; 6 | import { AppState, IAppDispatch } from "../../../states/appState"; 7 | import { sleep } from "../../misc/util"; 8 | import { runCommand } from "../../serial/commands"; 9 | 10 | export const useSerialCommand = ( 11 | configState: Config, 12 | appState: AppState, 13 | appDispatch: IAppDispatch, 14 | refData: StateRef 15 | ) => { 16 | useEffect(() => { 17 | if (!(window as any).__TAURI_IPC__) return; 18 | 19 | let isCancelled = false; 20 | let unlistenSerialCommand: UnlistenFn | undefined; 21 | 22 | const startListen = async () => { 23 | await sleep(250); 24 | if (isCancelled) return; 25 | 26 | unlistenSerialCommand = await listen("serial_command", async () => { 27 | const { command, args } = await appState.serialApi!.readSerialCommand(); 28 | runCommand(command, args, configState, appDispatch, refData); 29 | }); 30 | }; 31 | 32 | startListen(); 33 | 34 | return () => { 35 | isCancelled = true; 36 | unlistenSerialCommand?.(); 37 | }; 38 | // eslint-disable-next-line react-hooks/exhaustive-deps 39 | }, [configState]); 40 | }; 41 | -------------------------------------------------------------------------------- /src/lib/image/base64Encode.ts: -------------------------------------------------------------------------------- 1 | const base64Encode = (input: Buffer) => { 2 | var keyStr = 3 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; 4 | var output = ""; 5 | var chr1, chr2, chr3, enc1, enc2, enc3, enc4; 6 | var i = 0; 7 | 8 | while (i < input.length) { 9 | chr1 = input[i++]; 10 | chr2 = i < input.length ? input[i++] : Number.NaN; // Not sure if the index 11 | chr3 = i < input.length ? input[i++] : Number.NaN; // checks are needed here 12 | 13 | enc1 = chr1 >> 2; 14 | enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); 15 | enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); 16 | enc4 = chr3 & 63; 17 | 18 | if (isNaN(chr2)) { 19 | enc3 = enc4 = 64; 20 | } else if (isNaN(chr3)) { 21 | enc4 = 64; 22 | } 23 | output += 24 | keyStr.charAt(enc1) + 25 | keyStr.charAt(enc2) + 26 | keyStr.charAt(enc3) + 27 | keyStr.charAt(enc4); 28 | } 29 | return output; 30 | }; 31 | 32 | export const getBase64Image = (image: Buffer) => { 33 | const prefix = "data:image/bmp;base64,"; 34 | return prefix + base64Encode(image); 35 | }; 36 | -------------------------------------------------------------------------------- /src/lib/image/colorToMonoBitmap.ts: -------------------------------------------------------------------------------- 1 | import { monochrome128by64BitmapHeader } from "../../definitions/headers"; 2 | 3 | export const colorBitmapToMonochromeBitmap = async ( 4 | bitmapBuffer: Buffer, 5 | width: number, 6 | height: number 7 | ) => { 8 | // strip the 24bit color header out 9 | const bitmapBody = bitmapBuffer.slice(bitmapBuffer.readUInt32LE(10)); 10 | const blackAndWhite = []; 11 | 12 | // black and white through threshold 13 | for (let pixel = 0; pixel < width * height; pixel++) { 14 | const r = bitmapBody.readUInt8(pixel * 3); 15 | const g = bitmapBody.readUInt8(pixel * 3 + 1); 16 | const b = bitmapBody.readUInt8(pixel * 3 + 2); 17 | const saturation = (r + g + b) / 3 > 128 ? 1 : 0; 18 | blackAndWhite.push(saturation); 19 | } 20 | 21 | //black and white to monochrome bitmap format 22 | const monochromeBody = []; 23 | for (let i = 0; i < (width * height) / 8; i++) { 24 | const offset = i * 8; 25 | const part = blackAndWhite.slice(offset, offset + 8).join(""); 26 | const number = parseInt(part, 2); 27 | monochromeBody.push(number); // thats faster 28 | } 29 | //put a monochrome bitmap header above the body again 30 | return Buffer.concat([ 31 | new Buffer(monochrome128by64BitmapHeader()), 32 | new Buffer(monochromeBody), 33 | ]); 34 | }; 35 | -------------------------------------------------------------------------------- /src/lib/localisation/keyboard.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | import { keys } from "../../definitions/keys"; 4 | 5 | export const useTranslateKeyboardLayout = (values: number[]) => { 6 | const translatable = !(navigator as any).keyboard; 7 | const [translatedKeys, setTranslatedKeys] = useState([]); 8 | useEffect(() => { 9 | if (!values || !values.length) { 10 | setTranslatedKeys([]); 11 | return; 12 | } 13 | if (translatable) { 14 | setTranslatedKeys( 15 | values.map( 16 | (key) => 17 | Object.keys(keys).find( 18 | (displayName) => keys[displayName]?.hid === key 19 | ) || "" 20 | ) 21 | ); 22 | } else { 23 | (navigator as any).keyboard.getLayoutMap().then((layout: any) => { 24 | const translatedKeys = values.map((value) => { 25 | const key = 26 | Object.keys(keys).find( 27 | (displayName) => keys[displayName]?.hid === value 28 | ) || ""; 29 | const jsKey = keys[key]?.js; 30 | const localKey = layout.get(jsKey); 31 | return localKey ?? key; 32 | }); 33 | setTranslatedKeys(translatedKeys); 34 | }); 35 | } // eslint-disable-next-line 36 | }, [values]); 37 | return translatedKeys; 38 | }; 39 | -------------------------------------------------------------------------------- /src/lib/misc/createToast.tsx: -------------------------------------------------------------------------------- 1 | import { XMarkIcon } from "@heroicons/react/24/outline"; 2 | import React from "react"; 3 | import toast, { Toast } from "react-hot-toast"; 4 | 5 | import { FDButton } from "../components/Button"; 6 | import { TitleBox } from "../components/Title"; 7 | 8 | interface IProps { 9 | primary?: (t: Toast) => JSX.Element; 10 | danger?: (t: Toast) => JSX.Element; 11 | title?: string; 12 | text: string; 13 | } 14 | export const createToast = ({ danger, primary, title, text }: IProps) => { 15 | toast( 16 | (t) => ( 17 |
18 | {!!title && ( 19 | 20 |
{text}
21 |
22 | {!!primary && primary(t)} 23 | {!!danger ? ( 24 | danger(t) 25 | ) : ( 26 | } 28 | type="danger" 29 | size={2} 30 | onClick={() => toast.dismiss(t.id)} 31 | > 32 | Close 33 | 34 | )} 35 |
36 |
37 | )} 38 |
39 | ), 40 | { 41 | duration: Infinity, 42 | position: "bottom-right", 43 | style: { background: "#fff0" }, 44 | className: "text-white border-0 p-0 m-0 border-white text-lg", 45 | } 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /src/lib/misc/eventListeners.tsx: -------------------------------------------------------------------------------- 1 | import { CloudArrowDownIcon, XMarkIcon } from "@heroicons/react/24/outline"; 2 | import { useEffect } from "react"; 3 | import toast from "react-hot-toast"; 4 | 5 | import { IAppDispatch } from "../../states/appState"; 6 | import { FDButton } from "../components/Button"; 7 | import { createToast } from "./createToast"; 8 | import { isMacOS } from "./util"; 9 | 10 | export const AddEventListeners = ({ 11 | appDispatchContext, 12 | }: { 13 | appDispatchContext: IAppDispatch; 14 | }) => { 15 | const { setCtrl } = appDispatchContext; 16 | return useEffect(() => { 17 | window.addEventListener("beforeinstallprompt", (e: Event) => { 18 | e.preventDefault(); 19 | if (!localStorage.getItem("closedPWACTA")) 20 | createToast({ 21 | text: "You can install the configurator to have it offline! Click here to install", 22 | danger: (t) => ( 23 | { 25 | localStorage.setItem("closedPWACTA", "true"); 26 | toast.dismiss(t.id); 27 | }} 28 | prefix={} 29 | > 30 | Close 31 | 32 | ), 33 | primary: (t) => ( 34 | { 37 | localStorage.setItem("closedPWACTA", "true"); 38 | toast.dismiss(t.id); 39 | (e as any).prompt(); 40 | }} 41 | prefix={} 42 | > 43 | Install 44 | 45 | ), 46 | }); 47 | }); 48 | const onKeyDown = (event: KeyboardEvent) => { 49 | if (isMacOS && event.key === "Meta") { 50 | setCtrl(true); 51 | } else if (event.key === "Control") { 52 | setCtrl(true); 53 | } 54 | }; 55 | const onKeyUp = (event: KeyboardEvent) => { 56 | if (isMacOS && event.key === "Meta") { 57 | setCtrl(false); 58 | } else if (event.key === "Control") { 59 | setCtrl(false); 60 | } 61 | }; 62 | document.addEventListener("keydown", onKeyDown); 63 | document.addEventListener("keyup", onKeyUp); 64 | window.onblur = () => setCtrl(false); 65 | // eslint-disable-next-line 66 | }, []); // only execute on page load 67 | }; 68 | -------------------------------------------------------------------------------- /src/lib/misc/scrollToPage.tsx: -------------------------------------------------------------------------------- 1 | export const scrollToPage = (pageId: string) => { 2 | const contentBodyRef = document.getElementById("contentBody"); 3 | const pageRef = document.getElementById(`page_${pageId}`); 4 | const headerRef = document.getElementById("header"); 5 | if (pageRef && contentBodyRef) { 6 | contentBodyRef.scrollTo({ 7 | top: pageRef.offsetTop - (headerRef?.clientHeight ?? 150) + 24, 8 | }); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/lib/misc/util.ts: -------------------------------------------------------------------------------- 1 | import { Collection, Page } from "../../generated"; 2 | export const getPageName = (pageId: string, page?: Page) => { 3 | return page?.name ? page.name : pageId.slice(-4); 4 | }; 5 | 6 | export const getCollectionName = ( 7 | collectionId: string, 8 | collection?: Collection 9 | ) => { 10 | return collection?.name ? collection.name : collectionId.slice(-4); 11 | }; 12 | 13 | export const isMacOS = navigator.userAgent.indexOf("Mac OS X") !== -1; 14 | 15 | export function sleep(ms: number) { 16 | return new Promise((resolve) => 17 | setTimeout(() => { 18 | try { 19 | resolve(); 20 | } catch (e) { 21 | console.log(e); 22 | } 23 | }, ms) 24 | ); 25 | } 26 | 27 | export function compareVersions(a: string, b: string) { 28 | const aParts = a.split("."); 29 | const bParts = b.split("."); 30 | for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { 31 | const aPart = parseInt(aParts[i] || "0"); 32 | const bPart = parseInt(bParts[i] || "0"); 33 | if (aPart < bPart) return -1; 34 | if (aPart > bPart) return 1; 35 | } 36 | return 0; 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/serial/commands.ts: -------------------------------------------------------------------------------- 1 | import { invoke } from "@tauri-apps/api"; 2 | 3 | import { RefState, StateRef } from "../../App"; 4 | import { Config } from "../../generated"; 5 | import { IAppDispatch } from "../../states/appState"; 6 | 7 | const pressKeys = (args: number[], configState: Config) => { 8 | const pageId = configState.pages.sorted[args[0]]; 9 | const button = configState.pages.byId[pageId].displayButtons[args[1]].button; 10 | const text = args[2] 11 | ? button.secondary.values.text 12 | : button.primary.values.text; 13 | invoke("press_keys", { keyString: text ?? "nothing" }); 14 | }; 15 | 16 | export const saveCurrentPage = ( 17 | result: number, 18 | appDispatch: IAppDispatch, 19 | stateRef: StateRef 20 | ) => { 21 | let data: RefState["deck"]; 22 | if (result < 0) { 23 | data = { 24 | currentPage: Math.abs(result + 1), 25 | dontSwitchPage: true, 26 | }; 27 | } else if (result >= 0) { 28 | data = { currentPage: result, dontSwitchPage: false }; 29 | } else { 30 | data = { currentPage: null, dontSwitchPage: false }; 31 | } 32 | stateRef.current.deck = data; 33 | appDispatch.setDeck(data); 34 | }; 35 | 36 | export const runCommand = ( 37 | command: number, 38 | args: number[], 39 | configState: Config, 40 | appDispatch: IAppDispatch, 41 | refData: StateRef 42 | ) => { 43 | switch (command) { 44 | case 0x10: 45 | pressKeys(args, configState); 46 | break; 47 | case 0x20: 48 | saveCurrentPage(args[0], appDispatch, refData); 49 | break; 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /src/lib/serial/index.ts: -------------------------------------------------------------------------------- 1 | export type SerialFilter = { usbVendorId: number }[]; 2 | export enum connectionStatus { 3 | disconnect, 4 | connect, 5 | } 6 | export type connectCallback = (status: connectionStatus) => void; 7 | export type PortsChangedCallback = ( 8 | ports: string[], 9 | connectedPortIndex: number 10 | ) => void; 11 | export interface SerialConnector { 12 | requestNewPort: () => Promise; 13 | connect: (portIndex: number, showError?: boolean) => Promise; 14 | disconnect: () => Promise; 15 | write: (data: number[]) => Promise; 16 | flush: () => void; 17 | read: (timeout?: number) => Promise; 18 | readLine: (timeout?: number) => Promise; 19 | readSerialCommand: () => Promise<{ command: number; args: number[] }>; 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/serial/tauri-serial.ts: -------------------------------------------------------------------------------- 1 | import { invoke } from "@tauri-apps/api"; 2 | import { listen } from "@tauri-apps/api/event"; 3 | 4 | import { PortsChangedCallback, SerialConnector } from "."; 5 | 6 | export class TauriSerialConnector implements SerialConnector { 7 | portsChangedCallback: PortsChangedCallback; 8 | port = ""; 9 | ports: string[] = []; 10 | 11 | constructor(portsChangedCallback: PortsChangedCallback) { 12 | this.portsChangedCallback = portsChangedCallback; 13 | this.port = ""; 14 | this.refreshPorts(true).then(() => 15 | listen("ports_changed", ({ payload }) => { 16 | this.refreshPorts(true, payload); 17 | }) 18 | ); 19 | } 20 | async connect(portIndex: number, showError = false): Promise { 21 | try { 22 | await invoke("open", { path: this.ports[portIndex], baudRate: 4000000 }); 23 | this.port = this.ports[portIndex]; 24 | this.portsChangedCallback(this.ports, portIndex); 25 | } catch (e: any) { 26 | if (showError) alert(`${this.ports[portIndex]} - ${e as string}`); 27 | throw e; 28 | } 29 | } 30 | async disconnect(): Promise { 31 | await invoke("close", { path: this.port }); 32 | this.port = ""; 33 | this.portsChangedCallback(this.ports, -1); 34 | } 35 | async refreshPorts(autoConnect: boolean, prePorts?: string[]) { 36 | const ports: string[] = prePorts?.length 37 | ? prePorts 38 | : await invoke("get_ports"); 39 | this.ports = ports; 40 | if (this.ports.length === 0) return this.portsChangedCallback(ports, -1); 41 | const portIndex = this.ports.findIndex((port) => port === this.port); 42 | if (portIndex === -1 && autoConnect) { 43 | let i = 0; 44 | do { 45 | try { 46 | await this.connect(i); 47 | return this.portsChangedCallback(ports, i); 48 | } catch (e) { 49 | i++; 50 | } 51 | } while (i < ports.length); 52 | } 53 | this.portsChangedCallback(ports, portIndex); 54 | } 55 | async requestNewPort(): Promise {} 56 | 57 | async write(data: number[] | Uint8Array) { 58 | await invoke("write", { data }); 59 | } 60 | 61 | async flush() { 62 | await invoke("flush"); 63 | } 64 | 65 | async read(timeout = 1000): Promise { 66 | let data: number[] = []; 67 | do { 68 | data = await invoke("read"); 69 | } while (data.length === 0); 70 | return data; 71 | } 72 | 73 | async readLine(timeout = 1000): Promise { 74 | let data: number[] = []; 75 | do { 76 | data = await invoke("read_line").catch(() => []); 77 | } while (data.length === 0); 78 | return data; 79 | } 80 | 81 | async readSerialCommand() { 82 | let start = (await this.readLine()).filter((x) => x !== 13 && x !== 10); 83 | if (start[0] !== 3) { 84 | console.log(start, await this.readLine()); 85 | throw new Error("wrong start byte"); 86 | } 87 | let command = (await this.readLine()).filter( 88 | (x) => x !== 13 && x !== 10 89 | )[0]; 90 | if (command < 16) throw new Error("invalid command"); 91 | const data = (await this.readLine()).filter((x) => x !== 13 && x !== 10); 92 | const args = String.fromCharCode(...data) 93 | .split("\t") 94 | .map((x) => parseInt(x)); 95 | return { command, args }; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/schemas/button.ts: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | 3 | import { EAction, FDSettings } from "../definitions/modes"; 4 | 5 | export const ButtonValuesSchema = Joi.object({ 6 | [EAction.changePage]: Joi.string().allow(""), 7 | [EAction.hotkeys]: Joi.array().items(Joi.number()).failover([]).required(), 8 | [EAction.settings]: Joi.object({ 9 | setting: Joi.number() 10 | .valid(FDSettings.absolute_brightness, FDSettings.change_brightness) 11 | .failover(FDSettings.change_brightness) 12 | .required(), 13 | value: Joi.number().failover(128).required(), 14 | }).required(), 15 | [EAction.special_keys]: Joi.number().failover(0).required(), 16 | [EAction.text]: Joi.string().allow("").failover(" "), 17 | }).meta({ 18 | className: "ButtonValues", 19 | }); 20 | 21 | export const LeavePageSchema = Joi.object({ 22 | enabled: Joi.boolean().failover(false).required(), 23 | pageId: Joi.string(), 24 | }).meta({ className: "LeavePage" }); 25 | 26 | export const ButtonSettingSchema = Joi.object({ 27 | mode: Joi.string() 28 | .valid( 29 | EAction.changePage, 30 | EAction.hotkeys, 31 | EAction.noop, 32 | EAction.settings, 33 | EAction.special_keys, 34 | EAction.text 35 | ) 36 | .failover(EAction.noop) 37 | .strict() 38 | .required(), 39 | values: ButtonValuesSchema.required().failover(ButtonValuesSchema), 40 | leavePage: LeavePageSchema.required().failover(LeavePageSchema), 41 | }).meta({ 42 | className: "ButtonSetting", 43 | }); 44 | 45 | export const ButtonSchema = Joi.object({ 46 | primary: ButtonSettingSchema.required().failover(ButtonSettingSchema), 47 | secondary: ButtonSettingSchema.required().failover(ButtonSettingSchema), 48 | }).meta({ className: "Button" }); 49 | -------------------------------------------------------------------------------- /src/schemas/display.ts: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | 3 | import { getEmptyConvertedImage } from "../definitions/emptyConvertedImage"; 4 | import { 5 | fontLarge, 6 | fontMedium, 7 | fontSmall, 8 | fontSmaller, 9 | } from "../definitions/fonts"; 10 | import { EImageMode, ETextPosition } from "../definitions/modes"; 11 | import { getBase64Image } from "../lib/image/base64Encode"; 12 | 13 | export const TextWithIconSettingsSchema = Joi.object({ 14 | iconWidthMultiplier: Joi.number().min(0).max(1).failover(0.35).required(), 15 | }).meta({ 16 | className: "TextWithIconSettings", 17 | }); 18 | 19 | export const TextSettingsSchema = Joi.object({ 20 | font: Joi.string() 21 | .valid(fontSmaller, fontSmall, fontMedium, fontLarge) 22 | .failover(fontMedium) 23 | .required(), 24 | text: Joi.string().allow(""), 25 | position: Joi.number() 26 | .valid(ETextPosition.bottom, ETextPosition.right) 27 | .failover(ETextPosition.bottom) 28 | .required(), 29 | }).meta({ 30 | className: "TextSettings", 31 | }); 32 | 33 | export const ImageSettingsSchema = Joi.object({ 34 | blackThreshold: Joi.number().failover(192).required(), 35 | whiteThreshold: Joi.number().failover(64).required(), 36 | brightness: Joi.number().failover(0).required(), 37 | contrast: Joi.number().failover(0).required(), 38 | autoCrop: Joi.bool().failover(true).required(), 39 | imageMode: Joi.valid(EImageMode.dither, EImageMode.normal, EImageMode.hybrid) 40 | .failover(EImageMode.normal) 41 | .required(), 42 | invert: Joi.bool().failover(false).required(), 43 | }).meta({ 44 | className: "ImageSettings", 45 | }); 46 | 47 | export const DisplaySchema = Joi.object({ 48 | // @ts-ignore 49 | convertedImage: Joi.any(), 50 | imageSettings: ImageSettingsSchema.required().failover(ImageSettingsSchema), 51 | isGeneratedFromDefaultBackImage: Joi.bool().required().failover(false), 52 | // @ts-ignore 53 | originalImage: Joi.any(), 54 | previewImage: Joi.string() 55 | .required() 56 | .failover(getBase64Image(getEmptyConvertedImage())), 57 | textSettings: TextSettingsSchema.required().failover(TextSettingsSchema), 58 | textWithIconSettings: TextWithIconSettingsSchema.required().failover( 59 | TextWithIconSettingsSchema 60 | ), 61 | }).meta({ 62 | className: "Display", 63 | }); 64 | -------------------------------------------------------------------------------- /src/scripts/joiTypes.ts: -------------------------------------------------------------------------------- 1 | import { convertFromDirectory } from "joi-to-typescript"; 2 | 3 | async function generateTypes(): Promise { 4 | console.log("Running joi-to-typescript..."); 5 | 6 | // Configure your settings here 7 | const result = await convertFromDirectory({ 8 | schemaDirectory: "./src/schemas", 9 | typeOutputDirectory: "./src/generated", 10 | debug: true, 11 | schemaFileSuffix: "Schema", 12 | }); 13 | 14 | if (result) { 15 | console.log("Completed joi-to-typescript"); 16 | } else { 17 | console.log("Failed to run joi-to-typescrip"); 18 | } 19 | } 20 | 21 | generateTypes(); 22 | -------------------------------------------------------------------------------- /src/service-worker.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /* eslint-disable no-restricted-globals */ 3 | 4 | // This service worker can be customized! 5 | // See https://developers.google.com/web/tools/workbox/modules 6 | // for the list of available Workbox modules, or add any other 7 | // code you'd like. 8 | // You can also remove this file if you'd prefer not to use a 9 | // service worker, and the Workbox build step will be skipped. 10 | 11 | import { clientsClaim } from "workbox-core"; 12 | import { ExpirationPlugin } from "workbox-expiration"; 13 | import { precacheAndRoute, createHandlerBoundToURL } from "workbox-precaching"; 14 | import { registerRoute } from "workbox-routing"; 15 | import { StaleWhileRevalidate } from "workbox-strategies"; 16 | 17 | declare const self: ServiceWorkerGlobalScope; 18 | 19 | clientsClaim(); 20 | 21 | // Precache all of the assets generated by your build process. 22 | // Their URLs are injected into the manifest variable below. 23 | // This variable must be present somewhere in your service worker file, 24 | // even if you decide not to use precaching. See https://cra.link/PWA 25 | precacheAndRoute(self.__WB_MANIFEST); 26 | 27 | // Set up App Shell-style routing, so that all navigation requests 28 | // are fulfilled with your index.html shell. Learn more at 29 | // https://developers.google.com/web/fundamentals/architecture/app-shell 30 | const fileExtensionRegexp = new RegExp("/[^/?]+\\.[^/]+$"); 31 | registerRoute( 32 | // Return false to exempt requests from being fulfilled by index.html. 33 | ({ request, url }: { request: Request; url: URL }) => { 34 | // If this isn't a navigation, skip. 35 | if (request.mode !== "navigate") { 36 | return false; 37 | } 38 | 39 | // If this is a URL that starts with /_, skip. 40 | if (url.pathname.startsWith("/_")) { 41 | return false; 42 | } 43 | 44 | // If this looks like a URL for a resource, because it contains 45 | // a file extension, skip. 46 | if (url.pathname.match(fileExtensionRegexp)) { 47 | return false; 48 | } 49 | 50 | // Return true to signal that we want to use the handler. 51 | return true; 52 | }, 53 | createHandlerBoundToURL(process.env.PUBLIC_URL + "/index.html") 54 | ); 55 | 56 | // An example runtime caching route for requests that aren't handled by the 57 | // precache, in this case same-origin .png requests like those from in public/ 58 | registerRoute( 59 | // Add in any other file extensions or routing criteria as needed. 60 | ({ url }) => 61 | url.origin === self.location.origin && url.pathname.endsWith(".png"), 62 | // Customize this strategy as needed, e.g., by changing to CacheFirst. 63 | new StaleWhileRevalidate({ 64 | cacheName: "images", 65 | plugins: [ 66 | // Ensure that once this runtime cache reaches a maximum size the 67 | // least-recently used images are removed. 68 | new ExpirationPlugin({ maxEntries: 50 }), 69 | ], 70 | }) 71 | ); 72 | 73 | // This allows the web app to trigger skipWaiting via 74 | // registration.waiting.postMessage({type: 'SKIP_WAITING'}) 75 | self.addEventListener("message", (event) => { 76 | if (event.data && event.data.type === "SKIP_WAITING") { 77 | self.skipWaiting(); 78 | } 79 | }); 80 | 81 | // Any other custom service worker logic can go here. 82 | -------------------------------------------------------------------------------- /src/states/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface BaseActions { 2 | [key: string]: (...args: any[]) => RetType; 3 | } 4 | export type Actions = BaseActions>; 5 | export type FunctionForFirstParamType = (arg0: ParamType) => void; 6 | 7 | declare global { 8 | interface Window { 9 | advancedAlert: (title: string, text: string) => void; 10 | advancedConfirm: (title: string, text: string, onAccept: () => any) => any; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | * { 6 | box-sizing: border-box; 7 | } 8 | 9 | p { 10 | @apply py-4; 11 | } 12 | 13 | @layer base { 14 | body { 15 | @apply bg-gray-900 text-white font-sans; 16 | } 17 | @media screen and (-webkit-min-device-pixel-ratio: 0) { 18 | input[type="range"]::-webkit-slider-thumb { 19 | -webkit-appearance: none; 20 | @apply h-4 w-4 rounded bg-white; 21 | } 22 | } 23 | 24 | 25 | } 26 | ::-webkit-scrollbar { 27 | @apply w-2; 28 | } 29 | ::-webkit-scrollbar-track { 30 | @apply bg-gray-200; 31 | } 32 | 33 | ::-webkit-scrollbar-thumb { 34 | @apply bg-primary-500; 35 | } -------------------------------------------------------------------------------- /syncVersions.js: -------------------------------------------------------------------------------- 1 | const { readFileSync, writeFileSync } = require("fs"); 2 | 3 | const packageJson = require("./package.json"); 4 | 5 | async function main() { 6 | const toml = await readFileSync("./src-tauri/Cargo.toml", "utf8"); 7 | const lines = toml.split("\n"); 8 | const versionLineIndex = lines.findIndex((line) => 9 | line.includes("version = ") 10 | ); 11 | lines[versionLineIndex] = `version = "${packageJson.version}"`; 12 | const newToml = lines.join("\n"); 13 | await writeFileSync("./src-tauri/Cargo.toml", newToml); 14 | 15 | const tauriJson = await readFileSync("./src-tauri/tauri.conf.json", "utf8"); 16 | const tauriJsonObject = JSON.parse(tauriJson); 17 | tauriJsonObject.package.version = packageJson.version; 18 | 19 | await writeFileSync( 20 | "./src-tauri/tauri.conf.json", 21 | JSON.stringify(tauriJsonObject, null, 2) 22 | ); 23 | } 24 | 25 | main(); 26 | -------------------------------------------------------------------------------- /tsconfig.common.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "commonjs", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | "noFallthroughCasesInSwitch": true 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react-jsx", 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------