├── .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 |
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 |
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 && }
53 | >
54 | )}
55 |
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 |
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 |
--------------------------------------------------------------------------------