├── .editorconfig
├── .gitignore
├── .npmrc
├── CHANGELOG.md
├── LICENSE.txt
├── README.md
├── assets
├── css
│ ├── aframe.less
│ └── kbd.css
├── fonts
│ ├── Roboto-msdf.json
│ └── Roboto-msdf.png
├── icons
│ ├── README.md
│ ├── linux
│ │ └── skybrush.png
│ ├── mac
│ │ └── skybrush.icns
│ ├── splash.png
│ └── win
│ │ └── skybrush.ico
├── img
│ └── logo.png
├── models
│ ├── flapper-drone.obj
│ └── quadcopter.obj
└── shows
│ └── demo.json
├── config
├── baseline.ts
├── default.ts
├── demo.ts
├── index.ts
└── webapp.ts
├── electron-builder.json
├── i18next-parser.config.json
├── index.html
├── launcher.mjs
├── package-lock.json
├── package.json
├── patches
├── aframe-environment-component+1.3.7.patch
├── express+4.21.2.patch
├── meshline+3.1.0.patch
└── react-chartjs-2+2.11.2.patch
├── scripts
└── skyc-to-json.mjs
├── src
├── aframe
│ ├── components
│ │ ├── drone-flock.js
│ │ └── glow-material.js
│ ├── index.js
│ ├── materials
│ │ └── GlowingMaterial.js
│ └── primitives
│ │ └── drone-flock.js
├── app.tsx
├── components
│ ├── AudioController.tsx
│ ├── CameraSelectorChip.tsx
│ ├── CentralHelperPanel.tsx
│ ├── DragDropHandler.tsx
│ ├── LoadingScreen.tsx
│ ├── MainTopLevelView.tsx
│ ├── PageLoadingIndicator.tsx
│ ├── Sidebar.tsx
│ ├── SkybrushLogo.tsx
│ ├── SplashScreen.tsx
│ ├── WelcomeScreen.tsx
│ ├── WindowDragMoveArea.tsx
│ ├── WindowTitleManager.tsx
│ └── buttons
│ │ ├── OpenButton.tsx
│ │ ├── SettingsButton.tsx
│ │ ├── TrackDronesButton.tsx
│ │ ├── VolumeButton.tsx
│ │ └── ZoomOutButton.tsx
├── constants.ts
├── custom.d.ts
├── desktop
│ ├── index.js
│ ├── launcher
│ │ ├── api-v1.mjs
│ │ ├── app-menu.mjs
│ │ ├── dialogs.mjs
│ │ ├── file-opener.mjs
│ │ ├── http-server.mjs
│ │ ├── index.mjs
│ │ ├── ipc.mjs
│ │ ├── media-buffers.mjs
│ │ ├── media-protocol.mjs
│ │ ├── show-loader.mjs
│ │ ├── utils.mjs
│ │ └── window-title.mjs
│ └── preload
│ │ ├── index.js
│ │ └── ipc.js
├── features
│ ├── audio
│ │ ├── selectors.ts
│ │ └── slice.ts
│ ├── hotkeys
│ │ ├── AppHotkeys.tsx
│ │ ├── HotkeyDialog.tsx
│ │ ├── ShowHotkeysDialogButton.tsx
│ │ ├── keymap.ts
│ │ ├── selectors.ts
│ │ ├── slice.ts
│ │ ├── types.ts
│ │ └── utils.ts
│ ├── index.ts
│ ├── playback
│ │ ├── actions.ts
│ │ ├── selectors.ts
│ │ └── slice.ts
│ ├── settings
│ │ ├── DroneModelSelector.tsx
│ │ ├── DroneSizeSlider.tsx
│ │ ├── LanguageSelector.tsx
│ │ ├── PlaybackSpeedSelector.tsx
│ │ ├── ScenerySelector.tsx
│ │ ├── ThreeDViewSettingToggles.tsx
│ │ ├── actions.ts
│ │ ├── selectors.ts
│ │ ├── slice.ts
│ │ └── types.ts
│ ├── sharing
│ │ ├── ShareButton.tsx
│ │ └── actions.ts
│ ├── show
│ │ ├── actions.ts
│ │ ├── async.ts
│ │ ├── selectors.ts
│ │ ├── slice.ts
│ │ ├── types.ts
│ │ └── utils.ts
│ ├── sidebar
│ │ └── slice.ts
│ ├── three-d
│ │ ├── actions.ts
│ │ ├── saga.ts
│ │ ├── selectors.ts
│ │ └── slice.ts
│ ├── ui
│ │ ├── actions.ts
│ │ ├── modes.ts
│ │ ├── selectors.ts
│ │ └── slice.ts
│ └── validation
│ │ ├── AltitudeChartPanel.tsx
│ │ ├── ChartPanel.tsx
│ │ ├── HorizontalAccelerationChartPanel.tsx
│ │ ├── HorizontalVelocityChartPanel.tsx
│ │ ├── ProximityChartPanel.tsx
│ │ ├── ToggleValidationModeButton.tsx
│ │ ├── VerticalAccelerationChartPanel.tsx
│ │ ├── VerticalVelocityChartPanel.tsx
│ │ ├── actions.ts
│ │ ├── closest-pair.ts
│ │ ├── constants.ts
│ │ ├── items.ts
│ │ ├── panels.ts
│ │ ├── selectors.ts
│ │ ├── slice.ts
│ │ ├── types.ts
│ │ └── utils.ts
├── hooks
│ ├── store.ts
│ └── useDarkMode.ts
├── i18n
│ ├── LanguageWatcher.tsx
│ ├── de.json
│ ├── en.json
│ ├── hu.json
│ ├── index.ts
│ ├── it.json
│ ├── ja.json
│ ├── pl.json
│ ├── ru.json
│ └── zh-Hans.json
├── icons
│ └── VirtualReality.tsx
├── index.tsx
├── sagas
│ ├── index.ts
│ └── loader.ts
├── startup.tsx
├── store.ts
├── theme.ts
├── utils
│ ├── formatters.ts
│ ├── platform.ts
│ └── types.ts
├── views
│ ├── player
│ │ ├── BottomOverlay.tsx
│ │ ├── CameraSelectorChip.tsx
│ │ ├── CoordinateSystemAxes.tsx
│ │ ├── OverlayVisibilityController.ts
│ │ ├── Overlays.tsx
│ │ ├── PlaybackSlider.tsx
│ │ ├── PlayerView.tsx
│ │ ├── Scenery.tsx
│ │ ├── ThreeDView.tsx
│ │ ├── TopOverlay.tsx
│ │ ├── constants.ts
│ │ ├── index.ts
│ │ └── utils.ts
│ └── validation
│ │ ├── ChartGrid.tsx
│ │ ├── PanelToggleChip.tsx
│ │ ├── ValidationHeader.tsx
│ │ ├── ValidationSidebar.tsx
│ │ ├── ValidationView.tsx
│ │ └── index.ts
└── window.ts
├── tsconfig.json
├── types
└── config
│ └── index.d.ts
└── webpack
├── base.config.js
├── browser.config.js
├── dist.config.js
├── electron.config.js
├── helpers.js
├── launcher.config.js
└── preload.config.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 |
7 | [*.{js,jsx,css,less}]
8 | charset = utf-8
9 | indent_style = space
10 | indent_size = 2
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # VSCode settings
2 | .vscode
3 |
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 |
9 | # Runtime data
10 | pids
11 | *.pid
12 | *.seed
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # node-waf configuration
27 | .lock-wscript
28 |
29 | # Compiled binary addons (http://nodejs.org/api/addons.html)
30 | build/Release
31 |
32 | # Generated API documentation
33 | doc/api/
34 |
35 | # Stuff packed by webpack
36 | build/
37 | dist/
38 | webpack-stats.json
39 |
40 | # Typescript type definitions
41 | typings/
42 |
43 | # Dependency directories
44 | node_modules
45 | jspm_packages
46 |
47 | # Optional npm cache directory
48 | .npm
49 |
50 | # Optional REPL history
51 | .node_repl_history
52 |
53 | # Environment file containing sensitive data (e.g.: API keys)
54 | .env
55 |
56 | # Show files used as examples during testing
57 | assets/shows/
58 |
59 | # Large icons
60 | assets/icons/*512.png
61 | assets/icons/*1024.png
62 |
63 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | registry=https://registry.npmjs.org
2 | @collmot:registry=https://npm.collmot.com
3 | @skybrush:registry=https://npm.collmot.com
4 | legacy-peer-deps=true
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Skybrush Viewer
2 |
3 | This repo contains the source code of Skybrush Viewer, the viewer app for
4 | Skybrush drone shows.
5 |
6 | ## Usage
7 |
8 | Make sure that you are using a recent LTS Node.js release, then run the
9 | following commands:
10 |
11 | ```
12 | npm install
13 | npm run start:electron
14 | ```
15 |
16 | You may also run the app inside a browser environment:
17 |
18 | ```
19 | npm install
20 | npm run start
21 | ```
22 |
23 | Navigate to `http://localhost:8080` after startup to use the app in
24 | a browser. Note that not all features may be available in a browser
25 | environment.
26 |
27 | ## Support
28 |
29 | For any support questions please contact us on our [Discord server](https://skybrush.io/r/discord).
30 |
31 | ## License
32 |
33 | Copyright 2020-2024 CollMot Robotics Ltd.
34 |
35 | Skybrush Viewer is free software: you can redistribute it and/or modify it under
36 | the terms of the GNU General Public License as published by the Free Software
37 | Foundation, either version 3 of the License, or (at your option) any later
38 | version.
39 |
40 | Skybrush Viewer is distributed in the hope that it will be useful, but WITHOUT
41 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
42 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
43 | more details.
44 |
45 | You should have received a copy of the GNU General Public License along with
46 | this program. If not, see .
47 |
--------------------------------------------------------------------------------
/assets/css/aframe.less:
--------------------------------------------------------------------------------
1 | /* Tweaks for A-Frame's dialogs and modals to fit our styling */
2 |
3 | .a-dialog {
4 | background-color: unset;
5 | font-family: unset;
6 | font-size: unset;
7 | }
8 |
9 | .a-dialog-text {
10 | font-size: unset;
11 | }
12 |
13 | .a-dialog-button {
14 | font-size: unset;
15 | border-radius: 30px;
16 | text-transform: uppercase;
17 | font-weight: bold;
18 | }
19 |
20 | .a-dialog-ok-button {
21 | background-color: #F44336; /* Material-UI red[500] */
22 | color: white;
23 | }
24 |
--------------------------------------------------------------------------------
/assets/css/kbd.css:
--------------------------------------------------------------------------------
1 | /**
2 | * CSS styling for tags.
3 | *
4 | * The MIT License (MIT)
5 | *
6 | * Copyright (c) 2015 Auth0, Inc. (http://auth0.com)
7 | *
8 | * Permission is hereby granted, free of charge, to any person obtaining a copy
9 | * of this software and associated documentation files (the "Software"), to deal
10 | * in the Software without restriction, including without limitation the rights
11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | * copies of the Software, and to permit persons to whom the Software is
13 | * furnished to do so, subject to the following conditions:
14 | *
15 | * The above copyright notice and this permission notice shall be included in all
16 | * copies or substantial portions of the Software.
17 | *
18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 | * SOFTWARE.
25 | */
26 | kbd {
27 | font-family: 'Fira Sans', Arial, sans-serif;
28 | display: inline-block;
29 | border-radius: 3px;
30 | padding: 0px 4px;
31 | box-shadow: 1px 1px 1px #777;
32 | margin: 2px;
33 | background: #eee;
34 | font-weight: normal;
35 | color: #555;
36 | cursor: pointer;
37 |
38 | /* Prevent selection */
39 | -webkit-touch-callout: none;
40 | -webkit-user-select: none;
41 | -khtml-user-select: none;
42 | -moz-user-select: none;
43 | -ms-user-select: none;
44 | user-select: none;
45 | }
46 |
47 | kbd:hover, kbd:hover * {
48 | color: black;
49 | /* box-shadow: 1px 1px 1px #333; */
50 | }
51 | kbd:active, kbd:active * {
52 | color: black;
53 | box-shadow: 1px 1px 0px #ddd inset;
54 | }
55 |
56 | kbd kbd {
57 | padding: 0px;
58 | margin: 0 1px;
59 | box-shadow: 0px 0px 0px black;
60 | vertical-align: baseline;
61 | background: none;
62 | }
63 |
64 | kbd kbd:hover {
65 | box-shadow: 0px 0px 0px black;
66 | }
67 |
68 | kbd:active kbd {
69 | box-shadow: 0px 0px 0px black;
70 | background: none;
71 | }
72 |
73 | /* Deep blue */
74 | kbd.deep-blue, .deep-blue kbd {
75 | background: steelblue;
76 | color: #eee;
77 | }
78 |
79 | kbd.deep-blue:hover, kbd.deep-blue:hover *, .deep-blue kbd:hover {
80 | color: white;
81 | }
82 |
83 | /* Dark apple */
84 | kbd.dark-apple, .dark-apple kbd {
85 | background: black;
86 | color: #ddd;
87 | }
88 |
89 | kbd.dark-apple:hover, kbd.dark-apple:hover *, .dark-apple kbd:hover {
90 | color: white;
91 | }
92 |
93 | /* Type writer */
94 | kbd.type-writer, .type-writer kbd {
95 | border-radius: 10px;
96 | background: #333;
97 | color: white;
98 | }
99 |
100 | /* Keyboard tag styling speciifc for dark mode */
101 | kbd {
102 | background: #616161 !important;
103 | box-shadow: 1px 1px 1px #333;
104 | color: #ddd !important;
105 | }
106 |
107 | kbd:hover,
108 | kbd:hover * {
109 | color: white !important;
110 | }
111 |
--------------------------------------------------------------------------------
/assets/fonts/Roboto-msdf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skybrush-io/viewer/3e240765797d4b258763034372ddd3e89085ef78/assets/fonts/Roboto-msdf.png
--------------------------------------------------------------------------------
/assets/icons/README.md:
--------------------------------------------------------------------------------
1 | Current icon was generated with the _Launcher icon generator_ from the
2 | _Android Asset Studio_:
3 |
4 | https://romannurik.github.io/AndroidAssetStudio/icons-launcher.html
5 |
6 | Background color: #dc3545 ("Skybrush red", probably from the Bootstrap
7 | palette)
8 | Clipart: search
9 | Font (if we need): Allura
10 | Padding (if we need text): 0%
11 |
12 | Take the 512px version, scale it up to 1024px and add rounded corners in
13 | GIMP with a corner radius of 180px. Then upload the image to the following
14 | URL to get it converted to .icns format:
15 |
16 | https://cloudconvert.com/
17 |
18 | Also add rounded corners to the 512px version with a corner radius of 90px,
19 | and upload this icon to the following URL to get it converted to .ico format
20 | for Windows:
21 |
22 | https://icoconvert.com/
23 |
--------------------------------------------------------------------------------
/assets/icons/linux/skybrush.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skybrush-io/viewer/3e240765797d4b258763034372ddd3e89085ef78/assets/icons/linux/skybrush.png
--------------------------------------------------------------------------------
/assets/icons/mac/skybrush.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skybrush-io/viewer/3e240765797d4b258763034372ddd3e89085ef78/assets/icons/mac/skybrush.icns
--------------------------------------------------------------------------------
/assets/icons/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skybrush-io/viewer/3e240765797d4b258763034372ddd3e89085ef78/assets/icons/splash.png
--------------------------------------------------------------------------------
/assets/icons/win/skybrush.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skybrush-io/viewer/3e240765797d4b258763034372ddd3e89085ef78/assets/icons/win/skybrush.ico
--------------------------------------------------------------------------------
/assets/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skybrush-io/viewer/3e240765797d4b258763034372ddd3e89085ef78/assets/img/logo.png
--------------------------------------------------------------------------------
/config/baseline.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Baseline values for the configuration options of the application.
3 | */
4 |
5 | import { type Config } from 'config';
6 |
7 | const baseline: Config = {
8 | buttons: {
9 | playbackHint: false,
10 | },
11 | io: {
12 | localFiles: true,
13 | },
14 | language: {
15 | default: 'en',
16 | enabled: ['en', 'hu', 'zh-Hans'],
17 | fallback: 'en',
18 | },
19 | modes: {
20 | deepLinking: false,
21 | player: true,
22 | validation: true,
23 | vr: false,
24 | },
25 | preloadedShow: {},
26 | startAutomatically: true,
27 | useWelcomeScreen: true,
28 | };
29 |
30 | export default baseline;
31 |
--------------------------------------------------------------------------------
/config/default.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Empty configuration override, which preserves all the defaults.
3 | */
4 |
5 | import { type ConfigOverrides } from 'config-overrides';
6 |
7 | const overrides: ConfigOverrides = {};
8 |
9 | export default overrides;
10 |
--------------------------------------------------------------------------------
/config/demo.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Default application configuration at startup to run a local hardcoded demo.
3 | */
4 |
5 | import type { ShowSpecification } from '@skybrush/show-format';
6 | import { type ConfigOverrides } from 'config-overrides';
7 |
8 | import audio from '~/../assets/shows/demo.mp3';
9 |
10 | const show = async (): Promise =>
11 | import(
12 | /* webpackChunkName: "show" */ '~/../assets/shows/demo.json'
13 | ) as any as ShowSpecification;
14 |
15 | const overrides: ConfigOverrides = {
16 | buttons: {
17 | playbackHint: true,
18 | },
19 | electronBuilder: {
20 | productName: 'Skybrush Viewer Demo',
21 | },
22 | io: {
23 | localFiles: false,
24 | },
25 | modes: {
26 | deepLinking: true,
27 | validation: false,
28 | },
29 | preloadedShow: {
30 | audio,
31 | show,
32 | },
33 | useWelcomeScreen: false,
34 | };
35 |
36 | export default overrides;
37 |
--------------------------------------------------------------------------------
/config/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file File for merging the default config with overrides from external files.
3 | */
4 | import mergeWith from 'lodash-es/mergeWith.js';
5 |
6 | import { type Config } from 'config';
7 | import overrides from 'config-overrides';
8 |
9 | import baseline from './baseline';
10 |
11 | // Completely replace arrays in the configuration instead of merging them.
12 | const customizer = (
13 | defaultValue: unknown,
14 | overrideValue: T
15 | ): T | undefined => {
16 | if (Array.isArray(defaultValue) && Array.isArray(overrideValue)) {
17 | return overrideValue;
18 | }
19 | };
20 |
21 | const merged: Config = mergeWith(baseline, overrides, customizer);
22 |
23 | export default merged;
24 |
--------------------------------------------------------------------------------
/config/webapp.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Default application configuration at startup when running as a web app.
3 | */
4 |
5 | import { type ConfigOverrides } from 'config-overrides';
6 |
7 | const overrides: ConfigOverrides = {
8 | buttons: {
9 | playbackHint: true,
10 | },
11 | io: {
12 | localFiles: false,
13 | },
14 | modes: {
15 | deepLinking: true,
16 | validation: false,
17 | },
18 | startAutomatically: false,
19 | useWelcomeScreen: false,
20 | };
21 |
22 | export default overrides;
23 |
--------------------------------------------------------------------------------
/electron-builder.json:
--------------------------------------------------------------------------------
1 | {
2 | "appId": "com.collmot.skybrush.viewer",
3 | "productName": "Skybrush Viewer",
4 |
5 | "artifactName": "${productName} ${version}.${ext}",
6 |
7 | "files": ["!**/*", "package.json", { "from": "build" }],
8 |
9 | "fileAssociations": [{
10 | "ext": "skyc",
11 | "description": "Skybrush compiled show file",
12 | "role": "Viewer"
13 | }],
14 |
15 | "linux": {
16 | "category": "Utility",
17 | "target": {
18 | "target": "AppImage",
19 | "arch": "x64"
20 | }
21 | },
22 |
23 | "mac": {
24 | "category": "public.app-category.utilities",
25 | "target": {
26 | "target": "dmg",
27 | "arch": "universal"
28 | }
29 | },
30 |
31 | "win": {
32 | "target": {
33 | "target": "portable",
34 | "arch": "x64"
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/i18next-parser.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "input": ["src/**/*.{ts,tsx}"],
3 | "locales": ["en", "hu"],
4 | "sort": true,
5 | "output": "src/i18n/$LOCALE.json"
6 | }
7 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
15 |
16 |
--------------------------------------------------------------------------------
/launcher.mjs:
--------------------------------------------------------------------------------
1 | import main from './src/desktop/launcher/index.mjs';
2 |
3 | await main();
4 |
--------------------------------------------------------------------------------
/patches/aframe-environment-component+1.3.7.patch:
--------------------------------------------------------------------------------
1 | diff --git a/node_modules/aframe-environment-component/index.js b/node_modules/aframe-environment-component/index.js
2 | index 33781bf..778f3ce 100644
3 | --- a/node_modules/aframe-environment-component/index.js
4 | +++ b/node_modules/aframe-environment-component/index.js
5 | @@ -258,7 +258,6 @@ AFRAME.registerComponent('environment', {
6 | Object.assign(this.environmentData, this.data);
7 | Object.assign(this.environmentData, this.presets[this.data.preset]);
8 | Object.assign(this.environmentData, this.el.components.environment.attrValue);
9 | - console.log(this.environmentData);
10 | }
11 |
12 | var skyType = this.environmentData.skyType;
13 | @@ -453,7 +452,6 @@ AFRAME.registerComponent('environment', {
14 | str += ', ';
15 | }
16 | str += '}';
17 | - console.log(str);
18 | },
19 |
20 | // dumps current component settings to console.
21 | @@ -497,7 +495,6 @@ AFRAME.registerComponent('environment', {
22 | }
23 | }
24 | }
25 | - console.log('%c' + params.join('; '), 'color: #f48;font-weight:bold');
26 | },
27 |
28 | // Custom Math.random() with seed. Given this.environmentData.seed and x, it always returns the same "random" number
29 |
--------------------------------------------------------------------------------
/patches/express+4.21.2.patch:
--------------------------------------------------------------------------------
1 | diff --git a/node_modules/express/lib/view.js b/node_modules/express/lib/view.js
2 | index c08ab4d..f49ddea 100644
3 | --- a/node_modules/express/lib/view.js
4 | +++ b/node_modules/express/lib/view.js
5 | @@ -78,7 +78,7 @@ function View(name, options) {
6 | debug('require "%s"', mod)
7 |
8 | // default engine export
9 | - var fn = require(mod).__express
10 | + var fn = null // require(mod).__express
11 |
12 | if (typeof fn !== 'function') {
13 | throw new Error('Module "' + mod + '" does not provide a view engine.')
14 |
--------------------------------------------------------------------------------
/patches/react-chartjs-2+2.11.2.patch:
--------------------------------------------------------------------------------
1 | diff --git a/node_modules/react-chartjs-2/es/index.js b/node_modules/react-chartjs-2/es/index.js
2 | index 7ca177d..a3c9be6 100644
3 | --- a/node_modules/react-chartjs-2/es/index.js
4 | +++ b/node_modules/react-chartjs-2/es/index.js
5 | @@ -249,7 +249,9 @@ var ChartComponent = /*#__PURE__*/function (_React$Component) {
6 | if (current && current.type === next.type && next.data) {
7 | // Be robust to no data. Relevant for other update mechanisms as in chartjs-plugin-streaming.
8 | // The data array must be edited in place. As chart.js adds listeners to it.
9 | - current.data.splice(next.data.length);
10 | + if (current.data.length > next.data.length) {
11 | + current.data.splice(next.data.length);
12 | + }
13 | next.data.forEach(function (point, pid) {
14 | current.data[pid] = next.data[pid];
15 | });
16 |
--------------------------------------------------------------------------------
/scripts/skyc-to-json.mjs:
--------------------------------------------------------------------------------
1 | import { promises as fs } from 'node:fs';
2 | import process from 'node:process';
3 |
4 | import { program } from 'commander';
5 | import showFormat from '@skybrush/show-format';
6 |
7 | const { loadCompiledShow } = showFormat;
8 |
9 | program
10 | .storeOptionsAsProperties(false)
11 | .requiredOption('-i, --input ', 'name of the input file')
12 | .requiredOption('-o, --output ', 'name of the output file')
13 | .parse(process.argv);
14 |
15 | async function main(options) {
16 | const data = await fs.readFile(options.input);
17 | const show = await loadCompiledShow(data);
18 | const output = JSON.stringify(show, null, 2);
19 | await fs.writeFile(options.output, output);
20 | }
21 |
22 | await main(program.opts());
23 |
--------------------------------------------------------------------------------
/src/aframe/components/glow-material.js:
--------------------------------------------------------------------------------
1 | import AFrame from '@skybrush/aframe-components';
2 |
3 | import GlowingMaterial from '../materials/GlowingMaterial';
4 |
5 | AFrame.registerComponent('glow-material', {
6 | schema: {
7 | color: { type: 'color', is: 'uniform', default: '#0080ff' },
8 | falloff: { type: 'number', is: 'uniform', default: 0.1 },
9 | internalRadius: { type: 'number', is: 'uniform', default: 6 },
10 | sharpness: { type: 'number', is: 'uniform', default: 1 },
11 | opacity: { type: 'number', is: 'uniform', default: 1 },
12 | },
13 |
14 | init() {
15 | this.material = new GlowingMaterial(this._getMaterialProperties());
16 | this.el.addEventListener('loaded', () => {
17 | const mesh = this.el.getObject3D('mesh');
18 | if (mesh) {
19 | mesh.material = this.material;
20 | }
21 | });
22 | },
23 |
24 | update() {
25 | this.material?.setValues(this._getMaterialProperties());
26 | },
27 |
28 | _getMaterialProperties() {
29 | const { color, falloff, internalRadius, sharpness, opacity } = this.data;
30 | return {
31 | color,
32 | falloff,
33 | internalRadius,
34 | sharpness,
35 | opacity,
36 | };
37 | },
38 | });
39 |
--------------------------------------------------------------------------------
/src/aframe/index.js:
--------------------------------------------------------------------------------
1 | import AFrame from '@skybrush/aframe-components';
2 |
3 | import 'aframe-environment-component';
4 | import 'aframe-look-at-component';
5 |
6 | import '@skybrush/aframe-components/advanced-camera-controls';
7 | import '@skybrush/aframe-components/deallocate';
8 | import '@skybrush/aframe-components/meshline';
9 | import { createSyncPoseWithStoreComponent } from '@skybrush/aframe-components/factories';
10 |
11 | import './components/drone-flock';
12 | import './components/glow-material';
13 |
14 | import './primitives/drone-flock';
15 |
16 | AFrame.registerComponent(
17 | 'sync-pose-with-store',
18 | createSyncPoseWithStoreComponent({
19 | getCameraPose() {},
20 |
21 | setCameraPose() {},
22 | })
23 | );
24 |
25 | // eslint-disable-next-line unicorn/prefer-export-from
26 | export default AFrame;
27 |
--------------------------------------------------------------------------------
/src/aframe/primitives/drone-flock.js:
--------------------------------------------------------------------------------
1 | /**
2 | * A-Frame primitive that creates a drone flock entity that will contain the
3 | * individual drones.
4 | */
5 |
6 | import AFrame from '@skybrush/aframe-components';
7 |
8 | AFrame.registerPrimitive('a-drone-flock', {
9 | // Attaches the 'drone-flock' component by default.
10 | defaultComponents: {
11 | 'drone-flock': {},
12 | },
13 | mappings: {
14 | 'drone-model': 'drone-flock.droneModel',
15 | 'drone-radius': 'drone-flock.droneRadius',
16 | indoor: 'drone-flock.indoor',
17 | 'label-color': 'drone-flock.labelColor',
18 | 'scale-labels': 'drone-flock.scaleLabels',
19 | 'show-glow': 'drone-flock.showGlow',
20 | 'show-labels': 'drone-flock.showLabels',
21 | 'show-yaw': 'drone-flock.showYaw',
22 | size: 'drone-flock.size',
23 | },
24 | });
25 |
--------------------------------------------------------------------------------
/src/app.tsx:
--------------------------------------------------------------------------------
1 | import delay from 'delay';
2 | import React from 'react';
3 | import { Toaster } from 'react-hot-toast';
4 | import { Provider as StoreProvider } from 'react-redux';
5 | import { PersistGate } from 'redux-persist/es/integration/react';
6 |
7 | import CssBaseline from '@mui/material/CssBaseline';
8 | import { StyledEngineProvider } from '@mui/material/styles';
9 |
10 | import DragDropHandler from './components/DragDropHandler';
11 | import MainTopLevelView from './components/MainTopLevelView';
12 | import Sidebar from './components/Sidebar';
13 | import SplashScreen from './components/SplashScreen';
14 | import WindowTitleManager from './components/WindowTitleManager';
15 |
16 | import AppHotkeys from './features/hotkeys/AppHotkeys';
17 | import { loadShowFromRequest } from './features/show/slice';
18 | import { type ShowLoadingRequest } from './features/show/types';
19 | import rootSaga from './sagas';
20 | import { persistor, store } from './store';
21 | import ThemeProvider, { toastOptions } from './theme';
22 |
23 | import '~/../assets/css/aframe.less';
24 | import '~/../assets/css/kbd.css';
25 |
26 | import '@fontsource/fira-sans/400.css';
27 | import '@fontsource/fira-sans/500.css';
28 | import 'react-cover-page/themes/dark.css';
29 | import LanguageWatcher from './i18n/LanguageWatcher';
30 | import HotkeyDialog from './features/hotkeys/HotkeyDialog';
31 |
32 | interface AppProps {
33 | readonly initialShow?: ShowLoadingRequest;
34 | }
35 |
36 | const App = ({ initialShow }: AppProps) => {
37 | const waitForTopLevelView = React.useCallback(async () => {
38 | // Start the root saga
39 | store.runSaga(rootSaga);
40 |
41 | // Load the initial show file (if any)
42 | if (initialShow) {
43 | store.dispatch(loadShowFromRequest(initialShow));
44 | }
45 |
46 | // Give some time for the 3D scene to initialize itself
47 | await delay(1000);
48 | }, [initialShow]);
49 |
50 | return (
51 |
52 |
53 |
54 |
55 |
56 | {(bootstrapped) => (
57 | <>
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | >
68 | )}
69 |
70 |
71 |
72 |
73 | );
74 | };
75 |
76 | export default App;
77 |
--------------------------------------------------------------------------------
/src/components/AudioController.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Component that controls the audio playback synchronized to the
3 | * visuals.
4 | */
5 |
6 | import React, { useCallback, useEffect, useRef } from 'react';
7 | import toast from 'react-hot-toast';
8 | import { connect } from 'react-redux';
9 |
10 | import {
11 | notifyAudioCanPlay,
12 | notifyAudioMetadataLoaded,
13 | notifyAudioSeeked,
14 | notifyAudioSeeking,
15 | } from '~/features/audio/slice';
16 | import {
17 | getElapsedSecondsGetter,
18 | isAdjustingPlaybackPosition,
19 | isPlayingInRealTime,
20 | } from '~/features/playback/selectors';
21 | import type { RootState } from '~/store';
22 |
23 | interface AudioControllerProps {
24 | readonly elapsedSecondsGetter: () => number;
25 | readonly muted: boolean;
26 | readonly onCanPlay: () => void;
27 | readonly onLoadedMetadata: () => void;
28 | readonly onSeeked: () => void;
29 | readonly onSeeking: () => void;
30 | readonly playing: boolean;
31 | readonly url?: string;
32 | }
33 |
34 | const AudioController = ({
35 | elapsedSecondsGetter,
36 | muted,
37 | onCanPlay,
38 | onLoadedMetadata,
39 | onSeeked,
40 | onSeeking,
41 | playing,
42 | url,
43 | }: AudioControllerProps) => {
44 | const audioRef = useRef(null);
45 | const onError = useCallback(() => {
46 | toast.error('Error while playing audio; playback stopped.');
47 |
48 | if (audioRef?.current) {
49 | console.error(audioRef.current.error);
50 | }
51 | }, [audioRef]);
52 |
53 | // Effect that takes care of stopping / starting the audio and re-syncing the
54 | // playback position when needed
55 | useEffect(() => {
56 | if (audioRef.current) {
57 | if (playing) {
58 | // TODO(ntamas): there is a hardcoded delay between the audio and the
59 | // visuals. I don't know why it's needed or whether it varies from
60 | // machine to machine. We need to test it.
61 | audioRef.current.currentTime = elapsedSecondsGetter() + 0.15;
62 | void audioRef.current.play();
63 | } else {
64 | audioRef.current.pause();
65 | }
66 | }
67 | }, [elapsedSecondsGetter, playing]);
68 |
69 | return url ? (
70 |
81 | ) : null;
82 | };
83 |
84 | export default connect(
85 | // mapStateToProps
86 | (state: RootState) => ({
87 | ...state.audio,
88 | elapsedSecondsGetter: getElapsedSecondsGetter(state),
89 | playing: isPlayingInRealTime(state) && !isAdjustingPlaybackPosition(state),
90 | }),
91 | // mapDispatchToProps
92 | {
93 | onCanPlay: notifyAudioCanPlay,
94 | onLoadedMetadata: notifyAudioMetadataLoaded,
95 | onSeeking: notifyAudioSeeking,
96 | onSeeked: notifyAudioSeeked,
97 | }
98 | )(AudioController);
99 |
--------------------------------------------------------------------------------
/src/components/CentralHelperPanel.tsx:
--------------------------------------------------------------------------------
1 | import Color from 'color';
2 | import React from 'react';
3 |
4 | import Box from '@mui/material/Box';
5 | import IconButton from '@mui/material/IconButton';
6 | import Fade from '@mui/material/Fade';
7 | import { type Theme } from '@mui/material/styles';
8 | import Close from '@mui/icons-material/Close';
9 |
10 | import { isThemeDark } from '@skybrush/app-theme-mui';
11 |
12 | const styles = {
13 | root: {
14 | background: (theme: Theme) =>
15 | String(
16 | new Color(
17 | isThemeDark(theme)
18 | ? theme.palette.common.black
19 | : theme.palette.background.default
20 | )
21 | .alpha(0.7)
22 | .string()
23 | ),
24 | borderRadius: 1,
25 | boxShadow: 8,
26 | fontSize: 'fontSize',
27 | minWidth: 200,
28 | p: 6,
29 | position: 'absolute',
30 | left: '50%',
31 | top: '50%',
32 | textAlign: 'center',
33 | transform: 'translate(-50%, -50%)',
34 | },
35 |
36 | inner: {
37 | position: 'absolute',
38 | right: 4,
39 | top: 4,
40 | opacity: 0.5,
41 | },
42 | };
43 |
44 | interface CentralHelperPanelProps {
45 | readonly canDismiss?: boolean;
46 | readonly children: React.ReactNode;
47 | readonly onDismiss?: () => void;
48 | readonly visible: boolean;
49 | }
50 |
51 | const CentralHelperPanel = ({
52 | canDismiss,
53 | children,
54 | onDismiss,
55 | visible,
56 | }: CentralHelperPanelProps) => (
57 |
58 |
59 | {children}
60 | {canDismiss && (
61 |
62 |
63 |
64 |
65 |
66 | )}
67 |
68 |
69 | );
70 |
71 | export default CentralHelperPanel;
72 |
--------------------------------------------------------------------------------
/src/components/DragDropHandler.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import { loadShowFromLocalFile } from '~/features/show/actions';
5 |
6 | const extractFileFromEvent = (event: DragEvent) => {
7 | if (!event.dataTransfer) {
8 | return undefined;
9 | }
10 |
11 | const { files, items } = event.dataTransfer;
12 | let file;
13 |
14 | if (items && items.length === 1 && items[0].kind === 'file') {
15 | file = items[0].getAsFile();
16 | } else if (files && files.length === 1) {
17 | file = files[0];
18 | }
19 |
20 | if (!file) {
21 | return undefined;
22 | }
23 |
24 | // The requirement of the file.path property prevents this from working in
25 | // the browser, but that's exactly what we want
26 | return file.name.endsWith('.skyc') && (file as any).path
27 | ? ((file as any).path as string)
28 | : undefined;
29 | };
30 |
31 | const onFileDragging = (event: DragEvent) => {
32 | // Prevent the default behaviour of the browser
33 | event.preventDefault();
34 | };
35 |
36 | interface DragDropHandlerProps {
37 | readonly onFileDropped: (filename: string) => void;
38 | }
39 |
40 | const DragDropHandler = ({ onFileDropped }: DragDropHandlerProps) => {
41 | const handleDrop = useCallback(
42 | (event: DragEvent) => {
43 | const filename = extractFileFromEvent(event);
44 | if (filename) {
45 | onFileDropped(filename);
46 | }
47 | },
48 | [onFileDropped]
49 | );
50 |
51 | useEffect(() => {
52 | const target = document;
53 |
54 | target.addEventListener('dragover', onFileDragging);
55 | target.addEventListener('drop', handleDrop);
56 | return () => {
57 | target.removeEventListener('dragover', onFileDragging);
58 | target.removeEventListener('drop', handleDrop);
59 | };
60 | }, [handleDrop]);
61 | return null;
62 | };
63 |
64 | export default connect(
65 | // mapStateToProps
66 | null,
67 | // mapDispatchToProps
68 | {
69 | onFileDropped: loadShowFromLocalFile,
70 | }
71 | )(DragDropHandler);
72 |
--------------------------------------------------------------------------------
/src/components/LoadingScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import Box from '@mui/material/Box';
5 | import CircularProgress from '@mui/material/CircularProgress';
6 | import Fab from '@mui/material/Fab';
7 | import PlayIcon from '@mui/icons-material/PlayArrow';
8 | import WarningIcon from '@mui/icons-material/Warning';
9 |
10 | import { rewind, togglePlayback } from '~/features/playback/actions';
11 | import { userInteractedWithPlayback } from '~/features/playback/selectors';
12 | import {
13 | hasLoadedShowFile,
14 | isLoadingShowFile,
15 | } from '~/features/show/selectors';
16 | import { shouldShowPlaybackHintButton } from '~/features/settings/selectors';
17 | import type { RootState } from '~/store';
18 |
19 | import CentralHelperPanel from './CentralHelperPanel';
20 |
21 | const styles = {
22 | root: {
23 | position: 'relative',
24 | display: 'inline-block',
25 | },
26 |
27 | progress: {
28 | position: 'absolute',
29 | top: -4,
30 | left: -4,
31 | zIndex: 1,
32 | },
33 |
34 | label: {
35 | mt: 4,
36 | textAlign: 'center',
37 | userSelect: 'none',
38 | },
39 | } as const;
40 |
41 | interface LoadingScreenProps {
42 | readonly canPlay: boolean;
43 | readonly error: string | null;
44 | readonly loading: boolean;
45 | readonly onDismiss: () => void;
46 | readonly onPlay: () => void;
47 | readonly visible: boolean;
48 | }
49 |
50 | const LoadingScreen = ({
51 | canPlay,
52 | error,
53 | loading,
54 | onDismiss,
55 | onPlay,
56 | visible,
57 | }: LoadingScreenProps) => (
58 |
63 |
64 | {loading && }
65 |
72 | {error && }
73 | {canPlay && !error && }
74 |
75 |
76 |
77 | {loading && 'Loading show'}
78 | {!loading && error ? error : null}
79 | {!loading && !error && canPlay && 'Click to play'}
80 |
81 |
82 | );
83 |
84 | export default connect(
85 | // mapStateToProps
86 | (state: RootState) => ({
87 | canPlay: hasLoadedShowFile(state),
88 | error: state.show.error,
89 | loading: isLoadingShowFile(state),
90 | visible:
91 | isLoadingShowFile(state) ||
92 | (!userInteractedWithPlayback(state) && shouldShowPlaybackHintButton()) ||
93 | Boolean(state.show.error),
94 | }),
95 | // mapDispatchToProps
96 | {
97 | onDismiss: rewind,
98 | onPlay: togglePlayback,
99 | }
100 | )(LoadingScreen);
101 |
--------------------------------------------------------------------------------
/src/components/MainTopLevelView.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Component that shows a three-dimensional view of the drone flock.
3 | */
4 |
5 | import React, { Suspense, useRef } from 'react';
6 | import { connect } from 'react-redux';
7 |
8 | import Box from '@mui/material/Box';
9 |
10 | import { UIMode } from '~/features/ui/modes';
11 | import { getCurrentMode } from '~/features/ui/selectors';
12 | import type { RootState } from '~/store';
13 | import PlayerView from '~/views/player';
14 |
15 | import PageLoadingIndicator from './PageLoadingIndicator';
16 |
17 | const LazyValidationView = React.lazy(
18 | async () => import(/* webpackChunkName: "validation" */ '~/views/validation')
19 | );
20 |
21 | interface MainTopLevelViewProps {
22 | readonly mode: UIMode;
23 | }
24 |
25 | const MainTopLevelView = ({ mode }: MainTopLevelViewProps) => {
26 | const ref = useRef(null);
27 |
28 | return (
29 |
30 | }>
31 | {mode === UIMode.PLAYER && }
32 | {mode === UIMode.VALIDATION && }
33 |
34 |
35 | );
36 | };
37 |
38 | export default connect(
39 | // mapStateToProps
40 | (state: RootState) => ({
41 | mode: getCurrentMode(state),
42 | }),
43 | // mapDispatchToProps
44 | {}
45 | )(MainTopLevelView);
46 |
--------------------------------------------------------------------------------
/src/components/PageLoadingIndicator.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Box from '@mui/material/Box';
4 | import CircularProgress from '@mui/material/CircularProgress';
5 |
6 | const styles = {
7 | root: {
8 | position: 'relative',
9 | display: 'inline-block',
10 | flex: 1,
11 | },
12 |
13 | progress: {
14 | position: 'absolute',
15 | top: '50%',
16 | left: '50%',
17 | transform: 'translate(-50%, -50%)',
18 | },
19 | } as const;
20 |
21 | const PageLoadingIndicator = () => (
22 |
23 |
24 |
25 |
26 |
27 | );
28 |
29 | export default PageLoadingIndicator;
30 |
--------------------------------------------------------------------------------
/src/components/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Box from '@mui/material/Box';
4 | import Drawer from '@mui/material/Drawer';
5 | import List from '@mui/material/List';
6 | import { type Theme } from '@mui/material/styles';
7 |
8 | import { isThemeDark } from '@skybrush/app-theme-mui';
9 |
10 | import { closeSidebar } from '~/features/sidebar/slice';
11 | import DroneModelSelector from '~/features/settings/DroneModelSelector';
12 | import DroneSizeSlider from '~/features/settings/DroneSizeSlider';
13 | import LanguageSelector from '~/features/settings/LanguageSelector';
14 | import PlaybackSpeedSelector from '~/features/settings/PlaybackSpeedSelector';
15 | import ScenerySelector from '~/features/settings/ScenerySelector';
16 | import ThreeDViewSettingToggles from '~/features/settings/ThreeDViewSettingToggles';
17 | import { useAppDispatch, useAppSelector } from '~/hooks/store';
18 |
19 | import SkybrushLogo from './SkybrushLogo';
20 |
21 | const styles = {
22 | contents: {
23 | height: '100%',
24 | width: 250,
25 | display: 'flex',
26 | flexDirection: 'column',
27 | pt: 1,
28 | pb: 2,
29 | },
30 |
31 | footer: {
32 | px: 2,
33 | pt: 2,
34 | textAlign: 'center',
35 | opacity: 0.4,
36 | },
37 |
38 | list: {
39 | background: 'unset',
40 | flex: 1,
41 | overflowX: 'hidden',
42 | },
43 |
44 | root: {
45 | '& .MuiDrawer-paper': {
46 | background: (theme: Theme) =>
47 | isThemeDark(theme)
48 | ? 'linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) 80%, black)'
49 | : 'rgba(255, 255, 255, 0.7)',
50 | },
51 | },
52 | } as const;
53 |
54 | const modalProps = {
55 | BackdropProps: {
56 | invisible: true,
57 | },
58 | };
59 |
60 | /**
61 | * Sidebar drawer component for the application.
62 | */
63 | const SidebarDrawer = () => {
64 | const dispatch = useAppDispatch();
65 | const open = useAppSelector((state) => state.sidebar.open);
66 |
67 | return (
68 | {
74 | dispatch(closeSidebar());
75 | }}
76 | >
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | );
106 | };
107 |
108 | export default SidebarDrawer;
109 |
--------------------------------------------------------------------------------
/src/components/SkybrushLogo.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import logo from '~/../assets/img/logo.png';
4 |
5 | interface SkybrushLogoProps extends React.ComponentPropsWithoutRef<'img'> {
6 | readonly width?: number;
7 | }
8 |
9 | const SkybrushLogo = ({ width = 160, ...rest }: SkybrushLogoProps) => (
10 |
11 | );
12 |
13 | export default SkybrushLogo;
14 |
--------------------------------------------------------------------------------
/src/components/SplashScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CoverPagePresentation as CoverPage } from 'react-cover-page';
3 |
4 | import logo from '~/../assets/icons/splash.png';
5 |
6 | interface SplashScreenProps {
7 | readonly visible: boolean;
8 | }
9 |
10 | const SplashScreen = ({ visible = true }: SplashScreenProps) => (
11 | }
15 | title={
16 |
17 | skybrush viewer
18 |
19 | }
20 | />
21 | );
22 |
23 | export default SplashScreen;
24 |
--------------------------------------------------------------------------------
/src/components/WelcomeScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useTranslation } from 'react-i18next';
3 | import { connect } from 'react-redux';
4 |
5 | import Box from '@mui/material/Box';
6 | import Button from '@mui/material/Button';
7 | import Folder from '@mui/icons-material/Folder';
8 |
9 | import { shouldUseWelcomeScreen } from '~/features/settings/selectors';
10 | import { pickLocalFileAndLoadShow } from '~/features/show/actions';
11 | import {
12 | canLoadShowFromLocalFile,
13 | hasLoadedShowFile,
14 | isLoadingShowFile,
15 | lastLoadingAttemptFailed,
16 | } from '~/features/show/selectors';
17 | import type { RootState } from '~/store';
18 |
19 | import CentralHelperPanel from './CentralHelperPanel';
20 | import SkybrushLogo from './SkybrushLogo';
21 |
22 | interface WelcomeScreenProps {
23 | readonly canLoadShowFromLocalFile: boolean;
24 | readonly onLoadShowFromLocalFile: () => void;
25 | readonly visible: boolean;
26 | }
27 |
28 | const WelcomeScreen = ({
29 | canLoadShowFromLocalFile,
30 | onLoadShowFromLocalFile,
31 | visible,
32 | }: WelcomeScreenProps) => {
33 | const { t } = useTranslation();
34 | return (
35 |
36 |
37 |
38 |
39 | {canLoadShowFromLocalFile && (
40 | }
45 | onClick={onLoadShowFromLocalFile}
46 | >
47 | {t('buttons.openShowFile')}
48 |
49 | )}
50 |
51 | );
52 | };
53 |
54 | export default connect(
55 | // mapStateToProps
56 | (state: RootState) => ({
57 | canLoadShowFromLocalFile: canLoadShowFromLocalFile(),
58 | visible:
59 | shouldUseWelcomeScreen() &&
60 | !hasLoadedShowFile(state) &&
61 | !isLoadingShowFile(state) &&
62 | !lastLoadingAttemptFailed(state),
63 | }),
64 | // mapDispatchToProps
65 | {
66 | onLoadShowFromLocalFile: pickLocalFileAndLoadShow,
67 | }
68 | )(WelcomeScreen);
69 |
--------------------------------------------------------------------------------
/src/components/WindowDragMoveArea.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Box, { type BoxProps } from '@mui/material/Box';
4 | import { systemFont } from '@skybrush/app-theme-mui';
5 |
6 | import { isElectronWindow } from '~/window';
7 | import { isRunningOnMac } from '~/utils/platform';
8 |
9 | const isShowingDragMoveArea = isElectronWindow(window) && isRunningOnMac;
10 |
11 | export const WINDOW_DRAG_MOVE_AREA_HEIGHT = isShowingDragMoveArea ? 36 : 0;
12 |
13 | const style = {
14 | display: 'flex',
15 | justifyContent: 'center',
16 | alignItems: 'center',
17 | fontFamily: systemFont,
18 | WebkitAppRegion: 'drag',
19 | WebkitUserSelect: 'none',
20 | left: 0,
21 | top: 0,
22 | right: 0,
23 | height: WINDOW_DRAG_MOVE_AREA_HEIGHT,
24 | position: 'absolute',
25 | textAlign: 'center',
26 | } as const;
27 |
28 | /**
29 | * Overlay at the top of the window that acts as a draggable area on macOS
30 | * to allow the window to be moved around.
31 | */
32 | const WindowDragMoveArea = (props: BoxProps) =>
33 | isShowingDragMoveArea ? : null;
34 |
35 | export default WindowDragMoveArea;
36 |
--------------------------------------------------------------------------------
/src/components/WindowTitleManager.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import { hasLoadedShowFile, getShowTitle } from '~/features/show/selectors';
5 | import { isElectronWindow } from '~/window';
6 |
7 | import type { RootState } from '~/store';
8 |
9 | interface WindowTitleManagerProps {
10 | readonly appName: string;
11 | readonly showTitle: string;
12 | }
13 |
14 | const WindowTitleManager = ({
15 | appName,
16 | showTitle,
17 | }: WindowTitleManagerProps) => {
18 | useEffect(() => {
19 | if (isElectronWindow(window)) {
20 | // Running inside Electron, use the bridge API to ask the renderer
21 | // process to change the window title.
22 | window.bridge.setTitle({ appName });
23 | } else {
24 | // Running inside the browser, set the title of the document
25 | document.title = showTitle ? `${showTitle} | ${appName}` : appName;
26 | }
27 | }, [appName, showTitle]);
28 |
29 | return null;
30 | };
31 |
32 | export default connect(
33 | // mapStateToProps
34 | (state: RootState) => ({
35 | showTitle: hasLoadedShowFile(state) ? getShowTitle(state) : '',
36 | }),
37 | // mapDispatchToProps
38 | {}
39 | )(WindowTitleManager);
40 |
--------------------------------------------------------------------------------
/src/components/buttons/OpenButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import IconButton, { type IconButtonProps } from '@mui/material/IconButton';
4 | import Folder from '@mui/icons-material/Folder';
5 | import Tooltip from '@skybrush/mui-components/lib/Tooltip';
6 | import { useTranslation } from 'react-i18next';
7 |
8 | const OpenButton = (props: IconButtonProps) => {
9 | const { t } = useTranslation();
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default OpenButton;
20 |
--------------------------------------------------------------------------------
/src/components/buttons/SettingsButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useTranslation } from 'react-i18next';
3 | import { useDispatch } from 'react-redux';
4 |
5 | import IconButton, { type IconButtonProps } from '@mui/material/IconButton';
6 | import Settings from '@mui/icons-material/Settings';
7 | import Tooltip from '@skybrush/mui-components/lib/Tooltip';
8 |
9 | import { toggleSidebar } from '~/features/sidebar/slice';
10 |
11 | /**
12 | * Toggle button for the settings sidebar.
13 | */
14 | const SettingsButton = (props: IconButtonProps) => {
15 | const dispatch = useDispatch();
16 | const handleClick = () => dispatch(toggleSidebar());
17 | const { t } = useTranslation();
18 | return (
19 |
20 |
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | export default SettingsButton;
28 |
--------------------------------------------------------------------------------
/src/components/buttons/TrackDronesButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useTranslation } from 'react-i18next';
3 |
4 | import IconButton, { type IconButtonProps } from '@mui/material/IconButton';
5 | import CenterFocusStrong from '@mui/icons-material/CenterFocusStrong';
6 | import Tooltip from '@skybrush/mui-components/lib/Tooltip';
7 |
8 | const TrackDronesButton = (props: IconButtonProps) => {
9 | const { t } = useTranslation();
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default TrackDronesButton;
20 |
--------------------------------------------------------------------------------
/src/components/buttons/VolumeButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import IconButton, { type IconButtonProps } from '@mui/material/IconButton';
4 | import VolumeOff from '@mui/icons-material/VolumeOff';
5 | import VolumeUp from '@mui/icons-material/VolumeUp';
6 |
7 | const VolumeButton = ({
8 | muted,
9 | ...rest
10 | }: IconButtonProps & { readonly muted: boolean }) => (
11 |
12 | {muted ? : }
13 |
14 | );
15 |
16 | export default VolumeButton;
17 |
--------------------------------------------------------------------------------
/src/components/buttons/ZoomOutButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useTranslation } from 'react-i18next';
3 |
4 | import IconButton, { type IconButtonProps } from '@mui/material/IconButton';
5 | import ZoomOut from '@mui/icons-material/ZoomOut';
6 | import Tooltip from '@skybrush/mui-components/lib/Tooltip';
7 |
8 | const ZoomOutButton = (props: IconButtonProps) => {
9 | const { t } = useTranslation();
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default ZoomOutButton;
20 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Number of frames per second to use for the playback slider.
3 | *
4 | * Note that the .skyc format itself does not have a concept of "frames". This
5 | * is just a convenience constant for the playback slider and it also defines
6 | * the smallest possible adjustment to the playback position.
7 | */
8 | export const PLAYBACK_FPS = 25;
9 |
10 | /**
11 | * Downscaling factor for indoor drones.
12 | *
13 | * The default setting for the drone size in the settings slice is always 1.0.
14 | * This setting is a multiplier to ensure that the default size "looks good" in
15 | * an indoor setting. For instance, a value of 0.15 means that in an indoor
16 | * setup, the drone will have a radius of 0.15 m, i.e. a diameter of 30 cm.
17 | */
18 | export const INDOOR_DRONE_SIZE_SCALING_FACTOR = 0.15;
19 |
20 | /**
21 | * Downscaling factor for outdoor drones.
22 | *
23 | * The default setting for the drone size in the settings slice is always 1.0.
24 | * This setting is a multiplier to ensure that the default size "looks good" in
25 | * an outdoor setting.
26 | */
27 | export const OUTDOOR_DRONE_SIZE_SCALING_FACTOR = 0.75;
28 |
29 | /**
30 | * Placeholder to use for the label of the default camera. Will be replaced
31 | * when the camera label is formatted to use an appropriate localized label.
32 | */
33 | export const DEFAULT_CAMERA_NAME_PLACEHOLDER = '$DEFAULT';
34 |
35 | /**
36 | * Placeholder to use for the label of the camera that originates from a
37 | * sharing request where the URL includes a camera pose. Will be replaced
38 | * when the camera label is formatted to use an appropriate localized label.
39 | */
40 | export const SHARED_CAMERA_NAME_PLACEHOLDER = '$SHARED';
41 |
42 | /**
43 | * Default drone model to use in 3D views.
44 | */
45 | export const DEFAULT_DRONE_MODEL = 'sphere';
46 |
--------------------------------------------------------------------------------
/src/custom.d.ts:
--------------------------------------------------------------------------------
1 | // Make .mp3 imports work nicely with Typescript
2 | declare module '*.mp3' {
3 | const value: string;
4 | export default value;
5 | }
6 |
7 | // Make PNG imports work nicely with Typescript
8 | declare module '*.png' {
9 | const value: string;
10 | export default value;
11 | }
12 |
13 | // Make .obj imports work nicely with Typescript
14 | declare module '*.obj' {
15 | const value: string;
16 | export default value;
17 | }
18 |
19 | // Custom elements used by A-Frame
20 | declare namespace JSX {
21 | interface IntrinsicElements {
22 | 'a-asset-item': any;
23 | 'a-assets': any;
24 | 'a-camera': any;
25 | 'a-drone-flock': any;
26 | 'a-entity': any;
27 | 'a-scene': any;
28 | }
29 | }
30 |
31 | // Provide typings for @skybrush/aframe-components
32 | declare module '@skybrush/aframe-components' {
33 | export function objectToString(object: any): string;
34 | }
35 |
36 | // Provide typings for @skybrush/aframe-components/lib/spatial
37 | declare module '@skybrush/aframe-components/lib/spatial' {
38 | type EulerRotation = [number, number, number];
39 | type Position = [number, number, number];
40 | type Quaternion = [number, number, number, number];
41 | type Pose = {
42 | position: Position;
43 | orientation: Quaternion;
44 | };
45 |
46 | type ThreeJsPositionTuple = [number, number, number];
47 | type ThreeJsQuaternionTuple = [number, number, number, number];
48 | type ThreeJsRotationTuple = [number, number, number];
49 | type ThreeJsPose = {
50 | position: ThreeJsPositionTuple;
51 | rotation: ThreeJsRotationTuple;
52 | };
53 |
54 | export function skybrushRotationToQuaternion(
55 | rotation: EulerRotation
56 | ): Quaternion;
57 | export function skybrushQuaternionToThreeJsRotation(
58 | quaternion: Quaternion
59 | ): ThreeJsRotationTuple;
60 | export function skybrushToThreeJsPose(pose: Pose): ThreeJsPose;
61 | export function skybrushToThreeJsPosition(
62 | position: Position
63 | ): ThreeJsPositionTuple;
64 | export function skybrushToThreeJsQuaternion(
65 | quaternion: Quaternion
66 | ): ThreeJsQuaternionTuple;
67 |
68 | export function threeJsToSkybrushPose(pose: ThreeJsPose): Pose;
69 | export function threeJsToSkybrushPosition(
70 | position: ThreeJsPositionTuple
71 | ): Position;
72 | export function threeJsToSkybrushQuaternion(
73 | quaternion: ThreeJsQuaternionTuple
74 | ): Quaternion;
75 | }
76 |
--------------------------------------------------------------------------------
/src/desktop/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Module that contains everything that is needed for Skybrush Viewer only
3 | * when it is being run as a desktop application.
4 | */
5 |
--------------------------------------------------------------------------------
/src/desktop/launcher/api-v1.mjs:
--------------------------------------------------------------------------------
1 | /* eslint-disable new-cap */
2 | import { setTimeout } from 'node:timers/promises';
3 |
4 | import { ipcMain as ipc } from 'electron-better-ipc';
5 | import express from 'express';
6 |
7 | import { getShowAsObjectFromBuffer } from './show-loader.mjs';
8 | import { getFirstMainWindow } from './utils.mjs';
9 | import { setTitle } from './window-title.mjs';
10 |
11 | const router = express.Router();
12 |
13 | router.post('/focus', async (req, res, next) => {
14 | try {
15 | const targetWindow = getFirstMainWindow();
16 | if (targetWindow) {
17 | targetWindow.show();
18 | }
19 |
20 | res.json({ result: true });
21 | } catch (error) {
22 | return next(error);
23 | }
24 | });
25 |
26 | router.post('/load', async (req, res, next) => {
27 | if (!req.is('application/skybrush-compiled')) {
28 | return res.sendStatus(400);
29 | }
30 |
31 | try {
32 | const proposedTitle = req.header('x-skybrush-viewer-title');
33 | const targetWindow = getFirstMainWindow({ required: true });
34 |
35 | const success = Promise.race([
36 | (async () => {
37 | const showSpec = await getShowAsObjectFromBuffer(req.body);
38 | await ipc.callRenderer(targetWindow, 'setUIMode', 'validation');
39 | await ipc.callRenderer(targetWindow, 'loadShowFromObject', showSpec);
40 |
41 | setTitle(targetWindow, {
42 | representedFile: null,
43 | alternateFile: proposedTitle || null,
44 | });
45 |
46 | targetWindow.show();
47 |
48 | return true;
49 | })(),
50 | setTimeout(10000, false),
51 | ]);
52 |
53 | if (success) {
54 | res.json({ result: true });
55 | } else {
56 | throw new Error('Timeout');
57 | }
58 | } catch (error) {
59 | return next(error);
60 | }
61 | });
62 |
63 | router.get('/ping', (_req, res) => {
64 | res.json({ result: true });
65 | });
66 |
67 | export default router;
68 |
--------------------------------------------------------------------------------
/src/desktop/launcher/app-menu.mjs:
--------------------------------------------------------------------------------
1 | import { app, Menu, shell } from 'electron';
2 | import { is, openUrlMenuItem } from 'electron-util';
3 | import { isDev, aboutMenuItem, appMenu } from 'electron-util/main';
4 |
5 | const helpSubmenu = [
6 | openUrlMenuItem({
7 | label: 'Website',
8 | url: 'https://skybrush.io',
9 | }),
10 | ];
11 |
12 | const macOsMenuTemplate = [
13 | appMenu([]),
14 | {
15 | role: 'editMenu',
16 | },
17 | {
18 | role: 'windowMenu',
19 | },
20 | {
21 | role: 'help',
22 | submenu: helpSubmenu,
23 | },
24 | ];
25 |
26 | const linuxWindowsMenuTemplate = [
27 | {
28 | label: 'File',
29 | submenu: [{ role: 'quit' }],
30 | },
31 | {
32 | role: 'editMenu',
33 | },
34 | {
35 | role: 'windowMenu',
36 | },
37 | {
38 | role: 'help',
39 | submenu: helpSubmenu,
40 | },
41 | ];
42 |
43 | if (!is.macos) {
44 | helpSubmenu.push(
45 | { type: 'separator' },
46 | aboutMenuItem({
47 | copyright: 'Copyright © CollMot Robotics',
48 | })
49 | );
50 | }
51 |
52 | const template = is.macos ? macOsMenuTemplate : linuxWindowsMenuTemplate;
53 |
54 | if (isDev) {
55 | template.push({
56 | label: 'Debug',
57 | submenu: [
58 | { role: 'reload' },
59 | { role: 'forcereload' },
60 | { role: 'toggledevtools' },
61 | { type: 'separator' },
62 | {
63 | label: 'Show App Data',
64 | click() {
65 | shell.openItem(app.getPath('userData'));
66 | },
67 | },
68 | {
69 | label: 'Delete App Data',
70 | click() {
71 | shell.moveItemToTrash(app.getPath('userData'));
72 | app.relaunch();
73 | app.quit();
74 | },
75 | },
76 | ],
77 | });
78 | }
79 |
80 | const createAppMenu = () => Menu.buildFromTemplate(template);
81 |
82 | export default createAppMenu;
83 |
--------------------------------------------------------------------------------
/src/desktop/launcher/dialogs.mjs:
--------------------------------------------------------------------------------
1 | import { dialog } from 'electron';
2 |
3 | export const selectLocalShowFileForOpening = async () => {
4 | const { filePaths } = await dialog.showOpenDialog({
5 | title: 'Open show file',
6 | properties: ['openFile'],
7 | filters: [
8 | { name: 'Skybrush shows', extensions: ['skyc'] },
9 | { name: 'All files', extensions: ['*'] },
10 | ],
11 | });
12 |
13 | if (filePaths && filePaths.length > 0) {
14 | return filePaths[0];
15 | }
16 |
17 | return undefined;
18 | };
19 |
--------------------------------------------------------------------------------
/src/desktop/launcher/file-opener.mjs:
--------------------------------------------------------------------------------
1 | import { app } from 'electron';
2 | import { ipcMain as ipc } from 'electron-better-ipc';
3 |
4 | import { getFirstMainWindow } from './utils.mjs';
5 |
6 | const handleFileOpeningRequestsWith = (function_, options = {}) => {
7 | const { async = false, filenames = [], maxCount = 1 } = options;
8 | const pendingFilesToOpen = [];
9 | const rendererIsReady = { value: false };
10 |
11 | const flushPendingFiles = async () => {
12 | if (!app.isReady() || !rendererIsReady.value) {
13 | return;
14 | }
15 |
16 | const filesToProcess = pendingFilesToOpen.concat();
17 | pendingFilesToOpen.length = 0;
18 |
19 | /* eslint-disable no-await-in-loop */
20 | for (const filename of filesToProcess) {
21 | if (async) {
22 | await function_(filename);
23 | } else {
24 | function_(filename);
25 | }
26 | }
27 | /* eslint-enable no-await-in-loop */
28 | };
29 |
30 | const processFile = async (filename) => {
31 | pendingFilesToOpen.push(filename);
32 | if (pendingFilesToOpen.length > maxCount) {
33 | pendingFilesToOpen.splice(0, pendingFilesToOpen.length - maxCount);
34 | }
35 |
36 | await flushPendingFiles();
37 | };
38 |
39 | app.on('will-finish-launching', () => {
40 | app.on('open-file', (event, file) => {
41 | processFile(file);
42 | event.preventDefault();
43 | });
44 | });
45 |
46 | app.on('ready', () => {
47 | flushPendingFiles();
48 | });
49 |
50 | if (Array.isArray(filenames)) {
51 | for (const filename of filenames) {
52 | if (typeof filename === 'string') {
53 | processFile(filename);
54 | }
55 | }
56 | }
57 |
58 | ipc.answerRenderer('readyForFileOpening', () => {
59 | rendererIsReady.value = true;
60 | flushPendingFiles();
61 | });
62 | };
63 |
64 | const setupFileOpener = (filenames) => {
65 | handleFileOpeningRequestsWith(
66 | async (filename) => {
67 | const mainWindow = getFirstMainWindow();
68 | if (mainWindow) {
69 | await ipc.callRenderer(
70 | mainWindow,
71 | 'notifyFileOpeningRequest',
72 | filename
73 | );
74 | }
75 | },
76 | {
77 | async: true,
78 | filenames,
79 | maxCount: 1,
80 | }
81 | );
82 | };
83 |
84 | export default setupFileOpener;
85 |
--------------------------------------------------------------------------------
/src/desktop/launcher/index.mjs:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 | import { fileURLToPath } from 'node:url';
3 |
4 | import { app, protocol } from 'electron';
5 | import ElectronStore from 'electron-store';
6 | import tmp from 'tmp-promise';
7 |
8 | import { setupApp, setupCli } from '@skybrush/electron-app-framework';
9 |
10 | import createAppMenu from './app-menu.mjs';
11 | import setupFileOpener from './file-opener.mjs';
12 | import setupIpc from './ipc.mjs';
13 | import registerMediaProtocol from './media-protocol.mjs';
14 |
15 | const rootDir =
16 | typeof __dirname !== 'undefined'
17 | ? __dirname
18 | : path.dirname(fileURLToPath(import.meta.url));
19 |
20 | // See webpack/launcher.config.js and https://github.com/visionmedia/debug/issues/467
21 | // for more information about why this is needed
22 | // eslint-disable-next-line camelcase
23 | global.__runtime_process_env = {
24 | DEBUG: false,
25 | };
26 |
27 | const ENABLE_HTTP_SERVER = true;
28 |
29 | /**
30 | * Main entry point of the application.
31 | *
32 | * @param {string[]} filenames the filenames passed in the command line arguments
33 | * @param {Object} options the parsed command line arguments
34 | */
35 | async function run(filenames, options) {
36 | // Clean up temporary files even when an uncaught exception occurs
37 | tmp.setGracefulCleanup();
38 |
39 | // Allow the Electron state store to be created in the renderer process
40 | ElectronStore.initRenderer();
41 |
42 | // Start an HTTP server in the background for processing incoming JSON show
43 | // data from the Blender plugin
44 | if (ENABLE_HTTP_SERVER) {
45 | const { setupHttpServer } = await import('./http-server.mjs');
46 | await setupHttpServer(options);
47 | }
48 |
49 | setupApp({
50 | appMenu: createAppMenu,
51 | mainWindow: {
52 | backgroundColor: '#20242a', // same as the background color of the cover page
53 | debug: options.debug,
54 | rootDir,
55 | showMenuBar: false,
56 | title: 'Skybrush Viewer',
57 | titleBarStyle: 'hiddenInset',
58 | webPreferences: {
59 | sandbox: false, // because we need Node.js modules from the preloader
60 | },
61 | },
62 | });
63 |
64 | // Register our soon-to-be-used media:// protocol as privileged so the
65 | // fetch() API can work with it
66 | protocol.registerSchemesAsPrivileged([
67 | { scheme: 'media', privileges: { bypassCSP: true } },
68 | ]);
69 | app.on('ready', () => {
70 | registerMediaProtocol();
71 | });
72 |
73 | // Set up IPC handlers
74 | setupIpc();
75 |
76 | // Set up file opener handlers
77 | setupFileOpener(filenames);
78 | }
79 |
80 | const main = async () => {
81 | const parser = setupCli();
82 |
83 | parser.option('-p, --port ', 'Start listener on a specific port');
84 | parser.parse();
85 |
86 | await run(parser.args, parser.opts());
87 | };
88 |
89 | export default main;
90 |
--------------------------------------------------------------------------------
/src/desktop/launcher/ipc.mjs:
--------------------------------------------------------------------------------
1 | import { ipcMain as ipc } from 'electron-better-ipc';
2 |
3 | import { selectLocalShowFileForOpening } from './dialogs.mjs';
4 | import { getShowAsObjectFromLocalFile } from './show-loader.mjs';
5 | import { setTitle } from './window-title.mjs';
6 |
7 | const setupIpc = () => {
8 | ipc.answerRenderer(
9 | 'getShowAsObjectFromLocalFile',
10 | getShowAsObjectFromLocalFile
11 | );
12 | ipc.answerRenderer(
13 | 'selectLocalShowFileForOpening',
14 | selectLocalShowFileForOpening
15 | );
16 | ipc.answerRenderer('setTitle', ({ appName, representedFile }, window) => {
17 | setTitle(window, { appName, representedFile });
18 | });
19 | };
20 |
21 | export default setupIpc;
22 |
--------------------------------------------------------------------------------
/src/desktop/launcher/media-buffers.mjs:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs/promises';
2 | import tmp from 'tmp-promise';
3 |
4 | /**
5 | * Variable that holds the currently loaded audio data.
6 | *
7 | * Each audio buffer slot is backed by a temporary file on the disk that holds
8 | * the actual buffer contents. This is because the HTML5