├── .editorconfig ├── .erb ├── configs │ ├── .eslintrc │ ├── webpack.config.base.js │ ├── webpack.config.eslint.js │ ├── webpack.config.main.prod.babel.js │ ├── webpack.config.renderer.dev.babel.js │ ├── webpack.config.renderer.dev.dll.babel.js │ ├── webpack.config.renderer.prod.babel.js │ └── webpack.paths.js ├── dll │ ├── node_modules_no-drift_src_worker_worker_js.dev.dll.js │ ├── renderer.dev.dll.js │ └── renderer.json ├── img │ ├── erb-banner.png │ ├── erb-logo.png │ ├── eslint-padded-90.png │ ├── eslint-padded.png │ ├── eslint.png │ ├── jest-padded-90.png │ ├── jest-padded.png │ ├── jest.png │ ├── js-padded.png │ ├── js.png │ ├── npm.png │ ├── react-padded-90.png │ ├── react-padded.png │ ├── react-router-padded-90.png │ ├── react-router-padded.png │ ├── react-router.png │ ├── react.png │ ├── webpack-padded-90.png │ ├── webpack-padded.png │ ├── webpack.png │ ├── yarn-padded-90.png │ ├── yarn-padded.png │ └── yarn.png ├── mocks │ └── fileMock.js └── scripts │ ├── .eslintrc │ ├── babel-register.js │ ├── check-build-exists.js │ ├── check-native-dep.js │ ├── check-node-env.js │ ├── check-port-in-use.js │ ├── clean.js │ ├── delete-source-maps.js │ ├── electron-rebuild.js │ ├── link-modules.js │ └── notarize.js ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── assets ├── assets.d.ts ├── entitlements.mac.plist ├── icon.icns ├── icon.ico ├── icon.png ├── icons │ ├── 1024x1024.png │ ├── 128x128.png │ ├── 16x16.png │ ├── 24x24.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 512x512.png │ ├── 64x64.png │ └── 96x96.png └── iconslib │ ├── blackICONS │ └── icon.ico │ ├── blackPNG │ ├── 1024x1024.png │ ├── 128x128.png │ ├── 16x16.png │ ├── 24x24.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 512x512.png │ ├── 64x64.png │ └── 96x96.png │ ├── icon.svg │ ├── whiteICONS │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ └── icon.svg │ └── whitePNG │ ├── 128x128.png │ ├── 16x16.png │ ├── 24x24.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 512x512.png │ ├── 64x64.png │ └── 96x96.png ├── babel.config.js ├── build └── app │ ├── package.json │ └── yarn.lock ├── package.json ├── src ├── main │ ├── api │ │ ├── apiServer.js │ │ ├── index.js │ │ └── vmixSocket.js │ ├── main.js │ ├── menu.js │ ├── preload.js │ ├── storeClass.js │ ├── timers.js │ ├── updater.js │ └── util.js └── renderer │ ├── App.jsx │ ├── app.css │ ├── components │ ├── baseColors.timer.jsx │ ├── baseColors.video.jsx │ ├── clock.formated.timer.jsx │ ├── clock.formated.video.jsx │ ├── clock.input.timer.jsx │ ├── colors.settings.jsx │ ├── directionOptions.timer.jsx │ ├── inputSelector.timer.jsx │ ├── inputSelector.video.jsx │ ├── ip.app.jsx │ ├── newFeatures.dialog.jsx │ ├── playPause.timer.jsx │ ├── postThing.app.jsx │ ├── refresh.app.jsx │ ├── socket.app.jsx │ ├── tabs.component.jsx │ ├── timerDown.timer.jsx │ ├── timerUp.timer.jsx │ ├── toast.app.jsx │ ├── trigger.color.jsx │ ├── trigger.details.jsx │ ├── trigger.layer.jsx │ ├── trigger.parent.jsx │ ├── trigger.playPause.jsx │ └── version.app.jsx │ ├── index.ejs │ ├── index.jsx │ ├── pages │ ├── settings.app.jsx │ ├── timer.app.jsx │ └── video.app.jsx │ ├── stores │ ├── alert.store.js │ ├── clockotron.store.js │ ├── color.store.js │ ├── store.context.jsx │ ├── timer.store.js │ ├── trigger.store.js │ ├── videoReader.store.js │ └── vmix.store.js │ └── utils │ ├── AppStyles.jsx │ ├── ColorPickerColors.jsx │ ├── Theme.jsx │ ├── formatTime.jsx │ └── options.jsx ├── version ├── major.js ├── minor.js └── patch.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.erb/configs/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Base webpack config used across other specific configs 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import webpackPaths from './webpack.paths.js'; 8 | import { dependencies as externals } from '../../build/app/package.json'; 9 | 10 | export default { 11 | externals: [...Object.keys(externals || {})], 12 | 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.[jt]sx?$/, 17 | exclude: /node_modules/, 18 | use: { 19 | loader: 'babel-loader', 20 | options: { 21 | cacheDirectory: true, 22 | }, 23 | }, 24 | }, 25 | ], 26 | }, 27 | 28 | output: { 29 | path: webpackPaths.srcPath, 30 | // https://github.com/webpack/webpack/issues/1114 31 | library: { 32 | type: 'commonjs2', 33 | }, 34 | }, 35 | 36 | /** 37 | * Determine the array of extensions that should be used to resolve modules. 38 | */ 39 | resolve: { 40 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], 41 | modules: [webpackPaths.srcPath, 'node_modules'], 42 | }, 43 | 44 | plugins: [ 45 | new webpack.EnvironmentPlugin({ 46 | NODE_ENV: 'production', 47 | }), 48 | ], 49 | }; 50 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.eslint.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-unresolved: off, import/no-self-import: off */ 2 | require('@babel/register'); 3 | 4 | module.exports = require('./webpack.config.renderer.dev.babel').default; 5 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.main.prod.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack config for production electron main process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import { merge } from 'webpack-merge'; 8 | import TerserPlugin from 'terser-webpack-plugin'; 9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 10 | import baseConfig from './webpack.config.base'; 11 | import webpackPaths from './webpack.paths.js'; 12 | import checkNodeEnv from '../scripts/check-node-env'; 13 | import deleteSourceMaps from '../scripts/delete-source-maps'; 14 | 15 | checkNodeEnv('production'); 16 | deleteSourceMaps(); 17 | 18 | const devtoolsConfig = 19 | process.env.DEBUG_PROD === 'true' 20 | ? { 21 | devtool: 'source-map', 22 | } 23 | : {}; 24 | 25 | export default merge(baseConfig, { 26 | ...devtoolsConfig, 27 | 28 | mode: 'production', 29 | 30 | target: 'electron-main', 31 | 32 | entry: { 33 | main: path.join(webpackPaths.srcMainPath, 'main.js'), 34 | preload: path.join(webpackPaths.srcMainPath, 'preload.js'), 35 | }, 36 | 37 | output: { 38 | path: webpackPaths.distMainPath, 39 | filename: '[name].js', 40 | }, 41 | 42 | optimization: { 43 | minimizer: [ 44 | new TerserPlugin({ 45 | parallel: true, 46 | }), 47 | ], 48 | }, 49 | 50 | plugins: [ 51 | new BundleAnalyzerPlugin({ 52 | analyzerMode: 53 | process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled', 54 | openAnalyzer: process.env.OPEN_ANALYZER === 'true', 55 | }), 56 | 57 | /** 58 | * Create global constants which can be configured at compile time. 59 | * 60 | * Useful for allowing different behaviour between development builds and 61 | * release builds 62 | * 63 | * NODE_ENV should be production so that modules do not perform certain 64 | * development checks 65 | */ 66 | new webpack.EnvironmentPlugin({ 67 | NODE_ENV: 'production', 68 | DEBUG_PROD: false, 69 | START_MINIMIZED: false, 70 | }), 71 | ], 72 | 73 | /** 74 | * Disables webpack processing of __dirname and __filename. 75 | * If you run the bundle in node.js it falls back to these values of node.js. 76 | * https://github.com/webpack/webpack/issues/2010 77 | */ 78 | node: { 79 | __dirname: false, 80 | __filename: false, 81 | }, 82 | }); 83 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.renderer.dev.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import webpack from 'webpack'; 4 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 5 | import chalk from 'chalk'; 6 | import { merge } from 'webpack-merge'; 7 | import { spawn, execSync } from 'child_process'; 8 | import baseConfig from './webpack.config.base'; 9 | import webpackPaths from './webpack.paths.js'; 10 | import checkNodeEnv from '../scripts/check-node-env'; 11 | import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'; 12 | 13 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's 14 | // at the dev webpack config is not accidentally run in a production environment 15 | if (process.env.NODE_ENV === 'production') { 16 | checkNodeEnv('development'); 17 | } 18 | 19 | const port = process.env.PORT || 1212; 20 | const publicPath = webpackPaths.distRendererPath; 21 | const manifest = path.resolve(webpackPaths.dllPath, 'renderer.json'); 22 | const requiredByDLLConfig = module.parent.filename.includes( 23 | 'webpack.config.renderer.dev.dll' 24 | ); 25 | 26 | /** 27 | * Warn if the DLL is not built 28 | */ 29 | if ( 30 | !requiredByDLLConfig && 31 | !(fs.existsSync(webpackPaths.dllPath) && fs.existsSync(manifest)) 32 | ) { 33 | console.log( 34 | chalk.black.bgYellow.bold( 35 | 'The DLL files are missing. Sit back while we build them for you with "yarn build-dll"' 36 | ) 37 | ); 38 | execSync('yarn postinstall'); 39 | } 40 | 41 | export default merge(baseConfig, { 42 | devtool: 'inline-source-map', 43 | 44 | mode: 'development', 45 | 46 | target: ['web', 'electron-renderer'], 47 | 48 | entry: [ 49 | 'webpack-dev-server/client?http://localhost:1212/dist', 50 | 'webpack/hot/only-dev-server', 51 | 'core-js', 52 | 'regenerator-runtime/runtime', 53 | path.join(webpackPaths.srcRendererPath, 'index.jsx'), 54 | ], 55 | 56 | output: { 57 | path: webpackPaths.distRendererPath, 58 | publicPath: '/', 59 | filename: 'renderer.dev.js', 60 | library: { 61 | type: 'umd', 62 | }, 63 | }, 64 | 65 | module: { 66 | rules: [ 67 | { 68 | test: /\.[jt]sx?$/, 69 | exclude: /node_modules/, 70 | use: [ 71 | { 72 | loader: require.resolve('babel-loader'), 73 | options: { 74 | plugins: [require.resolve('react-refresh/babel')].filter(Boolean), 75 | }, 76 | }, 77 | ], 78 | }, 79 | { 80 | test: /\.global\.css$/, 81 | use: [ 82 | { 83 | loader: 'style-loader', 84 | }, 85 | { 86 | loader: 'css-loader', 87 | options: { 88 | sourceMap: true, 89 | }, 90 | }, 91 | ], 92 | }, 93 | { 94 | test: /^((?!\.global).)*\.css$/, 95 | use: [ 96 | { 97 | loader: 'style-loader', 98 | }, 99 | { 100 | loader: 'css-loader', 101 | options: { 102 | modules: { 103 | localIdentName: '[name]__[local]__[hash:base64:5]', 104 | }, 105 | sourceMap: true, 106 | importLoaders: 1, 107 | }, 108 | }, 109 | ], 110 | }, 111 | // SASS support - compile all .global.scss files and pipe it to style.css 112 | { 113 | test: /\.global\.(scss|sass)$/, 114 | use: [ 115 | { 116 | loader: 'style-loader', 117 | }, 118 | { 119 | loader: 'css-loader', 120 | options: { 121 | sourceMap: true, 122 | }, 123 | }, 124 | { 125 | loader: 'sass-loader', 126 | }, 127 | ], 128 | }, 129 | // SASS support - compile all other .scss files and pipe it to style.css 130 | { 131 | test: /^((?!\.global).)*\.(scss|sass)$/, 132 | use: [ 133 | { 134 | loader: 'style-loader', 135 | }, 136 | { 137 | loader: '@teamsupercell/typings-for-css-modules-loader', 138 | }, 139 | { 140 | loader: 'css-loader', 141 | options: { 142 | modules: { 143 | localIdentName: '[name]__[local]__[hash:base64:5]', 144 | }, 145 | sourceMap: true, 146 | importLoaders: 1, 147 | }, 148 | }, 149 | { 150 | loader: 'sass-loader', 151 | }, 152 | ], 153 | }, 154 | // WOFF Font 155 | { 156 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 157 | use: { 158 | loader: 'url-loader', 159 | options: { 160 | limit: 10000, 161 | mimetype: 'application/font-woff', 162 | }, 163 | }, 164 | }, 165 | // WOFF2 Font 166 | { 167 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 168 | use: { 169 | loader: 'url-loader', 170 | options: { 171 | limit: 10000, 172 | mimetype: 'application/font-woff', 173 | }, 174 | }, 175 | }, 176 | // OTF Font 177 | { 178 | test: /\.otf(\?v=\d+\.\d+\.\d+)?$/, 179 | use: { 180 | loader: 'url-loader', 181 | options: { 182 | limit: 10000, 183 | mimetype: 'font/otf', 184 | }, 185 | }, 186 | }, 187 | // TTF Font 188 | { 189 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 190 | use: { 191 | loader: 'url-loader', 192 | options: { 193 | limit: 10000, 194 | mimetype: 'application/octet-stream', 195 | }, 196 | }, 197 | }, 198 | // EOT Font 199 | { 200 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, 201 | use: 'file-loader', 202 | }, 203 | // SVG Font 204 | { 205 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 206 | use: { 207 | loader: 'url-loader', 208 | options: { 209 | limit: 10000, 210 | mimetype: 'image/svg+xml', 211 | }, 212 | }, 213 | }, 214 | // Common Image Formats 215 | { 216 | test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/, 217 | use: 'url-loader', 218 | }, 219 | ], 220 | }, 221 | plugins: [ 222 | requiredByDLLConfig 223 | ? null 224 | : new webpack.DllReferencePlugin({ 225 | context: webpackPaths.dllPath, 226 | manifest: require(manifest), 227 | sourceType: 'var', 228 | }), 229 | 230 | new webpack.NoEmitOnErrorsPlugin(), 231 | 232 | /** 233 | * Create global constants which can be configured at compile time. 234 | * 235 | * Useful for allowing different behaviour between development builds and 236 | * release builds 237 | * 238 | * NODE_ENV should be production so that modules do not perform certain 239 | * development checks 240 | * 241 | * By default, use 'development' as NODE_ENV. This can be overriden with 242 | * 'staging', for example, by changing the ENV variables in the npm scripts 243 | */ 244 | new webpack.EnvironmentPlugin({ 245 | NODE_ENV: 'development', 246 | }), 247 | 248 | new webpack.LoaderOptionsPlugin({ 249 | debug: true, 250 | }), 251 | 252 | new ReactRefreshWebpackPlugin(), 253 | 254 | new HtmlWebpackPlugin({ 255 | filename: path.join('index.html'), 256 | template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), 257 | minify: { 258 | collapseWhitespace: true, 259 | removeAttributeQuotes: true, 260 | removeComments: true, 261 | }, 262 | isBrowser: false, 263 | env: process.env.NODE_ENV, 264 | isDevelopment: process.env.NODE_ENV !== 'production', 265 | nodeModules: webpackPaths.appNodeModulesPath, 266 | }), 267 | ], 268 | 269 | node: { 270 | __dirname: false, 271 | __filename: false, 272 | }, 273 | 274 | devServer: { 275 | port, 276 | publicPath: '/', 277 | compress: true, 278 | noInfo: false, 279 | stats: 'errors-only', 280 | inline: true, 281 | lazy: false, 282 | hot: true, 283 | headers: { 'Access-Control-Allow-Origin': '*' }, 284 | watchOptions: { 285 | aggregateTimeout: 300, 286 | ignored: /node_modules/, 287 | poll: 100, 288 | }, 289 | historyApiFallback: { 290 | verbose: true, 291 | disableDotRule: false, 292 | }, 293 | before() { 294 | console.log('Starting Main Process...'); 295 | spawn('npm', ['run', 'start:main'], { 296 | shell: true, 297 | env: process.env, 298 | stdio: 'inherit', 299 | }) 300 | .on('close', (code) => process.exit(code)) 301 | .on('error', (spawnError) => console.error(spawnError)); 302 | }, 303 | }, 304 | }); 305 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.renderer.dev.dll.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Builds the DLL for development electron renderer process 3 | */ 4 | 5 | import webpack from 'webpack'; 6 | import path from 'path'; 7 | import { merge } from 'webpack-merge'; 8 | import baseConfig from './webpack.config.base'; 9 | import webpackPaths from './webpack.paths.js'; 10 | import { dependencies } from '../../package.json'; 11 | import checkNodeEnv from '../scripts/check-node-env'; 12 | 13 | checkNodeEnv('development'); 14 | 15 | const dist = webpackPaths.dllPath; 16 | 17 | export default merge(baseConfig, { 18 | context: webpackPaths.rootPath, 19 | 20 | devtool: 'eval', 21 | 22 | mode: 'development', 23 | 24 | target: 'electron-renderer', 25 | 26 | externals: ['fsevents', 'crypto-browserify'], 27 | 28 | /** 29 | * Use `module` from `webpack.config.renderer.dev.js` 30 | */ 31 | module: require('./webpack.config.renderer.dev.babel').default.module, 32 | 33 | entry: { 34 | renderer: Object.keys(dependencies || {}), 35 | }, 36 | 37 | output: { 38 | path: dist, 39 | filename: '[name].dev.dll.js', 40 | library: { 41 | name: 'renderer', 42 | type: 'var', 43 | }, 44 | }, 45 | 46 | plugins: [ 47 | new webpack.DllPlugin({ 48 | path: path.join(dist, '[name].json'), 49 | name: '[name]', 50 | }), 51 | 52 | /** 53 | * Create global constants which can be configured at compile time. 54 | * 55 | * Useful for allowing different behaviour between development builds and 56 | * release builds 57 | * 58 | * NODE_ENV should be production so that modules do not perform certain 59 | * development checks 60 | */ 61 | new webpack.EnvironmentPlugin({ 62 | NODE_ENV: 'development', 63 | }), 64 | 65 | new webpack.LoaderOptionsPlugin({ 66 | debug: true, 67 | options: { 68 | context: webpackPaths.srcPath, 69 | output: { 70 | path: webpackPaths.dllPath, 71 | }, 72 | }, 73 | }), 74 | ], 75 | }); 76 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.renderer.prod.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Build config for electron renderer process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 8 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 10 | import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; 11 | import { merge } from 'webpack-merge'; 12 | import TerserPlugin from 'terser-webpack-plugin'; 13 | import baseConfig from './webpack.config.base'; 14 | import webpackPaths from './webpack.paths.js'; 15 | import checkNodeEnv from '../scripts/check-node-env'; 16 | import deleteSourceMaps from '../scripts/delete-source-maps'; 17 | 18 | checkNodeEnv('production'); 19 | deleteSourceMaps(); 20 | 21 | const devtoolsConfig = 22 | process.env.DEBUG_PROD === 'true' 23 | ? { 24 | devtool: 'source-map', 25 | } 26 | : {}; 27 | 28 | export default merge(baseConfig, { 29 | ...devtoolsConfig, 30 | 31 | mode: 'production', 32 | 33 | target: ['web', 'electron-renderer'], 34 | 35 | entry: [ 36 | 'core-js', 37 | 'regenerator-runtime/runtime', 38 | path.join(webpackPaths.srcRendererPath, 'index.jsx'), 39 | ], 40 | 41 | output: { 42 | path: webpackPaths.distRendererPath, 43 | publicPath: './', 44 | filename: 'renderer.js', 45 | library: { 46 | type: 'umd', 47 | }, 48 | }, 49 | 50 | module: { 51 | rules: [ 52 | { 53 | // CSS/SCSS 54 | test: /\.s?css$/, 55 | use: [ 56 | { 57 | loader: MiniCssExtractPlugin.loader, 58 | options: { 59 | // `./dist` can't be inerhited for publicPath for styles. Otherwise generated paths will be ./dist/dist 60 | publicPath: './', 61 | }, 62 | }, 63 | 'css-loader', 64 | // 'sass-loader', 65 | ], 66 | }, 67 | // WOFF Font 68 | { 69 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 70 | use: { 71 | loader: 'url-loader', 72 | options: { 73 | limit: 10000, 74 | mimetype: 'application/font-woff', 75 | }, 76 | }, 77 | }, 78 | // WOFF2 Font 79 | { 80 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 81 | use: { 82 | loader: 'url-loader', 83 | options: { 84 | limit: 10000, 85 | mimetype: 'application/font-woff', 86 | }, 87 | }, 88 | }, 89 | // OTF Font 90 | { 91 | test: /\.otf(\?v=\d+\.\d+\.\d+)?$/, 92 | use: { 93 | loader: 'url-loader', 94 | options: { 95 | limit: 10000, 96 | mimetype: 'font/otf', 97 | }, 98 | }, 99 | }, 100 | // TTF Font 101 | { 102 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 103 | use: { 104 | loader: 'url-loader', 105 | options: { 106 | limit: 10000, 107 | mimetype: 'application/octet-stream', 108 | }, 109 | }, 110 | }, 111 | // EOT Font 112 | { 113 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, 114 | use: 'file-loader', 115 | }, 116 | // SVG Font 117 | { 118 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 119 | use: { 120 | loader: 'url-loader', 121 | options: { 122 | limit: 10000, 123 | mimetype: 'image/svg+xml', 124 | }, 125 | }, 126 | }, 127 | // Common Image Formats 128 | { 129 | test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/, 130 | use: 'url-loader', 131 | }, 132 | ], 133 | }, 134 | 135 | optimization: { 136 | minimize: true, 137 | minimizer: [ 138 | new TerserPlugin({ 139 | parallel: true, 140 | }), 141 | new CssMinimizerPlugin(), 142 | ], 143 | }, 144 | 145 | plugins: [ 146 | /** 147 | * Create global constants which can be configured at compile time. 148 | * 149 | * Useful for allowing different behaviour between development builds and 150 | * release builds 151 | * 152 | * NODE_ENV should be production so that modules do not perform certain 153 | * development checks 154 | */ 155 | new webpack.EnvironmentPlugin({ 156 | NODE_ENV: 'production', 157 | DEBUG_PROD: false, 158 | }), 159 | 160 | new MiniCssExtractPlugin({ 161 | filename: 'style.css', 162 | }), 163 | 164 | new BundleAnalyzerPlugin({ 165 | analyzerMode: 166 | process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled', 167 | openAnalyzer: process.env.OPEN_ANALYZER === 'true', 168 | }), 169 | 170 | new HtmlWebpackPlugin({ 171 | filename: 'index.html', 172 | template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), 173 | minify: { 174 | collapseWhitespace: true, 175 | removeAttributeQuotes: true, 176 | removeComments: true, 177 | }, 178 | isBrowser: false, 179 | isDevelopment: process.env.NODE_ENV !== 'production', 180 | }), 181 | ], 182 | }); 183 | -------------------------------------------------------------------------------- /.erb/configs/webpack.paths.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const rootPath = path.join(__dirname, '../..'); 4 | 5 | const dllPath = path.join(__dirname, '../dll'); 6 | 7 | const srcPath = path.join(rootPath, 'src'); 8 | const srcMainPath = path.join(srcPath, 'main'); 9 | const srcRendererPath = path.join(srcPath, 'renderer'); 10 | 11 | const buildPath = path.join(rootPath, 'build'); 12 | const appPath = path.join(buildPath, 'app'); 13 | const appPackagePath = path.join(appPath, 'package.json'); 14 | const appNodeModulesPath = path.join(appPath, 'node_modules'); 15 | const srcNodeModulesPath = path.join(srcPath, 'node_modules'); 16 | 17 | const distPath = path.join(appPath, 'dist'); 18 | const distMainPath = path.join(distPath, 'main'); 19 | const distRendererPath = path.join(distPath, 'renderer'); 20 | 21 | const releasePath = path.join(buildPath, 'release'); 22 | 23 | module.exports = { 24 | rootPath, 25 | dllPath, 26 | srcPath, 27 | srcMainPath, 28 | srcRendererPath, 29 | buildPath, 30 | appPath, 31 | appPackagePath, 32 | appNodeModulesPath, 33 | srcNodeModulesPath, 34 | distPath, 35 | distMainPath, 36 | distRendererPath, 37 | releasePath, 38 | }; 39 | -------------------------------------------------------------------------------- /.erb/img/erb-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/.erb/img/erb-banner.png -------------------------------------------------------------------------------- /.erb/img/erb-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/.erb/img/erb-logo.png -------------------------------------------------------------------------------- /.erb/img/eslint-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/.erb/img/eslint-padded-90.png -------------------------------------------------------------------------------- /.erb/img/eslint-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/.erb/img/eslint-padded.png -------------------------------------------------------------------------------- /.erb/img/eslint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/.erb/img/eslint.png -------------------------------------------------------------------------------- /.erb/img/jest-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/.erb/img/jest-padded-90.png -------------------------------------------------------------------------------- /.erb/img/jest-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/.erb/img/jest-padded.png -------------------------------------------------------------------------------- /.erb/img/jest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/.erb/img/jest.png -------------------------------------------------------------------------------- /.erb/img/js-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/.erb/img/js-padded.png -------------------------------------------------------------------------------- /.erb/img/js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/.erb/img/js.png -------------------------------------------------------------------------------- /.erb/img/npm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/.erb/img/npm.png -------------------------------------------------------------------------------- /.erb/img/react-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/.erb/img/react-padded-90.png -------------------------------------------------------------------------------- /.erb/img/react-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/.erb/img/react-padded.png -------------------------------------------------------------------------------- /.erb/img/react-router-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/.erb/img/react-router-padded-90.png -------------------------------------------------------------------------------- /.erb/img/react-router-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/.erb/img/react-router-padded.png -------------------------------------------------------------------------------- /.erb/img/react-router.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/.erb/img/react-router.png -------------------------------------------------------------------------------- /.erb/img/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/.erb/img/react.png -------------------------------------------------------------------------------- /.erb/img/webpack-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/.erb/img/webpack-padded-90.png -------------------------------------------------------------------------------- /.erb/img/webpack-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/.erb/img/webpack-padded.png -------------------------------------------------------------------------------- /.erb/img/webpack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/.erb/img/webpack.png -------------------------------------------------------------------------------- /.erb/img/yarn-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/.erb/img/yarn-padded-90.png -------------------------------------------------------------------------------- /.erb/img/yarn-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/.erb/img/yarn-padded.png -------------------------------------------------------------------------------- /.erb/img/yarn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/.erb/img/yarn.png -------------------------------------------------------------------------------- /.erb/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | export default 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /.erb/scripts/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off", 6 | "import/no-extraneous-dependencies": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.erb/scripts/babel-register.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpackPaths = require('../configs/webpack.paths.js'); 3 | 4 | require('@babel/register')({ 5 | extensions: ['.es6', '.es', '.jsx', '.js', '.mjs', '.ts', '.tsx'], 6 | cwd: webpackPaths.rootPath, 7 | }); 8 | -------------------------------------------------------------------------------- /.erb/scripts/check-build-exists.js: -------------------------------------------------------------------------------- 1 | // Check if the renderer and main bundles are built 2 | import path from 'path'; 3 | import chalk from 'chalk'; 4 | import fs from 'fs'; 5 | import webpackPaths from '../configs/webpack.paths.js'; 6 | 7 | const mainPath = path.join(webpackPaths.distMainPath, 'main.js'); 8 | const rendererPath = path.join(webpackPaths.distRendererPath, 'renderer.js'); 9 | 10 | if (!fs.existsSync(mainPath)) { 11 | throw new Error( 12 | chalk.whiteBright.bgRed.bold( 13 | 'The main process is not built yet. Build it by running "yarn build:main"' 14 | ) 15 | ); 16 | } 17 | 18 | if (!fs.existsSync(rendererPath)) { 19 | throw new Error( 20 | chalk.whiteBright.bgRed.bold( 21 | 'The renderer process is not built yet. Build it by running "yarn build:renderer"' 22 | ) 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /.erb/scripts/check-native-dep.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import chalk from 'chalk'; 3 | import { execSync } from 'child_process'; 4 | import { dependencies } from '../../package.json'; 5 | 6 | if (dependencies) { 7 | const dependenciesKeys = Object.keys(dependencies); 8 | const nativeDeps = fs 9 | .readdirSync('node_modules') 10 | .filter((folder) => fs.existsSync(`node_modules/${folder}/binding.gyp`)); 11 | if (nativeDeps.length === 0) { 12 | process.exit(0); 13 | } 14 | try { 15 | // Find the reason for why the dependency is installed. If it is installed 16 | // because of a devDependency then that is okay. Warn when it is installed 17 | // because of a dependency 18 | const { dependencies: dependenciesObject } = JSON.parse( 19 | execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString() 20 | ); 21 | const rootDependencies = Object.keys(dependenciesObject); 22 | const filteredRootDependencies = rootDependencies.filter((rootDependency) => 23 | dependenciesKeys.includes(rootDependency) 24 | ); 25 | if (filteredRootDependencies.length > 0) { 26 | const plural = filteredRootDependencies.length > 1; 27 | console.log(` 28 | ${chalk.whiteBright.bgYellow.bold( 29 | 'Webpack does not work with native dependencies.' 30 | )} 31 | ${chalk.bold(filteredRootDependencies.join(', '))} ${ 32 | plural ? 'are native dependencies' : 'is a native dependency' 33 | } and should be installed inside of the "./build/app" folder. 34 | First, uninstall the packages from "./package.json": 35 | ${chalk.whiteBright.bgGreen.bold('yarn remove your-package')} 36 | ${chalk.bold( 37 | 'Then, instead of installing the package to the root "./package.json":' 38 | )} 39 | ${chalk.whiteBright.bgRed.bold('yarn add your-package')} 40 | ${chalk.bold('Install the package to "./build/app/package.json"')} 41 | ${chalk.whiteBright.bgGreen.bold('cd ./src && yarn add your-package')} 42 | Read more about native dependencies at: 43 | ${chalk.bold( 44 | 'https://electron-react-boilerplate.js.org/docs/adding-dependencies/#module-structure' 45 | )} 46 | `); 47 | process.exit(1); 48 | } 49 | } catch (e) { 50 | console.log('Native dependencies could not be checked'); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.erb/scripts/check-node-env.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export default function checkNodeEnv(expectedEnv) { 4 | if (!expectedEnv) { 5 | throw new Error('"expectedEnv" not set'); 6 | } 7 | 8 | if (process.env.NODE_ENV !== expectedEnv) { 9 | console.log( 10 | chalk.whiteBright.bgRed.bold( 11 | `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config` 12 | ) 13 | ); 14 | process.exit(2); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.erb/scripts/check-port-in-use.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import detectPort from 'detect-port'; 3 | 4 | const port = process.env.PORT || '1212'; 5 | 6 | detectPort(port, (err, availablePort) => { 7 | if (port !== String(availablePort)) { 8 | throw new Error( 9 | chalk.whiteBright.bgRed.bold( 10 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 yarn start` 11 | ) 12 | ); 13 | } else { 14 | process.exit(0); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /.erb/scripts/clean.js: -------------------------------------------------------------------------------- 1 | const rimraf = require('rimraf'); 2 | const webpackPaths = require('../configs/webpack.paths.js'); 3 | const process = require('process'); 4 | 5 | const args = process.argv.slice(2); 6 | const commandMap = { 7 | dist: webpackPaths.releasePath, 8 | release: webpackPaths.distPath, 9 | dll: webpackPaths.dllPath, 10 | }; 11 | 12 | args.forEach((x) => { 13 | const pathToRemove = commandMap[x]; 14 | if (pathToRemove !== undefined) { 15 | rimraf.sync(pathToRemove); 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /.erb/scripts/delete-source-maps.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import rimraf from 'rimraf'; 3 | import webpackPaths from '../configs/webpack.paths.js'; 4 | 5 | export default function deleteSourceMaps() { 6 | rimraf.sync(path.join(webpackPaths.distMainPath, '*.js.map')); 7 | rimraf.sync(path.join(webpackPaths.distRendererPath, '*.js.map')); 8 | } 9 | -------------------------------------------------------------------------------- /.erb/scripts/electron-rebuild.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { execSync } from 'child_process'; 3 | import fs from 'fs'; 4 | import { dependencies } from '../../build/app/package.json'; 5 | import webpackPaths from '../configs/webpack.paths.js'; 6 | 7 | if ( 8 | Object.keys(dependencies || {}).length > 0 && 9 | fs.existsSync(webpackPaths.appNodeModulesPath) 10 | ) { 11 | const electronRebuildCmd = 12 | '../../node_modules/.bin/electron-rebuild --parallel --force --types prod,dev,optional --module-dir .'; 13 | const cmd = 14 | process.platform === 'win32' 15 | ? electronRebuildCmd.replace(/\//g, '\\') 16 | : electronRebuildCmd; 17 | execSync(cmd, { 18 | cwd: webpackPaths.appPath, 19 | stdio: 'inherit', 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /.erb/scripts/link-modules.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { 3 | appNodeModulesPath, 4 | srcNodeModulesPath, 5 | } from '../configs/webpack.paths.js'; 6 | 7 | if (!fs.existsSync(srcNodeModulesPath) && fs.existsSync(appNodeModulesPath)) { 8 | fs.symlinkSync(appNodeModulesPath, srcNodeModulesPath); 9 | } 10 | -------------------------------------------------------------------------------- /.erb/scripts/notarize.js: -------------------------------------------------------------------------------- 1 | const { notarize } = require('electron-notarize'); 2 | const { build } = require('../../package.json'); 3 | 4 | exports.default = async function notarizeMacos(context) { 5 | const { electronPlatformName, appOutDir } = context; 6 | if (electronPlatformName !== 'darwin') { 7 | return; 8 | } 9 | 10 | if (!process.env.CI) { 11 | console.warn('Skipping notarizing step. Packaging is not running in CI'); 12 | return; 13 | } 14 | 15 | if (!('APPLE_ID' in process.env && 'APPLE_ID_PASS' in process.env)) { 16 | console.warn('Skipping notarizing step. APPLE_ID and APPLE_ID_PASS env variables must be set'); 17 | return; 18 | } 19 | 20 | const appName = context.packager.appInfo.productFilename; 21 | 22 | await notarize({ 23 | appBundleId: build.appId, 24 | appPath: `${appOutDir}/${appName}.app`, 25 | appleId: process.env.APPLE_ID, 26 | appleIdPassword: process.env.APPLE_ID_PASS, 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | 13 | # Dependency directory 14 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 15 | node_modules 16 | 17 | # OSX 18 | .DS_Store 19 | 20 | build/app/dist 21 | build/release 22 | 23 | # Need ./erb/dll to build in dev mode, maybe add subfolder in future 24 | # .erb/dll 25 | 26 | .idea 27 | npm-debug.log.* 28 | *.css.d.ts 29 | *.sass.d.ts 30 | *.scss.d.ts 31 | 32 | .env 33 | 34 | /version 35 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "EditorConfig.EditorConfig"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Electron: Main", 6 | "type": "node", 7 | "request": "launch", 8 | "protocol": "inspector", 9 | "runtimeExecutable": "yarn", 10 | "runtimeArgs": ["start:main --inspect=5858 --remote-debugging-port=9223"], 11 | "preLaunchTask": "Start Webpack Dev" 12 | }, 13 | { 14 | "name": "Electron: Renderer", 15 | "type": "chrome", 16 | "request": "attach", 17 | "port": 9223, 18 | "webRoot": "${workspaceFolder}", 19 | "timeout": 15000 20 | } 21 | ], 22 | "compounds": [ 23 | { 24 | "name": "Electron: All", 25 | "configurations": ["Electron: Main", "Electron: Renderer"] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | ".babelrc": "jsonc", 4 | ".eslintrc": "jsonc", 5 | ".prettierrc": "jsonc", 6 | ".eslintignore": "ignore" 7 | }, 8 | 9 | "javascript.validate.enable": true, 10 | "javascript.format.enable": true, 11 | "typescript.format.enable": false, 12 | // "eslint.enable": false, 13 | 14 | "search.exclude": { 15 | ".git": true, 16 | ".eslintcache": true, 17 | "src/dist": true, 18 | "bower_components": true, 19 | "dll": true, 20 | "release": true, 21 | "node_modules": true, 22 | "npm-debug.log.*": true, 23 | "test/**/__snapshots__": true, 24 | "yarn.lock": true, 25 | "*.{css,sass,scss}.d.ts": true, 26 | "renderer.dev.dll.js": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "label": "Start Webpack Dev", 7 | "script": "start:renderer", 8 | "options": { 9 | "cwd": "${workspaceFolder}" 10 | }, 11 | "isBackground": true, 12 | "problemMatcher": { 13 | "owner": "custom", 14 | "pattern": { 15 | "regexp": "____________" 16 | }, 17 | "background": { 18 | "activeOnStart": true, 19 | "beginsPattern": "Compiling\\.\\.\\.$", 20 | "endsPattern": "(Compiled successfully|Failed to compile)\\.$" 21 | } 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Clockotron 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clockotron 2 | 3 | Clockotron provides the ability to manipulate the time of a countdown timer. It talks directly to Vmix text inputs. 4 | 5 | ## Installation 6 | 7 | ==> Download an install file from the Releases tab, link: https://github.com/dlamon1/clockotron/releases/ 8 | 9 | or 10 | 11 | ==> Clone or fork this repo, USE YARN 12 | 13 | ## How it works 14 | 15 | The timer runs in the Clockotron software and sends the current time values to a Vmix Text Input via a socket connection. We have a Companion module (in version 2.2.\*\*) that is designed to give you a keypad style control over the current time, input the time you want and press enter. 16 | 17 | 1. Connect to Vmix by providing the IP of the computer that is running Vmix (do not include a port) 18 | 2. Create a TEXT INPUT in Vmix to use as a countdown timer (do not create a clock/timer input, the timer will run in the Clockotron software) 19 | 3. In Clockotron, click REFRESH INPUT LIST 20 | 4. In the dropdown menus, select the Input AND the Layer to assign the timer 21 | 5. With any issues, try reloading from the View menu in the toolbar at the top 22 | 6. When additional text inputs are created in Vmix, click the REFRESH INPUT LIST to view them 23 | 24 | ## Feedback 25 | 26 | Please submit bug reports to the issues tab on this repo. 27 | 28 | Please feel free to submit Pull Requests 29 | -------------------------------------------------------------------------------- /assets/assets.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-explicit-any: off */ 2 | 3 | declare module '*.svg' { 4 | const content: string; 5 | export default content; 6 | } 7 | 8 | declare module '*.png' { 9 | const content: string; 10 | export default content; 11 | } 12 | 13 | declare module '*.jpg' { 14 | const content: string; 15 | export default content; 16 | } 17 | -------------------------------------------------------------------------------- /assets/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-unsigned-executable-memory 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/icon.icns -------------------------------------------------------------------------------- /assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/icon.ico -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/icon.png -------------------------------------------------------------------------------- /assets/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/icons/1024x1024.png -------------------------------------------------------------------------------- /assets/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/icons/128x128.png -------------------------------------------------------------------------------- /assets/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/icons/16x16.png -------------------------------------------------------------------------------- /assets/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/icons/24x24.png -------------------------------------------------------------------------------- /assets/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/icons/256x256.png -------------------------------------------------------------------------------- /assets/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/icons/32x32.png -------------------------------------------------------------------------------- /assets/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/icons/48x48.png -------------------------------------------------------------------------------- /assets/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/icons/512x512.png -------------------------------------------------------------------------------- /assets/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/icons/64x64.png -------------------------------------------------------------------------------- /assets/icons/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/icons/96x96.png -------------------------------------------------------------------------------- /assets/iconslib/blackICONS/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/iconslib/blackICONS/icon.ico -------------------------------------------------------------------------------- /assets/iconslib/blackPNG/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/iconslib/blackPNG/1024x1024.png -------------------------------------------------------------------------------- /assets/iconslib/blackPNG/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/iconslib/blackPNG/128x128.png -------------------------------------------------------------------------------- /assets/iconslib/blackPNG/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/iconslib/blackPNG/16x16.png -------------------------------------------------------------------------------- /assets/iconslib/blackPNG/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/iconslib/blackPNG/24x24.png -------------------------------------------------------------------------------- /assets/iconslib/blackPNG/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/iconslib/blackPNG/256x256.png -------------------------------------------------------------------------------- /assets/iconslib/blackPNG/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/iconslib/blackPNG/32x32.png -------------------------------------------------------------------------------- /assets/iconslib/blackPNG/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/iconslib/blackPNG/48x48.png -------------------------------------------------------------------------------- /assets/iconslib/blackPNG/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/iconslib/blackPNG/512x512.png -------------------------------------------------------------------------------- /assets/iconslib/blackPNG/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/iconslib/blackPNG/64x64.png -------------------------------------------------------------------------------- /assets/iconslib/blackPNG/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/iconslib/blackPNG/96x96.png -------------------------------------------------------------------------------- /assets/iconslib/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /assets/iconslib/whiteICONS/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/iconslib/whiteICONS/icon.icns -------------------------------------------------------------------------------- /assets/iconslib/whiteICONS/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/iconslib/whiteICONS/icon.ico -------------------------------------------------------------------------------- /assets/iconslib/whiteICONS/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/iconslib/whiteICONS/icon.png -------------------------------------------------------------------------------- /assets/iconslib/whiteICONS/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /assets/iconslib/whitePNG/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/iconslib/whitePNG/128x128.png -------------------------------------------------------------------------------- /assets/iconslib/whitePNG/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/iconslib/whitePNG/16x16.png -------------------------------------------------------------------------------- /assets/iconslib/whitePNG/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/iconslib/whitePNG/24x24.png -------------------------------------------------------------------------------- /assets/iconslib/whitePNG/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/iconslib/whitePNG/256x256.png -------------------------------------------------------------------------------- /assets/iconslib/whitePNG/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/iconslib/whitePNG/32x32.png -------------------------------------------------------------------------------- /assets/iconslib/whitePNG/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/iconslib/whitePNG/48x48.png -------------------------------------------------------------------------------- /assets/iconslib/whitePNG/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/iconslib/whitePNG/512x512.png -------------------------------------------------------------------------------- /assets/iconslib/whitePNG/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/iconslib/whitePNG/64x64.png -------------------------------------------------------------------------------- /assets/iconslib/whitePNG/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlamon1/clockotron/2c6bb021604887942511fb5954ab8d4d66ccd53b/assets/iconslib/whitePNG/96x96.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: off, import/no-extraneous-dependencies: off */ 2 | 3 | const developmentEnvironments = ['development', 'test']; 4 | 5 | const developmentPlugins = [require('@babel/plugin-transform-runtime')]; 6 | 7 | const productionPlugins = [ 8 | require('babel-plugin-dev-expression'), 9 | 10 | // babel-preset-react-optimize 11 | require('@babel/plugin-transform-react-constant-elements'), 12 | require('@babel/plugin-transform-react-inline-elements'), 13 | require('babel-plugin-transform-react-remove-prop-types'), 14 | ]; 15 | 16 | module.exports = (api) => { 17 | // See docs about api at https://babeljs.io/docs/en/config-files#apicache 18 | 19 | const development = api.env(developmentEnvironments); 20 | 21 | return { 22 | presets: [ 23 | // @babel/preset-env will automatically target our browserslist targets 24 | require('@babel/preset-env'), 25 | require('@babel/preset-typescript'), 26 | [require('@babel/preset-react'), { development }], 27 | ], 28 | plugins: [ 29 | // Stage 0 30 | require('@babel/plugin-proposal-function-bind'), 31 | 32 | // Stage 1 33 | require('@babel/plugin-proposal-export-default-from'), 34 | require('@babel/plugin-proposal-logical-assignment-operators'), 35 | [require('@babel/plugin-proposal-optional-chaining')], 36 | [ 37 | require('@babel/plugin-proposal-pipeline-operator'), 38 | { proposal: 'minimal' }, 39 | ], 40 | [require('@babel/plugin-proposal-nullish-coalescing-operator')], 41 | require('@babel/plugin-proposal-do-expressions'), 42 | 43 | // Stage 2 44 | [require('@babel/plugin-proposal-decorators'), { legacy: true }], 45 | require('@babel/plugin-proposal-function-sent'), 46 | require('@babel/plugin-proposal-export-namespace-from'), 47 | require('@babel/plugin-proposal-numeric-separator'), 48 | require('@babel/plugin-proposal-throw-expressions'), 49 | 50 | // Stage 3 51 | require('@babel/plugin-syntax-dynamic-import'), 52 | require('@babel/plugin-syntax-import-meta'), 53 | [require('@babel/plugin-proposal-class-properties')], 54 | require('@babel/plugin-proposal-json-strings'), 55 | 56 | ...(development ? developmentPlugins : productionPlugins), 57 | ], 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /build/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clockotron", 3 | "productName": "Clockotron", 4 | "version": "1.0.2", 5 | "description": "Clockotron talks time directly to vMix text inputs", 6 | "main": "./dist/main/main.js", 7 | "author": { 8 | "name": "LEAD LED, LLC", 9 | "email": "devon@leadled.io", 10 | "url": "https://leadled.io" 11 | }, 12 | "scripts": { 13 | "electron-rebuild": "node -r ../../.erb/scripts/babel-register.js ../../.erb/scripts/electron-rebuild.js", 14 | "link-modules": "node -r ../../.erb/scripts/babel-register.js ../../.erb/scripts/link-modules.js", 15 | "postinstall": "yarn electron-rebuild && yarn link-modules" 16 | }, 17 | "license": "MIT", 18 | "dependencies": {} 19 | } 20 | -------------------------------------------------------------------------------- /build/app/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clockotron", 3 | "productName": "Clockotron", 4 | "description": "Clockotron talks time directly to vMix text inputs", 5 | "version": "1.0.2", 6 | "scripts": { 7 | "build": "concurrently \"yarn build:main\" \"yarn build:renderer\"", 8 | "build:main": "cross-env NODE_ENV=production webpack --config ./.erb/configs/webpack.config.main.prod.babel.js", 9 | "build:renderer": "cross-env NODE_ENV=production webpack --config ./.erb/configs/webpack.config.renderer.prod.babel.js", 10 | "rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir src", 11 | "lint": "cross-env NODE_ENV=development eslint . --cache --ext .js,.jsx,.ts,.tsx", 12 | "package": "node -r @babel/register ./.erb/scripts/clean.js dist release && yarn build && electron-builder build --publish never", 13 | "postinstall": "node -r @babel/register .erb/scripts/check-native-dep.js && electron-builder install-app-deps && yarn cross-env NODE_ENV=development webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.babel.js && opencollective-postinstall && yarn-deduplicate yarn.lock", 14 | "publish": "node -r @babel/register ./.erb/scripts/clean.js dist release && yarn build && electron-builder build -p always", 15 | "start": "node -r @babel/register ./.erb/scripts/check-port-in-use.js && yarn start:renderer", 16 | "start:main": "cross-env NODE_ENV=development electron -r ./.erb/scripts/babel-register ./src/main/main.js", 17 | "start:renderer": "cross-env NODE_ENV=development webpack serve --config ./.erb/configs/webpack.config.renderer.dev.babel.js", 18 | "test": "jest", 19 | "major": "yarn version --major && yarn run publish", 20 | "minor": "yarn version --minor && yarn run publish", 21 | "patch": "yarn version --patch && yarn run publish" 22 | }, 23 | "lint-staged": { 24 | "*.{js,jsx,ts,tsx}": [ 25 | "cross-env NODE_ENV=development eslint --cache" 26 | ], 27 | "*.json,.{babelrc,eslintrc,prettierrc}": [ 28 | "prettier --ignore-path .eslintignore --parser json --write" 29 | ], 30 | "*.{css,scss}": [ 31 | "prettier --ignore-path .eslintignore --single-quote --write" 32 | ], 33 | "*.{html,md,yml}": [ 34 | "prettier --ignore-path .eslintignore --single-quote --write" 35 | ] 36 | }, 37 | "build": { 38 | "productName": "Clockotron", 39 | "appId": "org.erb.Clockotron", 40 | "asar": false, 41 | "asarUnpack": "**\\*.{node,dll}", 42 | "icon": "assets/icon.icns", 43 | "files": [ 44 | "dist", 45 | "node_modules", 46 | "package.json" 47 | ], 48 | "afterSign": ".erb/scripts/notarize.js", 49 | "mac": { 50 | "type": "distribution", 51 | "hardenedRuntime": true, 52 | "entitlements": "assets/entitlements.mac.plist", 53 | "entitlementsInherit": "assets/entitlements.mac.plist", 54 | "gatekeeperAssess": false 55 | }, 56 | "dmg": { 57 | "contents": [ 58 | { 59 | "x": 130, 60 | "y": 220 61 | }, 62 | { 63 | "x": 410, 64 | "y": 220, 65 | "type": "link", 66 | "path": "/Applications" 67 | } 68 | ] 69 | }, 70 | "win": { 71 | "target": [ 72 | "nsis" 73 | ] 74 | }, 75 | "linux": { 76 | "target": [ 77 | "AppImage" 78 | ], 79 | "category": "Development" 80 | }, 81 | "directories": { 82 | "app": "build/app", 83 | "buildResources": "assets", 84 | "output": "build/release" 85 | }, 86 | "extraResources": [ 87 | "./assets/**" 88 | ], 89 | "publish": { 90 | "provider": "github", 91 | "owner": "dlamon1", 92 | "repo": "clockotron", 93 | "releaseType": "release" 94 | } 95 | }, 96 | "repository": { 97 | "type": "git", 98 | "url": "git+https://github.com/dlamon1/clockotron" 99 | }, 100 | "author": { 101 | "name": "LEAD LED, LLC", 102 | "email": "devon@leadled.io", 103 | "url": "https://leadled.io" 104 | }, 105 | "contributors": [ 106 | { 107 | "name": "Devon Lamond", 108 | "email": "devon@leadled.io", 109 | "url": "https://github.com/dlamon1" 110 | } 111 | ], 112 | "license": "MIT", 113 | "jest": { 114 | "testURL": "http://localhost/", 115 | "moduleNameMapper": { 116 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/.erb/mocks/fileMock.js", 117 | "\\.(css|less|sass|scss)$": "identity-obj-proxy" 118 | }, 119 | "moduleFileExtensions": [ 120 | "js", 121 | "jsx", 122 | "ts", 123 | "tsx", 124 | "json" 125 | ], 126 | "moduleDirectories": [ 127 | "node_modules", 128 | "build/app/node_modules" 129 | ], 130 | "setupFiles": [ 131 | "./.erb/scripts/check-build-exists.js" 132 | ] 133 | }, 134 | "devDependencies": { 135 | "@babel/core": "^7.14.8", 136 | "@babel/plugin-proposal-class-properties": "^7.14.5", 137 | "@babel/plugin-proposal-decorators": "^7.14.5", 138 | "@babel/plugin-proposal-do-expressions": "^7.14.5", 139 | "@babel/plugin-proposal-export-default-from": "^7.14.5", 140 | "@babel/plugin-proposal-export-namespace-from": "^7.14.5", 141 | "@babel/plugin-proposal-function-bind": "^7.14.5", 142 | "@babel/plugin-proposal-function-sent": "^7.14.5", 143 | "@babel/plugin-proposal-json-strings": "^7.14.5", 144 | "@babel/plugin-proposal-logical-assignment-operators": "^7.14.5", 145 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5", 146 | "@babel/plugin-proposal-optional-chaining": "^7.14.5", 147 | "@babel/plugin-proposal-pipeline-operator": "^7.14.8", 148 | "@babel/plugin-proposal-throw-expressions": "^7.14.5", 149 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 150 | "@babel/plugin-syntax-import-meta": "^7.10.4", 151 | "@babel/plugin-transform-react-constant-elements": "^7.14.5", 152 | "@babel/plugin-transform-react-inline-elements": "^7.14.5", 153 | "@babel/plugin-transform-runtime": "^7.14.5", 154 | "@babel/preset-env": "^7.14.8", 155 | "@babel/preset-react": "^7.14.5", 156 | "@babel/preset-typescript": "^7.14.5", 157 | "@babel/register": "^7.14.5", 158 | "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3", 159 | "@teamsupercell/typings-for-css-modules-loader": "^2.5.1", 160 | "@testing-library/jest-dom": "^5.12.0", 161 | "@testing-library/react": "^11.2.7", 162 | "@types/enzyme": "^3.10.9", 163 | "@types/enzyme-adapter-react-16": "^1.0.6", 164 | "@types/history": "4.7.8", 165 | "@types/jest": "^26.0.24", 166 | "@types/node": "15.0.2", 167 | "@types/react": "^17.0.9", 168 | "@types/react-dom": "^17.0.9", 169 | "@types/react-router": "^5.1.14", 170 | "@types/react-router-dom": "^5.1.6", 171 | "@types/react-test-renderer": "^17.0.1", 172 | "@types/webpack-env": "^1.16.0", 173 | "@typescript-eslint/eslint-plugin": "^4.22.1", 174 | "@typescript-eslint/parser": "^4.22.1", 175 | "babel-eslint": "^10.1.0", 176 | "babel-jest": "^26.1.0", 177 | "babel-loader": "^8.2.2", 178 | "babel-plugin-dev-expression": "^0.2.2", 179 | "babel-plugin-transform-react-remove-prop-types": "^0.4.24", 180 | "browserslist-config-erb": "^0.0.1", 181 | "chalk": "^4.1.1", 182 | "concurrently": "^6.0.2", 183 | "core-js": "^3.11.3", 184 | "cross-env": "^7.0.3", 185 | "css-loader": "^5.2.4", 186 | "css-minimizer-webpack-plugin": "^2.0.0", 187 | "detect-port": "^1.3.0", 188 | "electron": "^13.1.8", 189 | "electron-builder": "^22.11.1", 190 | "electron-devtools-installer": "^3.2.0", 191 | "electron-notarize": "^1.0.0", 192 | "electron-rebuild": "^2.3.5", 193 | "enzyme": "^3.11.0", 194 | "enzyme-adapter-react-16": "^1.15.6", 195 | "enzyme-to-json": "^3.6.2", 196 | "file-loader": "^6.2.0", 197 | "html-webpack-plugin": "^5.3.1", 198 | "identity-obj-proxy": "^3.0.0", 199 | "lint-staged": "^10.5.4", 200 | "mini-css-extract-plugin": "^1.6.0", 201 | "opencollective-postinstall": "^2.0.3", 202 | "prettier": "^2.2.1", 203 | "react-refresh": "^0.10.0", 204 | "react-test-renderer": "^17.0.2", 205 | "rimraf": "^3.0.0", 206 | "style-loader": "^2.0.0", 207 | "terser-webpack-plugin": "^5.1.1", 208 | "url-loader": "^4.1.0", 209 | "webpack": "^5.36.2", 210 | "webpack-bundle-analyzer": "^4.4.1", 211 | "webpack-cli": "^4.6.0", 212 | "webpack-dev-server": "^3.11.2", 213 | "webpack-merge": "^5.7.3", 214 | "yarn-deduplicate": "^3.1.0" 215 | }, 216 | "dependencies": { 217 | "@material-ui/core": "^4.11.4", 218 | "@material-ui/icons": "^4.11.2", 219 | "@material-ui/lab": "^4.0.0-alpha.60", 220 | "dotenv": "^10.0.0", 221 | "driftless": "^2.0.3", 222 | "electron-debug": "^3.2.0", 223 | "electron-fetch": "^1.7.3", 224 | "electron-log": "^4.3.5", 225 | "electron-store": "^8.0.1", 226 | "electron-updater": "^4.3.8", 227 | "express": "^4.17.1", 228 | "fast-xml-parser": "^4.0.1", 229 | "mobx": "^6.3.2", 230 | "mobx-react": "^7.2.0", 231 | "react": "^17.0.1", 232 | "react-color": "^2.19.3", 233 | "react-dom": "^17.0.1", 234 | "react-router-dom": "^5.2.0", 235 | "regenerator-runtime": "^0.13.5", 236 | "sass-loader": "^12.4.0", 237 | "uuid": "^8.3.2", 238 | "validator": "^13.6.0", 239 | "xmldom": "^0.6.0", 240 | "xpath": "^0.0.32" 241 | }, 242 | "devEngines": { 243 | "node": ">=10.x", 244 | "npm": ">=6.x", 245 | "yarn": ">=1.21.3" 246 | }, 247 | "browserslist": [], 248 | "prettier": { 249 | "overrides": [ 250 | { 251 | "files": [ 252 | ".prettierrc", 253 | ".babelrc", 254 | ".eslintrc" 255 | ], 256 | "options": { 257 | "parser": "json" 258 | } 259 | } 260 | ], 261 | "singleQuote": true 262 | }, 263 | "husky": { 264 | "hooks": { 265 | "pre-commit": "lint-staged" 266 | } 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/main/api/apiServer.js: -------------------------------------------------------------------------------- 1 | export function apiFunction(mainWindow, api) { 2 | api.listen(5491, () => {}); 3 | 4 | api.get('/api', (req, res) => { 5 | res.send('api'); 6 | }); 7 | api.get('/start', (req, res) => { 8 | res.send('start'); 9 | mainWindow.webContents.send('start'); 10 | }); 11 | api.get('/stop', (req, res) => { 12 | res.send('stop'); 13 | mainWindow.webContents.send('stop'); 14 | }); 15 | api.get('/slower', (req, res) => { 16 | res.send('slower'); 17 | mainWindow.webContents.send('slower'); 18 | }); 19 | api.get('/normal', (req, res) => { 20 | res.send('normal'); 21 | mainWindow.webContents.send('normal'); 22 | }); 23 | api.get('/faster', (req, res) => { 24 | res.send('faster'); 25 | mainWindow.webContents.send('faster'); 26 | }); 27 | api.get('/reset', (req, res) => { 28 | res.send('reset'); 29 | mainWindow.webContents.send('reset'); 30 | }); 31 | 32 | //TextInputList.js 33 | //TextInputList.js 34 | //TextInputList.js 35 | 36 | api.get('/digit', (req, res) => { 37 | res.send(req.query.value); 38 | let test = req.query.value.split(''); 39 | mainWindow.webContents.send(req.query.value); 40 | }); 41 | api.get('/timeSpecific', (req, res) => { 42 | res.send(req.query.value); 43 | mainWindow.webContents.send('timeSpecific', req.query.value); 44 | }); 45 | api.get('/clear', (req, res) => { 46 | res.send('clear'); 47 | mainWindow.webContents.send('clear'); 48 | }); 49 | api.get('/postClock', (req, res) => { 50 | res.send('postClock'); 51 | mainWindow.webContents.send('postClock'); 52 | }); 53 | 54 | // TimeDown.js 55 | // TimeDown.js 56 | // TimeDown.js 57 | api.get('/sDown', (req, res) => { 58 | res.send('sDown'); 59 | mainWindow.webContents.send('sDown'); 60 | }); 61 | api.get('/mDown', (req, res) => { 62 | res.send('mDown'); 63 | mainWindow.webContents.send('mDown'); 64 | }); 65 | api.get('/hDown', (req, res) => { 66 | res.send('hDown'); 67 | mainWindow.webContents.send('hDown'); 68 | }); 69 | 70 | //TimeUp.js 71 | //TimeUp.js 72 | //TimeUp.js 73 | api.get('/sUp', (req, res) => { 74 | res.send('sUp'); 75 | mainWindow.webContents.send('sUp'); 76 | }); 77 | api.get('/mUp', (req, res) => { 78 | res.send('mUp'); 79 | mainWindow.webContents.send('mUp'); 80 | }); 81 | api.get('/hUp', (req, res) => { 82 | res.send('hUp'); 83 | mainWindow.webContents.send('hUp'); 84 | }); 85 | } 86 | -------------------------------------------------------------------------------- /src/main/api/index.js: -------------------------------------------------------------------------------- 1 | import { apiFunction } from './apiServer'; 2 | import { vmixSocket } from './vmixSocket'; 3 | import express from 'express'; 4 | 5 | const api = express(); 6 | 7 | export function runNetConnections(mainWindow, connection) { 8 | apiFunction(mainWindow, api); 9 | vmixSocket(mainWindow, connection); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/api/vmixSocket.js: -------------------------------------------------------------------------------- 1 | import net from 'net'; 2 | import { ipcMain } from 'electron'; 3 | 4 | export function vmixSocket(mainWindow, connection) { 5 | let initListener; 6 | let vmixPostReqListener; 7 | let socketShutdownListener; 8 | let reqXmlListener; 9 | let reqTallyListener; 10 | let reqXmlToUpdateVideoPlayer; 11 | 12 | let waitingForXmlFromTallyReq = false; 13 | let waitingForXmlFromActsReq = false; 14 | 15 | // initialXmlReq is here to get the initial XML because 16 | // we are calling for Tally upon IP being set 17 | let initialXmlReq = false; 18 | 19 | const connect = (address) => { 20 | connection = net.connect( 21 | { port: 8099, host: address }, 22 | () => {}, 23 | () => { 24 | mainWindow.webContents.send('socket-connected'); 25 | } 26 | ); 27 | 28 | connection.on('data', function (data) { 29 | // console.log('*****new data*****'); 30 | splitDataResponseByNewline(data.toString()); 31 | }); 32 | 33 | connection.on('error', function (e) { 34 | handleError(e, connection); 35 | }); 36 | 37 | const requestXmlData = () => { 38 | connection.write('XML\r\n'); 39 | }; 40 | 41 | const requestTallyData = () => { 42 | connection.write('TALLY\r\n'); 43 | }; 44 | 45 | const requestVideoData = () => { 46 | connection.write('SUBSCRIBE ACTS\r\n'); 47 | connection.write('SUBSCRIBE TALLY\r\n'); 48 | }; 49 | 50 | const vmixPostReq = (cmd) => { 51 | connection.write('FUNCTION ' + cmd + '\r\n'); 52 | }; 53 | 54 | reqTallyListener = () => { 55 | ipcMain.handle('reqTally', () => { 56 | requestTallyData(); 57 | }); 58 | }; 59 | reqXmlListener = () => { 60 | ipcMain.handle('reqXml', () => { 61 | requestXmlData(); 62 | }); 63 | }; 64 | reqXmlToUpdateVideoPlayer = () => { 65 | ipcMain.handle('reqXmlToUpdateVideoPlayer', () => { 66 | waitingForXmlFromTallyReq = true; 67 | requestXmlData(); 68 | }); 69 | }; 70 | vmixPostReqListener = () => { 71 | ipcMain.handle('vmixPostReq', (__, cmd) => { 72 | vmixPostReq(cmd); 73 | }); 74 | }; 75 | socketShutdownListener = () => { 76 | ipcMain.handle('socket-shutdown', () => { 77 | removeIpcListeners(); 78 | requestShutdown(); 79 | }); 80 | }; 81 | 82 | reqTallyListener(); 83 | vmixPostReqListener(); 84 | reqXmlListener(); 85 | reqXmlToUpdateVideoPlayer(); 86 | socketShutdownListener(); 87 | requestVideoData(); 88 | }; 89 | 90 | const splitDataResponseByNewline = (data) => { 91 | if (data.includes('XML ')) { 92 | handleDataByResType(data); 93 | return; 94 | } 95 | let lines = createArraySplitByNewLine(data); 96 | lines.forEach((line) => { 97 | handleDataByResType(line); 98 | }); 99 | }; 100 | 101 | const handleDataByResType = (data) => { 102 | const resType = data.split(' ')[0]; 103 | console.log(resType); 104 | if (resType == 'XML') { 105 | handleActType_XML(data); 106 | } 107 | if (resType == 'ACTS') { 108 | handleResType_ACTS(data); 109 | } 110 | if (resType == 'TALLY') { 111 | handleResType_TALLY(data); 112 | } 113 | }; 114 | 115 | // 1 116 | // 1 117 | // Response from XML 118 | const handleActType_XML = (data) => { 119 | let vmixNodeString = data.split('')[1]; 120 | if (!vmixNodeString) { 121 | console.error('error parsing XML data: ', data); 122 | return; 123 | } 124 | 125 | let vmixNodeStringClean = vmixNodeString.replace(/(\r\n|\n|\r)/gm, ''); 126 | let domString = `${vmixNodeStringClean}`; 127 | 128 | if (waitingForXmlFromTallyReq && initialXmlReq) { 129 | mainWindow.webContents.send('handleXmlTallyData', domString); 130 | waitingForXmlFromTallyReq = false; 131 | waitingForXmlFromActsReq = false; 132 | } else if (waitingForXmlFromActsReq && initialXmlReq) { 133 | mainWindow.webContents.send('handleXmlActsData', domString); 134 | waitingForXmlFromTallyReq = false; 135 | waitingForXmlFromActsReq = false; 136 | } else { 137 | initialXmlReq = true; 138 | mainWindow.webContents.send('handleXmlData', domString); 139 | } 140 | }; 141 | 142 | // 2 143 | // 2 144 | // Response from TALLY 145 | const handleResType_TALLY = (line) => { 146 | waitingForXmlFromTallyReq = true; 147 | waitingForXmlFromActsReq = false; 148 | let tallyString = line.split(' ')[2]; 149 | mainWindow.webContents.send('videoTallyData', tallyString); 150 | connection.write('XML\r\n'); 151 | }; 152 | 153 | // 3 154 | // 3 155 | // Response from INPUT PLAYING 156 | const handleActType_INPUT_PLAYING = (data) => { 157 | waitingForXmlFromActsReq = true; 158 | waitingForXmlFromTallyReq = false; 159 | mainWindow.webContents.send('inputPlayingData', data); 160 | connection.write('XML\r\n'); 161 | }; 162 | 163 | const handleResType_ACTS = (line) => { 164 | let fullInputPlayingDataString = line.split('ACTS OK ')[1]; 165 | let action = line.split(' ')[2]; 166 | if (action === 'InputPlaying') { 167 | handleActType_INPUT_PLAYING(fullInputPlayingDataString); 168 | } 169 | }; 170 | 171 | const createArraySplitByNewLine = (data) => { 172 | let arrayByLines = data.split(/\r?\n/); 173 | return arrayByLines; 174 | }; 175 | 176 | const handleError = (e, connection) => { 177 | switch (e.code) { 178 | case 'EPIPE': 179 | requestShutdown(connection); 180 | removeIpcListeners(); 181 | initListener(); 182 | mainWindow.webContents.send('socket-error', e.code); 183 | break; 184 | case 'ECONNREFUSED': 185 | requestShutdown(connection); 186 | removeIpcListeners(); 187 | initListener(); 188 | break; 189 | default: 190 | break; 191 | } 192 | }; 193 | 194 | const requestShutdown = (connection) => { 195 | connection.write('QUIT\r\n'); 196 | connection && connection.end(); 197 | }; 198 | 199 | const removeIpcListeners = () => { 200 | ipcMain.removeHandler('vmixConnect', initListener); 201 | ipcMain.removeHandler('reqXml', reqXmlListener); 202 | ipcMain.removeHandler( 203 | 'reqXmlToUpdateVideoPlayer', 204 | reqXmlToUpdateVideoPlayer 205 | ); 206 | ipcMain.removeHandler('reqTally', reqTallyListener); 207 | ipcMain.removeHandler('vmixPostReq', vmixRequestXmlListener); 208 | ipcMain.removeHandler('socket-shutdown', socketShutdownListener); 209 | }; 210 | 211 | initListener = () => { 212 | ipcMain.handle('vmixConnect', async (__, address) => { 213 | connection = null; 214 | connect(address); 215 | }); 216 | }; 217 | 218 | initListener(); 219 | } 220 | -------------------------------------------------------------------------------- /src/main/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import 'core-js/stable'; 3 | import 'regenerator-runtime/runtime'; 4 | 5 | import { app, BrowserWindow, ipcMain, Menu } from 'electron'; 6 | import path from 'path'; 7 | import { updater } from './updater'; 8 | import MenuBuilder from './menu'; 9 | import { resolveHtmlPath } from './util'; 10 | import { runNetConnections } from './api'; 11 | import dotenv from 'dotenv'; 12 | import Store from 'electron-store'; 13 | import { StoreListeners } from './storeClass'; 14 | import { Timer } from './timers'; 15 | 16 | dotenv.config(); 17 | const store = new Store(); 18 | const storeListeners = new StoreListeners(store); 19 | storeListeners.mountListeners(); 20 | 21 | let mainWindow; 22 | let menuBuilder; 23 | let isDev = false; 24 | let connection = null; 25 | let isMac = process.platform === 'darwin'; 26 | 27 | app.commandLine.appendSwitch('disable-renderer-backgrounding'); 28 | app.commandLine.appendSwitch('ignore-certificate-errors'); 29 | 30 | if ( 31 | process.env.NODE_ENV !== undefined && 32 | process.env.NODE_ENV === 'development' 33 | ) { 34 | isDev = true; 35 | } 36 | 37 | if (process.env.NODE_ENV === 'production') { 38 | const sourceMapSupport = require('source-map-support'); 39 | sourceMapSupport.install(); 40 | } 41 | 42 | if (process.platform === 'win32') { 43 | app.commandLine.appendSwitch('high-dpi-support', 'true'); 44 | app.commandLine.appendSwitch('force-device-scale-factor', '1'); 45 | } 46 | 47 | const RESOURCES_PATH = app.isPackaged 48 | ? path.join(process.resourcesPath, 'assets') 49 | : path.join(__dirname, '../../assets'); 50 | 51 | const getAssetPath = (...paths) => { 52 | return path.join(RESOURCES_PATH, ...paths); 53 | }; 54 | 55 | function createWindow() { 56 | mainWindow = new BrowserWindow({ 57 | width: isDev ? 720 : 360, 58 | height: 1080, 59 | minWidth: 333, 60 | x: 0, 61 | y: 0, 62 | show: false, 63 | backgroundColor: '#202020', 64 | icon: isMac ? getAssetPath('icon.icns') : getAssetPath('icon.png'), 65 | webPreferences: { 66 | nodeIntegration: false, 67 | contextIsolation: true, 68 | preload: path.join(__dirname, 'preload.js'), 69 | pageVisibility: true, 70 | }, 71 | }); 72 | 73 | mainWindow.loadURL(resolveHtmlPath('index.html')); 74 | 75 | mainWindow.once('did-finish-load', () => console.log('did finish')); 76 | 77 | mainWindow.once('ready-to-show', () => { 78 | const timer = new Timer('timer', mainWindow); 79 | timer.startListener(); 80 | 81 | let hasNewFeaturesBeenSeen = store.get('hasNewFeaturesBeenSeen'); 82 | mainWindow.webContents.send( 83 | 'newFeaturesHaveBeenSeen', 84 | hasNewFeaturesBeenSeen 85 | ); 86 | mainWindow.webContents.send('version', app.getVersion()); 87 | 88 | updater(isDev, mainWindow, store); 89 | 90 | mainWindow.show(); 91 | 92 | isDev && mainWindow.webContents.openDevTools(); 93 | }); 94 | 95 | runNetConnections(mainWindow, connection); 96 | 97 | mainWindow.on('closed', function () { 98 | mainWindow = null; 99 | }); 100 | } 101 | 102 | let betaFeaturesListener = () => { 103 | ipcMain.handle('enableBetaButton', (__) => { 104 | menuBuilder.setBetaFeaturesEnabled(); 105 | Menu.getApplicationMenu().getMenuItemById('betaFeatures').enabled = true; 106 | }); 107 | }; 108 | betaFeaturesListener(); 109 | 110 | app.on('ready', () => { 111 | createWindow(); 112 | menuBuilder = new MenuBuilder(mainWindow); 113 | menuBuilder.buildMenu(); 114 | }); 115 | 116 | app.on('window-all-closed', () => { 117 | if (process.platform !== 'darwin') { 118 | app.quit(); 119 | } 120 | }); 121 | 122 | app.on('activate', () => { 123 | if (mainWindow === null) { 124 | createWindow(); 125 | } 126 | }); 127 | 128 | app.on('before-quit', () => { 129 | ipcMain.removeHandler('betaFeatures', betaFeaturesListener); 130 | storeListeners.removeListeners(); 131 | connection && connection.destroy(); 132 | connection && connection.clearAllListeners(); 133 | }); 134 | -------------------------------------------------------------------------------- /src/main/menu.js: -------------------------------------------------------------------------------- 1 | import { app, Menu, shell } from 'electron'; 2 | 3 | export default class MenuBuilder { 4 | mainWindow; 5 | betaFeaturesEnabled = false; 6 | 7 | constructor(mainWindow) { 8 | this.mainWindow = mainWindow; 9 | } 10 | 11 | setBetaFeaturesEnabled(boolean) { 12 | this.betaFeaturesEnabled = boolean; 13 | } 14 | 15 | toggleBetaFeatures() { 16 | this.betaFeaturesEnabled = !this.betaFeaturesEnabled; 17 | this.mainWindow.webContents.send('betaFeatures', this.betaFeaturesEnabled); 18 | } 19 | 20 | buildMenu() { 21 | if ( 22 | process.env.NODE_ENV === 'development' || 23 | process.env.DEBUG_PROD === 'true' 24 | ) { 25 | this.setupDevelopmentEnvironment(); 26 | } 27 | 28 | const template = 29 | process.platform === 'darwin' 30 | ? this.buildDarwinTemplate() 31 | : this.buildDefaultTemplate(); 32 | 33 | const menu = Menu.buildFromTemplate(template); 34 | Menu.setApplicationMenu(menu); 35 | 36 | return menu; 37 | } 38 | 39 | setupDevelopmentEnvironment() { 40 | this.mainWindow.webContents.on('context-menu', (_, props) => { 41 | const { x, y } = props; 42 | 43 | Menu.buildFromTemplate([ 44 | { 45 | label: 'Inspect element', 46 | click: () => { 47 | this.mainWindow.webContents.inspectElement(x, y); 48 | }, 49 | }, 50 | ]).popup({ window: this.mainWindow }); 51 | }); 52 | } 53 | 54 | buildDarwinTemplate() { 55 | const subMenuAbout = { 56 | label: 'Electron', 57 | submenu: [ 58 | { 59 | label: 'About ElectronReact', 60 | selector: 'orderFrontStandardAboutPanel:', 61 | }, 62 | { type: 'separator' }, 63 | { label: 'Services', submenu: [] }, 64 | { type: 'separator' }, 65 | { 66 | label: 'Hide ElectronReact', 67 | accelerator: 'Command+H', 68 | selector: 'hide:', 69 | }, 70 | { 71 | label: 'Hide Others', 72 | accelerator: 'Command+Shift+H', 73 | selector: 'hideOtherApplications:', 74 | }, 75 | { label: 'Show All', selector: 'unhideAllApplications:' }, 76 | { type: 'separator' }, 77 | { 78 | label: 'Quit', 79 | accelerator: 'Command+Q', 80 | click: () => { 81 | app.quit(); 82 | }, 83 | }, 84 | ], 85 | }; 86 | const subMenuEdit = { 87 | label: 'Edit', 88 | submenu: [ 89 | { label: 'Undo', accelerator: 'Command+Z', selector: 'undo:' }, 90 | { label: 'Redo', accelerator: 'Shift+Command+Z', selector: 'redo:' }, 91 | { type: 'separator' }, 92 | { label: 'Cut', accelerator: 'Command+X', selector: 'cut:' }, 93 | { label: 'Copy', accelerator: 'Command+C', selector: 'copy:' }, 94 | { label: 'Paste', accelerator: 'Command+V', selector: 'paste:' }, 95 | { 96 | label: 'Select All', 97 | accelerator: 'Command+A', 98 | selector: 'selectAll:', 99 | }, 100 | ], 101 | }; 102 | const subMenuViewDev = { 103 | label: 'View', 104 | submenu: [ 105 | { 106 | label: 'Reload', 107 | accelerator: 'Command+R', 108 | click: () => { 109 | this.mainWindow.webContents.reload(); 110 | }, 111 | }, 112 | { 113 | label: 'Toggle Full Screen', 114 | accelerator: 'Ctrl+Command+F', 115 | click: () => { 116 | this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); 117 | }, 118 | }, 119 | { 120 | label: 'Toggle Developer Tools', 121 | accelerator: 'Alt+Command+I', 122 | click: () => { 123 | this.mainWindow.webContents.toggleDevTools(); 124 | }, 125 | }, 126 | ], 127 | }; 128 | const subMenuViewProd = { 129 | label: 'View', 130 | submenu: [ 131 | { 132 | label: 'Toggle Full Screen', 133 | accelerator: 'Ctrl+Command+F', 134 | click: () => { 135 | this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); 136 | }, 137 | }, 138 | ], 139 | }; 140 | const subMenuWindow = { 141 | label: 'Window', 142 | submenu: [ 143 | { 144 | label: 'Minimize', 145 | accelerator: 'Command+M', 146 | selector: 'performMiniaturize:', 147 | }, 148 | { label: 'Close', accelerator: 'Command+W', selector: 'performClose:' }, 149 | { type: 'separator' }, 150 | { label: 'Bring All to Front', selector: 'arrangeInFront:' }, 151 | ], 152 | }; 153 | const subMenuHelp = { 154 | label: 'Help', 155 | submenu: [ 156 | { 157 | label: 'Learn More', 158 | click() { 159 | shell.openExternal('https://electronjs.org'); 160 | }, 161 | }, 162 | { 163 | label: 'Documentation', 164 | click() { 165 | shell.openExternal( 166 | 'https://github.com/electron/electron/tree/main/docs#readme' 167 | ); 168 | }, 169 | }, 170 | { 171 | label: 'Community Discussions', 172 | click() { 173 | shell.openExternal('https://www.electronjs.org/community'); 174 | }, 175 | }, 176 | { 177 | label: 'Search Issues', 178 | click() { 179 | shell.openExternal('https://github.com/electron/electron/issues'); 180 | }, 181 | }, 182 | ], 183 | }; 184 | 185 | const subMenuView = 186 | process.env.NODE_ENV === 'development' || 187 | process.env.DEBUG_PROD === 'true' 188 | ? subMenuViewDev 189 | : subMenuViewProd; 190 | 191 | return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp]; 192 | } 193 | 194 | buildDefaultTemplate() { 195 | const templateDefault = [ 196 | { 197 | label: '&File', 198 | submenu: 199 | process.env.NODE_ENV === 'development' || 200 | process.env.DEBUG_PROD === 'true' 201 | ? [ 202 | { 203 | label: '&Open', 204 | accelerator: 'Ctrl+O', 205 | }, 206 | { 207 | label: '&Close', 208 | accelerator: 'Ctrl+W', 209 | click: () => { 210 | this.mainWindow.close(); 211 | }, 212 | }, 213 | ] 214 | : [ 215 | { 216 | label: '&Close', 217 | accelerator: 'Ctrl+W', 218 | click: () => { 219 | this.mainWindow.close(); 220 | }, 221 | }, 222 | ], 223 | }, 224 | { 225 | label: '&View', 226 | submenu: 227 | process.env.NODE_ENV === 'development' || 228 | process.env.DEBUG_PROD === 'true' 229 | ? [ 230 | { 231 | id: 'betaFeatures', 232 | label: this.betaFeaturesEnabled 233 | ? 'Disable Beta Features' 234 | : 'Toggle Video TRT (beta)', 235 | click: () => { 236 | this.toggleBetaFeatures(); 237 | }, 238 | enabled: false, 239 | }, 240 | { 241 | label: '&Reload', 242 | accelerator: 'Ctrl+R', 243 | click: () => { 244 | this.mainWindow.webContents.reload(); 245 | }, 246 | }, 247 | { 248 | label: 'Toggle &Developer Tools', 249 | accelerator: 'F12', 250 | click: () => { 251 | this.mainWindow.webContents.toggleDevTools(); 252 | }, 253 | }, 254 | ] 255 | : [ 256 | { 257 | id: 'betaFeatures', 258 | label: this.betaFeaturesEnabled 259 | ? 'Disable Beta Features' 260 | : 'Toggle Video TRT (beta)', 261 | click: () => { 262 | this.toggleBetaFeatures(); 263 | }, 264 | enabled: false, 265 | }, 266 | { 267 | label: '&Reload', 268 | accelerator: 'Ctrl+R', 269 | click: () => { 270 | Menu.getApplicationMenu().getMenuItemById( 271 | 'betaFeatures' 272 | ).enabled = false; 273 | app.relaunch(); 274 | app.quit(); 275 | }, 276 | }, 277 | ], 278 | }, 279 | { 280 | label: 'Help', 281 | submenu: [ 282 | { 283 | label: 'How To', 284 | click() { 285 | shell.openExternal( 286 | 'https://www.youtube.com/playlist?list=PLmp58ureC93GrGy14z6HlSYHPQs5hfwp6' 287 | ); 288 | }, 289 | }, 290 | { 291 | label: 'Report Bug', 292 | click() { 293 | shell.openExternal( 294 | 'https://github.com/dlamon1/clockotron/issues' 295 | ); 296 | }, 297 | }, 298 | ], 299 | }, 300 | ]; 301 | 302 | return templateDefault; 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /src/main/preload.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer, contextBridge } = require('electron'); 2 | require('regenerator-runtime/runtime'); 3 | 4 | let messages = []; 5 | 6 | contextBridge.exposeInMainWorld('electron', { 7 | vmix: { 8 | connect: (address) => { 9 | ipcRenderer.invoke('vmixConnect', address); 10 | }, 11 | reqXml: () => { 12 | ipcRenderer.invoke('reqXml'); 13 | }, 14 | reqXmlToUpdateVideoPlayer: () => { 15 | ipcRenderer.invoke('reqXmlToUpdateVideoPlayer'); 16 | }, 17 | reqTally: () => { 18 | ipcRenderer.invoke('reqTally'); 19 | }, 20 | vmixPostReq: (cmd) => { 21 | ipcRenderer.invoke('vmixPostReq', cmd); 22 | }, 23 | shutdown: () => { 24 | ipcRenderer.invoke('socket-shutdown'); 25 | }, 26 | }, 27 | timer: { 28 | start: (id, currentSeconds, interval, isCountingDown) => { 29 | ipcRenderer.invoke( 30 | 'timer-start', 31 | id, 32 | currentSeconds, 33 | interval, 34 | isCountingDown 35 | ); 36 | }, 37 | stop: (id) => { 38 | ipcRenderer.invoke('timer-stop', id); 39 | }, 40 | updateCurrentSeconds: (id, seconds) => { 41 | ipcRenderer.invoke('timer-UpdateCurrentSeconds', id, seconds); 42 | }, 43 | direction: (id, directionIsDown) => { 44 | ipcRenderer.invoke('timer-direction', id, directionIsDown); 45 | }, 46 | upAfterDown: (id, upAfterDown) => { 47 | ipcRenderer.invoke('timer-upAfterDown', id, upAfterDown); 48 | }, 49 | interval: (id, interval) => { 50 | ipcRenderer.invoke('timer-interval', id, interval); 51 | }, 52 | }, 53 | store: { 54 | set: (key, value) => { 55 | ipcRenderer.invoke('store-set', key, value); 56 | }, 57 | get: async (key) => { 58 | let res = await ipcRenderer.invoke('store-get', key); 59 | return res; 60 | }, 61 | }, 62 | on(eventName, callback) { 63 | messages.indexOf(eventName) >= 0 64 | ? ipcRenderer.on(eventName, callback) 65 | : null; 66 | }, 67 | off(eventName, callback) { 68 | messages.indexOf(eventName) >= 0 69 | ? ipcRenderer.removeListener(eventName, callback) 70 | : null; 71 | }, 72 | all() { 73 | ipcRenderer.removeAllListeners(); 74 | }, 75 | enableBetaButton() { 76 | ipcRenderer.invoke('enableBetaButton'); 77 | }, 78 | }); 79 | 80 | messages = [ 81 | 'start', 82 | 'stop', 83 | 'slower', 84 | 'normal', 85 | 'faster', 86 | 'reset', 87 | 'timeSpecific', 88 | '0', 89 | '1', 90 | '2', 91 | '3', 92 | '4', 93 | '5', 94 | '6', 95 | '7', 96 | '8', 97 | '9', 98 | 'postClock', 99 | 'clear', 100 | 'sDown', 101 | 'mDown', 102 | 'hDown', 103 | 'sUp', 104 | 'mUp', 105 | 'hUp', 106 | 'vmixPostReq', 107 | 'socket-xmlDataRes', 108 | 'socket-connected', 109 | 'xmlDataRes', 110 | 'socket-error', 111 | 'version', 112 | 'playPause', 113 | 'handleXmlData', 114 | 'handleXmlTallyData', 115 | 'videoTallyData', 116 | 'inputPlayingData', 117 | 'handleXmlActsData', 118 | 'betaFeatures', 119 | 'newFeaturesHaveBeenSeen', 120 | 'timer-res', 121 | 'timer-stopped', 122 | 'timer-directionChange', 123 | ]; 124 | -------------------------------------------------------------------------------- /src/main/storeClass.js: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron'; 2 | 3 | export class StoreListeners { 4 | setListener; 5 | getListener; 6 | store; 7 | 8 | constructor(store) { 9 | this.store = store; 10 | } 11 | 12 | mountListeners() { 13 | this.setListener = () => { 14 | ipcMain.handle('store-set', (__, key, value) => { 15 | this.store.set(key, value); 16 | }); 17 | }; 18 | this.getListener = () => { 19 | ipcMain.handle('store-get', (__, key) => { 20 | let value = this.store.set(key); 21 | return value; 22 | }); 23 | }; 24 | this.setListener(); 25 | this.getListener(); 26 | } 27 | 28 | removeListeners() { 29 | ipcMain.removeListener('store-set', this.setListener); 30 | ipcMain.removeListener('store-get', this.getListener); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/timers.js: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron'; 2 | 3 | export class Timer { 4 | id; 5 | interval; 6 | startSeconds; 7 | timeout; 8 | expected; 9 | mainWindow; 10 | currentSeconds; 11 | startListenerVar; 12 | directionIsDown = true; 13 | countsUpAfterDown = false; 14 | upAfterDown = false; 15 | 16 | constructor(id, mainWindow) { 17 | this.id = id; 18 | this.mainWindow = mainWindow; 19 | } 20 | 21 | start() { 22 | if (this.directionIsDown) { 23 | this.sendTimerData(this.id, this.currentSeconds - 1); 24 | this.currentSeconds += -1; 25 | } else { 26 | this.sendTimerData(this.id, this.currentSeconds + 1); 27 | this.currentSeconds += 1; 28 | } 29 | this.expected = Date.now() + this.interval; 30 | this.timeout = setTimeout(this.session.bind(this), this.interval); 31 | } 32 | 33 | stop() { 34 | clearTimeout(this.timeout); 35 | } 36 | 37 | session() { 38 | if (this.currentSeconds == 0 && this.directionIsDown && this.upAfterDown) { 39 | this.directionIsDown = false; 40 | this.sendTimerDirectionChange(); 41 | } 42 | 43 | let drift = Date.now() - this.expected; 44 | if (drift > this.interval) { 45 | drift = 0; 46 | clearTimeout(this.timeout); 47 | console.log('error drive > this.interval'); 48 | } 49 | if (this.directionIsDown) { 50 | this.currentSeconds += -1; 51 | } else { 52 | this.currentSeconds += 1; 53 | } 54 | this.sendTimerData(this.id, this.currentSeconds); 55 | 56 | if (this.currentSeconds == 0 && !this.upAfterDown) { 57 | clearTimeout(this.timeout); 58 | this.sendTimerStopped(); 59 | return; 60 | } 61 | 62 | console.log(this.currentSeconds); 63 | 64 | this.expected += this.interval; 65 | 66 | this.timeout = setTimeout(this.session.bind(this), this.interval - drift); 67 | } 68 | 69 | sendTimerDirectionChange() { 70 | this.mainWindow.webContents.send( 71 | 'timer-directionChange', 72 | this.id, 73 | this.directionIsDown 74 | ); 75 | } 76 | 77 | sendTimerData(id, currentSeconds) { 78 | this.mainWindow.webContents.send('timer-res', id, currentSeconds); 79 | } 80 | 81 | sendTimerStopped() { 82 | this.mainWindow.webContents.send('timer-stopped', this.id); 83 | } 84 | 85 | startListener() { 86 | ipcMain.handle( 87 | 'timer-start', 88 | (__, id, currentSeconds, interval, isCountingDown) => { 89 | if (id == this.id) { 90 | this.currentSeconds = currentSeconds; 91 | this.interval = interval; 92 | this.direction = isCountingDown; 93 | this.start(); 94 | } 95 | } 96 | ); 97 | ipcMain.handle('timer-stop', (__, id) => { 98 | if (id == this.id) { 99 | clearTimeout(this.timeout); 100 | } 101 | }); 102 | ipcMain.handle('timer-direction', (__, id, isDirectionDown) => { 103 | if (id == this.id) { 104 | this.directionIsDown = isDirectionDown; 105 | } 106 | }); 107 | ipcMain.handle('timer-upAfterDown', (__, id, upAfterDown) => { 108 | if (id == this.id) { 109 | this.upAfterDown = upAfterDown; 110 | } 111 | }); 112 | ipcMain.handle('timer-interval', (__, id, interval) => { 113 | if (id == this.id) { 114 | this.interval = interval; 115 | } 116 | }); 117 | ipcMain.handle('timer-UpdateCurrentSeconds', (__, id, currentSeconds) => { 118 | if (id == this.id) { 119 | this.currentSeconds = currentSeconds; 120 | } 121 | }); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/updater.js: -------------------------------------------------------------------------------- 1 | import { app, Notification, dialog } from 'electron'; 2 | import { autoUpdater } from 'electron-updater'; 3 | 4 | export function updater(isDev, mainWindow, store) { 5 | if (!isDev) { 6 | autoUpdater.checkForUpdates(); 7 | 8 | setInterval(() => { 9 | autoUpdater.checkForUpdates(); 10 | }, 300000); 11 | 12 | autoUpdater.on('error', (message) => { 13 | console.error('There was a problem updating the application'); 14 | console.error(message); 15 | }); 16 | 17 | const restart = () => { 18 | if (process.platform === 'darwin') { 19 | setImmediate(() => { 20 | app.removeAllListeners('window-all-closed'); 21 | if (mainWindow != null) { 22 | mainWindow.close(); 23 | } 24 | autoUpdater.quitAndInstall(false); 25 | }); 26 | } else { 27 | setImmediate(() => { 28 | autoUpdater.quitAndInstall(); 29 | }); 30 | } 31 | }; 32 | 33 | autoUpdater.on('update-downloaded', (event, releaseNotes, releaseName) => { 34 | store.set('hasNewFeaturesBeenSeen', false); 35 | const dialogOpts = { 36 | type: 'info', 37 | buttons: ['Restart', 'Later'], 38 | title: 'Application Update', 39 | message: process.platform === 'win32' ? releaseNotes : releaseName, 40 | detail: 41 | 'A new version has been downloaded. Restart the application to apply the updates.', 42 | }; 43 | 44 | dialog.showMessageBox(dialogOpts).then((returnValue) => { 45 | if (returnValue.response === 0) { 46 | restart(); 47 | } 48 | }); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/util.js: -------------------------------------------------------------------------------- 1 | /* eslint import/prefer-default-export: off, import/no-mutable-exports: off */ 2 | import { URL } from 'url'; 3 | import path from 'path'; 4 | // import { getVideoDurationInSeconds } from 'get-video-duration'; 5 | 6 | export let resolveHtmlPath; 7 | 8 | if (process.env.NODE_ENV === 'development') { 9 | const port = process.env.PORT || 1212; 10 | resolveHtmlPath = (htmlFileName) => { 11 | const url = new URL(`http://localhost:${port}`); 12 | url.pathname = htmlFileName; 13 | return url.href; 14 | }; 15 | } else { 16 | resolveHtmlPath = (htmlFileName) => { 17 | return `file://${path.resolve(__dirname, '../renderer/', htmlFileName)}`; 18 | }; 19 | } 20 | 21 | // export async function edit(filePath) { 22 | // await getVideoDurationInSeconds(filePath).then((duration) => { 23 | // return duration; 24 | // }); 25 | // } 26 | -------------------------------------------------------------------------------- /src/renderer/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect } from 'react'; 2 | import { observer } from 'mobx-react-lite'; 3 | 4 | import { StoreContext } from './stores/store.context.jsx'; 5 | 6 | import Grid from '@material-ui/core/Grid'; 7 | import Box from '@material-ui/core/Box'; 8 | 9 | import IpInput from './components/ip.app.jsx'; 10 | import Socket from './components/socket.app.jsx'; 11 | import Toast from './components/toast.app.jsx'; 12 | import PostThings from './components/postThing.app.jsx'; 13 | import { PageTab } from './components/tabs.component.jsx'; 14 | import Refresh from './components/refresh.app'; 15 | import { NewFeaturesDialog } from './components/newFeatures.dialog'; 16 | 17 | import Settings from './pages/settings.app.jsx'; 18 | import Timer from './pages/timer.app'; 19 | import Video from './pages/video.app'; 20 | 21 | import './app.css'; 22 | 23 | const App = observer(() => { 24 | const { vmix, clockotron } = useContext(StoreContext); 25 | 26 | return ( 27 | 39 | 40 | 41 | {!vmix.ip ? ( 42 | 43 | ) : ( 44 | <> 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | {clockotron.areBetaFeaturesEnabled && 56 | 57 | ); 58 | }); 59 | 60 | export default App; 61 | -------------------------------------------------------------------------------- /src/renderer/app.css: -------------------------------------------------------------------------------- 1 | html { 2 | user-select: none; 3 | } 4 | ::-webkit-scrollbar { 5 | width: 0; /* Remove scrollbar space */ 6 | background: transparent; 7 | /* optional: just make scrollbar invisible; */ 8 | } 9 | .app::-webkit-scrollbar { 10 | display: none; 11 | } 12 | -------------------------------------------------------------------------------- /src/renderer/components/baseColors.timer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { GithubPicker } from 'react-color'; 4 | 5 | import { colors } from '../utils/ColorPickerColors'; 6 | 7 | import Grid from '@material-ui/core/Grid'; 8 | import Typography from '@material-ui/core/Typography'; 9 | import Accordion from '@material-ui/core/Accordion'; 10 | import AccordionSummary from '@material-ui/core/AccordionSummary'; 11 | import AccordionDetails from '@material-ui/core/AccordionDetails'; 12 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; 13 | import Paper from '@material-ui/core/Paper'; 14 | 15 | import { StoreContext } from '../stores/store.context'; 16 | 17 | const BaseColors = observer((props) => { 18 | const { timer, clockotron } = useContext(StoreContext); 19 | 20 | return ( 21 | clockotron.tabValue === 0 && ( 22 | <> 23 | 24 | 25 | 28 | 31 | } 32 | style={{ backgroundColor: '' }} 33 | > 34 | 37 | DOWN COLOR 38 | 39 | 40 | 41 | 42 | 47 | 55 | 56 | timer.setDownColor(e.hex)} 58 | colors={clockotron.colors} 59 | triangle="hide" 60 | /> 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 75 | } 76 | style={{ backgroundColor: '' }} 77 | > 78 | 81 | UP COLOR 82 | 83 | 84 | 85 | 86 | 91 | 99 | 100 | timer.setUpColor(e.hex)} 102 | colors={clockotron.colors} 103 | triangle="hide" 104 | /> 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | ) 115 | ); 116 | }); 117 | 118 | export default BaseColors; 119 | -------------------------------------------------------------------------------- /src/renderer/components/baseColors.video.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { GithubPicker } from 'react-color'; 4 | 5 | import { colors } from '../utils/ColorPickerColors'; 6 | 7 | import Grid from '@material-ui/core/Grid'; 8 | import Typography from '@material-ui/core/Typography'; 9 | import Accordion from '@material-ui/core/Accordion'; 10 | import AccordionSummary from '@material-ui/core/AccordionSummary'; 11 | import AccordionDetails from '@material-ui/core/AccordionDetails'; 12 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; 13 | import Paper from '@material-ui/core/Paper'; 14 | 15 | import { StoreContext } from '../stores/store.context'; 16 | 17 | const BaseColors = observer((props) => { 18 | const { videoReader, clockotron } = useContext(StoreContext); 19 | 20 | return ( 21 | clockotron.tabValue === 1 && ( 22 | <> 23 | 24 | 25 | 28 | 33 | } 34 | style={{ backgroundColor: '' }} 35 | > 36 | 39 | DOWN COLOR 40 | 41 | 42 | 43 | 44 | 49 | 57 | 58 | 60 | videoReader.setDownColor(e.hex) 61 | } 62 | colors={colors} 63 | triangle="hide" 64 | /> 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | ) 75 | ); 76 | }); 77 | 78 | export default BaseColors; 79 | -------------------------------------------------------------------------------- /src/renderer/components/clock.formated.timer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useContext } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | import { formatTime } from 'renderer/utils/formatTime.jsx'; 5 | 6 | import Grid from '@material-ui/core/Grid'; 7 | import Typography from '@material-ui/core/Typography'; 8 | import { ChevronRight, ChevronLeft } from '@material-ui/icons'; 9 | import { IconButton } from '@material-ui/core'; 10 | 11 | import { StoreContext } from '../stores/store.context'; 12 | 13 | const ClockFormated = observer(() => { 14 | const { timer, clockotron } = useContext(StoreContext); 15 | 16 | useEffect(() => { 17 | let res = formatTime(timer.currentSeconds, timer.formatPositions); 18 | timer.setFormatedTime(res); 19 | }, [timer.currentSeconds, timer.formatPositions]); 20 | 21 | return ( 22 | clockotron.tabValue === 0 && ( 23 | <> 24 | 25 | 26 | timer.setFormatPositions(-1)} 28 | disabled={timer.formatPositions === 1} 29 | > 30 | 31 | 32 | 33 | {timer.formatedTime} 34 | 35 | timer.setFormatPositions(1)} 37 | disabled={timer.formatPositions === 3} 38 | > 39 | 40 | 41 | 42 | 43 | 44 | ) 45 | ); 46 | }); 47 | 48 | export default ClockFormated; 49 | -------------------------------------------------------------------------------- /src/renderer/components/clock.formated.video.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useContext } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | import { formatTime } from 'renderer/utils/formatTime.jsx'; 5 | 6 | import Grid from '@material-ui/core/Grid'; 7 | import Typography from '@material-ui/core/Typography'; 8 | import { ChevronRight, ChevronLeft } from '@material-ui/icons'; 9 | import { IconButton } from '@material-ui/core'; 10 | 11 | import { StoreContext } from '../stores/store.context'; 12 | 13 | const ClockFormated = observer((props) => { 14 | const { videoReader, clockotron } = useContext(StoreContext); 15 | 16 | useEffect(() => { 17 | let res = formatTime( 18 | videoReader.currentSeconds, 19 | videoReader.formatPositions 20 | ); 21 | videoReader.setFormatedTime(res); 22 | }, [videoReader.currentSeconds, videoReader.formatPositions]); 23 | 24 | return ( 25 | clockotron.tabValue === 1 && ( 26 | <> 27 | 28 | 29 | videoReader.setFormatPositions(-1)} 31 | disabled={videoReader.formatPositions === 1} 32 | > 33 | 34 | 35 | 36 | {videoReader.formatedTime} 37 | 38 | videoReader.setFormatPositions(1)} 40 | disabled={videoReader.formatPositions === 3} 41 | > 42 | 43 | 44 | 45 | 46 | 47 | ) 48 | ); 49 | }); 50 | 51 | export default ClockFormated; 52 | -------------------------------------------------------------------------------- /src/renderer/components/clock.input.timer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef, useContext } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import isNumeric from 'validator/lib/isNumeric'; 4 | 5 | import Grid from '@material-ui/core/Grid'; 6 | import TextField from '@material-ui/core/TextField'; 7 | import Button from '@material-ui/core/Button'; 8 | import Box from '@material-ui/core/Box'; 9 | 10 | import { StoreContext } from '../stores/store.context.jsx'; 11 | import { useStyles } from '../utils/AppStyles.jsx'; 12 | 13 | const ClockInput = observer(() => { 14 | const { timer, clockotron } = useContext(StoreContext); 15 | 16 | const classes = useStyles(); 17 | 18 | const [clockStartValue, setClockStartValue] = useState(''); 19 | const [inputSeconds, setInputSeconds] = useState(0); 20 | const ref = useRef(''); 21 | 22 | const formatToHoursMinutesSeconds = (input) => { 23 | const formated = 24 | (input % 100) + 25 | Math.floor((input / 100) % 100) * 60 + 26 | Math.floor(input / 10000) * 3600; 27 | return formated; 28 | }; 29 | 30 | const apiAppendDigitToInput = (digit) => { 31 | ref.current = ref.current + digit; 32 | updateInputForm(ref.current); 33 | }; 34 | 35 | const updateInputForm = (x) => { 36 | if (isNumeric(x)) { 37 | setInputSeconds(formatToHoursMinutesSeconds(x)); 38 | setClockStartValue(x); 39 | } 40 | }; 41 | 42 | const apiUpdateTimeSpecific = (__, valueReceived) => { 43 | postClockGlobally(valueReceived); 44 | }; 45 | 46 | const apiPostClockReq = () => { 47 | postClockGlobally(formatToHoursMinutesSeconds(ref.current)); 48 | }; 49 | 50 | const handleKeyDown = (event) => { 51 | event.key === 'Enter' && postClockGlobally(inputSeconds); 52 | }; 53 | 54 | const postClockGlobally = (input) => { 55 | timer.setCurrentSeconds(input); 56 | timer.setResetSeconds(input); 57 | setClockStartValue(''); 58 | setInputSeconds(0); 59 | ref.current = ''; 60 | }; 61 | 62 | const apiClearInput = () => { 63 | ref.current = ''; 64 | handleChange(ref.current); 65 | setClockStartValue(''); 66 | setInputSeconds(0); 67 | }; 68 | 69 | useEffect(() => { 70 | window.electron.on('0', () => { 71 | apiAppendDigitToInput('0'); 72 | }); 73 | window.electron.on('1', () => { 74 | apiAppendDigitToInput('1'); 75 | }); 76 | window.electron.on('2', () => { 77 | apiAppendDigitToInput('2'); 78 | }); 79 | window.electron.on('3', () => { 80 | apiAppendDigitToInput('3'); 81 | }); 82 | window.electron.on('4', () => { 83 | apiAppendDigitToInput('4'); 84 | }); 85 | window.electron.on('5', () => { 86 | apiAppendDigitToInput('5'); 87 | }); 88 | window.electron.on('6', () => { 89 | apiAppendDigitToInput('6'); 90 | }); 91 | window.electron.on('7', () => { 92 | apiAppendDigitToInput('7'); 93 | }); 94 | window.electron.on('8', () => { 95 | apiAppendDigitToInput('8'); 96 | }); 97 | window.electron.on('9', () => { 98 | apiAppendDigitToInput('9'); 99 | }); 100 | window.electron.on('postClock', apiPostClockReq); 101 | window.electron.on('clear', apiClearInput); 102 | window.electron.on('timeSpecific', apiUpdateTimeSpecific); 103 | 104 | return () => { 105 | window.electron.all(); 106 | }; 107 | }, []); 108 | 109 | return ( 110 | clockotron.tabValue === 0 && ( 111 | <> 112 | 113 | 119 | 120 | 126 | updateInputForm(e.target.value)} 134 | style={{ backgroundColor: '', width: '50%' }} 135 | focus="true" 136 | onKeyDown={handleKeyDown} 137 | InputProps={{ 138 | classes: { 139 | notchedOutline: classes.notchedOutline, 140 | }, 141 | }} 142 | /> 143 | 150 | 151 | 152 | 153 | 154 | 155 | ) 156 | ); 157 | }); 158 | 159 | export default ClockInput; 160 | -------------------------------------------------------------------------------- /src/renderer/components/colors.settings.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from 'react'; 2 | import { GithubPicker, ChromePicker } from 'react-color'; 3 | 4 | import { observer } from 'mobx-react-lite'; 5 | 6 | import { StoreContext } from '../stores/store.context'; 7 | import Grid from '@material-ui/core/Grid'; 8 | import { Typography } from '@material-ui/core'; 9 | 10 | const Colors = observer(() => { 11 | const [i, setI] = useState(0); 12 | 13 | const pickColor = (e) => { 14 | let i = clockotron.indexOfColorInColors(e.hex); 15 | setI(i); 16 | }; 17 | 18 | const updateColor = (e) => { 19 | clockotron.setColor(i, e.hex); 20 | }; 21 | 22 | const { clockotron } = useContext(StoreContext); 23 | return ( 24 | clockotron.tabValue === 2 && ( 25 | 26 | 27 | 30 | Color Selections 31 | 32 | 33 | 34 | updateColor(e)} 36 | color={clockotron.colors[i]} 37 | /> 38 | 39 | 45 | pickColor(e)} 47 | colors={clockotron.colors} 48 | triangle="hide" 49 | /> 50 | 51 | 52 | ) 53 | ); 54 | }); 55 | 56 | export default Colors; 57 | -------------------------------------------------------------------------------- /src/renderer/components/directionOptions.timer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef, useContext } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | import Grid from '@material-ui/core/Grid'; 5 | import Box from '@material-ui/core/Box'; 6 | import Typography from '@material-ui/core/Typography'; 7 | import Checkbox from '@material-ui/core/Checkbox'; 8 | import Switch from '@material-ui/core/Switch'; 9 | 10 | import { StoreContext } from '../stores/store.context'; 11 | 12 | const DirectionOptions = observer(() => { 13 | const { timer, clockotron } = useContext(StoreContext); 14 | 15 | useEffect(() => { 16 | window.electron.timer.direction('timer', true); 17 | }, []); 18 | 19 | return ( 20 | clockotron.tabValue === 0 && ( 21 | <> 22 | 23 | 29 | 30 | 36 | UP 37 | 38 | 41 | timer.setIsCountingDownToMainThread(!timer.isCountingDown) 42 | } 43 | name="checkedA" 44 | /> 45 | DOWN 46 | 47 | 53 | 56 | timer.setCountUpAfterDownReachesZero( 57 | !timer.countUpAfterDownReachesZero 58 | ) 59 | } 60 | inputProps={{ 'aria-label': 'primary checkbox' }} 61 | /> 62 | 63 | UP AFTER DOWN 64 | 65 | 66 | 67 | 68 | 69 | 70 | ) 71 | ); 72 | }); 73 | 74 | export default DirectionOptions; 75 | -------------------------------------------------------------------------------- /src/renderer/components/inputSelector.timer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useContext } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | import Select from '@material-ui/core/Select'; 5 | import MenuItem from '@material-ui/core/MenuItem'; 6 | import Grid from '@material-ui/core/Grid'; 7 | import FormControl from '@material-ui/core/FormControl'; 8 | import InputLabel from '@material-ui/core/InputLabel'; 9 | 10 | import { StoreContext } from '../stores/store.context'; 11 | 12 | const TextInputList = observer(() => { 13 | const { vmix, timer, clockotron } = useContext(StoreContext); 14 | 15 | const [inSelected, setInSelected] = useState(''); 16 | const [textSelected, setTextSelected] = useState(''); 17 | const [inputList, setInputList] = useState([]); 18 | const [textList, setTextList] = useState([]); 19 | 20 | const handleChange = (event) => { 21 | setTextSelected(''); 22 | setInSelected(event.target.value); 23 | timer.setInput(event.target.value); 24 | let selected = inputList.filter( 25 | (input) => input.title == event.target.value 26 | ); 27 | let arr = selected[0].text; 28 | let texts; 29 | switch (true) { 30 | case Array.isArray(arr): 31 | texts = []; 32 | arr.forEach((text) => texts.push(text.name)); 33 | setTextList(texts); 34 | break; 35 | case true: 36 | texts = arr.name; 37 | let textsList = []; 38 | textsList.push(texts); 39 | setTextList(textsList); 40 | break; 41 | default: 42 | break; 43 | } 44 | }; 45 | 46 | const handleTextChange = (event) => { 47 | setTextSelected(event.target.value); 48 | timer.setText(event.target.value); 49 | }; 50 | 51 | const setInputs = () => { 52 | let list = vmix.inputs; 53 | let filtered = list.filter( 54 | (item) => item.type === 'GT' || item.type === 'Xaml' 55 | ); 56 | setInputList(filtered); 57 | }; 58 | 59 | useEffect(() => { 60 | vmix.inputs && setInputs(); 61 | }, [vmix.inputs]); 62 | 63 | return ( 64 | clockotron.tabValue === 0 && ( 65 | <> 66 | 67 | 68 | 69 | Input 70 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | Text Layer 88 | 99 | 100 | 101 | 102 | 103 | ) 104 | ); 105 | }); 106 | 107 | export default TextInputList; 108 | -------------------------------------------------------------------------------- /src/renderer/components/inputSelector.video.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useContext } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | import Select from '@material-ui/core/Select'; 5 | import MenuItem from '@material-ui/core/MenuItem'; 6 | import Grid from '@material-ui/core/Grid'; 7 | import FormControl from '@material-ui/core/FormControl'; 8 | import InputLabel from '@material-ui/core/InputLabel'; 9 | 10 | import { StoreContext } from '../stores/store.context'; 11 | 12 | const TextInputList = observer((props) => { 13 | const { i } = props; 14 | const { vmix, videoReader, clockotron } = useContext(StoreContext); 15 | 16 | const [inSelected, setInSelected] = useState(''); 17 | const [textSelected, setTextSelected] = useState(''); 18 | const [inputList, setInputList] = useState([]); 19 | const [textList, setTextList] = useState([]); 20 | 21 | const handleChange = (event) => { 22 | setTextSelected(''); 23 | setInSelected(event.target.value); 24 | videoReader.setInput(event.target.value); 25 | let selected = inputList.filter( 26 | (input) => input.title == event.target.value 27 | ); 28 | let arr = selected[0].text; 29 | let texts; 30 | switch (true) { 31 | case Array.isArray(arr): 32 | texts = []; 33 | arr.forEach((text) => texts.push(text.name)); 34 | setTextList(texts); 35 | break; 36 | case true: 37 | texts = arr.name; 38 | let textsList = []; 39 | textsList.push(texts); 40 | setTextList(textsList); 41 | break; 42 | default: 43 | break; 44 | } 45 | }; 46 | 47 | const handleTextChange = (event) => { 48 | setTextSelected(event.target.value); 49 | videoReader.setText(event.target.value); 50 | }; 51 | 52 | const setInputs = () => { 53 | let list = vmix.inputs; 54 | let filtered = list.filter( 55 | (item) => item.type === 'GT' || item.type === 'Xaml' 56 | ); 57 | setInputList(filtered); 58 | }; 59 | 60 | useEffect(() => { 61 | vmix.inputs && setInputs(); 62 | }, [vmix.inputs]); 63 | 64 | return ( 65 | clockotron.tabValue === 1 && ( 66 | <> 67 | 68 | 69 | 70 | Input 71 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | Text Layer 89 | 100 | 101 | 102 | 103 | 104 | ) 105 | ); 106 | }); 107 | 108 | export default TextInputList; 109 | -------------------------------------------------------------------------------- /src/renderer/components/ip.app.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useContext } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | import TextField from '@material-ui/core/TextField'; 5 | import Button from '@material-ui/core/Button'; 6 | import Grid from '@material-ui/core/Grid'; 7 | import Box from '@material-ui/core/Box'; 8 | import Version from './version.app.jsx'; 9 | 10 | import { StoreContext } from '../stores/store.context'; 11 | 12 | import { useStyles } from '../utils/AppStyles.jsx'; 13 | 14 | const IpForm = observer(() => { 15 | const classes = useStyles(); 16 | const { vmix, clockotron } = useContext(StoreContext); 17 | 18 | const [ip, setIpp] = useState('127.0.0.1'); 19 | 20 | const connected = () => { 21 | vmix.connected(); 22 | }; 23 | 24 | const newFeatures = (_, value) => { 25 | clockotron.setHasNewFeaturesDialogBeenSeen(value); 26 | }; 27 | 28 | useEffect(() => { 29 | window.electron.on('socket-connected', connected); 30 | window.electron.on('newFeaturesHaveBeenSeen', newFeatures); 31 | 32 | return () => { 33 | window.electron.all(); 34 | }; 35 | }, []); 36 | 37 | return ( 38 | 39 | 45 | 51 | 52 | 53 | setIpp(e.target.value)} 61 | style={{ backgroundColor: '', width: '90%' }} 62 | focus="true" 63 | InputProps={{ 64 | classes: { 65 | notchedOutline: classes.notchedOutline, 66 | }, 67 | }} 68 | /> 69 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | ); 89 | }); 90 | 91 | export default IpForm; 92 | -------------------------------------------------------------------------------- /src/renderer/components/newFeatures.dialog.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { observer } from 'mobx-react-lite'; 3 | 4 | import Dialog from '@material-ui/core/Dialog'; 5 | import Grid from '@material-ui/core/Grid'; 6 | import Typography from '@material-ui/core/Typography'; 7 | import Paper from '@material-ui/core/Paper'; 8 | import Button from '@material-ui/core/Button'; 9 | 10 | import { StoreContext } from '../stores/store.context'; 11 | 12 | export const NewFeaturesDialog = observer(() => { 13 | const { clockotron } = useContext(StoreContext); 14 | 15 | const close = () => { 16 | clockotron.setHasNewFeaturesDialogBeenSeen(true); 17 | }; 18 | 19 | return ( 20 | 21 | 22 | 31 | 41 | Update 1.0.2 42 | 43 | 52 | The formated time now reflects the total remaining time when 53 | adjusting the timer positions. Ex/ a one minute and 15 second timer 54 | with one position will show 75 seconds. Please report any bugs you 55 | find. Thanks! 56 | 57 | {/* 67 | To enable it, navigate to the menu bar, click View, then click 68 | Toggle Video TRT. 69 | */} 70 | 76 | 77 | 78 | 79 | ); 80 | }); 81 | -------------------------------------------------------------------------------- /src/renderer/components/playPause.timer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect, useContext } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | import Grid from '@material-ui/core/Grid'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import Button from '@material-ui/core/Button'; 7 | 8 | import { formatTime } from '../utils/formatTime'; 9 | import { StoreContext } from '../stores/store.context'; 10 | 11 | const PlayPause = observer(() => { 12 | const { timer, clockotron } = useContext(StoreContext); 13 | 14 | const [buttonState, setButtonState] = useState('Start'); 15 | const [speed, setSpeed] = useState(100); 16 | const [realRemaing, setRealRemaining] = useState(''); 17 | 18 | const directionRef = useRef(-1); 19 | 20 | let start = () => { 21 | toggle(); 22 | }; 23 | let stop = () => { 24 | toggle(); 25 | }; 26 | let slower = () => { 27 | timer.updateInterval(1.01); 28 | }; 29 | let normal = () => { 30 | timer.updateInterval(1); 31 | }; 32 | let faster = () => { 33 | timer.updateInterval(0.99); 34 | }; 35 | let resetApi = () => { 36 | reset(); 37 | }; 38 | 39 | function timerDataFromMainThread(__, id, currentSeconds) { 40 | if (id == 'timer') { 41 | timer.setCurrentSeconds(currentSeconds); 42 | } 43 | } 44 | 45 | function mainThreadTimerStopped() { 46 | timer.setIsRunning(false); 47 | setButtonState('start'); 48 | } 49 | 50 | function mainThreadTimeDirectionChange(__, id, directionIsDown) { 51 | if (id == 'timer') timer.setIsCountingDown(directionIsDown); 52 | } 53 | 54 | function toggle() { 55 | if (timer.currentSeconds + directionRef.current > -1) { 56 | timer.isRunning ? stopClock() : startClock(); 57 | } 58 | } 59 | 60 | function reset() { 61 | stopClock(); 62 | timer.setCurrentSeconds(0); 63 | } 64 | 65 | function startClock() { 66 | timer.startMainThreadTimer(timer.interval); 67 | timer.setIsRunning(true); 68 | setButtonState('pause'); 69 | } 70 | 71 | function stopClock() { 72 | timer.stopMainThreadTimer(); 73 | timer.setIsRunning(false); 74 | setButtonState('start'); 75 | } 76 | 77 | useEffect(() => { 78 | timer.intervalMainThreadTimer(); 79 | setSpeed(Math.round(100000 / timer.interval)); 80 | }, [timer.interval]); 81 | 82 | useEffect(() => { 83 | timer.isCountingDown 84 | ? (directionRef.current = -1) 85 | : (directionRef.current = 1); 86 | }, [timer.isCountingDown]); 87 | 88 | useEffect(() => { 89 | let x = Math.floor((timer.currentSeconds / speed) * 100); 90 | let formatedTime = formatTime(x, 3); 91 | setRealRemaining(formatedTime); 92 | }, [timer.currentSeconds]); 93 | 94 | useEffect(() => { 95 | window.electron.on('timer-directionChange', mainThreadTimeDirectionChange); 96 | window.electron.on('timer-res', timerDataFromMainThread); 97 | window.electron.on('timer-stopped', mainThreadTimerStopped); 98 | window.electron.on('start', start); 99 | window.electron.on('stop', stop); 100 | window.electron.on('slower', slower); 101 | window.electron.on('normal', normal); 102 | window.electron.on('faster', faster); 103 | window.electron.on('reset', resetApi); 104 | 105 | return () => { 106 | window.electron.all(); 107 | }; 108 | }, []); 109 | 110 | return ( 111 | clockotron.tabValue === 0 && ( 112 | <> 113 | 114 | 115 | 123 | 131 | 132 | 133 | 134 | 135 | 136 | 140 | Clock Speed: {speed} % 141 | 142 | 143 | 144 | 145 | 146 | 147 | Approx Remain : {realRemaing} 148 | 149 | 150 | 151 | 152 | 153 | 159 | 165 | 171 | 172 | 173 | 174 | ) 175 | ); 176 | }); 177 | 178 | export default PlayPause; 179 | -------------------------------------------------------------------------------- /src/renderer/components/postThing.app.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useContext } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | import { StoreContext } from '../stores/store.context'; 5 | 6 | const PostThings = observer((props) => { 7 | const { vmix, timer, videoReader } = useContext(StoreContext); 8 | 9 | const postTimes = async (time, input, text) => { 10 | window.electron.vmix.vmixPostReq( 11 | `SetText Input=${input}&SelectedName=${text}&Value=${time}` 12 | ); 13 | }; 14 | 15 | const postColor = async (input, text, color) => { 16 | window.electron.vmix.vmixPostReq( 17 | `SetTextColour Input=${input}&SelectedName=${text}&Value=${color}` 18 | ); 19 | }; 20 | 21 | useEffect(() => { 22 | try { 23 | vmix.ip && timer.input && timer.text && timer.color 24 | ? postColor(timer.input, timer.text, timer.color) 25 | : null; 26 | } catch (err) { 27 | console.log(err); 28 | } 29 | }, [timer.color, timer.text]); 30 | 31 | useEffect(() => { 32 | try { 33 | vmix.ip && timer.input && timer.text && timer.color 34 | ? postTimes(timer.formatedTime, timer.input, timer.text) 35 | : null; 36 | } catch (err) { 37 | console.log(err); 38 | } 39 | }, [timer.formatedTime, timer.text]); 40 | 41 | useEffect(() => { 42 | try { 43 | vmix.ip && videoReader.input && videoReader.text && videoReader.color 44 | ? postColor(videoReader.input, videoReader.text, videoReader.color) 45 | : null; 46 | } catch (err) { 47 | console.log(err); 48 | } 49 | }, [videoReader.color, videoReader.text]); 50 | 51 | useEffect(() => { 52 | try { 53 | vmix.ip && videoReader.input && videoReader.text && videoReader.color 54 | ? postTimes( 55 | videoReader.formatedTime, 56 | videoReader.input, 57 | videoReader.text 58 | ) 59 | : null; 60 | } catch (err) { 61 | console.log(err); 62 | } 63 | }, [videoReader.formatedTime, videoReader.text]); 64 | return <>; 65 | }); 66 | 67 | export default PostThings; 68 | -------------------------------------------------------------------------------- /src/renderer/components/refresh.app.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { observer } from 'mobx-react-lite'; 3 | 4 | import { StoreContext } from '../stores/store.context'; 5 | 6 | import Grid from '@material-ui/core/Grid'; 7 | import Button from '@material-ui/core/Button'; 8 | 9 | const Refresh = observer(() => { 10 | const { vmix, clockotron } = useContext(StoreContext); 11 | 12 | const timeout = () => { 13 | setTimeout(() => { 14 | vmix.ip && window.electron.vmix.reqXml(); 15 | timeout(); 16 | }, 3000); 17 | }; 18 | 19 | const refresh = () => { 20 | vmix.refresh(); 21 | }; 22 | 23 | // timeout(); 24 | 25 | return ( 26 | clockotron.tabValue === 2 || 27 | (clockotron.tabValue === 2 && ( 28 | 33 | 34 | 35 | )) 36 | ); 37 | }); 38 | 39 | export default Refresh; 40 | -------------------------------------------------------------------------------- /src/renderer/components/socket.app.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useContext } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | import { StoreContext } from '../stores/store.context'; 5 | 6 | const Socket = observer(() => { 7 | const { videoReader, vmix, clockotron } = useContext(StoreContext); 8 | 9 | const socketError = (__, error) => { 10 | connectError(); 11 | vmix.setIp(''); 12 | vmix.setIsSocketConnected(false); 13 | }; 14 | 15 | const handleXmlData = (__, data) => { 16 | videoReader.handleNewXmlData(data); 17 | vmix.updateInputList(data); 18 | }; 19 | 20 | const handleXmlTallyData = (__, data) => { 21 | videoReader.handleNewXmlData(data); 22 | vmix.updateInputList(data); 23 | videoReader.updateMountedInputIndex(); 24 | }; 25 | 26 | const handleXmlActsData = (__, data) => { 27 | videoReader.handleNewXmlData(data); 28 | videoReader.updateMountedInputIndex(); 29 | }; 30 | 31 | const handleTallyData = (__, data) => { 32 | videoReader.handleNewTallyData(data); 33 | }; 34 | 35 | const handleInputPlayingData = (__, data) => { 36 | videoReader.updateIsPlaying(data); 37 | }; 38 | 39 | const betaFeatures = (__, boolean) => { 40 | if (boolean) { 41 | clockotron.setAreBetaFeaturesEnabled(boolean); 42 | clockotron.setTabValue(1); 43 | } else { 44 | clockotron.setTabValue(0); 45 | clockotron.setAreBetaFeaturesEnabled(boolean); 46 | } 47 | }; 48 | 49 | useEffect(() => { 50 | clockotron.enableBetaButton(); 51 | window.electron.on('betaFeatures', betaFeatures); 52 | window.electron.on('socket-error', socketError); 53 | window.electron.on('handleXmlData', handleXmlData); 54 | window.electron.on('handleXmlTallyData', handleXmlTallyData); 55 | window.electron.on('handleXmlActsData', handleXmlActsData); 56 | window.electron.on('videoTallyData', handleTallyData); 57 | window.electron.on('inputPlayingData', handleInputPlayingData); 58 | 59 | return () => { 60 | vmix.isSocketConnect && window.electron.vmix.shutdown(); 61 | vmix.isSocketConnect && window.electron.all(); 62 | }; 63 | }, []); 64 | 65 | // When the IP is set, make an initial request for XML data 66 | useEffect(() => { 67 | vmix.ip && window.electron.vmix.reqTally(); 68 | }, [vmix.ip]); 69 | 70 | return <>; 71 | }); 72 | 73 | export default Socket; 74 | -------------------------------------------------------------------------------- /src/renderer/components/tabs.component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState, useEffect } from 'react'; 2 | import { observer } from 'mobx-react-lite'; 3 | 4 | import { StoreContext } from '../stores/store.context'; 5 | 6 | import Tabs from '@material-ui/core/Tabs'; 7 | import Tab from '@material-ui/core/Tab'; 8 | import Grid from '@material-ui/core/Grid'; 9 | import Paper from '@material-ui/core/Paper'; 10 | 11 | export const PageTab = observer(() => { 12 | const { clockotron, videoReader, vmix } = useContext(StoreContext); 13 | 14 | const [isOnAir, setIsOnAir] = useState(''); 15 | 16 | let areBetaFeaturesEnabled = clockotron.areBetaFeaturesEnabled; 17 | 18 | const changeTabs = (e, value) => { 19 | clockotron.setTabValue(value); 20 | }; 21 | 22 | useEffect(() => { 23 | let input = videoReader.vmixInputs[videoReader.mountedInputIndex]; 24 | if (!input) { 25 | return; 26 | } 27 | if (input.isVideo) { 28 | setIsOnAir('red'); 29 | } else { 30 | setIsOnAir(''); 31 | } 32 | }, [JSON.stringify(videoReader.vmixInputs), videoReader.mountedInputIndex]); 33 | 34 | return ( 35 | 40 | 41 | 46 | 51 | 56 | {areBetaFeaturesEnabled && ( 57 | 62 | )} 63 | 64 | 65 | 66 | ); 67 | }); 68 | -------------------------------------------------------------------------------- /src/renderer/components/timerDown.timer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useContext } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | import Grid from '@material-ui/core/Grid'; 5 | import ArrowDropDownRoundedIcon from '@material-ui/icons/ArrowDropDownRounded'; 6 | import IconButton from '@material-ui/core/IconButton'; 7 | 8 | import { StoreContext } from '../stores/store.context'; 9 | 10 | const TimeDown = observer(() => { 11 | const { timer, clockotron } = useContext(StoreContext); 12 | 13 | let m = -8.7; 14 | 15 | let hDown = () => 16 | 3600 <= parseInt(timer.currentSeconds) && 17 | timer.setCurrentSeconds(timer.currentSeconds - 3600); 18 | let mDown = () => 19 | timer.currentSeconds > 61 && 20 | timer.setCurrentSeconds(timer.currentSeconds - 60); 21 | let sDown = () => 22 | timer.currentSeconds > 0 && 23 | timer.setCurrentSeconds(timer.currentSeconds - 1); 24 | 25 | useEffect(() => { 26 | window.electron.on('sDown', sDown); 27 | window.electron.on('mDown', mDown); 28 | window.electron.on('hDown', hDown); 29 | 30 | return () => { 31 | window.electron.all(); 32 | }; 33 | }, []); 34 | 35 | return ( 36 | clockotron.tabValue === 0 && ( 37 | <> 38 | 39 | 40 | {timer.formatPositions >= 3 && ( 41 | 43 | timer.setCurrentSeconds(timer.currentSeconds - 3600) 44 | } 45 | disabled={3600 >= parseInt(timer.currentSeconds)} 46 | style={{ 47 | marginLeft: m, 48 | marginRight: m, 49 | }} 50 | > 51 | 59 | 60 | )} 61 | {timer.formatPositions >= 2 && ( 62 | 64 | timer.setCurrentSeconds(timer.currentSeconds - 60) 65 | } 66 | disabled={timer.currentSeconds < 61} 67 | style={{ 68 | marginLeft: m, 69 | marginRight: m, 70 | }} 71 | > 72 | 80 | 81 | )} 82 | {timer.formatPositions >= 1 && ( 83 | 85 | timer.setCurrentSeconds(timer.currentSeconds - 1) 86 | } 87 | disabled={timer.currentSeconds <= 0} 88 | style={{ 89 | marginLeft: m, 90 | marginRight: m, 91 | }} 92 | > 93 | 101 | 102 | )} 103 | 104 | 105 | 106 | ) 107 | ); 108 | }); 109 | 110 | export default TimeDown; 111 | -------------------------------------------------------------------------------- /src/renderer/components/timerUp.timer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useContext } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | import Grid from '@material-ui/core/Grid'; 5 | import ArrowDropUpRoundedIcon from '@material-ui/icons/ArrowDropUpRounded'; 6 | import IconButton from '@material-ui/core/IconButton'; 7 | import { StoreContext } from '../stores/store.context'; 8 | 9 | const TimeUp = observer((props) => { 10 | const { timer, clockotron } = useContext(StoreContext); 11 | 12 | let m = -8.7; 13 | 14 | let hUp = () => timer.setCurrentSeconds(timer.currentSeconds + 3600); 15 | let mUp = () => timer.setCurrentSeconds(timer.currentSeconds + 60); 16 | let sUp = () => timer.setCurrentSeconds(timer.currentSeconds + 1); 17 | 18 | useEffect(() => { 19 | window.electron.on('sUp', sUp); 20 | window.electron.on('mUp', mUp); 21 | window.electron.on('hUp', hUp); 22 | 23 | return () => { 24 | window.electron.all(); 25 | }; 26 | }, []); 27 | 28 | return ( 29 | clockotron.tabValue === 0 && ( 30 | <> 31 | 32 | 33 | {timer.formatPositions >= 3 && ( 34 | 36 | timer.setCurrentSeconds(timer.currentSeconds + 3600) 37 | } 38 | style={{ 39 | marginLeft: m, 40 | marginRight: m, 41 | }} 42 | > 43 | 52 | 53 | )} 54 | {timer.formatPositions >= 2 && ( 55 | 57 | timer.setCurrentSeconds(timer.currentSeconds + 60) 58 | } 59 | style={{ 60 | marginLeft: m, 61 | marginRight: m, 62 | }} 63 | > 64 | 73 | 74 | )} 75 | {timer.formatPositions >= 1 && ( 76 | 78 | timer.setCurrentSeconds(timer.currentSeconds + 1) 79 | } 80 | style={{ 81 | marginLeft: m, 82 | marginRight: m, 83 | }} 84 | > 85 | 94 | 95 | )} 96 | 97 | 98 | 99 | ) 100 | ); 101 | }); 102 | 103 | export default TimeUp; 104 | -------------------------------------------------------------------------------- /src/renderer/components/toast.app.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | import Snackbar from '@material-ui/core/Snackbar'; 5 | import MuiAlert from '@material-ui/lab/Alert'; 6 | 7 | import { makeStyles } from '@material-ui/core/styles'; 8 | import { StoreContext } from '../stores/store.context'; 9 | 10 | function Alert(props) { 11 | return ; 12 | } 13 | 14 | const useStyles = makeStyles((theme) => ({ 15 | root: { 16 | width: '100%', 17 | '& > * + *': { 18 | marginTop: theme.spacing(500), 19 | }, 20 | }, 21 | })); 22 | 23 | const Toast = observer(() => { 24 | const classes = useStyles(); 25 | const { alertStore } = useContext(StoreContext); 26 | 27 | const handleClose = (event, reason) => { 28 | if (reason === 'clickaway') { 29 | return; 30 | } 31 | setTimeout(alertStore.close(), 1500); 32 | }; 33 | 34 | return ( 35 |
36 | 41 | 42 | {alertStore.text} 43 | 44 | 45 |
46 | ); 47 | }); 48 | 49 | export default Toast; 50 | -------------------------------------------------------------------------------- /src/renderer/components/trigger.color.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { GithubPicker } from 'react-color'; 4 | 5 | import Grid from '@material-ui/core/Grid'; 6 | import Paper from '@material-ui/core/Paper'; 7 | import AccordionDetails from '@material-ui/core/AccordionDetails'; 8 | 9 | import { colors } from 'renderer/utils/ColorPickerColors'; 10 | 11 | import { StoreContext } from '../stores/store.context'; 12 | 13 | const ColorTrigger = observer((props) => { 14 | let { triggerId, colorId } = props; 15 | const { timer, clockotron } = useContext(StoreContext); 16 | let trigger = timer.triggers.filter((x) => x.id === triggerId)[0]; 17 | let color = trigger.colors.filter((x) => x.id === colorId)[0]; 18 | 19 | const pickColor = (x) => { 20 | trigger.setColor(x); 21 | color.setColor(x); 22 | }; 23 | 24 | return ( 25 | <> 26 | 27 | 37 | 38 | 39 | pickColor(e.hex)} 41 | colors={clockotron.colors} 42 | triangle="hide" 43 | /> 44 | 45 | 46 | 47 | 48 | 49 | ); 50 | }); 51 | 52 | export default ColorTrigger; 53 | -------------------------------------------------------------------------------- /src/renderer/components/trigger.details.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useContext } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | import LayerTrigger from './trigger.layer'; 5 | import ColorTrigger from './trigger.color'; 6 | import PlayPauseTrigger from './trigger.playPause'; 7 | 8 | import { formatTime } from 'renderer/utils/formatTime'; 9 | 10 | import Grid from '@material-ui/core/Grid'; 11 | import Typography from '@material-ui/core/Typography'; 12 | import Accordion from '@material-ui/core/Accordion'; 13 | import AccordionSummary from '@material-ui/core/AccordionSummary'; 14 | import AccordionDetails from '@material-ui/core/AccordionDetails'; 15 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; 16 | import TextField from '@material-ui/core/TextField'; 17 | import InputLabel from '@material-ui/core/InputLabel'; 18 | import FormControl from '@material-ui/core/FormControl'; 19 | import Select from '@material-ui/core/Select'; 20 | import Paper from '@material-ui/core/Paper'; 21 | import DeleteForeverIcon from '@material-ui/icons/DeleteForever'; 22 | import IconButton from '@material-ui/core/IconButton'; 23 | import Checkbox from '@material-ui/core/Checkbox'; 24 | 25 | import { StoreContext } from '../stores/store.context'; 26 | 27 | const TriggerDetail = observer((props) => { 28 | const { timer } = useContext(StoreContext); 29 | const { triggerId, timerId } = props; 30 | let trigger = timer.triggers.filter((x) => x.id === triggerId)[0]; 31 | 32 | const [unit, setUnit] = useState(1); 33 | const [backgroundColor, setBackgroundColor] = useState('#aaa'); 34 | const [time, setTime] = useState(trigger.time); 35 | const [title, setTitle] = useState('00:00:00'); 36 | const [type, setType] = useState(''); 37 | 38 | const removeTrigger = () => { 39 | timer.removeTrigger(trigger.id); 40 | }; 41 | 42 | const addType = (e) => { 43 | switch (e) { 44 | case 'color': 45 | trigger.addColor(); 46 | break; 47 | case 'layer': 48 | trigger.addLayer(); 49 | break; 50 | case 'playPause': 51 | trigger.addPlayPause(); 52 | break; 53 | } 54 | setType(''); 55 | }; 56 | 57 | useEffect(() => { 58 | trigger.setFontColor(); 59 | }, [trigger.color]); 60 | 61 | useEffect(() => { 62 | trigger.setTime(time * unit); 63 | let res = formatTime(time * unit, 3); 64 | setTitle(res); 65 | }, [unit, time]); 66 | 67 | useEffect(() => { 68 | let res = formatTime(trigger.time, 3); 69 | setTitle(res); 70 | addType('color'); 71 | }, []); 72 | 73 | return ( 74 | 75 | } 77 | style={{ backgroundColor: '' }} 78 | > 79 | 80 | @ {title} 81 | 82 | 83 | 89 | trigger.setIsUp(!trigger.isUp)} 92 | style={{ color: trigger.fontColor }} 93 | /> 94 | 95 | UP 96 | 97 | trigger.setIsDown(!trigger.isDown)} 100 | style={{ color: trigger.fontColor }} 101 | /> 102 | 103 | DOWN 104 | 105 | 106 | 107 | 116 | 117 | 118 | 119 | setTime(e.target.value)} 127 | /> 128 | 129 | Unit 130 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | {trigger.colors.map((color, index) => ( 149 | 155 | ))} 156 | {trigger.layers.map((layer, index) => ( 157 | 163 | ))} 164 | {trigger.playPauses.map((playPause, index) => ( 165 | 171 | ))} 172 | 173 | 182 | 183 | 184 | 189 | Add Type 190 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | ); 220 | }); 221 | 222 | export default TriggerDetail; 223 | -------------------------------------------------------------------------------- /src/renderer/components/trigger.layer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useContext } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | import Select from '@material-ui/core/Select'; 5 | import MenuItem from '@material-ui/core/MenuItem'; 6 | import Grid from '@material-ui/core/Grid'; 7 | import FormControl from '@material-ui/core/FormControl'; 8 | import InputLabel from '@material-ui/core/InputLabel'; 9 | import Paper from '@material-ui/core/Paper'; 10 | import AccordionDetails from '@material-ui/core/AccordionDetails'; 11 | 12 | import { StoreContext } from '../stores/store.context'; 13 | 14 | const LayerTrigger = observer((props) => { 15 | let { triggerId, layerId } = props; 16 | const { vmix, timer } = useContext(StoreContext); 17 | let trigger = timer.triggers.filter((x) => x.id === triggerId)[0]; 18 | let layer = trigger.layers.filter((x) => x.id === layerId)[0]; 19 | 20 | const [modeSelected, setModeSelected] = useState(''); 21 | const [inSelected, setInSelected] = useState(''); 22 | const [layerSelected, setLayerSelected] = useState(''); 23 | const [inputList, setInputList] = useState([]); 24 | const [textList, setTextList] = useState([]); 25 | const layerArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 26 | const modes = [ 27 | { label: 'Toggle', command: 'MultiViewOverlay' }, 28 | { label: 'On', command: 'MultiViewOverlayOn' }, 29 | { label: 'Off', command: 'MultiViewOverlayOff' }, 30 | ]; 31 | 32 | const handleModeChange = (event) => { 33 | setModeSelected(event.target.value); 34 | const i = modes.findIndex((m) => m.label == event.target.value); 35 | layer.setCommand(modes[i].command); 36 | }; 37 | 38 | const handleInputChange = (event) => { 39 | setInSelected(event.target.value); 40 | const i = inputList.findIndex((i) => i.title == event.target.value); 41 | layer.setInput(inputList[i].key); 42 | }; 43 | 44 | const handleLayerChange = (event) => { 45 | setLayerSelected(event.target.value); 46 | layer.setLayer(event.target.value); 47 | }; 48 | 49 | const triggerLayerUpdate = () => { 50 | let cmd = layer.command; 51 | let input = layer.input; 52 | let inputLayer = layer.layer; 53 | window.electron.vmix.vmixPostReq( 54 | `${cmd} Input=${input}&Value=${inputLayer}` 55 | ); 56 | }; 57 | 58 | useEffect(() => { 59 | if ( 60 | (timer.isRunning && 61 | timer.isCountingDown && 62 | trigger.isDown && 63 | layer.command && 64 | timer.currentSeconds == trigger.time) || 65 | (timer.isRunning && 66 | !timer.isCountingDown && 67 | trigger.isUp && 68 | layer.command && 69 | timer.currentSeconds == trigger.time) 70 | ) { 71 | triggerLayerUpdate(); 72 | } 73 | }, [timer.currentSeconds]); 74 | 75 | useEffect(() => { 76 | vmix.inputs && setInputList(vmix.inputs); 77 | }, [vmix.inputs]); 78 | 79 | return ( 80 | <> 81 | 82 | 92 | 93 | 94 | 95 | Mode 96 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | Input 114 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | Layer 132 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | ); 150 | }); 151 | 152 | export default LayerTrigger; 153 | -------------------------------------------------------------------------------- /src/renderer/components/trigger.parent.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useContext } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | import TriggerDetail from './trigger.details'; 5 | 6 | import Grid from '@material-ui/core/Grid'; 7 | import Button from '@material-ui/core/Button'; 8 | import { StoreContext } from '../stores/store.context'; 9 | 10 | const Triggers = observer(() => { 11 | const { timer, clockotron } = useContext(StoreContext); 12 | 13 | const updateColorWhileDecrementing = () => { 14 | let triggerArray = JSON.parse(JSON.stringify(timer.triggers)); 15 | let downColorObj = { time: 10000000, color: timer.downColor }; 16 | let filteredArray = triggerArray.filter( 17 | (trigger) => trigger.isDown === true 18 | ); 19 | filteredArray.push(downColorObj); 20 | filteredArray.sort(function (a, b) { 21 | return a.time - b.time; 22 | }); 23 | let newColor = filteredArray[0].color; 24 | for (let i = 0; i < filteredArray.length; i++) { 25 | newColor = filteredArray[i].color; 26 | if (timer.currentSeconds <= filteredArray[i].time) { 27 | break; 28 | } 29 | } 30 | timer.color != newColor && timer.setColor(newColor); 31 | }; 32 | 33 | const updateColorWhileIncrementing = () => { 34 | let triggerArray = JSON.parse(JSON.stringify(timer.triggers)); 35 | let upColorObj = { time: 10000000, color: timer.upColor }; 36 | let filteredArray = triggerArray.filter((trigger) => trigger.isUp === true); 37 | filteredArray.push(upColorObj); 38 | filteredArray.sort(function (a, b) { 39 | return a.time - b.time; 40 | }); 41 | let newColor = filteredArray[0].color; 42 | for (let i = 0; i < filteredArray.length; i++) { 43 | newColor = filteredArray[i].color; 44 | if (timer.currentSeconds <= filteredArray[i].time) { 45 | break; 46 | } 47 | } 48 | timer.color != newColor && timer.setColor(newColor); 49 | }; 50 | 51 | useEffect(() => { 52 | timer.isCountingDown && updateColorWhileDecrementing(); 53 | !timer.isCountingDown && updateColorWhileIncrementing(); 54 | }, [ 55 | timer.currentSeconds, 56 | timer.downColor, 57 | timer.upColor, 58 | timer.isCountingDown, 59 | JSON.stringify(timer.triggers), 60 | ]); 61 | 62 | return ( 63 | clockotron.tabValue === 0 && ( 64 | <> 65 | 66 | 67 | {timer.triggers.map((trigger, index) => ( 68 | 73 | ))} 74 | 87 | 88 | 89 | 90 | ) 91 | ); 92 | }); 93 | 94 | export default Triggers; 95 | -------------------------------------------------------------------------------- /src/renderer/components/trigger.playPause.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useContext } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | import Select from '@material-ui/core/Select'; 5 | import MenuItem from '@material-ui/core/MenuItem'; 6 | import Grid from '@material-ui/core/Grid'; 7 | import FormControl from '@material-ui/core/FormControl'; 8 | import InputLabel from '@material-ui/core/InputLabel'; 9 | import Paper from '@material-ui/core/Paper'; 10 | import AccordionDetails from '@material-ui/core/AccordionDetails'; 11 | 12 | import { StoreContext } from '../stores/store.context'; 13 | 14 | const PlayPauseTrigger = observer((props) => { 15 | let { triggerId, playPauseId } = props; 16 | const { vmix, timer } = useContext(StoreContext); 17 | let trigger = timer.triggers.filter((x) => x.id === triggerId)[0]; 18 | let playPause = trigger.playPauses.filter((x) => x.id === playPauseId)[0]; 19 | 20 | const [modeSelected, setModeSelected] = useState(''); 21 | const [inSelected, setInSelected] = useState(''); 22 | const [inputList, setInputList] = useState([]); 23 | const modes = [ 24 | { label: 'Play', command: 'Play' }, 25 | { label: 'Pause', command: 'Pause' }, 26 | { label: 'PlayPause', command: 'PlayPause' }, 27 | ]; 28 | 29 | const handleModeChange = (event) => { 30 | setModeSelected(event.target.value); 31 | const i = modes.findIndex((m) => m.label == event.target.value); 32 | playPause.setCommand(modes[i].command); 33 | }; 34 | 35 | const handleInputChange = (event) => { 36 | setInSelected(event.target.value); 37 | const i = inputList.findIndex((i) => i.title == event.target.value); 38 | playPause.setInput(inputList[i].key); 39 | }; 40 | 41 | const triggerPlayPause = () => { 42 | if (timer.currentSeconds == trigger.time && playPause.command) { 43 | let cmd = playPause.command; 44 | let input = playPause.input; 45 | window.electron.vmix.vmixPostReq(`${cmd} Input=${input}`); 46 | } 47 | }; 48 | 49 | useEffect(() => { 50 | if ( 51 | (timer.isRunning && timer.isCountingDown && trigger.isDown) || 52 | (timer.isRunning && !timer.isCountingDown && trigger.isUp) 53 | ) { 54 | triggerPlayPause(); 55 | } 56 | }, [timer.currentSeconds]); 57 | 58 | useEffect(() => { 59 | vmix.inputs && setInputList(vmix.inputs); 60 | }, [vmix.inputs]); 61 | 62 | return ( 63 | <> 64 | 65 | 75 | 76 | 77 | 78 | Mode 79 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | Input 97 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | ); 115 | }); 116 | 117 | export default PlayPauseTrigger; 118 | -------------------------------------------------------------------------------- /src/renderer/components/version.app.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | import Grid from '@material-ui/core/Grid'; 5 | import Typography from '@material-ui/core/Typography'; 6 | 7 | const ClockFormated = observer(() => { 8 | const [version, sVersion] = useState('0.0.0'); 9 | 10 | const setVersion = (__, version) => { 11 | sVersion(version); 12 | }; 13 | 14 | useEffect(() => { 15 | window.electron.on('version', setVersion); 16 | }, []); 17 | 18 | return ( 19 | 20 | 29 | v. {version} 30 | 31 | 32 | ); 33 | }); 34 | 35 | export default ClockFormated; 36 | -------------------------------------------------------------------------------- /src/renderer/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Clockotron 6 | 7 | 8 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/renderer/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { MuiThemeProvider } from '@material-ui/core'; 4 | 5 | import App from './App.jsx'; 6 | import { theme } from './utils/Theme.jsx'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | -------------------------------------------------------------------------------- /src/renderer/pages/settings.app.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | 3 | import { observer } from 'mobx-react-lite'; 4 | 5 | import Colors from '../components/colors.settings'; 6 | 7 | import { StoreContext } from '../stores/store.context.jsx'; 8 | 9 | const Settings = observer(() => { 10 | const { clockotron } = useContext(StoreContext); 11 | return ; 12 | }); 13 | 14 | export default Settings; 15 | -------------------------------------------------------------------------------- /src/renderer/pages/timer.app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { observer } from 'mobx-react-lite'; 4 | 5 | import TextInputList from '../components/inputSelector.timer.jsx'; 6 | import ClockInput from '../components/clock.input.timer.jsx'; 7 | import TimeUp from '../components/timerUp.timer.jsx'; 8 | import ClockFormated from '../components/clock.formated.timer.jsx'; 9 | import TimeDown from '../components/timerDown.timer.jsx'; 10 | import PlayPause from '../components/playPause.timer.jsx'; 11 | import DirectionOptions from '../components/directionOptions.timer.jsx'; 12 | import BaseColors from '../components/baseColors.timer.jsx'; 13 | import Triggers from '../components/trigger.parent.jsx'; 14 | 15 | const Timer = observer(() => { 16 | return ( 17 | <> 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | }); 30 | 31 | export default Timer; 32 | -------------------------------------------------------------------------------- /src/renderer/pages/video.app.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useContext, useRef } from 'react'; 2 | import { observer } from 'mobx-react-lite'; 3 | import { setDriftlessTimeout, clearDriftless } from 'driftless'; 4 | 5 | import Grid from '@material-ui/core/Grid'; 6 | 7 | import { StoreContext } from '../stores/store.context.jsx'; 8 | 9 | import InputSelector from '../components/inputSelector.video.jsx'; 10 | import ClockFormated from '../components/clock.formated.video.jsx'; 11 | import BaseColors from '../components/baseColors.video'; 12 | 13 | const Video = observer(() => { 14 | const { videoReader, vmix } = useContext(StoreContext); 15 | 16 | const timerRef = useRef(null); 17 | const timerRef2 = useRef(null); 18 | const timerRef3 = useRef(null); 19 | const timerRef4 = useRef(null); 20 | 21 | const timer = () => { 22 | let input = videoReader.vmixInputs[videoReader.mountedInputIndex]; 23 | window.electron.vmix.reqXmlToUpdateVideoPlayer(); 24 | 25 | if (videoReader.currentSeconds >= 1 && input.isPlaying) { 26 | timerRef4.current = setDriftlessTimeout( 27 | videoReader.setCurrentSeconds(videoReader.currentSeconds - 1), 28 | 433 29 | ); 30 | 31 | function scheduleFrame() { 32 | timerRef.current = setDriftlessTimeout( 33 | () => timer(), 34 | videoReader.interval 35 | ); 36 | } 37 | 38 | scheduleFrame(); 39 | } else { 40 | clearDriftless(timerRef.current); 41 | } 42 | }; 43 | 44 | // This is triggered when there is new XML data 45 | useEffect(() => { 46 | let input = videoReader.vmixInputs[videoReader.mountedInputIndex]; 47 | if (input) { 48 | let timeleft = input.duration - input.position; 49 | let rounded = Math.ceil(timeleft / 1000); 50 | let remainder = timeleft % 1000; 51 | videoReader.setCurrentSeconds(rounded); 52 | if (input.isPlaying && input.isVideo) { 53 | if (videoReader.currentSeconds >= 1) { 54 | timerRef2.current = setDriftlessTimeout( 55 | videoReader.setCurrentSeconds(rounded - 1), 56 | remainder 57 | ); 58 | } 59 | timerRef3.current = setDriftlessTimeout(timer, 1000); 60 | } else { 61 | clearDriftless(timerRef.current); 62 | videoReader.setCurrentSeconds(rounded); 63 | } 64 | } 65 | return () => { 66 | clearDriftless(timerRef.current); 67 | clearDriftless(timerRef2.current); 68 | clearDriftless(timerRef3.current); 69 | clearDriftless(timerRef4.current); 70 | }; 71 | }, [videoReader.mountedInputIndex, JSON.stringify(videoReader.vmixInputs)]); 72 | 73 | useEffect(() => { 74 | vmix.ip && window.electron.vmix.reqTally(); 75 | }, [vmix.ip]); 76 | 77 | return ( 78 | 79 | 80 | 81 | {/* */} 82 | 83 | ); 84 | }); 85 | 86 | export default Video; 87 | -------------------------------------------------------------------------------- /src/renderer/stores/alert.store.js: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable } from 'mobx'; 2 | 3 | export class AlertStore { 4 | text = ''; 5 | severity = ''; 6 | length = 1000; 7 | isOpen = false; 8 | 9 | constructor() { 10 | makeAutoObservable(this); 11 | } 12 | 13 | close() { 14 | this.isOpen = false; 15 | } 16 | 17 | cannotConnect() { 18 | this.text = 'Cannot make connection at this address'; 19 | this.severity = 'error'; 20 | this.length = 3000; 21 | this.isOpen = true; 22 | } 23 | 24 | connectionMadeToVmix() { 25 | this.text = 'Connection made to Vmix'; 26 | this.severity = 'success'; 27 | this.length = 3000; 28 | this.isOpen = true; 29 | } 30 | 31 | lostVmixConnection() { 32 | this.text = 'Lost connection to Vmix'; 33 | this.severity = 'error'; 34 | this.length = 5000; 35 | this.isOpen = true; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/renderer/stores/clockotron.store.js: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable } from 'mobx'; 2 | 3 | export class ClockotronState { 4 | // 0 = timer 5 | // 1 = video reader 6 | tabValue = 0; 7 | areBetaFeaturesEnabled = false; 8 | hasNewFeaturesDialogBeenSeen = false; 9 | colors = [ 10 | '#FF0000', 11 | '#DB3E00', 12 | '#FCCB00', 13 | '#00FF50', 14 | '#1B46F2', 15 | '#5300EB', 16 | '#FFFFFF', 17 | '#000000', 18 | ]; 19 | 20 | constructor() { 21 | makeAutoObservable(this); 22 | } 23 | 24 | setAreBetaFeaturesEnabled(boolean) { 25 | this.areBetaFeaturesEnabled = boolean; 26 | } 27 | 28 | setTabValue(value) { 29 | this.tabValue = value; 30 | } 31 | 32 | enableBetaButton() { 33 | window.electron.enableBetaButton(); 34 | } 35 | 36 | setHasNewFeaturesDialogBeenSeen(boolean) { 37 | this.hasNewFeaturesDialogBeenSeen = boolean; 38 | window.electron.store.set('hasNewFeaturesBeenSeen', true); 39 | } 40 | 41 | storeSet(key, value) { 42 | window.electron.store.set(key, value); 43 | } 44 | 45 | setColor(i, newHexValue) { 46 | this.colors[i] = newHexValue; 47 | } 48 | 49 | indexOfColorInColors(oldHexValue) { 50 | let i = this.colors.indexOf(oldHexValue.toUpperCase()); 51 | return i; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/renderer/stores/color.store.js: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable } from 'mobx'; 2 | 3 | export class ColorCheckpoint { 4 | color = '#5300eb'; 5 | time = 120; 6 | isDown = true; 7 | layer = 1; 8 | input = 1; 9 | doesToggle = false; 10 | multiviewCommand = ''; 11 | 12 | constructor(color, time, isDown) { 13 | this.color = color; 14 | this.time = time; 15 | this.isDown = isDown; 16 | makeAutoObservable(this); 17 | } 18 | 19 | setColor(color) { 20 | this.color = color; 21 | } 22 | 23 | setTime(time) { 24 | this.time = time; 25 | } 26 | 27 | setIsDown(boolean) { 28 | this.isDown = boolean; 29 | } 30 | s; 31 | 32 | setInput(input) { 33 | this.input = input; 34 | } 35 | 36 | setLayer(layer) { 37 | this.layer = layer; 38 | } 39 | 40 | setDoesToggle(boolean) { 41 | this.doesToggle = boolean; 42 | } 43 | 44 | setMultiviewCommand(command) { 45 | this.multiviewCommand = command; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/renderer/stores/store.context.jsx: -------------------------------------------------------------------------------- 1 | import React, { createContext } from 'react'; 2 | 3 | import { Timer } from './timer.store'; 4 | import { Vmix } from './vmix.store'; 5 | import { AlertStore } from './alert.store'; 6 | import { VideoReader } from './videoReader.store'; 7 | import { ClockotronState } from './clockotron.store'; 8 | 9 | const alertStore = new AlertStore(); 10 | const timer = new Timer(alertStore); 11 | const vmix = new Vmix(alertStore); 12 | const videoReader = new VideoReader(); 13 | const clockotron = new ClockotronState(); 14 | 15 | export const StoreContext = createContext({ 16 | timer, 17 | vmix, 18 | alertStore, 19 | videoReader, 20 | clockotron, 21 | }); 22 | -------------------------------------------------------------------------------- /src/renderer/stores/timer.store.js: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable } from 'mobx'; 2 | import { ColorCheckpoint } from './color.store'; 3 | import { Trigger } from './trigger.store'; 4 | import { v4 as uuidv4 } from 'uuid'; 5 | 6 | export class Timer { 7 | id = ''; 8 | color = '#00FF50'; 9 | input = ''; 10 | text = ''; 11 | currentSeconds = 0; 12 | formatedTime = '00:00:01'; 13 | isRunning = false; 14 | resetSeconds = 0; 15 | formatPositions = 3; 16 | isCountingDown = true; 17 | countUpAfterDownReachesZero = false; 18 | colors = []; 19 | triggers = []; 20 | downColor = '#00FF50'; 21 | upColor = '#FF0000'; 22 | downFontColor = '#000'; 23 | upFontColor = '#000'; 24 | interval = 1000; 25 | directionIsDown = true; 26 | 27 | constructor(alertStore) { 28 | const id = uuidv4(); 29 | this.id = id; 30 | this.alertStore = alertStore; 31 | this.addColor('#00FF50', 100000000, true); 32 | this.addColor('#FF0000', 100000000, false); 33 | makeAutoObservable(this); 34 | } 35 | 36 | addColor(color, time, isDown) { 37 | let newColor = new ColorCheckpoint(color, time, isDown); 38 | this.colors.push(newColor); 39 | } 40 | addTrigger() { 41 | let newTrigger = new Trigger(); 42 | this.triggers.push(newTrigger); 43 | } 44 | removeTrigger(id) { 45 | let index = this.triggers.map((trigger) => trigger.id).indexOf(id); 46 | if (index > -1) { 47 | this.triggers.splice(index, 1); 48 | } 49 | } 50 | 51 | getFontColor(color) { 52 | if (color == '#000000' || color == '#5300eb' || color == '#1b46f2') { 53 | return '#fff'; 54 | } else { 55 | return '#000'; 56 | } 57 | } 58 | 59 | setDownColor(color) { 60 | this.downColor = color; 61 | let fontColor = this.getFontColor(color); 62 | this.downFontColor = fontColor; 63 | } 64 | 65 | setUpColor(color) { 66 | this.upColor = color; 67 | let fontColor = this.getFontColor(color); 68 | this.upFontColor = fontColor; 69 | } 70 | 71 | setCurrentSeconds(time) { 72 | if (this.currentSeconds + time >= 0) { 73 | console.log(time); 74 | this.currentSeconds = time; 75 | } else { 76 | this.isRunning(false); 77 | } 78 | window.electron.timer.updateCurrentSeconds('timer', this.currentSeconds); 79 | } 80 | 81 | setFormatedTime(time) { 82 | this.formatedTime = time; 83 | } 84 | 85 | setIsRunning(boolean) { 86 | this.isRunning = boolean; 87 | } 88 | 89 | setColor(color) { 90 | this.color = color; 91 | } 92 | 93 | setInput(input) { 94 | this.input = input; 95 | } 96 | 97 | setText(text) { 98 | this.text = text; 99 | } 100 | 101 | setResetSeconds(time) { 102 | this.resetSeconds = time; 103 | } 104 | 105 | setFormatPositions(num) { 106 | if (this.formatPositions + num < 4 && this.formatPositions + num > 0) { 107 | this.formatPositions = this.formatPositions + num; 108 | } 109 | } 110 | 111 | setIsCountingDown(boolean) { 112 | this.isCountingDown = boolean; 113 | } 114 | 115 | setIsCountingDownToMainThread(boolean) { 116 | this.isCountingDown = boolean; 117 | window.electron.timer.direction('timer', this.isCountingDown); 118 | } 119 | 120 | setCountUpAfterDownReachesZero(boolean) { 121 | this.countUpAfterDownReachesZero = boolean; 122 | window.electron.timer.upAfterDown( 123 | 'timer', 124 | this.countUpAfterDownReachesZero 125 | ); 126 | } 127 | 128 | startMainThreadTimer() { 129 | window.electron.timer.start( 130 | 'timer', 131 | this.currentSeconds, 132 | this.interval, 133 | this.isCountingDown 134 | ); 135 | } 136 | 137 | stopMainThreadTimer() { 138 | window.electron.timer.stop('timer'); 139 | } 140 | 141 | directionMainThreadTimer() { 142 | window.electron.timer.direction('timer', this.isCountingDown); 143 | } 144 | 145 | intervalMainThreadTimer() { 146 | window.electron.timer.interval('timer', this.interval); 147 | } 148 | 149 | updateInterval(x) { 150 | if (x == 1) { 151 | this.interval = 1000; 152 | } else { 153 | this.interval = this.interval * x; 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/renderer/stores/trigger.store.js: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable } from 'mobx'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | 4 | class Layer { 5 | layer = 1; 6 | input = 1; 7 | command = ''; 8 | id = ''; 9 | 10 | constructor() { 11 | const id = uuidv4(); 12 | this.id = id; 13 | makeAutoObservable(this); 14 | } 15 | 16 | setLayer(layer) { 17 | this.layer = layer; 18 | } 19 | 20 | setInput(input) { 21 | this.input = input; 22 | } 23 | 24 | setCommand(command) { 25 | this.command = command; 26 | } 27 | } 28 | 29 | class Color { 30 | color = '#00FF50'; 31 | id = ''; 32 | 33 | constructor() { 34 | const id = uuidv4(); 35 | this.id = id; 36 | makeAutoObservable(this); 37 | } 38 | 39 | setColor(color) { 40 | this.color = color; 41 | } 42 | } 43 | 44 | class PlayPause { 45 | id = ''; 46 | input = 1; 47 | command = ''; 48 | 49 | constructor() { 50 | const id = uuidv4(); 51 | this.id = id; 52 | makeAutoObservable(this); 53 | } 54 | 55 | setInput(input) { 56 | this.input = input; 57 | } 58 | setCommand(command) { 59 | this.command = command; 60 | } 61 | } 62 | 63 | export class Trigger { 64 | id = ''; 65 | time = 120; 66 | isDown = true; 67 | isUp = false; 68 | layers = []; 69 | colors = []; 70 | playPauses = []; 71 | color = '#00FF50'; 72 | fontColor = '#fff'; 73 | 74 | constructor() { 75 | const id = uuidv4(); 76 | this.id = id; 77 | makeAutoObservable(this); 78 | } 79 | 80 | setTime(time) { 81 | this.time = time; 82 | } 83 | 84 | setIsDown(boolean) { 85 | this.isDown = boolean; 86 | } 87 | setIsUp(boolean) { 88 | this.isUp = boolean; 89 | } 90 | 91 | setColor(color) { 92 | this.color = color; 93 | } 94 | 95 | setFontColor() { 96 | if ( 97 | this.color == '#000000' || 98 | this.color == '#5300eb' || 99 | this.color == '#1b46f2' 100 | ) { 101 | this.fontColor = '#fff'; 102 | } else { 103 | this.fontColor = '#000'; 104 | } 105 | } 106 | 107 | addLayer() { 108 | let newLayer = new Layer(); 109 | this.layers.push(newLayer); 110 | } 111 | addColor() { 112 | let newColor = new Color(); 113 | this.colors.push(newColor); 114 | } 115 | addPlayPause() { 116 | let newPlayPause = new PlayPause(); 117 | this.playPauses.push(newPlayPause); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/renderer/stores/videoReader.store.js: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable, toJS } from 'mobx'; 2 | import { XMLParser } from 'fast-xml-parser'; 3 | import { options } from '../utils/options'; 4 | 5 | let videoTypes = ['Video', 'VideoList']; 6 | 7 | export class VideoReader { 8 | vmixInputs = []; 9 | pgmString = ''; 10 | vmixInputsInPgm = []; 11 | tallyArray = []; 12 | inputsOnPgm = []; 13 | rawXmlInputs = []; 14 | input = ''; 15 | text = ''; 16 | formatPositions = 3; 17 | currentSeconds = 0; 18 | formatedTime = '00:00:00'; 19 | color = '#ff0000'; 20 | downColor = '#00FF50'; 21 | downFontColor = '#000'; 22 | allActiveInputs = []; 23 | mountedTimer = 0; 24 | mountedInputIndex; 25 | isMountedPlaying = false; 26 | isMountedInputAVideo = false; 27 | interval = 1000; 28 | 29 | constructor() { 30 | makeAutoObservable(this); 31 | } 32 | 33 | setInput(input) { 34 | this.input = input; 35 | } 36 | 37 | setText(text) { 38 | this.text = text; 39 | } 40 | 41 | setFormatedTime(time) { 42 | this.formatedTime = time; 43 | } 44 | 45 | setFormatPositions(num) { 46 | if (this.formatPositions + num < 4 && this.formatPositions + num > 0) { 47 | this.formatPositions = this.formatPositions + num; 48 | } 49 | } 50 | 51 | // create array of each channel 52 | // create array of each channel in pgm 53 | handleNewTallyData(data) { 54 | this.tallyArray = data.split(''); 55 | this.updateInputsOnPgm(); 56 | } 57 | 58 | updateInputsOnPgm() { 59 | let a = []; 60 | this.tallyArray.forEach((input, index) => { 61 | if (input == '1') { 62 | a.push(parseInt(index)); 63 | } 64 | }); 65 | this.inputsOnPgm = a; 66 | } 67 | 68 | handleNewXmlData(data) { 69 | let jsonObj = this.parseXmlToJSON(data); 70 | this.rawXmlInputs = jsonObj.xml.vmix.inputs.input; 71 | 72 | this.rawXmlInputs.forEach((input) => { 73 | let index = this.checkForInput(input.key); 74 | if (index == -1) { 75 | this.addInput(input); 76 | } else { 77 | let tallyArrayObj = toJS(this.tallyArray); 78 | this.updateInputXmlData(index, input, tallyArrayObj); 79 | } 80 | }); 81 | return; 82 | } 83 | 84 | // has input changed 85 | // is input video 86 | updateMountedInputIndex() { 87 | let pgm = { isVideo: false, inputIndex: 0, isPlaying: false }; 88 | this.inputsOnPgm.every((input) => { 89 | let isVideo = this.checkTypeIsVideo(this.vmixInputs[input].type); 90 | if (isVideo) { 91 | pgm.isVideo = true; 92 | pgm.inputIndex = input; 93 | this.isMountedInputAVideo = true; 94 | return false; 95 | } 96 | return true; 97 | }); 98 | if (!pgm.isVideo) { 99 | pgm.inputIndex = this.inputsOnPgm[0]; 100 | } 101 | pgm.key = this.vmixInputs[pgm.inputIndex].key; 102 | let mountedKey; 103 | if (this.vmixInputs[this.mountedInputIndex]) { 104 | mountedKey = this.vmixInputs[this.mountedInputIndex].key; 105 | } 106 | if (mountedKey != pgm.key) { 107 | this.interval = 1000; 108 | this.mountedInputIndex = pgm.inputIndex; 109 | } else { 110 | this.updateInterval(pgm); 111 | } 112 | } 113 | 114 | updateIsPlaying(data) { 115 | let inputIndex = parseInt(data.split(' ')[1]) - 1; 116 | let inputPlayingStatus = data.split(' ')[2]; 117 | let isPlaying = false; 118 | if (inputPlayingStatus == '1') { 119 | isPlaying = true; 120 | } 121 | if (inputIndex == this.mountedInputIndex) { 122 | this.isMountedPlaying = isPlaying; 123 | } 124 | } 125 | 126 | updateInterval(pgm) { 127 | let input = this.vmixInputs[pgm.inputIndex]; 128 | let newInterval = (input.duration - input.position) / this.currentSeconds; 129 | this.interval = newInterval; 130 | } 131 | 132 | checkTypeIsVideo(type) { 133 | let i = videoTypes.indexOf(type); 134 | if (i > -1) { 135 | return true; 136 | } else { 137 | return false; 138 | } 139 | } 140 | 141 | setCurrentSeconds(time) { 142 | if (typeof time === 'number') this.currentSeconds = time; 143 | } 144 | 145 | parseXmlToJSON(data) { 146 | const parser = new XMLParser(options); 147 | let jsonObj = parser.parse(data); 148 | return jsonObj; 149 | } 150 | 151 | checkForInput(key) { 152 | let res = this.vmixInputs.findIndex((input) => input.key == key); 153 | return res; 154 | } 155 | 156 | addInput(inputObj) { 157 | let input = new Input(inputObj); 158 | this.vmixInputs.push(input); 159 | } 160 | 161 | updateInputXmlData(index, data, tally) { 162 | this.vmixInputs[index].update(data, tally); 163 | } 164 | 165 | getFontColor(color) { 166 | if (color == '#000000' || color == '#5300eb' || color == '#1b46f2') { 167 | return '#fff'; 168 | } else { 169 | return '#000'; 170 | } 171 | } 172 | 173 | setDownColor(color) { 174 | console.log(color); 175 | this.downColor = color; 176 | this.color = color; 177 | let fontColor = this.getFontColor(color); 178 | this.downFontColor = fontColor; 179 | } 180 | } 181 | 182 | class Input { 183 | inputNumber = 1; 184 | key = ''; 185 | duration = 0; 186 | position = 0; 187 | isPlaying = false; 188 | title = ''; 189 | isCountingDown = false; 190 | isOnPgm = false; 191 | type = ''; 192 | isVideo = false; 193 | 194 | constructor(input) { 195 | makeAutoObservable(this); 196 | this.inputNumber = parseInt(input.number); 197 | this.key = input.key; 198 | this.isPlaying = this.setIsPlaying(input); 199 | this.title = input.title; 200 | this.type = input.type; 201 | this.setIsVideo(input.type); 202 | } 203 | 204 | setIsVideo(type) { 205 | let i = videoTypes.indexOf(type); 206 | if (i > -1) { 207 | this.isVideo = true; 208 | } else { 209 | this.isVideo = false; 210 | } 211 | } 212 | 213 | setInput(input) { 214 | this.input = input; 215 | } 216 | 217 | setText(text) { 218 | this.text = text; 219 | } 220 | 221 | setFormatedTime(time) { 222 | this.formatedTime = time; 223 | } 224 | 225 | setIsPlaying(input) { 226 | if (input.state === 'Running') { 227 | return true; 228 | } else { 229 | return false; 230 | } 231 | } 232 | 233 | setDuration(input) { 234 | if (input.duration) { 235 | let dur = parseInt(input.duration); 236 | return dur; 237 | } 238 | return 0; 239 | } 240 | 241 | setPosition(input) { 242 | if (input.position) { 243 | let pos = parseInt(input.position); 244 | return pos; 245 | } 246 | return 0; 247 | } 248 | 249 | update(input, tally) { 250 | this.inputNumber = parseInt(input.number); 251 | this.duration = this.setDuration(input); 252 | this.position = this.setPosition(input); 253 | this.isPlaying = this.setIsPlaying(input); 254 | this.isOnPgm = this.setIsOnPgm(input, tally); 255 | this.key = input.key; 256 | this.title = input.title; 257 | this.type = input.type; 258 | this.setIsVideo(input.type); 259 | } 260 | 261 | setIsOnPgm(input, tally) { 262 | let status = tally[input.number - 1]; 263 | 264 | if (status == '1') { 265 | return true; 266 | } else { 267 | return false; 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/renderer/stores/vmix.store.js: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable } from 'mobx'; 2 | import { XMLParser } from 'fast-xml-parser'; 3 | import { options } from '../utils/options'; 4 | 5 | export class Vmix { 6 | unconfirmedIp = ''; 7 | ip = ''; 8 | xmlRaw = ''; 9 | isSocketConnected = false; 10 | connectionTimeout; 11 | alertStore; 12 | inputs = []; 13 | 14 | constructor(alertStore) { 15 | this.alertStore = alertStore; 16 | makeAutoObservable(this); 17 | } 18 | 19 | setIp(ip) { 20 | this.ip = ip; 21 | } 22 | 23 | setXmlRaw(data) { 24 | this.xmlRaw = data; 25 | } 26 | 27 | updateInputList(data) { 28 | const parser = new XMLParser(options); 29 | let jsonObj = parser.parse(data); 30 | let list = jsonObj.xml.vmix.inputs.input; 31 | this.inputs = list; 32 | } 33 | 34 | setIsSocketConnected(boolean) { 35 | this.isSocketConnected = boolean; 36 | } 37 | 38 | attemptVmixConnection(ip) { 39 | this.unconfirmedIp = ip; 40 | window.electron.vmix.connect(ip); 41 | this.connectionTimeout = setTimeout(() => this.connectError(), 5000); 42 | } 43 | 44 | refresh() { 45 | this.ip && window.electron.vmix.reqXml(); 46 | } 47 | 48 | connected() { 49 | this.ip = this.unconfirmedIp; 50 | this.setIsSocketConnected = true; 51 | clearTimeout(this.connectionTimeout); 52 | this.alertStore.connectionMadeToVmix(); 53 | } 54 | 55 | connectError() { 56 | this.alertStore.cannotConnect(); 57 | } 58 | 59 | lostSocketConnection(__, error) { 60 | this.alertStore.lostVmixConnection(); 61 | this.setIp = ''; 62 | this.isSocketConnected = false; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/renderer/utils/AppStyles.jsx: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@material-ui/core/styles'; 2 | 3 | export const useStyles = makeStyles({ 4 | notchedOutline: { 5 | borderWidth: '1px', 6 | borderColor: '#eee !important', 7 | }, 8 | root: { 9 | width: '100%', 10 | }, 11 | heading: {}, 12 | over: { 13 | display: 'block', 14 | width: 200, 15 | overflow: 'hidden', 16 | textoverflow: 'ellipsis', 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /src/renderer/utils/ColorPickerColors.jsx: -------------------------------------------------------------------------------- 1 | export const colors = [ 2 | '#FF0000', 3 | '#DB3E00', 4 | '#FCCB00', 5 | '#00FF50', 6 | '#1B46F2', 7 | '#5300EB', 8 | '#FFF', 9 | '#000', 10 | ]; 11 | -------------------------------------------------------------------------------- /src/renderer/utils/Theme.jsx: -------------------------------------------------------------------------------- 1 | import { createTheme } from '@material-ui/core'; 2 | 3 | export const theme = createTheme({ 4 | palette: { 5 | type: 'dark', 6 | primary: { 7 | main: '#fff' 8 | }, 9 | secondary: { 10 | main: '#fefe' 11 | }, 12 | text: { 13 | primary: '#ffffff', 14 | secondary: '#fff', 15 | disabled: '#fff', 16 | hint: '#fff' 17 | }, 18 | background: { 19 | paper: '#12050E' 20 | }, 21 | action: { 22 | disabledBackground: '#FFF' 23 | } 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /src/renderer/utils/formatTime.jsx: -------------------------------------------------------------------------------- 1 | export const formatTime = (time, positions) => { 2 | let timeArray = []; 3 | if (positions == 3) { 4 | timeArray.push(Math.floor(time / 3600)); 5 | timeArray.push(Math.floor((time / 60) % 60)); 6 | timeArray.push(time % 60); 7 | } 8 | if (positions == 2) { 9 | timeArray.push(Math.floor(time / 3600)); 10 | timeArray.push(Math.floor((time / 60) % 60) + timeArray[0] * 60); 11 | timeArray.push(time % 60); 12 | } 13 | if (positions == 1) { 14 | timeArray.push(Math.floor(time / 3600)); 15 | timeArray.push(Math.floor((time / 60) % 60) + timeArray[0] * 60); 16 | timeArray.push((time % 60) + timeArray[1] * 60); 17 | } 18 | 19 | let formatedTime = ''; 20 | 21 | if (positions >= 3) { 22 | formatedTime = formatedTime + add0(timeArray[0]) + ':'; 23 | } 24 | if (positions >= 2) { 25 | formatedTime = formatedTime + add0(timeArray[1]) + ':'; 26 | } 27 | if (positions >= 1) { 28 | formatedTime = formatedTime + add0(timeArray[2]); 29 | } 30 | 31 | function add0(time) { 32 | let y; 33 | let l = String(time).split(''); 34 | switch (l.length) { 35 | case 0: 36 | y = '00'; 37 | break; 38 | case 1: 39 | y = '0' + String(time); 40 | break; 41 | default: 42 | y = String(time); 43 | break; 44 | } 45 | return y; 46 | } 47 | return formatedTime; 48 | }; 49 | -------------------------------------------------------------------------------- /src/renderer/utils/options.jsx: -------------------------------------------------------------------------------- 1 | // export const options = { 2 | // attributeNamePrefix: '_', 3 | // attrNodeName: 'attr', //default is 'false' 4 | // textNodeName: '#text', 5 | // ignoreAttributes: false, 6 | // ignoreNameSpace: false, 7 | // allowBooleanAttributes: true, 8 | // parseNodeValue: true, 9 | // parseAttributeValue: true, 10 | // trimValues: false, 11 | // cdataTagName: '__cdata', //default is 'false' 12 | // cdataPositionChar: '\\c', 13 | // parseTrueNumberOnly: false, 14 | // arrayMode: false, //"strict" 15 | // // attrValueProcessor: (val, attrName) => he.decode(val, { isAttributeValue: true }),//default is a=>a 16 | // // tagValueProcessor: (val, tagName) => he.decode(val), //default is a=>a 17 | // stopNodes: ['parse-me-as-string'], 18 | // }; 19 | 20 | export const options = { 21 | attributeNamePrefix: '', 22 | attrNodeName: 'attr', 23 | ignoreAttributes: false, 24 | }; 25 | 26 | export const options2 = { 27 | attributeNamePrefix: '', 28 | attrNodeName: 'attr', //default is 'false' 29 | ignoreAttributes: false, 30 | ignoreNameSpace: false, 31 | allowBooleanAttributes: true, 32 | }; 33 | -------------------------------------------------------------------------------- /version/major.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | let rawdata = fs.readFileSync('package.json'); 5 | let package = JSON.parse(rawdata); 6 | let version = package.version; 7 | let a = version.split('.'); 8 | let major = parseInt(a[0]); 9 | let minor = parseInt(a[1]); 10 | let patch = parseInt(a[2]); 11 | 12 | major += 1; 13 | minor = 0; 14 | patch = 0; 15 | 16 | let newVersion = `${major}.${minor}.${patch}`; 17 | 18 | package.version = newVersion; 19 | let newPackage = JSON.stringify(package); 20 | 21 | let packagePathBuild = path.join( 22 | __dirname, 23 | '/../', 24 | 'build', 25 | 'app', 26 | 'package.json' 27 | ); 28 | let rawdataBuild = fs.readFileSync(packagePathBuild); 29 | let packageBuild = JSON.parse(rawdataBuild); 30 | packageBuild.version = newVersion; 31 | let newPackageBuild = JSON.stringify(packageBuild); 32 | 33 | fs.writeFileSync('package.json', newPackage); 34 | fs.writeFileSync(packagePathBuild, newPackageBuild); 35 | -------------------------------------------------------------------------------- /version/minor.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | let rawdata = fs.readFileSync('package.json'); 5 | let package = JSON.parse(rawdata); 6 | let version = package.version; 7 | let a = version.split('.'); 8 | let major = parseInt(a[0]); 9 | let minor = parseInt(a[1]); 10 | let patch = parseInt(a[2]); 11 | 12 | minor += 1; 13 | patch = 0; 14 | 15 | let newVersion = `${major}.${minor}.${patch}`; 16 | 17 | package.version = newVersion; 18 | let newPackage = JSON.stringify(package); 19 | 20 | let packagePathBuild = path.join( 21 | __dirname, 22 | '/../', 23 | 'build', 24 | 'app', 25 | 'package.json' 26 | ); 27 | let rawdataBuild = fs.readFileSync(packagePathBuild); 28 | let packageBuild = JSON.parse(rawdataBuild); 29 | packageBuild.version = newVersion; 30 | let newPackageBuild = JSON.stringify(packageBuild); 31 | 32 | fs.writeFileSync('package.json', newPackage); 33 | fs.writeFileSync(packagePathBuild, newPackageBuild); 34 | -------------------------------------------------------------------------------- /version/patch.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | let rawdata = fs.readFileSync('package.json'); 5 | let package = JSON.parse(rawdata); 6 | let version = package.version; 7 | let a = version.split('.'); 8 | let major = parseInt(a[0]); 9 | let minor = parseInt(a[1]); 10 | let patch = parseInt(a[2]); 11 | 12 | patch += 1; 13 | 14 | let newVersion = `${major}.${minor}.${patch}`; 15 | 16 | package.version = newVersion; 17 | let newPackage = JSON.stringify(package); 18 | 19 | let packagePathBuild = path.join( 20 | __dirname, 21 | '/../', 22 | 'build', 23 | 'app', 24 | 'package.json' 25 | ); 26 | let rawdataBuild = fs.readFileSync(packagePathBuild); 27 | let packageBuild = JSON.parse(rawdataBuild); 28 | packageBuild.version = newVersion; 29 | let newPackageBuild = JSON.stringify(packageBuild); 30 | 31 | fs.writeFileSync('package.json', newPackage); 32 | fs.writeFileSync(packagePathBuild, newPackageBuild); 33 | --------------------------------------------------------------------------------