├── app ├── utils │ ├── .gitkeep │ ├── mainConstants.js │ ├── queue.js │ ├── fileObject.js │ ├── db.js │ ├── openCVProperties.js │ ├── saveThumb.js │ ├── utilsForMain.js │ ├── faceDetection.js │ └── utilsForIndexedDB.js ├── app.icns ├── img │ ├── Thumb_IN.png │ ├── Thumb_ADD.png │ ├── Thumb_BACK.png │ ├── Thumb_EMPTY.png │ ├── Thumb_HIDE.png │ ├── Thumb_OUT.png │ ├── Thumb_REDO.png │ ├── Thumb_SAVE.png │ ├── Thumb_SCRUB.png │ ├── Thumb_SHOW.png │ ├── Thumb_UNDO.png │ ├── Thumb_CHOOSE.png │ ├── Thumb_FORWARD.png │ ├── Thumb_HANDLE.png │ ├── Thumb_HIDDEN.png │ ├── Thumb_VISIBLE.png │ ├── Thumb_MOVIEPRINT.png │ ├── Thumb_HANDLE_wide.png │ ├── Thumb_TRANSPARENT.png │ ├── MoviePrint_Corrupt_00000.jpg │ ├── MoviePrint_Logo_v002_128.jpg │ ├── icon-1x1.svg │ ├── icon-no-image.svg │ ├── icon-thumb-view.svg │ ├── icon-caret-down.svg │ ├── icon-filter.svg │ ├── icon-filter-enabled.svg │ ├── icon-2x2.svg │ ├── icon-filter-enabled copy.svg │ ├── icon-cut-view.svg │ ├── icon-image.svg │ ├── icon-copy.svg │ ├── icon-arrow-up.svg │ ├── icon-resize-vertical.svg │ ├── icon-resize-horizontal.svg │ ├── icon-barcode.svg │ ├── icon-expand.svg │ ├── icon-3x3.svg │ ├── icon-header.svg │ ├── icon-zoom-out.svg │ ├── icon-header-enabled.svg │ ├── icon-add-scene.svg │ ├── icon-zoom-in.svg │ ├── icon-add-interval.svg │ ├── icon-4x4.svg │ ├── icon-grid.svg │ ├── icon-unhide.svg │ ├── icon-sort.svg │ ├── icon-hide.svg │ ├── icon-frame-info.svg │ ├── icon-5x5.svg │ ├── icon-frame-info-enabled.svg │ ├── icon-show-face-rect.svg │ ├── icon-show-face-rect-enabled.svg │ ├── icon-add-face.svg │ ├── icon-6x6.svg │ ├── listOfNames.json │ └── MoviePrint-titleimage.svg ├── components │ ├── Conditional.js │ ├── ErrorBoundary.css │ ├── Popup.css │ ├── Timeline.css │ ├── Menu.css │ ├── ErrorBoundary.js │ ├── Footer.js │ ├── FloatingMenu.css │ ├── Scrub.css │ ├── HeaderComponent.js │ ├── VideoPlayer.css │ └── SceneGrid.css ├── webViewPreload.js ├── worker_printPDF.html ├── containers │ ├── WorkerRoot.js │ ├── Root.js │ ├── Settings.css │ ├── FileList.js │ └── VisibleSceneGrid.js ├── store │ ├── configureStore.js │ ├── configureStore.prod.js │ └── configureStore.dev.js ├── worker.js ├── reducers │ ├── index.js │ ├── visibilitySettings.js │ └── files.js ├── package.json ├── index.js ├── app.html ├── worker.html ├── worker_opencv.html ├── app.global.css └── worker_database.html ├── internals ├── mocks │ └── fileMock.js ├── flow │ ├── WebpackAsset.js.flow │ └── CSSModule.js.flow ├── img │ ├── js.png │ ├── flow.png │ ├── jest.png │ ├── npm.png │ ├── react.png │ ├── redux.png │ ├── yarn.png │ ├── eslint.png │ ├── webpack.png │ ├── js-padded.png │ ├── flow-padded.png │ ├── jest-padded.png │ ├── react-padded.png │ ├── react-router.png │ ├── redux-padded.png │ ├── yarn-padded.png │ ├── eslint-padded.png │ ├── flow-padded-90.png │ ├── jest-padded-90.png │ ├── react-padded-90.png │ ├── redux-padded-90.png │ ├── webpack-padded.png │ ├── yarn-padded-90.png │ ├── eslint-padded-90.png │ ├── webpack-padded-90.png │ ├── react-router-padded.png │ └── react-router-padded-90.png └── scripts │ ├── BabelRegister.js │ ├── RunTests.js │ ├── CheckNodeEnv.js │ ├── CheckPortInUse.js │ ├── ElectronRebuild.js │ ├── CheckBuildsExist.js │ └── CheckNativeDep.js ├── .stylelintrc ├── resources ├── icon.ico ├── icon.png ├── icon.icns ├── icons │ ├── 16x16.png │ ├── 24x24.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 64x64.png │ ├── 96x96.png │ ├── 128x128.png │ ├── 256x256.png │ ├── 512x512.png │ └── 1024x1024.png ├── font │ └── Franchise-Bold.woff ├── test_files │ └── test_movie_1.mp4 └── weights │ ├── age_gender_model-shard1 │ ├── face_expression_model-shard1 │ ├── face_landmark_68_model-shard1 │ ├── face_recognition_model-shard1 │ ├── face_recognition_model-shard2 │ ├── ssd_mobilenetv1_model-shard1 │ ├── ssd_mobilenetv1_model-shard2 │ └── face_expression_model-weights_manifest.json ├── flow-typed └── module_vx.x.x.js ├── .gitattributes ├── .eslintrc.js ├── test ├── example.js ├── .eslintrc ├── actions │ ├── __snapshots__ │ │ └── counter.spec.js.snap │ └── counter.spec.js ├── reducers │ ├── __snapshots__ │ │ └── counter.spec.js.snap │ └── counter.spec.js ├── components │ ├── __snapshots__ │ │ └── Counter.spec.js.snap │ └── Counter.spec.js ├── containers │ └── CounterPage.spec.js └── e2e │ └── e2e.spec.js ├── configs ├── webpack.config.eslint.js ├── webpack.config.renderer.dev.dll.babel.js ├── webpack.config.main.prod.babel.js └── webpack.config.base.js ├── .editorconfig ├── .prettierrc.json ├── .github └── stale.yml ├── appveyor.yml ├── .vscode └── settings.json ├── scripts ├── includeInDist.js └── opencv.js ├── .flowconfig ├── .dockerignore ├── .gitignore ├── .eslintignore ├── LICENSE ├── babel.config.js └── .travis.yml /app/utils/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internals/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | export default 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard" 3 | } 4 | -------------------------------------------------------------------------------- /app/app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/app/app.icns -------------------------------------------------------------------------------- /internals/flow/WebpackAsset.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | declare export default string 3 | -------------------------------------------------------------------------------- /resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/resources/icon.ico -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/resources/icon.png -------------------------------------------------------------------------------- /app/img/Thumb_IN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/app/img/Thumb_IN.png -------------------------------------------------------------------------------- /flow-typed/module_vx.x.x.js: -------------------------------------------------------------------------------- 1 | declare module 'module' { 2 | declare module.exports: any; 3 | } 4 | -------------------------------------------------------------------------------- /internals/flow/CSSModule.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare export default { [key: string]: string } -------------------------------------------------------------------------------- /internals/img/js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/js.png -------------------------------------------------------------------------------- /resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/resources/icon.icns -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.png binary 2 | *.jpg binary 3 | *.jpeg binary 4 | *.ico binary 5 | *.icns binary 6 | -------------------------------------------------------------------------------- /app/img/Thumb_ADD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/app/img/Thumb_ADD.png -------------------------------------------------------------------------------- /app/img/Thumb_BACK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/app/img/Thumb_BACK.png -------------------------------------------------------------------------------- /app/img/Thumb_EMPTY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/app/img/Thumb_EMPTY.png -------------------------------------------------------------------------------- /app/img/Thumb_HIDE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/app/img/Thumb_HIDE.png -------------------------------------------------------------------------------- /app/img/Thumb_OUT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/app/img/Thumb_OUT.png -------------------------------------------------------------------------------- /app/img/Thumb_REDO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/app/img/Thumb_REDO.png -------------------------------------------------------------------------------- /app/img/Thumb_SAVE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/app/img/Thumb_SAVE.png -------------------------------------------------------------------------------- /app/img/Thumb_SCRUB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/app/img/Thumb_SCRUB.png -------------------------------------------------------------------------------- /app/img/Thumb_SHOW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/app/img/Thumb_SHOW.png -------------------------------------------------------------------------------- /app/img/Thumb_UNDO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/app/img/Thumb_UNDO.png -------------------------------------------------------------------------------- /internals/img/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/flow.png -------------------------------------------------------------------------------- /internals/img/jest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/jest.png -------------------------------------------------------------------------------- /internals/img/npm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/npm.png -------------------------------------------------------------------------------- /internals/img/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/react.png -------------------------------------------------------------------------------- /internals/img/redux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/redux.png -------------------------------------------------------------------------------- /internals/img/yarn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/yarn.png -------------------------------------------------------------------------------- /app/img/Thumb_CHOOSE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/app/img/Thumb_CHOOSE.png -------------------------------------------------------------------------------- /app/img/Thumb_FORWARD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/app/img/Thumb_FORWARD.png -------------------------------------------------------------------------------- /app/img/Thumb_HANDLE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/app/img/Thumb_HANDLE.png -------------------------------------------------------------------------------- /app/img/Thumb_HIDDEN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/app/img/Thumb_HIDDEN.png -------------------------------------------------------------------------------- /app/img/Thumb_VISIBLE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/app/img/Thumb_VISIBLE.png -------------------------------------------------------------------------------- /internals/img/eslint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/eslint.png -------------------------------------------------------------------------------- /internals/img/webpack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/webpack.png -------------------------------------------------------------------------------- /resources/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/resources/icons/16x16.png -------------------------------------------------------------------------------- /resources/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/resources/icons/24x24.png -------------------------------------------------------------------------------- /resources/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/resources/icons/32x32.png -------------------------------------------------------------------------------- /resources/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/resources/icons/48x48.png -------------------------------------------------------------------------------- /resources/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/resources/icons/64x64.png -------------------------------------------------------------------------------- /resources/icons/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/resources/icons/96x96.png -------------------------------------------------------------------------------- /app/img/Thumb_MOVIEPRINT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/app/img/Thumb_MOVIEPRINT.png -------------------------------------------------------------------------------- /internals/img/js-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/js-padded.png -------------------------------------------------------------------------------- /resources/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/resources/icons/128x128.png -------------------------------------------------------------------------------- /resources/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/resources/icons/256x256.png -------------------------------------------------------------------------------- /resources/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/resources/icons/512x512.png -------------------------------------------------------------------------------- /app/img/Thumb_HANDLE_wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/app/img/Thumb_HANDLE_wide.png -------------------------------------------------------------------------------- /app/img/Thumb_TRANSPARENT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/app/img/Thumb_TRANSPARENT.png -------------------------------------------------------------------------------- /internals/img/flow-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/flow-padded.png -------------------------------------------------------------------------------- /internals/img/jest-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/jest-padded.png -------------------------------------------------------------------------------- /internals/img/react-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/react-padded.png -------------------------------------------------------------------------------- /internals/img/react-router.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/react-router.png -------------------------------------------------------------------------------- /internals/img/redux-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/redux-padded.png -------------------------------------------------------------------------------- /internals/img/yarn-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/yarn-padded.png -------------------------------------------------------------------------------- /resources/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/resources/icons/1024x1024.png -------------------------------------------------------------------------------- /internals/img/eslint-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/eslint-padded.png -------------------------------------------------------------------------------- /internals/img/flow-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/flow-padded-90.png -------------------------------------------------------------------------------- /internals/img/jest-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/jest-padded-90.png -------------------------------------------------------------------------------- /internals/img/react-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/react-padded-90.png -------------------------------------------------------------------------------- /internals/img/redux-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/redux-padded-90.png -------------------------------------------------------------------------------- /internals/img/webpack-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/webpack-padded.png -------------------------------------------------------------------------------- /internals/img/yarn-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/yarn-padded-90.png -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'erb', 3 | settings: { 4 | 'import/resolver': 'webpack' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /internals/img/eslint-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/eslint-padded-90.png -------------------------------------------------------------------------------- /internals/img/webpack-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/webpack-padded-90.png -------------------------------------------------------------------------------- /resources/font/Franchise-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/resources/font/Franchise-Bold.woff -------------------------------------------------------------------------------- /app/img/MoviePrint_Corrupt_00000.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/app/img/MoviePrint_Corrupt_00000.jpg -------------------------------------------------------------------------------- /app/img/MoviePrint_Logo_v002_128.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/app/img/MoviePrint_Logo_v002_128.jpg -------------------------------------------------------------------------------- /internals/img/react-router-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/react-router-padded.png -------------------------------------------------------------------------------- /resources/test_files/test_movie_1.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/resources/test_files/test_movie_1.mp4 -------------------------------------------------------------------------------- /internals/img/react-router-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/internals/img/react-router-padded-90.png -------------------------------------------------------------------------------- /resources/weights/age_gender_model-shard1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/resources/weights/age_gender_model-shard1 -------------------------------------------------------------------------------- /test/example.js: -------------------------------------------------------------------------------- 1 | describe('description', () => { 2 | it('should have description', () => { 3 | expect(1 + 2).toBe(3); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /resources/weights/face_expression_model-shard1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/resources/weights/face_expression_model-shard1 -------------------------------------------------------------------------------- /resources/weights/face_landmark_68_model-shard1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/resources/weights/face_landmark_68_model-shard1 -------------------------------------------------------------------------------- /resources/weights/face_recognition_model-shard1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/resources/weights/face_recognition_model-shard1 -------------------------------------------------------------------------------- /resources/weights/face_recognition_model-shard2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/resources/weights/face_recognition_model-shard2 -------------------------------------------------------------------------------- /resources/weights/ssd_mobilenetv1_model-shard1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/resources/weights/ssd_mobilenetv1_model-shard1 -------------------------------------------------------------------------------- /resources/weights/ssd_mobilenetv1_model-shard2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakob/MoviePrint_v004/HEAD/resources/weights/ssd_mobilenetv1_model-shard2 -------------------------------------------------------------------------------- /internals/scripts/BabelRegister.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | require('@babel/register')({ 4 | cwd: path.join(__dirname, '..', '..') 5 | }); 6 | -------------------------------------------------------------------------------- /app/utils/mainConstants.js: -------------------------------------------------------------------------------- 1 | // const { app } = require('electron').remote; 2 | // throws error as it would be packed into main.js where this is can not be required 3 | -------------------------------------------------------------------------------- /app/img/icon-1x1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/components/Conditional.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const Conditional = (props) => { 4 | return( 5 | !!props.if && props.children 6 | ); 7 | } 8 | 9 | export default Conditional; 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /app/img/icon-no-image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/webViewPreload.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer } = require('electron'); 2 | 3 | document.addEventListener( 'wpcf7mailsent', ( event ) => { 4 | // document.addEventListener( 'wpcf7submit', ( event ) => { 5 | ipcRenderer.sendToHost('wpcf7mailsent', event.detail.inputs); 6 | }, false ); 7 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest/globals": true 4 | }, 5 | "plugins": [ 6 | "jest" 7 | ], 8 | "rules": { 9 | "jest/no-disabled-tests": "warn", 10 | "jest/no-focused-tests": "error", 11 | "jest/no-identical-title": "error" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "files": [".prettierrc", ".babelrc", ".eslintrc", ".stylelintrc"], 5 | "options": { 6 | "parser": "json" 7 | } 8 | } 9 | ], 10 | "singleQuote": true, 11 | "trailingComma": "all", 12 | "printWidth": 120 13 | } 14 | -------------------------------------------------------------------------------- /app/img/icon-thumb-view.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/img/icon-caret-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/worker_printPDF.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | -------------------------------------------------------------------------------- /app/img/icon-filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/actions/__snapshots__/counter.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`actions should decrement should create decrement action 1`] = ` 4 | Object { 5 | "type": "DECREMENT_COUNTER", 6 | } 7 | `; 8 | 9 | exports[`actions should increment should create increment action 1`] = ` 10 | Object { 11 | "type": "INCREMENT_COUNTER", 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /app/img/icon-filter-enabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/reducers/__snapshots__/counter.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`reducers counter should handle DECREMENT_COUNTER 1`] = `0`; 4 | 5 | exports[`reducers counter should handle INCREMENT_COUNTER 1`] = `2`; 6 | 7 | exports[`reducers counter should handle initial state 1`] = `0`; 8 | 9 | exports[`reducers counter should handle unknown action type 1`] = `1`; 10 | -------------------------------------------------------------------------------- /app/img/icon-2x2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/containers/WorkerRoot.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { Provider } from 'react-redux'; 4 | import WorkerApp from './WorkerApp'; 5 | 6 | type RootType = { 7 | store: {}, 8 | history: {} 9 | }; 10 | 11 | export default function WorkerRoot({ store, history }: RootType) { 12 | return ( 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/store/configureStore.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import configureStoreDev from './configureStore.dev'; 3 | import configureStoreProd from './configureStore.prod'; 4 | 5 | const selectedConfigureStore = 6 | 7 | process.env.NODE_ENV === 'production' 8 | ? configureStoreProd 9 | : configureStoreDev; 10 | 11 | export const { configureStore } = selectedConfigureStore; 12 | 13 | export const { history } = selectedConfigureStore; 14 | -------------------------------------------------------------------------------- /internals/scripts/RunTests.js: -------------------------------------------------------------------------------- 1 | import spawn from 'cross-spawn'; 2 | import path from 'path'; 3 | 4 | const pattern = 5 | process.argv[2] === 'e2e' 6 | ? 'test/e2e/.+\\.spec\\.js' 7 | : 'test/(?!e2e/)[^/]+/.+\\.spec\\.js$'; 8 | 9 | const result = spawn.sync( 10 | path.normalize('./node_modules/.bin/jest'), 11 | [pattern, ...process.argv.slice(2)], 12 | { stdio: 'inherit' } 13 | ); 14 | 15 | process.exit(result.status); 16 | -------------------------------------------------------------------------------- /app/img/icon-filter-enabled copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/img/icon-cut-view.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /internals/scripts/CheckNodeEnv.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import chalk from 'chalk'; 3 | 4 | export default function CheckNodeEnv(expectedEnv: string) { 5 | if (!expectedEnv) { 6 | throw new Error('"expectedEnv" not set'); 7 | } 8 | 9 | if (process.env.NODE_ENV !== expectedEnv) { 10 | console.log( 11 | chalk.whiteBright.bgRed.bold( 12 | `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config` 13 | ) 14 | ); 15 | process.exit(2); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/img/icon-image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/containers/Root.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { Provider } from 'react-redux'; 4 | import { hot } from 'react-hot-loader/root'; 5 | import App from './App'; 6 | import ErrorBoundary from '../components/ErrorBoundary'; 7 | 8 | type Props = { 9 | store: Store, 10 | history: {} 11 | }; 12 | 13 | const Root = ({ store, history }: Props) => ( 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | 21 | export default hot(Root); 22 | -------------------------------------------------------------------------------- /app/img/icon-copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /internals/scripts/CheckPortInUse.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import chalk from 'chalk'; 3 | import detectPort from 'detect-port'; 4 | 5 | (function CheckPortInUse() { 6 | const port: string = process.env.PORT || '1212'; 7 | 8 | detectPort(port, (err: ?Error, availablePort: number) => { 9 | if (port !== String(availablePort)) { 10 | throw new Error( 11 | chalk.whiteBright.bgRed.bold( 12 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 yarn dev` 13 | ) 14 | ); 15 | } else { 16 | process.exit(0); 17 | } 18 | }); 19 | })(); 20 | -------------------------------------------------------------------------------- /app/img/icon-arrow-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/components/ErrorBoundary.css: -------------------------------------------------------------------------------- 1 | .ErrorContainer{ 2 | position: relative;/* need this to position inner content */ 3 | background-color: #1e1e1e; 4 | width: 100%; /* Full width */ 5 | height: 100vh; /* Full height */ 6 | } 7 | 8 | .ErrorContent{ 9 | position: absolute; 10 | top: 50%; 11 | left: 50%; 12 | right: auto; 13 | bottom: auto; 14 | margin-right: -50%; 15 | transform: translate(-50%, -50%); 16 | outline: 0; 17 | font-size: 100px; 18 | font-weight: 600; 19 | line-height: 100px; 20 | text-align: center; 21 | color: #fff; 22 | letter-spacing: 1px; 23 | margin: auto; 24 | font-family: 'Franchise', 'Roboto Condensed'; 25 | } 26 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pr 8 | - discussion 9 | - e2e 10 | - enhancement 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /app/img/icon-resize-vertical.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/img/icon-resize-horizontal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2017 2 | 3 | platform: 4 | - x64 5 | 6 | environment: 7 | matrix: 8 | - nodejs_version: 10 9 | - nodejs_version: 9 10 | 11 | cache: 12 | - '%LOCALAPPDATA%/Yarn' 13 | - node_modules -> package.json 14 | - app/node_modules -> app/package.json 15 | - flow-typed 16 | - '%USERPROFILE%\.electron' 17 | 18 | matrix: 19 | fast_finish: true 20 | 21 | build: off 22 | 23 | version: '{build}' 24 | 25 | shallow_clone: true 26 | 27 | clone_depth: 1 28 | 29 | install: 30 | - ps: Install-Product node $env:nodejs_version x64 31 | - set CI=true 32 | - yarn 33 | 34 | test_script: 35 | - node --version 36 | - yarn lint 37 | - yarn package 38 | - yarn test 39 | - yarn test-e2e 40 | -------------------------------------------------------------------------------- /app/img/icon-barcode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | ".eslintrc": "jsonc", 4 | ".babelrc": "jsonc", 5 | ".prettierrc": "jsonc" 6 | }, 7 | 8 | "javascript.validate.enable": false, 9 | "javascript.format.enable": false, 10 | "typescript.validate.enable": false, 11 | "typescript.format.enable": false, 12 | 13 | "flow.useNPMPackagedFlow": true, 14 | "search.exclude": { 15 | ".git": true, 16 | ".eslintcache": true, 17 | "app/dist": true, 18 | "app/main.prod.js": true, 19 | "app/main.prod.js.map": true, 20 | "bower_components": true, 21 | "dll": true, 22 | "flow-typed": true, 23 | "release": true, 24 | "node_modules": true, 25 | "npm-debug.log.*": true, 26 | "test/**/__snapshots__": true, 27 | "yarn.lock": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/img/icon-expand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/img/icon-3x3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/reducers/counter.spec.js: -------------------------------------------------------------------------------- 1 | import counter from '../../app/reducers/counter'; 2 | import { 3 | INCREMENT_COUNTER, 4 | DECREMENT_COUNTER 5 | } from '../../app/actions/counter'; 6 | 7 | describe('reducers', () => { 8 | describe('counter', () => { 9 | it('should handle initial state', () => { 10 | expect(counter(undefined, {})).toMatchSnapshot(); 11 | }); 12 | 13 | it('should handle INCREMENT_COUNTER', () => { 14 | expect(counter(1, { type: INCREMENT_COUNTER })).toMatchSnapshot(); 15 | }); 16 | 17 | it('should handle DECREMENT_COUNTER', () => { 18 | expect(counter(1, { type: DECREMENT_COUNTER })).toMatchSnapshot(); 19 | }); 20 | 21 | it('should handle unknown action type', () => { 22 | expect(counter(1, { type: 'unknown' })).toMatchSnapshot(); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /app/img/icon-header.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /scripts/includeInDist.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import log from 'electron-log'; 3 | 4 | const shell = require('shelljs'); 5 | 6 | // as the dist folder is not synced on github, we copy files into it before packaging 7 | log.info('running includeInDist script to copy some files into the dist folder for later packaging'); 8 | 9 | // set variables 10 | const moviePrintDir = shell.pwd().stdout; 11 | const distDir = path.resolve(moviePrintDir, 'app/dist/'); 12 | const resourcesDir = path.resolve(moviePrintDir, 'resources/'); 13 | // log.debug(moviePrintDir); 14 | // log.debug(distDir); 15 | // log.debug(resourcesDir); 16 | 17 | // copy files 18 | shell.set('-v'); // verbose 19 | shell.mkdir('-p', distDir); // create folder if it does not exist yet 20 | shell.cp('-nf', path.resolve(resourcesDir, 'font/Franchise-Bold.woff'), distDir); 21 | shell.cp('-nfr', path.resolve(resourcesDir, 'weights/'), distDir); 22 | -------------------------------------------------------------------------------- /app/img/icon-zoom-out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /internals/scripts/ElectronRebuild.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import path from 'path'; 3 | import { execSync } from 'child_process'; 4 | import fs from 'fs'; 5 | import { dependencies } from '../../app/package.json'; 6 | 7 | (() => { 8 | const nodeModulesPath = path.join( 9 | __dirname, 10 | '..', 11 | '..', 12 | 'app', 13 | 'node_modules' 14 | ); 15 | 16 | if ( 17 | Object.keys(dependencies || {}).length > 0 && 18 | fs.existsSync(nodeModulesPath) 19 | ) { 20 | const electronRebuildCmd = 21 | '../node_modules/.bin/electron-rebuild --parallel --force --types prod,dev,optional --module-dir .'; 22 | const cmd = 23 | process.platform === 'win32' 24 | ? electronRebuildCmd.replace(/\//g, '\\') 25 | : electronRebuildCmd; 26 | execSync(cmd, { 27 | cwd: path.join(__dirname, '..', '..', 'app'), 28 | stdio: 'inherit' 29 | }); 30 | } 31 | })(); 32 | -------------------------------------------------------------------------------- /app/img/icon-header-enabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/worker.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader'; 4 | import { ActionCreators as UndoActionCreators } from 'redux-undo'; 5 | import WorkerRoot from './containers/WorkerRoot'; 6 | import { configureStore, history } from './store/configureStore'; 7 | 8 | const store = configureStore(); 9 | 10 | render( 11 | 12 | 13 | , 14 | document.getElementById('worker') 15 | ); 16 | 17 | if (module.hot) { 18 | module.hot.accept('./containers/WorkerRoot', () => { 19 | const NextWorkerRoot = require('./containers/WorkerRoot').default; // eslint-disable-line global-require 20 | render( 21 | 22 | 23 | , 24 | document.getElementById('worker') 25 | ); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /internals/scripts/CheckBuildsExist.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // Check if the renderer and main bundles are built 3 | import path from 'path'; 4 | import chalk from 'chalk'; 5 | import fs from 'fs'; 6 | 7 | function CheckBuildsExist() { 8 | const mainPath = path.join(__dirname, '..', '..', 'app', 'main.prod.js'); 9 | const rendererPath = path.join( 10 | __dirname, 11 | '..', 12 | '..', 13 | 'app', 14 | 'dist', 15 | 'renderer.prod.js' 16 | ); 17 | 18 | if (!fs.existsSync(mainPath)) { 19 | throw new Error( 20 | chalk.whiteBright.bgRed.bold( 21 | 'The main process is not built yet. Build it by running "yarn build-main"' 22 | ) 23 | ); 24 | } 25 | 26 | if (!fs.existsSync(rendererPath)) { 27 | throw new Error( 28 | chalk.whiteBright.bgRed.bold( 29 | 'The renderer process is not built yet. Build it by running "yarn build-renderer"' 30 | ) 31 | ); 32 | } 33 | } 34 | 35 | CheckBuildsExist(); 36 | -------------------------------------------------------------------------------- /app/img/icon-add-scene.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /app/main.prod.js 3 | /app/main.prod.js.map 4 | /app/dist/.* 5 | /resources/.* 6 | /node_modules/webpack-cli 7 | /release/.* 8 | /dll/.* 9 | /release/.* 10 | /git/.* 11 | 12 | [include] 13 | 14 | [libs] 15 | 16 | [options] 17 | esproposal.class_static_fields=enable 18 | esproposal.class_instance_fields=enable 19 | esproposal.export_star_as=enable 20 | module.name_mapper.extension='css' -> '/internals/flow/CSSModule.js.flow' 21 | module.name_mapper.extension='styl' -> '/internals/flow/CSSModule.js.flow' 22 | module.name_mapper.extension='scss' -> '/internals/flow/CSSModule.js.flow' 23 | module.name_mapper.extension='png' -> '/internals/flow/WebpackAsset.js.flow' 24 | module.name_mapper.extension='jpg' -> '/internals/flow/WebpackAsset.js.flow' 25 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe 26 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue 27 | -------------------------------------------------------------------------------- /app/img/icon-zoom-in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | .eslintcache 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | app/node_modules 30 | 31 | # OSX 32 | .DS_Store 33 | 34 | # flow-typed 35 | flow-typed/npm/* 36 | !flow-typed/npm/module_vx.x.x.js 37 | 38 | # App packaged 39 | release 40 | app/main.prod.js 41 | app/main.prod.js.map 42 | app/renderer.prod.js 43 | app/renderer.prod.js.map 44 | app/style.css 45 | app/style.css.map 46 | dist 47 | dll 48 | main.js 49 | main.js.map 50 | 51 | .idea 52 | npm-debug.log.* 53 | .*.dockerfile -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | .eslintcache 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | app/node_modules 30 | 31 | # OSX 32 | .DS_Store 33 | 34 | # flow-typed 35 | flow-typed/npm/* 36 | !flow-typed/npm/module_vx.x.x.js 37 | 38 | # App packaged 39 | release 40 | app/main.prod.js 41 | app/main.prod.js.map 42 | app/renderer.prod.js 43 | app/renderer.prod.js.map 44 | app/style.css 45 | app/style.css.map 46 | dist 47 | dll 48 | main.js 49 | main.js.map 50 | 51 | .idea 52 | npm-debug.log.* 53 | .env 54 | electron-builder.env 55 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | .eslintcache 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | app/node_modules 30 | 31 | # OSX 32 | .DS_Store 33 | 34 | # flow-typed 35 | flow-typed/npm/* 36 | !flow-typed/npm/module_vx.x.x.js 37 | 38 | # App packaged 39 | release 40 | app/main.prod.js 41 | app/main.prod.js.map 42 | app/renderer.prod.js 43 | app/renderer.prod.js.map 44 | app/style.css 45 | app/style.css.map 46 | dist 47 | dll 48 | main.js 49 | main.js.map 50 | 51 | .idea 52 | npm-debug.log.* 53 | __snapshots__ 54 | 55 | # Package.json 56 | package.json 57 | .travis.yml 58 | -------------------------------------------------------------------------------- /app/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import undoable, { excludeAction, groupByActionTypes } from 'redux-undo'; 3 | import sheetsByFileId from './sheetsByFileId'; 4 | import files from './files'; 5 | import settings from './settings'; 6 | import visibilitySettings from './visibilitySettings'; 7 | import { UNDO_STEPS_LIMIT } from '../utils/constants'; 8 | 9 | const rootReducer = combineReducers({ 10 | visibilitySettings, 11 | undoGroup: undoable( 12 | combineReducers({ 13 | settings, 14 | sheetsByFileId, 15 | files, 16 | }), 17 | { 18 | filter: excludeAction([ 19 | // 'UPDATE_MOVIE_LIST_ITEM_USERATIO' 20 | ]), 21 | groupBy: groupByActionTypes([ 22 | 'UPDATE_MOVIE_LIST_ITEM_USERATIO', 23 | 'UPDATE_FRAMENUMBER_AND_COLORARRAY_OF_THUMB', 24 | 'UPDATE_OBJECTURL_FROM_THUMBLIST', 25 | 'UPDATE_SHEET_COLUMNCOUNT', 26 | 'SET_DEFAULT_MARGIN', 27 | 'SET_DEFAULT_SHOW_HEADER', 28 | 'SET_DEFAULT_ROUNDED_CORNERS', 29 | ]), 30 | limit: UNDO_STEPS_LIMIT, 31 | }, 32 | ), 33 | }); 34 | 35 | export default rootReducer; 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-present Jakob Schindegger 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 | -------------------------------------------------------------------------------- /app/store/configureStore.prod.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { createStore, applyMiddleware } from 'redux'; 3 | import thunk from 'redux-thunk'; 4 | import { createHashHistory } from 'history'; 5 | import { routerMiddleware } from 'connected-react-router'; 6 | import throttle from 'lodash/throttle'; 7 | import rootReducer from '../reducers'; 8 | import { loadState, saveState } from './localStorage'; 9 | 10 | const history = createHashHistory(); 11 | const router = routerMiddleware(history); 12 | const enhancer = applyMiddleware(thunk, router); 13 | 14 | function configureStore(initialState) { 15 | let persistedState; 16 | if (initialState === undefined) { 17 | persistedState = loadState(); 18 | } else { 19 | persistedState = initialState; 20 | } 21 | 22 | // Create Store 23 | const store = createStore(rootReducer, persistedState, enhancer); // eslint-disable-line 24 | 25 | store.subscribe(throttle(() => { 26 | saveState(store.getState()); 27 | // // only store thumbs in localStorage 28 | // saveState({ 29 | // thumbs: store.getState().thumbs 30 | // }); 31 | }, 1000)); 32 | 33 | return store; 34 | } 35 | 36 | export default { configureStore, history }; 37 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MoviePrint_v004", 3 | "productName": "MoviePrint_v004", 4 | "version": "0.2.23", 5 | "description": "A tool which lets you create screenshots of entire movies in an instant.", 6 | "main": "./main.prod.js", 7 | "author": { 8 | "name": "Jakob Schindegger", 9 | "email": "jakob@movieprint.org", 10 | "url": "https://movieprint.org" 11 | }, 12 | "license": "MIT", 13 | "bugs": { 14 | "url": "https://github.com/fakob/MoviePrint_v004/issues" 15 | }, 16 | "keywords": [ 17 | "opencv", 18 | "screenshot", 19 | "movie" 20 | ], 21 | "homepage": "https://github.com/fakob/MoviePrint_v004#readme", 22 | "scripts": { 23 | "electron-rebuild": "node -r ../internals/scripts/BabelRegister.js ../internals/scripts/ElectronRebuild.js", 24 | "postinstall": "yarn electron-rebuild -w opencv4nodejs,better-sqlite3,@tensorflow/tfjs-node" 25 | }, 26 | "dependencies": { 27 | "@tensorflow/tfjs-node": "^1.5.2", 28 | "better-sqlite3": "^5.4.0", 29 | "face-api.js": "^0.22.1", 30 | "opencv4nodejs": "^4.9.1", 31 | "typeface-open-sans": "^0.0.54", 32 | "typeface-roboto-condensed": "^0.0.54", 33 | "typeface-ubuntu": "^0.0.65" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/img/icon-add-interval.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { AppContainer as ReactHotAppContainer } from 'react-hot-loader'; 4 | import { ActionCreators as UndoActionCreators } from 'redux-undo'; 5 | import Root from './containers/Root'; 6 | import { configureStore, history } from './store/configureStore'; 7 | 8 | const { ipcRenderer } = require('electron'); 9 | 10 | ipcRenderer.on('undo', () => { 11 | store.dispatch(UndoActionCreators.undo()); 12 | }); 13 | 14 | ipcRenderer.on('redo', () => { 15 | store.dispatch(UndoActionCreators.redo()); 16 | }); 17 | 18 | const store = configureStore(); 19 | 20 | const AppContainer = process.env.PLAIN_HMR ? Fragment : ReactHotAppContainer; 21 | 22 | render( 23 | 24 | 25 | , 26 | document.getElementById('root') 27 | ); 28 | 29 | // if (module.hot) { 30 | // module.hot.accept('./containers/Root', () => { 31 | // // eslint-disable-line global-require 32 | // 33 | // const NextRoot = require('./containers/Root').default; 34 | // render( 35 | // 36 | // 37 | // , 38 | // document.getElementById('root') 39 | // ); 40 | // }); 41 | // } 42 | -------------------------------------------------------------------------------- /app/img/icon-4x4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/utils/queue.js: -------------------------------------------------------------------------------- 1 | /* eslint func-names: ["error", "never"] */ 2 | 3 | // import log from 'electron-log'; 4 | 5 | // the queue adds all new ones in front 6 | // const q = new Queue(): 7 | // q.add(1); 8 | // q.add(2); 9 | // q.add(3); 10 | // console.log(q); 11 | // // [3,2,1] 12 | 13 | function Queue() { 14 | this.data = []; 15 | } 16 | 17 | Queue.prototype.add = function(record) { 18 | this.data.unshift(record); 19 | } 20 | 21 | Queue.prototype.addArray = function(arrayOfRecords) { 22 | const combinedArray = [...this.data, ...arrayOfRecords]; 23 | this.data = combinedArray; 24 | } 25 | 26 | Queue.prototype.removeFirst = function() { 27 | return this.data.shift(); 28 | } 29 | 30 | Queue.prototype.removeFirstMany = function(amount) { 31 | return this.data.splice(0, amount); 32 | } 33 | 34 | Queue.prototype.removeLastMany = function(amount) { 35 | return this.data.splice(amount * -1, amount); 36 | } 37 | 38 | Queue.prototype.removeLast = function() { 39 | return this.data.pop(); 40 | } 41 | 42 | Queue.prototype.clear = function() { 43 | this.data = []; 44 | } 45 | 46 | Queue.prototype.first = function() { 47 | return this.data[0]; 48 | } 49 | 50 | Queue.prototype.last = function() { 51 | return this.data[this.data.length - 1]; 52 | } 53 | 54 | Queue.prototype.size = function() { 55 | return this.data.length; 56 | } 57 | 58 | export default Queue; 59 | -------------------------------------------------------------------------------- /app/reducers/visibilitySettings.js: -------------------------------------------------------------------------------- 1 | const visibilitySettings = (state = {}, action) => { 2 | switch (action.type) { 3 | case 'SET_VISIBILITY_FILTER': 4 | return { ...state, visibilityFilter: action.filter }; 5 | case 'TOGGLE_MOVIELIST': 6 | return { ...state, showMovielist: !state.showMovielist }; 7 | case 'SHOW_MOVIELIST': 8 | return { ...state, showMovielist: true }; 9 | case 'HIDE_MOVIELIST': 10 | return { ...state, showMovielist: false }; 11 | case 'TOGGLE_SETTINGS': 12 | return { ...state, showSettings: !state.showSettings }; 13 | case 'SHOW_SETTINGS': 14 | return { ...state, showSettings: true }; 15 | case 'HIDE_SETTINGS': 16 | return { ...state, showSettings: false }; 17 | case 'SET_VIEW': 18 | return { ...state, defaultView: action.defaultView }; 19 | case 'SET_SHEETVIEW': 20 | return { ...state, defaultSheetView: action.defaultSheetView }; 21 | case 'SET_ZOOMLEVEL': 22 | return { ...state, defaultZoomLevel: action.defaultZoomLevel }; 23 | // case 'TOGGLE_ZOOM_OUT': 24 | // return { ...state, zoomOut: !state.zoomOut }; 25 | // case 'ZOOM_OUT': 26 | // return { ...state, zoomOut: true }; 27 | // case 'ZOOM_IN': 28 | // return { ...state, zoomOut: false }; 29 | default: 30 | return state; 31 | } 32 | }; 33 | 34 | export default visibilitySettings; 35 | -------------------------------------------------------------------------------- /app/components/Popup.css: -------------------------------------------------------------------------------- 1 | .popup { 2 | margin-bottom: 0 !important; 3 | font-family: Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif !important; 4 | font-weight: 100 !important; 5 | font-size: 12px !important; 6 | } 7 | 8 | .popupSmall { 9 | /* background-color: rgba(30, 30, 30, 0.9) !important; */ 10 | /* color: #ffffff !important; */ 11 | /* border: none !important; */ 12 | margin: 0 !important; 13 | padding: 4px 8px !important; 14 | font-family: Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif !important; 15 | font-weight: 100 !important; 16 | font-size: 12px !important; 17 | } 18 | 19 | .popupSmall::before { 20 | display: none !important; 21 | } 22 | 23 | .toast { 24 | padding: 20px; 25 | border-radius: 4px; 26 | /* overflow-wrap: break-word; */ 27 | overflow-wrap: anywhere; 28 | hyphens: auto; 29 | } 30 | 31 | .toastInfo { 32 | /* background-color: rgba(60, 60, 60, 1.0); */ 33 | background-color: rgba(60, 60, 60, 0.97); 34 | box-shadow: 0px 0px 64px 0px rgba(255,255,255,0.1); 35 | color: white; 36 | } 37 | 38 | .toastSuccess { 39 | background-color: rgba(56, 143, 2, 0.97); 40 | /* background-color: #388F02; */ 41 | color: white; 42 | } 43 | 44 | .toastError { 45 | background-color: rgba(194, 0, 0, 0.97); 46 | /* background-color: #c20000; */ 47 | color: white; 48 | } 49 | 50 | .toastProgress { 51 | background: #f2711c; 52 | } 53 | -------------------------------------------------------------------------------- /app/utils/fileObject.js: -------------------------------------------------------------------------------- 1 | // import Dexie from 'dexie'; 2 | import log from 'electron-log'; 3 | import imageDB from './db'; 4 | 5 | const FileObject = imageDB.frameList.defineClass({ 6 | frameId: String, 7 | lastModified: Number, 8 | lastModifiedDate: String, 9 | name: String, 10 | path: String, 11 | size: Number, 12 | frameNumber: Number, 13 | type: String, 14 | webkitRelativePath: String, 15 | base64: String, 16 | data: Blob 17 | }); 18 | 19 | FileObject.prototype.objectUrl = ''; 20 | 21 | FileObject.prototype.getObjectUrl2 = () => { 22 | console.log(this); 23 | const objectUrl = window.URL.createObjectURL(this.data); 24 | return objectUrl; 25 | }; 26 | 27 | FileObject.prototype.getObjectUrl = () => { 28 | if (this.objectUrl !== '' && !this.disposed) { 29 | return this.objectUrl; 30 | } 31 | if (!this.disposed) { 32 | this.objectUrl = window.URL.createObjectURL(this.data); 33 | return this.objectUrl; 34 | } 35 | log.warn('File disposed!'); 36 | throw 'File disposed!'; 37 | }; 38 | 39 | FileObject.prototype.revokeObjectURL = () => { 40 | URL.revokeObjectURL(this.objectUrl); 41 | this.objectUrl = ''; 42 | }; 43 | 44 | FileObject.prototype.disposed = false; 45 | 46 | FileObject.prototype.disposeData = () => { 47 | URL.revokeObjectURL(this.objectUrl); 48 | this.objectUrl = ''; 49 | this.data = null; 50 | this.disposed = true; 51 | }; 52 | 53 | export default FileObject; 54 | -------------------------------------------------------------------------------- /app/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MoviePrint 6 | 17 | 18 | 19 |
20 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/worker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MoviePrint_worker 6 | 17 | 18 | 19 |
20 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/components/Timeline.css: -------------------------------------------------------------------------------- 1 | 2 | .container { 3 | 4 | } 5 | 6 | .timelineWrapperSelection { 7 | width: 100%; 8 | position: relative; 9 | /* background-color: #421400; */ 10 | background-color: rgba(142, 44, 2, 1.0); 11 | height: 28px; 12 | margin-bottom: 4px; 13 | cursor: col-resize; 14 | } 15 | 16 | .timelineWrapper { 17 | width: 100%; 18 | position: relative; 19 | /* background-color: #421400; */ 20 | height: 16px; 21 | /* cursor: col-resize; */ 22 | } 23 | 24 | .timelinePlayheadSelection { 25 | position: absolute; 26 | /* bottom: 0; */ 27 | /* top: 0; */ 28 | width: 2px; 29 | background-color: rgba(255, 80, 6, 1); 30 | height: 24px; 31 | margin-top: 2px; 32 | } 33 | 34 | .timelinePlayhead { 35 | position: absolute; 36 | /* bottom: 0; */ 37 | /* top: 0; */ 38 | width: 2px; 39 | background-color: rgba(255, 80, 6, 1); 40 | height: 16px; 41 | margin-top: 0px; 42 | } 43 | 44 | .timelineCutSelection { 45 | position: absolute; 46 | background-color: rgba(255, 80, 6, 0.4); 47 | height: 16px; 48 | cursor: col-resize; 49 | } 50 | 51 | .timelineCut { 52 | position: absolute; 53 | background-color: #421400; 54 | height: 16px; 55 | cursor: col-resize; 56 | } 57 | 58 | .timelineWrapper .currentTime { 59 | background-color: #FF5006; 60 | z-index: 2; 61 | } 62 | .timelineWrapper .cutStartTime { 63 | background-color: rgba(0, 0, 0, 0.3); 64 | border-left: 1px solid black; 65 | border-right: 1px solid black; 66 | z-index: 1; 67 | } 68 | -------------------------------------------------------------------------------- /app/worker_opencv.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MoviePrint_opencvWorker 6 | 17 | 18 | 19 |
20 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/app.global.css: -------------------------------------------------------------------------------- 1 | /* 2 | * @NOTE: Prepend a `~` to css file paths that are in your node_modules 3 | * See https://github.com/webpack-contrib/sass-loader#imports 4 | */ 5 | 6 | @import '~semantic-ui-css/semantic.min.css'; 7 | @import '~react-toastify/dist/ReactToastify.min.css'; 8 | @import '~typeface-open-sans'; 9 | @import '~typeface-roboto-condensed'; 10 | @import '~typeface-ubuntu'; 11 | @import '~rc-slider/assets/index.css'; 12 | @font-face { 13 | font-family: Franchise; 14 | src: url("./dist/Franchise-Bold.woff"); 15 | } 16 | 17 | .rc-slider-tooltip { 18 | z-index: 1000; 19 | } 20 | 21 | .rc-slider-disabled { 22 | opacity: 0.3; 23 | background-color: transparent; 24 | } 25 | 26 | body { 27 | color: white; 28 | } 29 | 30 | label { 31 | color: white; 32 | } 33 | 34 | mark { 35 | margin-left: 4px; 36 | margin-right: 4px; 37 | background-color: rgba(255, 80, 6, 0.9); 38 | color: white; 39 | white-space: nowrap; 40 | } 41 | 42 | mark::before { 43 | content: '\00a0\00a0'; 44 | } 45 | 46 | mark::after { 47 | content: '\00a0\00a0'; 48 | } 49 | 50 | /* width */ 51 | ::-webkit-scrollbar { 52 | width: 6px !important; 53 | } 54 | 55 | /* Track */ 56 | ::-webkit-scrollbar-track { 57 | background: #1e1e1e !important; 58 | } 59 | 60 | 61 | /* Handle */ 62 | ::-webkit-scrollbar-thumb { 63 | background: #1e1e1e !important; 64 | } 65 | 66 | /* Handle on hover */ 67 | ::-webkit-scrollbar-thumb:hover { 68 | background: #2c2c2c !important; 69 | } 70 | 71 | h6 { 72 | font-size: 0.7rem; 73 | } 74 | -------------------------------------------------------------------------------- /app/worker_database.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MoviePrint_databaseWorker 6 | 17 | 18 | 19 |
20 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /test/components/__snapshots__/Counter.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Counter component should match exact snapshot 1`] = ` 4 |
5 |
6 |
10 | 14 | 17 | 18 |
19 |
23 | 1 24 |
25 |
28 | 38 | 48 | 56 | 64 |
65 |
66 |
67 | `; 68 | -------------------------------------------------------------------------------- /test/actions/counter.spec.js: -------------------------------------------------------------------------------- 1 | import { spy } from 'sinon'; 2 | import * as actions from '../../app/actions/counter'; 3 | 4 | describe('actions', () => { 5 | it('should increment should create increment action', () => { 6 | expect(actions.increment()).toMatchSnapshot(); 7 | }); 8 | 9 | it('should decrement should create decrement action', () => { 10 | expect(actions.decrement()).toMatchSnapshot(); 11 | }); 12 | 13 | it('should incrementIfOdd should create increment action', () => { 14 | const fn = actions.incrementIfOdd(); 15 | expect(fn).toBeInstanceOf(Function); 16 | const dispatch = spy(); 17 | const getState = () => ({ counter: 1 }); 18 | fn(dispatch, getState); 19 | expect(dispatch.calledWith({ type: actions.INCREMENT_COUNTER })).toBe(true); 20 | }); 21 | 22 | it('should incrementIfOdd shouldnt create increment action if counter is even', () => { 23 | const fn = actions.incrementIfOdd(); 24 | const dispatch = spy(); 25 | const getState = () => ({ counter: 2 }); 26 | fn(dispatch, getState); 27 | expect(dispatch.called).toBe(false); 28 | }); 29 | 30 | // There's no nice way to test this at the moment... 31 | it('should incrementAsync', done => { 32 | const fn = actions.incrementAsync(1); 33 | expect(fn).toBeInstanceOf(Function); 34 | const dispatch = spy(); 35 | fn(dispatch); 36 | setTimeout(() => { 37 | expect(dispatch.calledWith({ type: actions.INCREMENT_COUNTER })).toBe( 38 | true 39 | ); 40 | done(); 41 | }, 5); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /app/img/icon-grid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/utils/db.js: -------------------------------------------------------------------------------- 1 | import Dexie from 'dexie'; 2 | import log from 'electron-log'; 3 | // import FileObject from './fileObject'; 4 | 5 | // Force debug mode to get async stacks from exceptions. 6 | if (process.env.NODE_ENV === 'production') { 7 | Dexie.debug = false; 8 | } else { 9 | Dexie.debug = true; // In production, set to false to increase performance a little. 10 | } 11 | const imageDB = new Dexie('ImageDatabase'); 12 | imageDB.version(1).stores({ 13 | frameList: '&frameId, fileId, frameNumber, [fileId+frameNumber]', 14 | // fileScanList: '&fileId', 15 | }); 16 | 17 | const FileObject = imageDB.frameList.defineClass({ 18 | frameId: String, 19 | fileId: String, 20 | frameNumber: Number, 21 | data: Blob 22 | }); 23 | 24 | FileObject.prototype.objectUrl = ''; 25 | 26 | FileObject.prototype.getObjectUrl2 = () => { 27 | console.log(this); 28 | const objectUrl = window.URL.createObjectURL(this.data); 29 | return objectUrl; 30 | }; 31 | 32 | FileObject.prototype.getObjectUrl = () => { 33 | if (this.objectUrl !== '' && !this.disposed) { 34 | return this.objectUrl; 35 | } 36 | if (!this.disposed) { 37 | this.objectUrl = window.URL.createObjectURL(this.data); 38 | return this.objectUrl; 39 | } 40 | log.warn('File disposed!'); 41 | throw 'File disposed!'; 42 | }; 43 | 44 | FileObject.prototype.revokeObjectURL = () => { 45 | URL.revokeObjectURL(this.objectUrl); 46 | this.objectUrl = ''; 47 | }; 48 | 49 | FileObject.prototype.disposed = false; 50 | 51 | FileObject.prototype.disposeData = () => { 52 | URL.revokeObjectURL(this.objectUrl); 53 | this.objectUrl = ''; 54 | this.data = null; 55 | this.disposed = true; 56 | }; 57 | 58 | export default imageDB; 59 | -------------------------------------------------------------------------------- /app/img/icon-unhide.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/containers/Settings.css: -------------------------------------------------------------------------------- 1 | 2 | .slider { 3 | width: 90%; 4 | margin: 10px; 5 | background-color: #3e3e3e; 6 | } 7 | 8 | .label { 9 | color: white !important; 10 | } 11 | 12 | .subCheckbox { 13 | margin-left: 30px; 14 | } 15 | 16 | .input { 17 | height: 34px; 18 | width: 80px; 19 | } 20 | 21 | .edit { 22 | padding-left: 4px; 23 | } 24 | 25 | .colorPickerColor { 26 | position: relative; 27 | width: 190px; 28 | height: 32px; 29 | border-radius: 2px; 30 | } 31 | 32 | .colorPickerSwatch { 33 | padding: 2px; 34 | background: #fff; 35 | border-radius: 1px; 36 | box-shadow: 0 0 0 1px rgba(0,0,0,.1); 37 | display: inline-block; 38 | cursor: pointer; 39 | } 40 | 41 | .colorPickerPopover { 42 | position: absolute; 43 | z-index: 2; 44 | } 45 | 46 | .colorPickerCover { 47 | position: fixed; 48 | top: 0px; 49 | right: 0px; 50 | bottom: 0px; 51 | left: 0px; 52 | } 53 | 54 | .colorPickerText { 55 | padding: 6px; 56 | font-size: 20px; 57 | font-family: 'Open sans'; 58 | text-align: center; 59 | } 60 | 61 | .smallText { 62 | margin-top: 8px; 63 | font-size: 0.9rem; 64 | } 65 | 66 | .previewCustomName { 67 | background-color: rgba(0,0,0,0.8) !important; 68 | color: rgba(255,255,255,0.5) !important; 69 | font-style: italic !important; 70 | font-weight: 200 !important; 71 | letter-spacing: 0.3px; 72 | line-height: 1.2rem !important; 73 | } 74 | 75 | .smallDivider { 76 | margin: 0.4rem 0 !important; 77 | } 78 | 79 | .attributeButton { 80 | margin: 1px !important; 81 | } 82 | 83 | .accordion > :global(.active) { 84 | background-color: rgba(255,255,255,0.05) !important; 85 | } 86 | 87 | .accordion :global(.title) { 88 | border-width: 1px 0 0 0 !important; 89 | border-style: solid !important; 90 | border-color: rgba(255,255,255,0.05) !important; 91 | } 92 | -------------------------------------------------------------------------------- /app/img/icon-sort.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/components/Menu.css: -------------------------------------------------------------------------------- 1 | .container { 2 | z-index: 200; 3 | background-color: rgba(30,30,30,1); 4 | } 5 | 6 | .menu { 7 | z-index: 200; 8 | /* background-color: #1e1e1e; */ 9 | background-color: rgba(30,30,30,1) !important; 10 | } 11 | 12 | .saveButton { 13 | background-color: rgba(255, 80, 6, 1.0) !important; 14 | } 15 | 16 | .saveButton:hover { 17 | background-color: rgba(255, 80, 6, 0.5) !important; 18 | } 19 | 20 | .saveButton :global(.ui.loader) { 21 | margin-right: 4.5px !important; 22 | } 23 | 24 | .saveButton :global(.ui.mini.loader) { 25 | height: 0.8rem !important; 26 | } 27 | 28 | .saveButton :global(.ui.loader:after) { 29 | border-color: #ffffff transparent transparent !important; 30 | } 31 | 32 | @keyframes rotating { 33 | from { 34 | -ms-transform: rotate(0deg); 35 | -moz-transform: rotate(0deg); 36 | -webkit-transform: rotate(0deg); 37 | -o-transform: rotate(0deg); 38 | transform: rotate(0deg); 39 | } 40 | to { 41 | -ms-transform: rotate(360deg); 42 | -moz-transform: rotate(360deg); 43 | -webkit-transform: rotate(360deg); 44 | -o-transform: rotate(360deg); 45 | transform: rotate(360deg); 46 | } 47 | } 48 | 49 | @keyframes loader{ 50 | from { 51 | -webkit-transform:rotate(0); 52 | transform:rotate(0) 53 | } to { 54 | -webkit-transform:rotate(360deg); 55 | transform:rotate(360deg) 56 | }} 57 | 58 | .spinner { 59 | animation:loader .6s linear; 60 | animation-iteration-count:infinite; 61 | border-radius:500rem; 62 | border-color:#767676 transparent transparent; 63 | border-style:solid; 64 | width: 4px; 65 | border-width:.2em; 66 | box-shadow:0 0 0 1px transparent 67 | } 68 | 69 | /* .headerItem { 70 | float: left; 71 | opacity: 0.5; 72 | } 73 | 74 | .headerItem:hover { 75 | cursor: pointer; 76 | opacity: 1; 77 | } 78 | 79 | .subHeader { 80 | float: left; 81 | opacity: 0.5; 82 | font-family: 'Open sans'; 83 | font-size: 12px; 84 | color: #eee; 85 | padding: 12px; 86 | } */ 87 | -------------------------------------------------------------------------------- /app/components/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button, Divider } from 'semantic-ui-react'; 3 | import log from 'electron-log'; 4 | import styles from './ErrorBoundary.css'; 5 | 6 | const { ipcRenderer } = require('electron'); 7 | 8 | class ErrorBoundary extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { hasError: false }; 12 | 13 | this.onReloadClick = this.onReloadClick.bind(this); 14 | this.onResetClick = this.onResetClick.bind(this); 15 | } 16 | 17 | componentDidCatch(error, info) { 18 | // Display fallback UI 19 | this.setState({ hasError: true }); 20 | // You can also log the error to an error reporting service 21 | log.error(error); 22 | log.warn(info); 23 | } 24 | 25 | onReloadClick() { 26 | log.info('reloadclick'); 27 | ipcRenderer.send('reload-application'); 28 | } 29 | 30 | onResetClick() { 31 | log.info('resetclick'); 32 | ipcRenderer.send('reset-application'); 33 | } 34 | 35 | render() { 36 | if (this.state.hasError) { 37 | // You can render any custom fallback UI 38 | return ( 39 |
40 |
41 | SOMETHING WENT WRONG 42 | {/* */} 43 | 50 |
62 |
63 | ); 64 | } 65 | return this.props.children; 66 | } 67 | } 68 | 69 | export default ErrorBoundary; 70 | -------------------------------------------------------------------------------- /app/img/icon-hide.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /configs/webpack.config.renderer.dev.dll.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: off, import/no-dynamic-require: off */ 2 | 3 | /** 4 | * Builds the DLL for development electron renderer process 5 | */ 6 | 7 | import webpack from 'webpack'; 8 | import path from 'path'; 9 | import merge from 'webpack-merge'; 10 | import baseConfig from './webpack.config.base'; 11 | import { dependencies } from '../package.json'; 12 | import CheckNodeEnv from '../internals/scripts/CheckNodeEnv'; 13 | 14 | CheckNodeEnv('development'); 15 | 16 | const dist = path.join(__dirname, '..', 'dll'); 17 | 18 | export default merge.smart(baseConfig, { 19 | context: path.join(__dirname, '..'), 20 | 21 | devtool: 'eval', 22 | 23 | mode: 'development', 24 | 25 | target: 'electron-renderer', 26 | 27 | externals: ['fsevents', 'crypto-browserify'], 28 | 29 | /** 30 | * Use `module` from `webpack.config.renderer.dev.babel.js` 31 | */ 32 | module: require('./webpack.config.renderer.dev.babel').default.module, 33 | 34 | entry: { 35 | renderer: Object.keys(dependencies || {}) 36 | }, 37 | 38 | output: { 39 | library: 'renderer', 40 | path: dist, 41 | filename: '[name].dev.dll.js', 42 | libraryTarget: 'var' 43 | }, 44 | 45 | plugins: [ 46 | new webpack.DllPlugin({ 47 | path: path.join(dist, '[name].json'), 48 | name: '[name]' 49 | }), 50 | 51 | /** 52 | * Create global constants which can be configured at compile time. 53 | * 54 | * Useful for allowing different behaviour between development builds and 55 | * release builds 56 | * 57 | * NODE_ENV should be production so that modules do not perform certain 58 | * development checks 59 | */ 60 | new webpack.EnvironmentPlugin({ 61 | NODE_ENV: 'development' 62 | }), 63 | 64 | new webpack.LoaderOptionsPlugin({ 65 | debug: true, 66 | options: { 67 | context: path.join(__dirname, '..', 'app'), 68 | output: { 69 | path: path.join(__dirname, '..', 'dll') 70 | } 71 | } 72 | }) 73 | ] 74 | }); 75 | -------------------------------------------------------------------------------- /app/img/icon-frame-info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /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 CheckNodeEnv from '../internals/scripts/CheckNodeEnv'; 12 | 13 | CheckNodeEnv('production'); 14 | 15 | export default merge.smart(baseConfig, { 16 | devtool: 'source-map', 17 | 18 | mode: 'production', 19 | 20 | target: 'electron-main', 21 | 22 | entry: './app/main.dev', 23 | 24 | output: { 25 | path: path.join(__dirname, '..'), 26 | filename: './app/main.prod.js' 27 | }, 28 | 29 | optimization: { 30 | minimizer: process.env.E2E_BUILD 31 | ? [] 32 | : [ 33 | new TerserPlugin({ 34 | parallel: true, 35 | sourceMap: true, 36 | cache: true 37 | }) 38 | ] 39 | }, 40 | 41 | plugins: [ 42 | new BundleAnalyzerPlugin({ 43 | analyzerMode: 44 | process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled', 45 | openAnalyzer: process.env.OPEN_ANALYZER === 'true' 46 | }), 47 | 48 | /** 49 | * Create global constants which can be configured at compile time. 50 | * 51 | * Useful for allowing different behaviour between development builds and 52 | * release builds 53 | * 54 | * NODE_ENV should be production so that modules do not perform certain 55 | * development checks 56 | */ 57 | new webpack.EnvironmentPlugin({ 58 | NODE_ENV: 'production', 59 | DEBUG_PROD: false, 60 | START_MINIMIZED: false 61 | }) 62 | ], 63 | 64 | /** 65 | * Disables webpack processing of __dirname and __filename. 66 | * If you run the bundle in node.js it falls back to these values of node.js. 67 | * https://github.com/webpack/webpack/issues/2010 68 | */ 69 | node: { 70 | __dirname: false, 71 | __filename: false 72 | } 73 | }); 74 | -------------------------------------------------------------------------------- /test/containers/CounterPage.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Enzyme, { mount } from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | import { Provider } from 'react-redux'; 5 | import { createBrowserHistory } from 'history'; 6 | import { ConnectedRouter } from 'react-router-redux'; 7 | import CounterPage from '../../app/containers/CounterPage'; 8 | import { configureStore } from '../../app/store/configureStore'; 9 | 10 | Enzyme.configure({ adapter: new Adapter() }); 11 | 12 | function setup(initialState) { 13 | const store = configureStore(initialState); 14 | const history = createBrowserHistory(); 15 | const provider = ( 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | const app = mount(provider); 23 | return { 24 | app, 25 | buttons: app.find('button'), 26 | p: app.find('.counter') 27 | }; 28 | } 29 | 30 | describe('containers', () => { 31 | describe('App', () => { 32 | it('should display initial count', () => { 33 | const { p } = setup(); 34 | expect(p.text()).toMatch(/^0$/); 35 | }); 36 | 37 | it('should display updated count after increment button click', () => { 38 | const { buttons, p } = setup(); 39 | buttons.at(0).simulate('click'); 40 | expect(p.text()).toMatch(/^1$/); 41 | }); 42 | 43 | it('should display updated count after decrement button click', () => { 44 | const { buttons, p } = setup(); 45 | buttons.at(1).simulate('click'); 46 | expect(p.text()).toMatch(/^-1$/); 47 | }); 48 | 49 | it('shouldnt change if even and if odd button clicked', () => { 50 | const { buttons, p } = setup(); 51 | buttons.at(2).simulate('click'); 52 | expect(p.text()).toMatch(/^0$/); 53 | }); 54 | 55 | it('should change if odd and if odd button clicked', () => { 56 | const { buttons, p } = setup({ counter: 1 }); 57 | buttons.at(2).simulate('click'); 58 | expect(p.text()).toMatch(/^2$/); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /app/img/icon-5x5.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/img/icon-frame-info-enabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/utils/openCVProperties.js: -------------------------------------------------------------------------------- 1 | export const VideoCaptureProperties = Object.freeze({ 2 | CAP_PROP_POS_MSEC: 0, 3 | CAP_PROP_POS_FRAMES: 1, 4 | CAP_PROP_POS_AVI_RATIO: 2, 5 | CAP_PROP_FRAME_WIDTH: 3, 6 | CAP_PROP_FRAME_HEIGHT: 4, 7 | CAP_PROP_FPS: 5, 8 | CAP_PROP_FOURCC: 6, 9 | CAP_PROP_FRAME_COUNT: 7, 10 | CAP_PROP_FORMAT: 8, 11 | CAP_PROP_MODE: 9, 12 | CAP_PROP_BRIGHTNESS: 10, 13 | CAP_PROP_CONTRAST: 11, 14 | CAP_PROP_SATURATION: 12, 15 | CAP_PROP_HUE: 13, 16 | CAP_PROP_GAIN: 14, 17 | CAP_PROP_EXPOSURE: 15, 18 | CAP_PROP_CONVERT_RGB: 16, 19 | CAP_PROP_WHITE_BALANCE_BLUE_U: 17, 20 | CAP_PROP_RECTIFICATION: 18, 21 | CAP_PROP_MONOCHROME: 19, 22 | CAP_PROP_SHARPNESS: 20, 23 | CAP_PROP_AUTO_EXPOSURE: 21, 24 | CAP_PROP_GAMMA: 22, 25 | CAP_PROP_TEMPERATURE: 23, 26 | CAP_PROP_TRIGGER: 24, 27 | CAP_PROP_TRIGGER_DELAY: 25, 28 | CAP_PROP_WHITE_BALANCE_RED_V: 26, 29 | CAP_PROP_ZOOM: 27, 30 | CAP_PROP_FOCUS: 28, 31 | CAP_PROP_GUID: 29, 32 | CAP_PROP_ISO_SPEED: 30, 33 | CAP_PROP_BACKLIGHT: 32, 34 | CAP_PROP_PAN: 33, 35 | CAP_PROP_TILT: 34, 36 | CAP_PROP_ROLL: 35, 37 | CAP_PROP_IRIS: 36, 38 | CAP_PROP_SETTINGS: 37, 39 | CAP_PROP_BUFFERSIZE: 38, 40 | CAP_PROP_AUTOFOCUS: 39, 41 | }); 42 | 43 | export const ImwriteFlags = Object.freeze({ 44 | IMWRITE_JPEG_QUALITY: 1, 45 | IMWRITE_JPEG_PROGRESSIVE: 2, 46 | IMWRITE_JPEG_OPTIMIZE: 3, 47 | IMWRITE_JPEG_RST_INTERVAL: 4, 48 | IMWRITE_JPEG_LUMA_QUALITY: 5, 49 | IMWRITE_JPEG_CHROMA_QUALITY: 6, 50 | IMWRITE_PNG_COMPRESSION: 16, 51 | IMWRITE_PNG_STRATEGY: 17, 52 | IMWRITE_PNG_BILEVEL: 18, 53 | IMWRITE_PXM_BINARY: 32, 54 | // IMWRITE_EXR_TYPE: (3 << 4) + 0, 55 | IMWRITE_WEBP_QUALITY: 64, 56 | IMWRITE_PAM_TUPLETYPE: 128, 57 | IMWRITE_TIFF_RESUNIT: 256, 58 | IMWRITE_TIFF_XDPI: 257, 59 | IMWRITE_TIFF_YDPI: 258, 60 | IMWRITE_TIFF_COMPRESSION: 259, 61 | }); 62 | 63 | export const RotateFlags = Object.freeze({ 64 | ROTATE_90_CLOCKWISE: 0, // Rotate 90 degrees clockwise 65 | ROTATE_180: 1, // Rotate 180 degrees clockwise 66 | ROTATE_90_COUNTERCLOCKWISE: 2, // Rotate 270 degrees clockwise 67 | NO_ROTATION: 3, // No rotation - Artifical no rotation flag for opencv! 68 | }); 69 | -------------------------------------------------------------------------------- /test/components/Counter.spec.js: -------------------------------------------------------------------------------- 1 | import { spy } from 'sinon'; 2 | import React from 'react'; 3 | import Enzyme, { shallow } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import { BrowserRouter as Router } from 'react-router-dom'; 6 | import renderer from 'react-test-renderer'; 7 | import Counter from '../../app/components/Counter'; 8 | 9 | Enzyme.configure({ adapter: new Adapter() }); 10 | 11 | function setup() { 12 | const actions = { 13 | increment: spy(), 14 | incrementIfOdd: spy(), 15 | incrementAsync: spy(), 16 | decrement: spy() 17 | }; 18 | const component = shallow(); 19 | return { 20 | component, 21 | actions, 22 | buttons: component.find('button'), 23 | p: component.find('.counter') 24 | }; 25 | } 26 | 27 | describe('Counter component', () => { 28 | it('should should display count', () => { 29 | const { p } = setup(); 30 | expect(p.text()).toMatch(/^1$/); 31 | }); 32 | 33 | it('should first button should call increment', () => { 34 | const { buttons, actions } = setup(); 35 | buttons.at(0).simulate('click'); 36 | expect(actions.increment.called).toBe(true); 37 | }); 38 | 39 | it('should match exact snapshot', () => { 40 | const { actions } = setup(); 41 | const counter = ( 42 |
43 | 44 | 45 | 46 |
47 | ); 48 | const tree = renderer.create(counter).toJSON(); 49 | 50 | expect(tree).toMatchSnapshot(); 51 | }); 52 | 53 | it('should second button should call decrement', () => { 54 | const { buttons, actions } = setup(); 55 | buttons.at(1).simulate('click'); 56 | expect(actions.decrement.called).toBe(true); 57 | }); 58 | 59 | it('should third button should call incrementIfOdd', () => { 60 | const { buttons, actions } = setup(); 61 | buttons.at(2).simulate('click'); 62 | expect(actions.incrementIfOdd.called).toBe(true); 63 | }); 64 | 65 | it('should fourth button should call incrementAsync', () => { 66 | const { buttons, actions } = setup(); 67 | buttons.at(3).simulate('click'); 68 | expect(actions.incrementAsync.called).toBe(true); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /app/img/icon-show-face-rect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/img/icon-show-face-rect-enabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/utils/saveThumb.js: -------------------------------------------------------------------------------- 1 | import pathR from 'path'; 2 | import log from 'electron-log'; 3 | import { DEFAULT_THUMB_JPG_QUALITY, DEFAULT_THUMB_FORMAT } from './constants'; 4 | import { ensureDirectoryExistence, getFilePathObject } from './utils'; 5 | import { getBase64Object } from './utilsForOpencv'; 6 | 7 | const { ipcRenderer } = require('electron'); 8 | const { app } = require('electron').remote; 9 | 10 | const saveThumb = ( 11 | filePath, 12 | fileUseRatio, 13 | movieFileName, 14 | sheetName, 15 | frameNumber, 16 | fileNameTemplate, 17 | frameId, 18 | transformObject, 19 | saveToFolder = '', 20 | overwrite = false, 21 | defaultThumbFormat = DEFAULT_THUMB_FORMAT, 22 | defaultThumbJpgQuality = DEFAULT_THUMB_JPG_QUALITY, 23 | fps = 25, 24 | ) => { 25 | // save thumbs in folder with the same name as moviePrint 26 | let newFolderName = app.getPath('desktop'); 27 | if (saveToFolder) { 28 | newFolderName = saveToFolder; 29 | ensureDirectoryExistence(newFolderName); 30 | } 31 | 32 | const frameSize = 0; // save frame in original size 33 | 34 | const newFilePathObject = getFilePathObject( 35 | movieFileName, 36 | sheetName, 37 | frameNumber, 38 | fileNameTemplate, 39 | defaultThumbFormat, 40 | newFolderName, 41 | overwrite, 42 | fps, 43 | ); 44 | const newFilePathAndName = pathR.join(newFilePathObject.dir, newFilePathObject.base); 45 | 46 | const thumbFormatObject = { 47 | defaultThumbFormat, 48 | defaultThumbJpgQuality, 49 | }; 50 | 51 | const base64Object = getBase64Object( 52 | filePath, 53 | fileUseRatio, 54 | [ 55 | { 56 | frameId, 57 | frameNumber, 58 | }, 59 | ], 60 | frameSize, 61 | transformObject, 62 | thumbFormatObject, 63 | true, 64 | ); 65 | const base64 = base64Object[frameId]; 66 | 67 | if (base64 === '') { 68 | ipcRenderer.send('message-from-workerWindow-to-mainWindow', 'received-saved-file-error', 'Frame is empty'); 69 | } else { 70 | const buf = Buffer.from(base64, 'base64'); 71 | 72 | ipcRenderer.send('send-save-file', frameId, newFilePathAndName, buf); 73 | log.debug(`Saving ${JSON.stringify({ newFilePathAndName, size: buf.length })}`); 74 | } 75 | }; 76 | 77 | export default saveThumb; 78 | -------------------------------------------------------------------------------- /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 fs from 'fs'; 8 | import { dependencies as externals } from '../app/package.json'; 9 | // import { dependencies as possibleExternals } from '../package.json'; 10 | 11 | // // Find all the dependencies without a `main` property and add them as webpack externals 12 | // function filterDepWithoutEntryPoints(dep: string): boolean { 13 | // // Return true if we want to add a dependency to externals 14 | // try { 15 | // // If the root of the dependency has an index.js, return true 16 | // if (fs.existsSync(path.join(__dirname, '..', `node_modules/${dep}/index.js`))) { 17 | // return false; 18 | // } 19 | // const pgkString = fs 20 | // .readFileSync(path.join(__dirname, '..', `node_modules/${dep}/package.json`)) 21 | // .toString(); 22 | // const pkg = JSON.parse(pgkString); 23 | // const fields = ['main', 'module', 'jsnext:main', 'browser']; 24 | // return !fields.some(field => field in pkg); 25 | // } catch (e) { 26 | // console.log(e); 27 | // return true; 28 | // } 29 | // } 30 | 31 | export default { 32 | externals: [ 33 | ...Object.keys(externals || {}) 34 | // ...Object.keys(possibleExternals || {}).filter(filterDepWithoutEntryPoints) 35 | ], 36 | 37 | module: { 38 | rules: [ 39 | { 40 | test: /\.jsx?$/, 41 | exclude: /node_modules/, 42 | use: { 43 | loader: 'babel-loader', 44 | options: { 45 | cacheDirectory: true 46 | } 47 | } 48 | } 49 | ] 50 | }, 51 | 52 | output: { 53 | path: path.join(__dirname, '..', 'app'), 54 | // https://github.com/webpack/webpack/issues/1114 55 | libraryTarget: 'commonjs2' 56 | }, 57 | 58 | /** 59 | * Determine the array of extensions that should be used to resolve modules. 60 | */ 61 | resolve: { 62 | extensions: ['.js', '.jsx', '.json'], 63 | modules: [path.join(__dirname, '..', 'app'), 'node_modules'] 64 | }, 65 | 66 | plugins: [ 67 | new webpack.EnvironmentPlugin({ 68 | NODE_ENV: 'production' 69 | }), 70 | 71 | new webpack.NamedModulesPlugin() 72 | ] 73 | }; 74 | -------------------------------------------------------------------------------- /internals/scripts/CheckNativeDep.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import fs from 'fs'; 3 | import chalk from 'chalk'; 4 | import { execSync } from 'child_process'; 5 | import { dependencies } from '../../package.json'; 6 | 7 | (() => { 8 | if (!dependencies) return; 9 | 10 | const dependenciesKeys = Object.keys(dependencies); 11 | const nativeDeps = fs 12 | .readdirSync('node_modules') 13 | .filter(folder => fs.existsSync(`node_modules/${folder}/binding.gyp`)); 14 | 15 | try { 16 | // Find the reason for why the dependency is installed. If it is installed 17 | // because of a devDependency then that is okay. Warn when it is installed 18 | // because of a dependency 19 | const dependenciesObject = JSON.parse( 20 | execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString() 21 | ); 22 | const rootDependencies = Object.keys(dependenciesObject.dependencies); 23 | const filteredRootDependencies = rootDependencies.filter(rootDependency => 24 | dependenciesKeys.includes(rootDependency) 25 | ); 26 | 27 | if (filteredRootDependencies.length > 0) { 28 | const plural = filteredRootDependencies.length > 1; 29 | console.log(` 30 | 31 | ${chalk.whiteBright.bgYellow.bold( 32 | 'Webpack does not work with native dependencies.' 33 | )} 34 | ${chalk.bold(filteredRootDependencies.join(', '))} ${ 35 | plural ? 'are native dependencies' : 'is a native dependency' 36 | } and should be installed inside of the "./app" folder. 37 | 38 | 39 | First uninstall the packages from "./package.json": 40 | ${chalk.whiteBright.bgGreen.bold('npm uninstall your-package')} 41 | 42 | ${chalk.bold( 43 | 'Then, instead of installing the package to the root "./package.json":' 44 | )} 45 | ${chalk.whiteBright.bgRed.bold('npm install your-package --save')} 46 | 47 | ${chalk.bold('Install the package to "./app/package.json"')} 48 | ${chalk.whiteBright.bgGreen.bold('cd ./app && npm install your-package --save')} 49 | 50 | 51 | Read more about native dependencies at: 52 | ${chalk.bold( 53 | 'https://github.com/chentsulin/electron-react-boilerplate/wiki/Module-Structure----Two-package.json-Structure' 54 | )} 55 | 56 | 57 | `); 58 | 59 | process.exit(1); 60 | } 61 | } catch (e) { 62 | console.log('Native dependencies could not be checked'); 63 | } 64 | })(); 65 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: off */ 2 | 3 | const developmentEnvironments = ['development', 'test']; 4 | 5 | const developmentPlugins = [require('react-hot-loader/babel')]; 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 | [ 24 | require('@babel/preset-env'), 25 | { 26 | targets: { electron: require('electron/package.json').version } 27 | } 28 | ], 29 | require('@babel/preset-flow'), 30 | [require('@babel/preset-react'), { development }] 31 | ], 32 | plugins: [ 33 | // Stage 0 34 | require('@babel/plugin-proposal-function-bind'), 35 | 36 | // Stage 1 37 | require('@babel/plugin-proposal-export-default-from'), 38 | require('@babel/plugin-proposal-logical-assignment-operators'), 39 | [require('@babel/plugin-proposal-optional-chaining'), { loose: false }], 40 | [ 41 | require('@babel/plugin-proposal-pipeline-operator'), 42 | { proposal: 'minimal' } 43 | ], 44 | [ 45 | require('@babel/plugin-proposal-nullish-coalescing-operator'), 46 | { loose: false } 47 | ], 48 | require('@babel/plugin-proposal-do-expressions'), 49 | 50 | // Stage 2 51 | [require('@babel/plugin-proposal-decorators'), { legacy: true }], 52 | require('@babel/plugin-proposal-function-sent'), 53 | require('@babel/plugin-proposal-export-namespace-from'), 54 | require('@babel/plugin-proposal-numeric-separator'), 55 | require('@babel/plugin-proposal-throw-expressions'), 56 | 57 | // Stage 3 58 | require('@babel/plugin-syntax-dynamic-import'), 59 | require('@babel/plugin-syntax-import-meta'), 60 | [require('@babel/plugin-proposal-class-properties'), { loose: true }], 61 | require('@babel/plugin-proposal-json-strings'), 62 | 63 | ...(development ? developmentPlugins : productionPlugins) 64 | ] 65 | }; 66 | }; 67 | -------------------------------------------------------------------------------- /app/img/icon-add-face.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/utils/utilsForMain.js: -------------------------------------------------------------------------------- 1 | import log from 'electron-log'; 2 | import path from 'path'; 3 | 4 | export const clearCache = win => { 5 | log.debug('clearCache'); 6 | win.webContents.session 7 | .getCacheSize() 8 | .then(cacheSizeBefore => { 9 | log.debug(`cacheSize before: ${cacheSizeBefore}`); 10 | return win.webContents.session.clearCache(); 11 | }) 12 | .then(() => { 13 | return win.webContents.session.clearStorageData(); 14 | }) 15 | .then(() => { 16 | return win.webContents.session.getCacheSize(); 17 | }) 18 | .then(cacheSizeAfter => { 19 | log.debug(`cacheSize after: ${cacheSizeAfter}`); 20 | // and reload to use initialStateJSON 21 | win.webContents.reload(); 22 | return undefined; 23 | }) 24 | .catch(error => { 25 | log.error(`There has been a problem with your clearCache operation: ${error.message}`); 26 | }); 27 | }; 28 | 29 | export const resetApplication = (mainWindow, workerWindow, opencvWorkerWindow, databaseWorkerWindow) => { 30 | mainWindow.webContents.send('delete-all-tables'); 31 | setTimeout(() => { 32 | clearCache(mainWindow); 33 | workerWindow.webContents.reload(); 34 | opencvWorkerWindow.webContents.reload(); 35 | databaseWorkerWindow.webContents.reload(); // needs reload to open indexedDB connection 36 | }, 1000); 37 | }; 38 | 39 | // soft reset only deletes the indexedDB table, and does not clear cache nor all storage data 40 | // and also does not reload the windows 41 | export const softResetApplication = mainWindow => { 42 | mainWindow.webContents.send('delete-all-tables'); 43 | }; 44 | 45 | export const reloadApplication = (mainWindow, workerWindow, opencvWorkerWindow, databaseWorkerWindow) => { 46 | mainWindow.webContents.reload(); 47 | workerWindow.webContents.reload(); 48 | opencvWorkerWindow.webContents.reload(); 49 | databaseWorkerWindow.webContents.reload(); 50 | }; 51 | 52 | export const getPathOfLogFileAndFolder = (processPlatform, appName) => { 53 | let pathOfLogFolder; 54 | switch (processPlatform) { 55 | case 'darwin': 56 | pathOfLogFolder = path.resolve(process.env.HOME || process.env.USERPROFILE, 'Library/Logs/', `${appName}/`); 57 | break; 58 | default: 59 | pathOfLogFolder = path.resolve( 60 | process.env.HOME || process.env.USERPROFILE, 61 | 'AppData\\Roaming\\', 62 | `${appName}`, 63 | 'logs/', 64 | ); 65 | } 66 | const pathOfLogFile = path.resolve(pathOfLogFolder, 'main.log'); 67 | return { pathOfLogFile, pathOfLogFolder }; 68 | }; 69 | -------------------------------------------------------------------------------- /app/store/configureStore.dev.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import { createHashHistory } from 'history'; 4 | import { routerMiddleware, push } from 'connected-react-router'; 5 | import { createLogger } from 'redux-logger'; 6 | import throttle from 'lodash/throttle'; 7 | import rootReducer from '../reducers'; 8 | import * as thumbActions from '../actions/index'; 9 | import { loadState, saveState } from './localStorage'; 10 | 11 | const history = createHashHistory(); 12 | 13 | const configureStore = (initialState) => { 14 | // store State in localStorage 15 | let persistedState; 16 | if (initialState === undefined) { 17 | persistedState = loadState(); 18 | } else { 19 | persistedState = initialState; 20 | } 21 | 22 | // Redux Configuration 23 | const middleware = []; 24 | const enhancers = []; 25 | 26 | // Thunk Middleware 27 | middleware.push(thunk); 28 | 29 | // Logging Middleware 30 | const logger = createLogger({ 31 | level: 'info', 32 | collapsed: true 33 | }); 34 | 35 | // Skip redux logs in console during the tests 36 | if (process.env.NODE_ENV !== 'test') { 37 | middleware.push(logger); 38 | } 39 | 40 | // Router Middleware 41 | const router = routerMiddleware(history); 42 | middleware.push(router); 43 | 44 | // Redux DevTools Configuration 45 | const actionCreators = { 46 | ...thumbActions, 47 | push, 48 | }; 49 | // If Redux DevTools Extension is installed use it, otherwise use Redux compose 50 | /* eslint-disable no-underscore-dangle */ 51 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 52 | ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ 53 | // Options: http://extension.remotedev.io/docs/API/Arguments.html 54 | actionCreators 55 | }) 56 | : compose; 57 | /* eslint-enable no-underscore-dangle */ 58 | 59 | // Apply Middleware & Compose Enhancers 60 | enhancers.push(applyMiddleware(...middleware)); 61 | const enhancer = composeEnhancers(...enhancers); 62 | 63 | // Create Store 64 | const store = createStore(rootReducer, persistedState, enhancer); 65 | 66 | store.subscribe(throttle(() => { 67 | saveState(store.getState()); 68 | // // only store thumbs in localStorage 69 | // saveState({ 70 | // thumbs: store.getState().thumbs 71 | // }); 72 | }, 1000)); 73 | 74 | if (module.hot) { 75 | module.hot.accept( 76 | '../reducers', 77 | // eslint-disable-line global-require 78 | 79 | () => store.replaceReducer(require('../reducers').default) 80 | ); 81 | } 82 | 83 | return store; 84 | }; 85 | 86 | export default { configureStore, history }; 87 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config adapted from: 2 | # https://www.electron.build/multi-platform-build#sample-travisyml-to-build-electron-app-for-macos-linux-and-windows 3 | sudo: true 4 | 5 | matrix: 6 | include: 7 | - os: osx 8 | osx_image: xcode9.4 9 | language: node_js 10 | node_js: 11 | - node 12 | - 9 13 | env: 14 | - ELECTRON_CACHE=$HOME/.cache/electron 15 | - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder 16 | 17 | - os: linux 18 | services: docker 19 | language: node_js 20 | node_js: 21 | - node 22 | - 9 23 | addons: 24 | apt: 25 | sources: 26 | - ubuntu-toolchain-r-test 27 | packages: 28 | - g++-4.8 29 | - icnsutils 30 | - graphicsmagick 31 | - xz-utils 32 | - xorriso 33 | 34 | before_cache: 35 | - rm -rf $HOME/.cache/electron-builder/wine 36 | 37 | cache: 38 | yarn: true 39 | directories: 40 | - node_modules 41 | - app/node_modules 42 | - $(npm config get prefix)/lib/node_modules 43 | - flow-typed 44 | - $HOME/.cache/electron 45 | - $HOME/.cache/electron-builder 46 | - $HOME/docker 47 | 48 | install: 49 | - export CXX="g++-4.8" 50 | - yarn 51 | # On Linux, initialize "virtual display". See before_script 52 | - | 53 | if [ "$TRAVIS_OS_NAME" == "linux" ]; then 54 | /sbin/start-stop-daemon \ 55 | --start \ 56 | --quiet \ 57 | --pidfile /tmp/custom_xvfb_99.pid \ 58 | --make-pidfile \ 59 | --background \ 60 | --exec /usr/bin/Xvfb \ 61 | -- :99 -ac -screen 0 1280x1024x16 62 | else 63 | : 64 | fi 65 | 66 | before_script: 67 | # On Linux, create a "virtual display". This allows browsers to work properly 68 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export DISPLAY=:99.0; fi 69 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sh -e /etc/init.d/xvfb start; fi 70 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sleep 3; fi 71 | 72 | script: 73 | - yarn package 74 | - yarn lint 75 | - yarn flow 76 | - yarn test 77 | - yarn test-e2e 78 | # On Linux, check if packaging to windows works 79 | - | 80 | if [ "$TRAVIS_OS_NAME" == "linux" ]; then 81 | docker run --rm \ 82 | --env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CIRCLE|TRAVIS|APPVEYOR_|CSC_|_TOKEN|_KEY|AWS_|STRIP|BUILD_') \ 83 | -v ${PWD}:/project \ 84 | -v ~/.cache/electron:/root/.cache/electron \ 85 | -v ~/.cache/electron-builder:/root/.cache/electron-builder \ 86 | electronuserland/builder:wine \ 87 | /bin/bash -c "yarn --link-duplicates --pure-lockfile && yarn package --linux --win" 88 | else 89 | : 90 | fi 91 | -------------------------------------------------------------------------------- /app/img/icon-6x6.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/containers/FileList.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import FileListElement from '../components/FileListElement'; 5 | import styles from '../components/FileList.css'; 6 | import { 7 | getObjectProperty, 8 | } from '../utils/utils'; 9 | import { MENU_HEADER_HEIGHT, MENU_FOOTER_HEIGHT } from '../utils/constants'; 10 | 11 | class SortedFileList extends Component { 12 | 13 | render() { 14 | const { files, settings, posterobjectUrlObjects, visibilitySettings } = this.props; 15 | 16 | return ( 17 |
23 |
    26 | {files.length === 0 ? 27 | ( 28 |
    31 | The movie list is empty. 32 |

    33 | Please drag in one or more movies. 34 |
    35 | ) : 36 | (files.map((file, index) => ( 37 | 64 | ))) 65 | } 66 |
67 |
68 | ); 69 | } 70 | } 71 | 72 | export default SortedFileList; 73 | -------------------------------------------------------------------------------- /app/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Menu, Popup, Icon, Dropdown } from 'semantic-ui-react'; 4 | import { MENU_FOOTER_HEIGHT, SHEET_VIEW, VIEW } from '../utils/constants'; 5 | import styles from './Menu.css'; 6 | import stylesPop from './Popup.css'; 7 | 8 | const Footer = ({ 9 | defaultView, 10 | file, 11 | onSaveAllMoviePrints, 12 | onOpenFileExplorer, 13 | onSaveMoviePrint, 14 | savingMoviePrint, 15 | sheetView, 16 | }) => { 17 | return ( 18 |
24 | 30 | 31 | {file && 32 | (sheetView === SHEET_VIEW.GRIDVIEW || sheetView === SHEET_VIEW.TIMELINEVIEW) && 33 | defaultView === VIEW.STANDARDVIEW && ( 34 | 44 | {savingMoviePrint ?
: } 45 | Save MoviePrint 46 | 47 | } 48 | mouseEnterDelay={1000} 49 | on={['hover']} 50 | position="top center" 51 | offset="0,8px" 52 | pinned 53 | className={stylesPop.popup} 54 | content={ 55 | 56 | Save MoviePrint M 57 | 58 | } 59 | /> 60 | )} 61 | {file && ( 62 | 72 | 73 | onOpenFileExplorer()} 78 | /> 79 | 85 | 86 | 87 | )} 88 | 89 |
90 |
91 | ); 92 | }; 93 | 94 | Footer.defaultProps = {}; 95 | 96 | Footer.propTypes = { 97 | file: PropTypes.object, 98 | }; 99 | 100 | export default Footer; 101 | -------------------------------------------------------------------------------- /app/components/FloatingMenu.css: -------------------------------------------------------------------------------- 1 | 2 | .floatingMenu { 3 | transition: transform 0.5s ease; 4 | position: fixed; 5 | transform: translate(-50%); 6 | top: 8px; 7 | left: calc(50% + 10px); 8 | z-index: 990; 9 | width: 760px; 10 | /* background-color: rgba(255, 0, 0, 0.5); */ 11 | } 12 | 13 | .imageButton { 14 | color: white !important; 15 | /* background-color: rgba(30, 15, 0, 1.0) !important; */ 16 | background: rgba(0,0,0,0.8) !important; 17 | /* box-shadow: none !important; */ 18 | padding-left: 11px !important; 19 | padding-right: 11px !important; 20 | /* padding-top: 10px !important; */ 21 | /* padding-bottom: 9px !important; */ 22 | } 23 | 24 | .imageButton:hover { 25 | /* background-color: rgba(60, 30, 0, 1.0) !important; */ 26 | background-color: rgba(255, 80, 6, 1.0) !important; 27 | } 28 | 29 | .dropDownButton { 30 | color: white !important; 31 | /* background-color: rgba(30, 15, 0, 1.0) !important; */ 32 | background: rgba(0,0,0,0.8) !important; 33 | /* box-shadow: none !important; */ 34 | padding-left: 15px !important; 35 | padding-right: 11px !important; 36 | /* padding-top: 11px !important; */ 37 | /* padding-bottom: 10px !important; */ 38 | font-size: 16px !important; 39 | } 40 | 41 | .dropDownButton:hover { 42 | /* background-color: rgba(60, 30, 0, 1.0) !important; */ 43 | background-color: rgba(255, 80, 6, 1.0) !important; 44 | } 45 | 46 | .dropDownButton i{ 47 | opacity: 0.9 !important; 48 | } 49 | 50 | .dropDownMenu { 51 | /* background-color: rgba(60, 30, 0, 1.0) !important; */ 52 | } 53 | 54 | .dropDownMenuFilter { 55 | width: 260px; 56 | } 57 | 58 | .dropDownItem { 59 | height: 40px !important; 60 | } 61 | 62 | .dropDownItem img{ 63 | margin-top: -2px !important; 64 | } 65 | 66 | .dropDownItemIconInvert img{ 67 | filter: invert(100%); 68 | } 69 | 70 | .dropDownItemCheckbox{ 71 | height: 40px !important; 72 | } 73 | 74 | .dropDownItemCheckboxAndSlider{ 75 | height: 72px !important; 76 | } 77 | 78 | .dropDownItemRadioGroup{ 79 | height: 40px !important; 80 | margin: 16px; 81 | } 82 | 83 | .dropDownItemHidden { 84 | visibility: hidden; 85 | /* height: 32px !important; */ 86 | } 87 | 88 | .dropDownItemRadioGroup > div{ 89 | margin-right: 16px; 90 | } 91 | 92 | .slider{ 93 | margin-top: 8px !important; 94 | } 95 | 96 | .normalButton { 97 | color: white !important; 98 | /* background-color: rgba(30, 15, 0, 1.0) !important; */ 99 | background: rgba(0,0,0,0.8) !important; 100 | font-family: Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif !important; 101 | box-shadow: none !important; 102 | } 103 | 104 | .normalButton:hover { 105 | /* background-color: rgba(60, 30, 0, 1.0) !important; */ 106 | background-color: rgba(255, 80, 6, 1.0) !important; 107 | } 108 | 109 | .textButton { 110 | color: white !important; 111 | background-color: rgba(0, 0, 0, 0.2) !important; 112 | font-family: Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif !important; 113 | font-weight: 600 !important; 114 | font-size: 14px !important; 115 | height: 34px !important; 116 | border-radius: 0 !important; 117 | padding-top: 11px !important; 118 | } 119 | 120 | .textButton:hover { 121 | background-color: rgba(60, 30, 0, 1.0) !important; 122 | } 123 | 124 | .selected { 125 | background-color: rgba(255, 80, 6, 0.5) !important; 126 | color: white !important; 127 | } 128 | 129 | .selected:hover { 130 | background-color: rgba(255, 80, 6, 1.0) !important; 131 | /* background-color: rgba(60, 30, 0, 1.0) !important; */ 132 | color: white !important; 133 | } 134 | 135 | .hidden { 136 | visibility: hidden; 137 | } 138 | 139 | .noBackground { 140 | background-color: none !important; 141 | box-shadow: none !important; 142 | padding: 0px 16px 16px 16px !important; 143 | opacity: 0.5 !important; 144 | } 145 | -------------------------------------------------------------------------------- /scripts/opencv.js: -------------------------------------------------------------------------------- 1 | /* eslint no-inner-declarations: 0 */ 2 | 3 | import path from 'path'; 4 | import log from 'electron-log'; 5 | 6 | const shell = require('shelljs'); 7 | 8 | // cross platform variables 9 | const projectRoot = shell.pwd().stdout; 10 | 11 | if (process.platform === 'darwin') { 12 | 13 | // Include all opencv dependencies including ffmpeg 14 | log.info( 15 | 'running opencv script to copy all its dependencies including ffmpeg library files into the opencv folder for later packaging and relink them if necessary' 16 | ); 17 | 18 | function fixDeps(dirPath, dependencyDirName) { 19 | log.info(`checking all dylib files in ${dirPath}, copy dependencies into ${dependencyDirName} and change the dylib linking`); 20 | const dylibs = shell.ls(dirPath) 21 | const allDeps = [] 22 | dylibs 23 | .filter(file => file.indexOf('dylib') !== -1) 24 | .forEach(dylibFilename => { 25 | const dylib = path.join(dirPath, dylibFilename); 26 | console.log(`checking outer dependencies of file ${dylib}`); 27 | const outerDeps = shell 28 | .exec(`otool -l ${dylib} | grep 'name /usr/local'`) 29 | .stdout.split('\n') 30 | .filter(dep => dep !== ''); 31 | 32 | if (outerDeps.length > 0) { 33 | shell.exec(`install_name_tool -add_rpath @loader_path/${dependencyDirName}`); 34 | } 35 | outerDeps.forEach(depLine => { 36 | const regex = /name (.+?(?=\ \(offset))/g; // transform "name /usr/lib/libc++.1.dylib (offset 24)" -> "/usr/lib/libc++.1.dylib" 37 | const depfilePath = regex.exec(depLine)[1]; // "/usr/lib/libc++.1.dylib" 38 | const depfileName = depfilePath.replace(/^.*[\\\/]/, ''); 39 | 40 | if (!allDeps.find(e => e === depfilePath)) { 41 | // if not added already -> add it 42 | allDeps.push(depfilePath); 43 | console.log(` copying outer dependency ${depfilePath} to ${outerDependencyDir}`); 44 | shell.cp('-n', depfilePath, outerDependencyDir); 45 | } 46 | shell.chmod('-v', '666', dylib); 47 | const fixCommand = `install_name_tool -change ${depfilePath} @loader_path/${dependencyDirName}/${depfileName} ${dylib}`; 48 | console.log(` fix with command: ${fixCommand}`); 49 | shell.exec(fixCommand); 50 | }) 51 | 52 | console.log(`\n\n`); 53 | }) 54 | console.info('All outer dependencies:'); 55 | console.log(allDeps); 56 | return allDeps; 57 | } 58 | 59 | // osx path variables 60 | const opencvLibDir = path.resolve( 61 | projectRoot, 62 | 'app/node_modules/opencv-build/opencv/build/lib/' 63 | ); 64 | const dependencyDir = 'dependencies' 65 | const outerDependencyDir = path.join(opencvLibDir, dependencyDir); 66 | 67 | log.debug(`projectRoot: ${projectRoot}`); 68 | log.debug(`outerDependencyDir: ${outerDependencyDir}`); 69 | log.debug(`opencvLibDir: ${opencvLibDir}`); 70 | 71 | // create dependencies folder 72 | shell.mkdir('-p', outerDependencyDir); 73 | 74 | fixDeps(opencvLibDir, dependencyDir); 75 | fixDeps(outerDependencyDir, ''); 76 | fixDeps(outerDependencyDir, ''); 77 | fixDeps(outerDependencyDir, ''); 78 | 79 | } else if (process.platform === 'win32') { 80 | 81 | // it seems that on windows opencv is already bundled with ffmpeg 82 | // but the redistributable files need to be copied into the the apps root folder 83 | // I did not manage to configure electron-builder to copy the dll's directly 84 | // therefore this is done in a 2 step process 85 | // 1. this script copies the dll's into the dist folder 86 | // 2. electron-builder copies the dll's into the root folder when packaging 87 | 88 | const distDir = path.resolve(projectRoot, 'app/dist/redistributable/'); 89 | shell.mkdir('-p', distDir); // create folder if it does not exist yet 90 | 91 | // copy necessary redistributable files 92 | shell.cp('-n', '/Windows/system32/CONCRT140.dll',distDir ); 93 | shell.cp('-n', '/Windows/system32/MSVCP140.dll',distDir ); 94 | shell.cp('-n', '/Windows/system32/VCRUNTIME140.dll',distDir ); 95 | } 96 | -------------------------------------------------------------------------------- /app/components/Scrub.css: -------------------------------------------------------------------------------- 1 | .scrubContainerBackground { 2 | /* outline-style: solid; 3 | outline-color: #0c3158; 4 | outline-width: 15px; 5 | outline-offset: -15px; */ 6 | position: fixed; /* Stay in place */ 7 | left: 0; 8 | top: 0; 9 | width: 100%; /* Full width */ 10 | height: 100%; /* Full height */ 11 | background-color: rgba(0,0,0,0.8); /* Black w/ opacity */ 12 | z-index: 999; 13 | cursor: col-resize; 14 | } 15 | 16 | .scrubInfo { 17 | position: absolute; 18 | top: 30px; 19 | left: 50%; 20 | transform: translateX(-50%); 21 | background-color: rgba(0,0,0,1); /* Black w/ opacity */ 22 | font-family: 'Franchise', 'Roboto Condensed'; 23 | color: #ffffff; 24 | font-size: 30px; 25 | opacity: 0.5; 26 | -webkit-user-select:none; 27 | } 28 | 29 | .scrubContainer { 30 | position: absolute; 31 | top: 50%; 32 | left: 50%; 33 | transform: translate(-50%,-50%); 34 | background-color: rgba(0,0,0,1); /* Black w/ opacity */ 35 | } 36 | 37 | .scrubInnerContainer { 38 | position: absolute; 39 | top: 50%; 40 | left: 50%; 41 | transform: translate(-50%,-50%); 42 | } 43 | 44 | .scrubThumb { 45 | position: relative; 46 | } 47 | 48 | .scrubThumbLeft { 49 | /* position: absolute; */ 50 | left: 0; 51 | display: inline-block; 52 | background-position: right center; 53 | background-size: cover; 54 | /* margin-right: 2px; */ 55 | /* height: 24px; */ 56 | /* width: 24px; */ 57 | object-fit: cover; 58 | transform: translateY(-50%); 59 | } 60 | 61 | .scrubThumbRight { 62 | /* position: absolute; */ 63 | right: 0; 64 | display: inline-block; 65 | background-position: left center; 66 | background-size: cover; 67 | /* margin-left: 2px; */ 68 | /* height: 24px; */ 69 | /* width: 24px; */ 70 | object-fit: cover; 71 | transform: translateY(-50%); 72 | } 73 | 74 | .scrubCancelBar { 75 | position: absolute; 76 | bottom: 0; 77 | width: 100%; 78 | background-color: rgba(100, 0, 0, 1); 79 | z-index: 1000; 80 | text-align: center; 81 | padding-top: 6px; 82 | cursor: pointer; 83 | } 84 | 85 | .scrubDescription { 86 | position: absolute; 87 | top: 0; 88 | width: 100%; 89 | /* background-color: #333333; */ 90 | z-index: 1000; 91 | text-align: center; 92 | padding-top: 6px; 93 | } 94 | 95 | .scrubLine { 96 | position: absolute; 97 | top: 0; 98 | height: 100%; 99 | width: 1px; 100 | background-color: #FF5006; 101 | z-index: 2000; 102 | } 103 | 104 | .frameNumberOrTimeCode { 105 | position: absolute; 106 | /* top: 0; */ 107 | left: 0; 108 | top: 0; 109 | transform: translate(-50%, -20px); 110 | background: #eee; 111 | border-radius:2px; 112 | font-family: 'Open sans'; 113 | font-size: 14px; 114 | line-height: 14px; 115 | padding: 1px; 116 | color: #000000; 117 | -webkit-user-select:none; 118 | z-index: 2001; 119 | } 120 | 121 | .scrubThumbLine { 122 | position: absolute; 123 | top: 0; 124 | height: 100%; 125 | width: 1px; 126 | background-color: rgba(255, 255, 255, 0.2); 127 | z-index: 2000; 128 | } 129 | 130 | .scrubThumbframeNumberOrTimeCode { 131 | position: absolute; 132 | /* top: 0; */ 133 | left: 0; 134 | top: 0; 135 | transform: translate(-50%, -20px); 136 | background-color: rgba(100, 100, 100, 1); 137 | border-radius:2px; 138 | font-family: 'Open sans'; 139 | font-size: 14px; 140 | line-height: 14px; 141 | padding: 1px; 142 | color: #000000; 143 | -webkit-user-select:none; 144 | z-index: 1999; 145 | /* opacity: 0.5; */ 146 | } 147 | 148 | .timelineWrapper { 149 | bottom: -24px; 150 | width: 100%; 151 | position: absolute; 152 | background-color: #421400; 153 | height: 24px; 154 | } 155 | 156 | .timelineCut { 157 | position: absolute; 158 | background-color: rgba(255, 80, 6, 0.4); 159 | height: 24px; 160 | } 161 | 162 | .timelinePlayhead { 163 | position: absolute; 164 | /* bottom: 0; */ 165 | /* top: 0; */ 166 | width: 2px; 167 | background-color: rgba(255, 80, 6, 1); 168 | height: 20px; 169 | margin-top: 2px; 170 | } 171 | 172 | .timelineScrubThumb { 173 | position: absolute; 174 | /* bottom: 0; */ 175 | /* top: 0; */ 176 | width: 1px; 177 | background-color: rgba(255, 255, 255, 0.2); 178 | height: 20px; 179 | margin-top: 2px; 180 | } 181 | 182 | /* .timelineWrapper .currentTime { 183 | background-color: #FF5006; 184 | z-index: 2; 185 | } */ 186 | 187 | /* .timelineWrapper .cutStartTime { 188 | background-color: rgba(0, 0, 0, 0.3); 189 | border-left: 1px solid black; 190 | border-right: 1px solid black; 191 | z-index: 1; 192 | } */ 193 | -------------------------------------------------------------------------------- /app/components/HeaderComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Menu, Dropdown, Icon, Popup } from 'semantic-ui-react'; 4 | import { MENU_HEADER_HEIGHT } from '../utils/constants'; 5 | import styles from './Menu.css'; 6 | import stylesPop from './Popup.css'; 7 | 8 | const Header = ({ 9 | file, 10 | visibilitySettings, 11 | openMoviesDialog, 12 | onOpenFeedbackForm, 13 | onImportMoviePrint, 14 | fileCount, 15 | onClearMovieList, 16 | checkForUpdates, 17 | isCheckingForUpdates, 18 | }) => { 19 | return ( 20 |
26 | 32 | 35 | 36 | {file ? 'Add Movies' : 'Add Movies'} 37 | 38 | } 39 | mouseEnterDelay={1000} 40 | on={['hover']} 41 | position="bottom center" 42 | className={stylesPop.popup} 43 | content={ 44 | 45 | Add one or more movies A 46 | 47 | } 48 | /> 49 | 52 | 53 | onImportMoviePrint()} 60 | /> 61 | } 62 | mouseEnterDelay={1000} 63 | on={['hover']} 64 | position="right center" 65 | className={stylesPop.popup} 66 | content="Import a MoviePrint from JSON file or a PNG file with embedded data" 67 | /> 68 | {fileCount > 0 && ( 69 | 77 | } 78 | mouseEnterDelay={1000} 79 | on={['hover']} 80 | position="right center" 81 | className={stylesPop.popup} 82 | content="Clear Movie list - THIS CAN NOT BE UNDONE!" 83 | /> 84 | )} 85 | 86 | 87 | } 88 | mouseEnterDelay={1000} 89 | on={['hover']} 90 | position="right center" 91 | className={stylesPop.popup} 92 | content="more options" 93 | /> 94 | 95 | 98 | 99 | Check for updates 100 | 101 | } 102 | mouseEnterDelay={1000} 103 | on={['hover']} 104 | position="bottom center" 105 | className={stylesPop.popup} 106 | content="Check online if there are updates available" 107 | /> 108 | 111 | 112 | Contact us 113 | 114 | } 115 | mouseEnterDelay={1000} 116 | on={['hover']} 117 | position="bottom right" 118 | offset="0,8px" 119 | className={stylesPop.popup} 120 | content="Feedback or feature request? Click here or contact us at support@movieprint.org" 121 | /> 122 | 123 | 124 |
125 | ); 126 | }; 127 | 128 | Header.defaultProps = { 129 | file: undefined, 130 | }; 131 | 132 | Header.propTypes = { 133 | file: PropTypes.object, 134 | }; 135 | 136 | export default Header; 137 | -------------------------------------------------------------------------------- /app/img/listOfNames.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"firstName": "Adriana", "fullName": "Adriana Herlers"}, 3 | {"firstName": "Agnieszka", "fullName": "Agnieszka Holland"}, 4 | {"firstName": "Agnès", "fullName": "Agnès Godard"}, 5 | {"firstName": "Agnès", "fullName": "Agnès Varda"}, 6 | {"firstName": "Akie", "fullName": "Akie Namiki"}, 7 | {"firstName": "Alice", "fullName": "Alice Guy-Blaché"}, 8 | {"firstName": "Andrea", "fullName": "Andrea Arnold"}, 9 | {"firstName": "Anna", "fullName": "Anna Muylaert"}, 10 | {"firstName": "Anne", "fullName": "Anne Bancroft"}, 11 | {"firstName": "Archana", "fullName": "Archana Phadke"}, 12 | {"firstName": "Autumn", "fullName": "Autumn Durald"}, 13 | {"firstName": "Barbara", "fullName": "Barbara Hammer"}, 14 | {"firstName": "Barbra", "fullName": "Barbra Streisand"}, 15 | {"firstName": "Candida", "fullName": "Candida Beltrán Rendón"}, 16 | {"firstName": "Carmen", "fullName": "Carmen Santos"}, 17 | {"firstName": "Caroline", "fullName": "Caroline Champetier"}, 18 | {"firstName": "Caroline", "fullName": "Caroline Link"}, 19 | {"firstName": "Chantal", "fullName": "Chantal Akerman"}, 20 | {"firstName": "Claire", "fullName": "Claire Denis"}, 21 | {"firstName": "Deepa", "fullName": "Deepa Mehta"}, 22 | {"firstName": "Dolores", "fullName": "Dolores Herlers"}, 23 | {"firstName": "Dominique", "fullName": "Dominique Gonzales-Foerster"}, 24 | {"firstName": "Doris", "fullName": "Doris Dörrie"}, 25 | {"firstName": "Dorota", "fullName": "Dorota Kedzierzawska"}, 26 | {"firstName": "Dorothy", "fullName": "Dorothy Arzner"}, 27 | {"firstName": "Edith", "fullName": "Edith Carlmar"}, 28 | {"firstName": "Elaine", "fullName": "Elaine May"}, 29 | {"firstName": "Eleanor", "fullName": "Eleanor Coppola"}, 30 | {"firstName": "Ellen", "fullName": "Ellen Kuras"}, 31 | {"firstName": "Elvira", "fullName": "Elvira Notari"}, 32 | {"firstName": "Eriko", "fullName": "Eriko Sonoda"}, 33 | {"firstName": "G.B.", "fullName": "G. B. Jones"}, 34 | {"firstName": "Gabriela", "fullName": "Gabriela von Bussenius Vega"}, 35 | {"firstName": "Germaine", "fullName": "Germaine Dulac"}, 36 | {"firstName": "Gilda", "fullName": "Gilda de Abreu"}, 37 | {"firstName": "Gunvor", "fullName": "Gunvor Nelson"}, 38 | {"firstName": "Helen", "fullName": "Helen Levitt"}, 39 | {"firstName": "Hyeong", "fullName": "Hyeong-ju Kim"}, 40 | {"firstName": "Icíar", "fullName": "Icíar Bollaín"}, 41 | {"firstName": "Ida", "fullName": "Ida Lupino"}, 42 | {"firstName": "Ida", "fullName": "Ida May Park"}, 43 | {"firstName": "Isabel", "fullName": "Isabel Coixet"}, 44 | {"firstName": "Jane", "fullName": "Jane Campion"}, 45 | {"firstName": "Jasmila", "fullName": "Jasmila Zbanic"}, 46 | {"firstName": "Jennifer", "fullName": "Jennifer Fox"}, 47 | {"firstName": "Jessica", "fullName": "Jessica Dimmock"}, 48 | {"firstName": "Joanna", "fullName": "Joanna Priestley"}, 49 | {"firstName": "Jocelyn", "fullName": "Jocelyn Moorhouse"}, 50 | {"firstName": "Julie", "fullName": "Julie Taymor"}, 51 | {"firstName": "Kate", "fullName": "Kate Brooks"}, 52 | {"firstName": "Kathryn", "fullName": "Kathryn Bigelow"}, 53 | {"firstName": "Katia", "fullName": "Katia Lund"}, 54 | {"firstName": "Kirsten", "fullName": "Kirsten Johnson"}, 55 | {"firstName": "Larisa", "fullName": "Larisa Shepitko"}, 56 | {"firstName": "Laurel", "fullName": "Laurel Nakadate"}, 57 | {"firstName": "Leni", "fullName": "Leni Riefenstahl"}, 58 | {"firstName": "Lina", "fullName": "Lina Wertmüller"}, 59 | {"firstName": "Liz", "fullName": "Liz Canner"}, 60 | {"firstName": "Lois", "fullName": "Lois Weber"}, 61 | {"firstName": "Lone", "fullName": "Lone Scherfig"}, 62 | {"firstName": "Louise", "fullName": "Louise Kolm-Fleck"}, 63 | {"firstName": "Lucrecia", "fullName": "Lucrecia Martel"}, 64 | {"firstName": "Mabel", "fullName": "Mabel Normand"}, 65 | {"firstName": "Malgorzata", "fullName": "Malgorzata Szumowska"}, 66 | {"firstName": "Margarethe", "fullName": "Margarethe von Trotta"}, 67 | {"firstName": "Marie", "fullName": "Marie Louise Droop"}, 68 | {"firstName": "Marjane", "fullName": "Marjane Satrapi"}, 69 | {"firstName": "Mary", "fullName": "Mary McIlwain"}, 70 | {"firstName": "Maryse", "fullName": "Maryse Alberti"}, 71 | {"firstName": "Maya", "fullName": "Maya Deren"}, 72 | {"firstName": "Mimi", "fullName": "Mimi Derba"}, 73 | {"firstName": "Mira", "fullName": "Mira Nair"}, 74 | {"firstName": "Muriel", "fullName": "Muriel Box"}, 75 | {"firstName": "Nadine", "fullName": "Nadine Labaki"}, 76 | {"firstName": "Natasha", "fullName": "Natasha Braier"}, 77 | {"firstName": "Niki", "fullName": "Niki Caro"}, 78 | {"firstName": "Olga", "fullName": "Olga Preobrazhenskaia"}, 79 | {"firstName": "Rachel", "fullName": "Rachel Morrison"}, 80 | {"firstName": "Randa", "fullName": "Randa Haines"}, 81 | {"firstName": "Reed", "fullName": "Reed Morano"}, 82 | {"firstName": "Sofia", "fullName": "Sofia Coppola"}, 83 | {"firstName": "Susanne", "fullName": "Susanne Bier"}, 84 | {"firstName": "Toni", "fullName": "Toni English"}, 85 | {"firstName": "Vera", "fullName": "Vera Chytilová"} 86 | ] 87 | -------------------------------------------------------------------------------- /app/utils/faceDetection.js: -------------------------------------------------------------------------------- 1 | /* eslint import/prefer-default-export: "off" */ 2 | 3 | import path from 'path'; 4 | import * as faceapi from 'face-api.js'; 5 | import log from 'electron-log'; 6 | import uuidV4 from 'uuid/v4'; 7 | 8 | import { roundNumber } from './utils'; 9 | import { FACE_CONFIDENCE_THRESHOLD, FACE_SIZE_THRESHOLD } from './constants'; 10 | 11 | const { ipcRenderer } = require('electron'); 12 | const { app } = require('electron').remote; 13 | 14 | const appPath = app.getAppPath(); 15 | const weightsPath = path.join(appPath, './dist/weights'); 16 | 17 | const loadNet = async () => { 18 | const toastId = 'loadNet'; 19 | ipcRenderer.send( 20 | 'message-from-opencvWorkerWindow-to-mainWindow', 21 | 'progressMessage', 22 | 'info', 23 | 'Initialising face detection', 24 | false, 25 | toastId, 26 | ); 27 | log.debug(weightsPath); 28 | log.debug(faceapi.nets); 29 | const detectionNet = faceapi.nets.ssdMobilenetv1; 30 | log.debug(detectionNet); 31 | await detectionNet.loadFromDisk(weightsPath); 32 | await faceapi.nets.faceLandmark68Net.loadFromDisk(weightsPath); 33 | await faceapi.nets.ageGenderNet.loadFromDisk(weightsPath); 34 | await faceapi.nets.faceRecognitionNet.loadFromDisk(weightsPath); 35 | ipcRenderer.send( 36 | 'message-from-opencvWorkerWindow-to-mainWindow', 37 | 'progressMessage', 38 | 'success', 39 | 'Face detection successfully initialised', 40 | 3000, 41 | toastId, 42 | true, 43 | ); 44 | return detectionNet; 45 | }; 46 | 47 | loadNet() 48 | .then(detectionNet => { 49 | log.debug(detectionNet); 50 | return undefined; 51 | }) 52 | .catch(error => { 53 | log.error(`There has been a problem with your loadNet operation: ${error.message}`); 54 | ipcRenderer.send( 55 | 'message-from-opencvWorkerWindow-to-mainWindow', 56 | 'progressMessage', 57 | 'error', 58 | `There has been a problem with your loadNet operation: ${error.message}`, 59 | false, 60 | 'loadNet', 61 | true, 62 | ); 63 | }); 64 | 65 | export const detectAllFaces = async ( 66 | image, 67 | frameNumber, 68 | detectionArray, 69 | defaultFaceConfidenceThreshold = FACE_SIZE_THRESHOLD, 70 | defaultFaceSizeThreshold = FACE_CONFIDENCE_THRESHOLD, 71 | ) => { 72 | // detect expression 73 | const detections = await faceapi 74 | .detectAllFaces(image) 75 | .withFaceLandmarks() 76 | .withAgeAndGender() 77 | .withFaceDescriptors(); 78 | console.log(frameNumber); 79 | 80 | const faceCount = detections.length; 81 | 82 | if (faceCount === 0) { 83 | console.log('no face detected!'); 84 | detectionArray.push({ 85 | frameNumber, 86 | faceCount, 87 | }); 88 | return detections; 89 | } 90 | console.log(`Face count: ${faceCount}`); 91 | 92 | console.log(detections); 93 | 94 | const facesArray = []; 95 | detections.forEach(face => { 96 | const { age, gender, descriptor, detection } = face; 97 | const { relativeBox, score } = detection; 98 | const size = Math.round(relativeBox.height * 100); 99 | const scoreInPercent = Math.round(score * 100); 100 | 101 | // create full copy of array to be pushed later 102 | const copyOfDescriptor = descriptor.slice(); 103 | 104 | // console.log(detection); 105 | // console.log(uniqueFaceArray); 106 | // console.log(copyOfDescriptor); 107 | 108 | // console.log(face); 109 | if (size < defaultFaceSizeThreshold || scoreInPercent < defaultFaceConfidenceThreshold) { 110 | console.log('detected face below size or confidence threshold!'); 111 | return undefined; 112 | } 113 | 114 | const simpleBox = { 115 | x: roundNumber(relativeBox.x, 4), 116 | y: roundNumber(relativeBox.y, 4), 117 | width: roundNumber(relativeBox.width, 4), 118 | height: roundNumber(relativeBox.height, 4), 119 | }; 120 | 121 | facesArray.push({ 122 | score: scoreInPercent, 123 | size, 124 | box: simpleBox, 125 | gender, 126 | age: Math.round(age), 127 | faceId: uuidV4(), 128 | faceDescriptor: copyOfDescriptor, 129 | }); 130 | 131 | // const drawBox = new faceapi.draw.DrawBox(box, { 132 | // // label: Math.round(age), 133 | // label: faceId, 134 | // lineWidth: 1, 135 | // boxColor: 'red' 136 | // }) 137 | // drawBox.draw(image); 138 | }); 139 | 140 | if (facesArray.length === 0) { 141 | // no faces stored as they are below thresholds 142 | detectionArray.push({ 143 | frameNumber, 144 | faceCount, 145 | }); 146 | return detections; 147 | } 148 | 149 | // sort faces by size with the largest one first 150 | facesArray.sort((face1, face2) => face2.size - face1.size); 151 | console.log(facesArray); 152 | 153 | detectionArray.push({ 154 | frameNumber, 155 | faceCount, 156 | largestSize: facesArray[0].size, 157 | facesArray, 158 | }); 159 | return detections; 160 | }; 161 | -------------------------------------------------------------------------------- /app/img/MoviePrint-titleimage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/utils/utilsForIndexedDB.js: -------------------------------------------------------------------------------- 1 | import log from 'electron-log'; 2 | import imageDB from './db'; 3 | 4 | const { ipcRenderer } = require('electron'); 5 | 6 | export const openDBConnection = () => { 7 | // dexie documentation: 8 | // Even though open() is asynchronous, 9 | // you can already now start interact with the database. 10 | // The operations will be pending until open() completes. 11 | // If open() succeeds, the operations below will resume. 12 | // If open() fails, the below operations below will fail and 13 | if (!imageDB.isOpen()) { 14 | imageDB.open().catch(err => { 15 | log.error(`Failed to open imageDB: ${err.stack || err}`); 16 | }); 17 | } 18 | }; 19 | 20 | export const deleteTableFramelist = () => 21 | imageDB.frameList.clear().catch(err => { 22 | log.error(`Failed to delete all objects in frameList: ${err.stack || err}`); 23 | }); 24 | 25 | export const addFrameToIndexedDB = (frameId, fileId, frameNumber, outBase64, objectUrlQueue) => { 26 | const url = `data:image/jpg;base64,${outBase64}`; 27 | return fetch(url) 28 | .then(res => res.blob()) 29 | .then(blob => { 30 | try { 31 | return imageDB 32 | .transaction('rw', imageDB.frameList, async () => { 33 | await imageDB.frameList.put({ 34 | frameId, 35 | fileId, 36 | frameNumber, 37 | data: blob, 38 | }); 39 | const key = await imageDB.frameList.get(frameId); 40 | // console.log(key); 41 | return key; 42 | }) 43 | .catch(e => { 44 | log.error('error inside addFrameToIndexedDB - transaction'); 45 | log.error(e.stack || e); 46 | }); 47 | } catch (e) { 48 | log.error(e.stack || e); 49 | return undefined; 50 | } 51 | }) 52 | .then(frame => { 53 | // console.log(frame); 54 | const objectUrl = window.URL.createObjectURL(frame.data); 55 | objectUrlQueue.add({ 56 | frameId, 57 | objectUrl, 58 | }); 59 | return objectUrl; 60 | }) 61 | .catch(e => { 62 | log.error(e.stack || e); 63 | }); 64 | }; 65 | 66 | export const updateFrameInIndexedDB = (frameId, outBase64, objectUrlQueue, fastTrack) => { 67 | if (outBase64 === '') { 68 | return undefined; 69 | } 70 | const url = `data:image/jpg;base64,${outBase64}`; 71 | fetch(url) 72 | .then(res => res.blob()) 73 | .then(blob => { 74 | return imageDB 75 | .transaction('rw', imageDB.frameList, async () => { 76 | await imageDB.frameList 77 | .where('frameId') 78 | .equals(frameId) 79 | .modify({ 80 | data: blob, 81 | }); 82 | const key = await imageDB.frameList.get(frameId); 83 | console.log(key); 84 | return key; 85 | }) 86 | .then(key => { 87 | console.log('Transaction committed'); 88 | return key; 89 | }) 90 | .catch(e => { 91 | log.error('error inside updateFrameInIndexedDB - transaction'); 92 | log.error(e.stack || e); 93 | }); 94 | }) 95 | .then(frame => { 96 | console.log(frame); 97 | if (frame !== undefined) { 98 | const objectUrl = window.URL.createObjectURL(frame.data); 99 | if (fastTrack) { 100 | ipcRenderer.send('message-from-databaseWorkerWindow-to-mainWindow', 'update-objectUrl', frameId, objectUrl); 101 | } else { 102 | objectUrlQueue.add({ 103 | frameId, 104 | objectUrl, 105 | }); 106 | } 107 | return objectUrl; 108 | } 109 | return undefined; 110 | }) 111 | .catch(e => { 112 | log.error(e.stack || e); 113 | }); 114 | }; 115 | 116 | export const getObjectUrlsFromFramelist = objectUrlQueue => { 117 | console.log('inside getObjectUrlsFromFramelist'); 118 | try { 119 | log.warn(imageDB.isOpen()); 120 | imageDB 121 | .transaction('r', imageDB.frameList, async () => { 122 | try { 123 | console.log(imageDB.isOpen()); 124 | const array = await imageDB.frameList.toArray().catch(e => { 125 | log.error('error inside promise catch'); 126 | log.error(e.stack || e); 127 | }); 128 | if (array.length === 0) { 129 | return []; 130 | } 131 | const arrayOfObjectUrls = []; 132 | array.map(frame => { 133 | const objectUrl = window.URL.createObjectURL(frame.data); 134 | if (objectUrl !== undefined) { 135 | arrayOfObjectUrls.push({ 136 | frameId: frame.frameId, 137 | objectUrl: window.URL.createObjectURL(frame.data), 138 | }); 139 | } 140 | return undefined; 141 | }); 142 | objectUrlQueue.addArray(arrayOfObjectUrls); 143 | return undefined; 144 | } catch (e) { 145 | log.error('error inside the inner try catch'); 146 | log.error(e.stack || e); 147 | } 148 | }) 149 | .catch(e => { 150 | log.error('error inside promise catch'); 151 | log.error(e.stack || e); 152 | }); 153 | } catch (e) { 154 | log.error('error inside try catch'); 155 | log.error(e.stack || e); 156 | } 157 | }; 158 | -------------------------------------------------------------------------------- /app/reducers/files.js: -------------------------------------------------------------------------------- 1 | /* eslint no-case-declarations: "off" */ 2 | 3 | import log from 'electron-log'; 4 | import { RotateFlags } from '../utils/openCVProperties'; 5 | 6 | const file = (state = {}, type, payload, index) => { 7 | switch (type) { 8 | case 'UPDATE_SHEETCOUNTER': 9 | if (state.id !== payload.fileId) { 10 | return state; 11 | } 12 | return { ...state, sheetCounter: (state.sheetCounter || 0) + payload.incrementValue }; 13 | case 'UPDATE_FILESCAN_STATUS': 14 | if (state.id !== payload.fileId) { 15 | return state; 16 | } 17 | return { ...state, fileScanStatus: payload.fileScanStatus }; 18 | case 'UPDATE_FILE_MISSING_STATUS': 19 | if (state.id !== payload.fileId) { 20 | return state; 21 | } 22 | return { ...state, fileMissingStatus: payload.fileMissingStatus }; 23 | case 'UPDATE_MOVIE_LIST_ITEM_USERATIO': 24 | if (state.id !== payload.fileId) { 25 | return state; 26 | } 27 | return { ...state, useRatio: payload.useRatio }; 28 | case 'REPLACE_MOVIE_LIST_ITEM': 29 | if (state.id !== payload.fileId) { 30 | return state; 31 | } 32 | return { 33 | ...state, 34 | path: payload.path, 35 | name: payload.name, 36 | size: payload.size, 37 | lastModified: payload.lastModified, 38 | }; 39 | case 'UPDATE_MOVIE_LIST_ITEM': { 40 | if (state.id !== payload.fileId) { 41 | return state; 42 | } 43 | const { fps, fourCC, frameCount } = payload; 44 | let { height, width } = payload; 45 | const originalWidth = width; 46 | const originalHeight = height; 47 | 48 | // if transformObject already exists then calculate width and height from it 49 | const { transformObject } = state; 50 | console.log(state); 51 | if (transformObject !== undefined) { 52 | width = originalWidth - transformObject.cropLeft - transformObject.cropRight; 53 | height = originalHeight - transformObject.cropTop - transformObject.cropBottom; 54 | } 55 | return { 56 | ...state, 57 | frameCount, 58 | originalWidth, 59 | width, 60 | originalHeight, 61 | height, 62 | fps, 63 | fourCC, 64 | }; 65 | } 66 | case 'SET_TRANSFORM': 67 | if (state.id !== payload.fileId) { 68 | return state; 69 | } 70 | return { ...state, transformObject: payload.transformObject }; 71 | case 'ROTATE_WIDTH_AND_HEIGHT': { 72 | if (state.id !== payload.fileId) { 73 | return state; 74 | } 75 | const { shouldRotate } = payload; 76 | if (shouldRotate) { 77 | return { ...state, width: state.originalHeight, height: state.originalWidth }; 78 | } 79 | return { ...state, width: state.originalWidth, height: state.originalHeight }; 80 | } 81 | case 'UPDATE_CROPPING': { 82 | if (state.id !== payload.fileId) { 83 | return state; 84 | } 85 | const { transformObject } = payload; 86 | const { cropTop, cropLeft, cropBottom, cropRight, rotationFlag } = transformObject; 87 | 88 | let origWidth = state.originalWidth; 89 | let origHeight = state.originalHeight; 90 | if (rotationFlag === RotateFlags.ROTATE_90_CLOCKWISE || rotationFlag === RotateFlags.ROTATE_90_COUNTERCLOCKWISE) { 91 | [origWidth, origHeight] = [origHeight, origWidth]; // swapping of width and height 92 | } 93 | const newWidth = origWidth - cropLeft - cropRight; 94 | const newHeight = origHeight - cropTop - cropBottom; 95 | return { ...state, width: newWidth, height: newHeight, transformObject }; 96 | } 97 | case 'UPDATE_ASPECT_RATIO': 98 | if (state.id !== payload.fileId) { 99 | return state; 100 | } 101 | return { 102 | ...state, transformObject: { 103 | ...state.transformObject, 104 | aspectRatioInv: payload.aspectRatioInv 105 | } 106 | }; 107 | case 'UPDATE_IN_OUT_POINT': 108 | if (state.id !== payload.fileId) { 109 | return state; 110 | } 111 | return { ...state, fadeInPoint: payload.fadeInPoint, fadeOutPoint: payload.fadeOutPoint }; 112 | default: 113 | return state; 114 | } 115 | }; 116 | 117 | const files = (state = [], { type, payload }) => { 118 | switch (type) { 119 | case 'CLEAR_MOVIE_LIST': 120 | return []; 121 | case 'ADD_MOVIE_LIST_ITEMS': { 122 | log.debug(payload); 123 | log.debug(state); 124 | // combine state array and new files array 125 | const combinedArray = state.concat(payload); 126 | log.debug(combinedArray); 127 | return combinedArray; 128 | } 129 | case 'REMOVE_MOVIE_LIST_ITEM': { 130 | const newArray = state.slice(); 131 | const indexOfItemToRemove = newArray.findIndex(singleFile => singleFile.id === payload.fileId); 132 | newArray.splice(indexOfItemToRemove, 1); 133 | return newArray; 134 | } 135 | case 'REPLACE_MOVIE_LIST_ITEM': 136 | case 'UPDATE_MOVIE_LIST_ITEM_USERATIO': 137 | case 'UPDATE_MOVIE_LIST_ITEM': 138 | case 'SET_TRANSFORM': 139 | case 'ROTATE_WIDTH_AND_HEIGHT': 140 | case 'UPDATE_CROPPING': 141 | case 'UPDATE_ASPECT_RATIO': 142 | case 'UPDATE_IN_OUT_POINT': 143 | case 'UPDATE_FILE_MISSING_STATUS': 144 | case 'UPDATE_FILESCAN_STATUS': 145 | case 'UPDATE_SHEETCOUNTER': 146 | return state.map((t, index) => file(t, type, payload, index)); 147 | default: 148 | return state; 149 | } 150 | }; 151 | 152 | export default files; 153 | -------------------------------------------------------------------------------- /app/containers/VisibleSceneGrid.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import PropTypes from 'prop-types'; 4 | import { arrayMove } from 'react-sortable-hoc'; 5 | import scrollIntoView from 'scroll-into-view'; 6 | import { 7 | toggleThumb, updateOrder, 8 | changeThumb, toggleScene, toggleSceneArray 9 | } from '../actions'; 10 | import styles from '../components/ThumbGrid.css'; 11 | import SortableSceneGrid from '../components/SceneGrid'; 12 | import { getLowestFrame, getHighestFrame } from '../utils/utils'; 13 | import { CHANGE_THUMB_STEP } from '../utils/constants'; 14 | 15 | class SortedVisibleSceneGrid extends Component { 16 | constructor(props) { 17 | super(props); 18 | this.state = { 19 | }; 20 | 21 | this.scrollIntoViewElement = React.createRef(); 22 | 23 | this.scrollThumbIntoView = this.scrollThumbIntoView.bind(this); 24 | this.onSelectClick = this.onSelectClick.bind(this); 25 | this.onDeselectClick = this.onDeselectClick.bind(this); 26 | } 27 | 28 | componentDidMount() { 29 | setTimeout(() => { 30 | this.scrollThumbIntoView(); 31 | }, 500); 32 | } 33 | 34 | componentDidUpdate(prevProps) { 35 | if (prevProps.selectedThumbsArray.length !== 0 && 36 | this.props.selectedThumbsArray.length !== 0 && 37 | (prevProps.selectedThumbsArray[0].thumbId !== this.props.selectedThumbsArray[0].thumbId)) { 38 | this.scrollThumbIntoView(); 39 | } 40 | // delay when switching to gridView so it waits for the sheetView to be ready 41 | if ((prevProps.view !== this.props.view) && 42 | prevProps.view) { 43 | setTimeout(() => { 44 | this.scrollThumbIntoView(); 45 | }, 500); 46 | } 47 | } 48 | 49 | onSelectClick = (sceneId, frameNumber) => { 50 | this.props.onSelectThumbMethod(sceneId, frameNumber); 51 | } 52 | 53 | onDeselectClick = () => { 54 | console.log('deselect') 55 | this.props.onDeselectThumbMethod(); 56 | } 57 | 58 | scrollThumbIntoView = () => { 59 | if (this.scrollIntoViewElement && this.scrollIntoViewElement.current !== null) { 60 | scrollIntoView(this.scrollIntoViewElement.current, { 61 | time: 300, 62 | align: { 63 | left: 0.5, 64 | } 65 | }); 66 | } 67 | }; 68 | 69 | render() { 70 | return ( 71 | 112 | ); 113 | } 114 | } 115 | 116 | const mapStateToProps = state => ({}); 117 | 118 | const mapDispatchToProps = (dispatch, ownProps) => { 119 | return { 120 | onSortEnd: ({ oldIndex, newIndex }) => { 121 | const { settings, sheetsByFileId } = ownProps; 122 | const newOrderedThumbs = arrayMove(sheetsByFileId[settings.currentFileId][settings.currentSheetId].thumbsArray, 123 | oldIndex, 124 | newIndex); 125 | dispatch(updateOrder( 126 | settings.currentFileId, 127 | settings.currentSheetId, 128 | newOrderedThumbs)); 129 | }, 130 | onToggleClick: (fileId, sceneId) => { 131 | dispatch(toggleScene(fileId, ownProps.settings.currentSheetId, sceneId)); 132 | }, 133 | onHideBeforeAfterClick: (fileId, sheetId, thumbIdArray) => { 134 | dispatch(toggleSceneArray( 135 | fileId, 136 | sheetId, 137 | thumbIdArray 138 | )); 139 | }, 140 | }; 141 | }; 142 | 143 | SortedVisibleSceneGrid.contextTypes = { 144 | }; 145 | 146 | SortedVisibleSceneGrid.defaultProps = { 147 | selectedThumbsArray: [], 148 | }; 149 | 150 | SortedVisibleSceneGrid.propTypes = { 151 | selectedThumbsArray: PropTypes.array, 152 | }; 153 | 154 | export default connect(mapStateToProps, mapDispatchToProps)(SortedVisibleSceneGrid); 155 | -------------------------------------------------------------------------------- /app/components/VideoPlayer.css: -------------------------------------------------------------------------------- 1 | .player { 2 | /* position: absolute; 3 | top: 0; 4 | left: 0; 5 | right: 0; 6 | bottom: 6rem; */ 7 | background: #1e1e1e; 8 | } 9 | 10 | .video { 11 | /* width: 100%; 12 | height: 100%; */ 13 | object-fit: contain; 14 | } 15 | 16 | .videoOverlay { 17 | position: absolute; 18 | transform-origin: center bottom; 19 | transform: translate(-50%, 0%); 20 | top: 0; 21 | left: 50%; 22 | width: 640px; 23 | object-fit: contain; 24 | background-color: rgba(0,0,0,0.8); /* Black w/ opacity */ 25 | z-index: 2; 26 | } 27 | 28 | .frameNumberOrTimeCode { 29 | position: absolute; 30 | top: 0; 31 | left: 0; 32 | background: #eee; 33 | border-radius:2px; 34 | font-family: 'Open sans'; 35 | font-size: 14px; 36 | line-height: 14px; 37 | padding: 1px; 38 | color: #000000; 39 | -webkit-user-select:none; 40 | z-index: 3; 41 | } 42 | 43 | .moveToMiddle { 44 | left: 50%; 45 | } 46 | 47 | .controlsWrapper button, .rightMenu button, .leftMenu button { 48 | /* background: black; */ 49 | border-radius: .3em; 50 | /* color: rgba(0, 0, 0, 0.7); */ 51 | fontSize: 60%; 52 | vertical-align: middle; 53 | /* padding: .2em .4em; */ 54 | /* margin: 0 .5em; */ 55 | border: none; 56 | } 57 | 58 | .controlsWrapper { 59 | left: 0; 60 | right: 0; 61 | bottom: 0; 62 | /* height: 5.75rem; */ 63 | /* background: #6b6b6b; */ 64 | text-align: center; 65 | margin-top: 4px; 66 | } 67 | 68 | #currentTimeDisplay { 69 | text-align: center; 70 | color: rgba(255, 255, 255, 0.3); 71 | /* padding: .5em; */ 72 | } 73 | 74 | .buttonWrapper { 75 | width: 100%; 76 | position: relative; 77 | /* background-color: #444; */ 78 | height: 32px; 79 | margin-top: 4px; 80 | padding: 4px; 81 | } 82 | 83 | .overVideoButtonWrapper { 84 | width: 100%; 85 | position: absolute; 86 | transform: translate(0%, -100%); 87 | height: 36px; 88 | /* background-color: rgba(0, 0, 0, 0.05); */ 89 | } 90 | 91 | .hoverButton { 92 | border: 0; 93 | cursor: pointer; 94 | background-color: transparent; 95 | outline:none; 96 | -webkit-user-select:none; 97 | } 98 | 99 | .inPoint { 100 | position: absolute; 101 | bottom: 0; 102 | left: 0; 103 | opacity: 0.5; 104 | } 105 | 106 | .outPoint { 107 | position: absolute; 108 | bottom: 0; 109 | right: 0; 110 | opacity: 0.5; 111 | } 112 | 113 | .choose { 114 | position: absolute; 115 | bottom: 0; 116 | /* left: calc(50% - 24px); */ 117 | /* top: 50%; */ 118 | left: 50%; 119 | transform: translate(-50%, 0%); 120 | opacity: 0.5; 121 | /* cursor: col-resize; */ 122 | } 123 | 124 | .previousScene { 125 | position: absolute; 126 | bottom: 0; 127 | left: 30%; 128 | transform-origin: center bottom; 129 | transform: translate(-100%, 0%); 130 | opacity: 0.5; 131 | } 132 | 133 | .nextScene { 134 | position: absolute; 135 | bottom: 0; 136 | right: 28%; 137 | transform-origin: center bottom; 138 | transform: translate(50%, 0%); 139 | opacity: 0.5; 140 | } 141 | 142 | .hundredFramesBack { 143 | position: absolute; 144 | bottom: 0; 145 | left: 35%; 146 | transform-origin: center bottom; 147 | transform: translate(-50%, 0%); 148 | opacity: 0.5; 149 | } 150 | 151 | .tenFramesBack { 152 | position: absolute; 153 | bottom: 0; 154 | left: 40%; 155 | transform-origin: center bottom; 156 | transform: translate(-50%, 0%); 157 | opacity: 0.5; 158 | } 159 | 160 | .oneFrameBack { 161 | position: absolute; 162 | bottom: 0; 163 | left: 44%; 164 | transform-origin: center bottom; 165 | transform: translate(-50%, 0%); 166 | opacity: 0.5; 167 | } 168 | 169 | .hundredFramesForward { 170 | position: absolute; 171 | bottom: 0; 172 | right: 35%; 173 | transform-origin: center bottom; 174 | transform: translate(-50%, 0%); 175 | opacity: 0.5; 176 | } 177 | 178 | .tenFramesForward { 179 | position: absolute; 180 | bottom: 0; 181 | right: 40%; 182 | transform-origin: center bottom; 183 | transform: translate(-50%, 0%); 184 | opacity: 0.5; 185 | } 186 | 187 | .oneFrameForward { 188 | position: absolute; 189 | bottom: 0; 190 | right: 44%; 191 | transform-origin: center bottom; 192 | transform: translate(-50%, 0%); 193 | opacity: 0.5; 194 | } 195 | 196 | .html5Button { 197 | position: absolute; 198 | top: 0; 199 | left: 0; 200 | margin-top: 8px; 201 | margin-left: 8px; 202 | z-Index: 1; 203 | } 204 | 205 | .saveFrameButton { 206 | position: absolute; 207 | bottom: 92px; 208 | left: 50%; 209 | margin-top: 8px; 210 | margin-right: 8px; 211 | z-Index: 1; 212 | } 213 | 214 | .centerTheButton { 215 | transform: translate(-50%, 0%); 216 | } 217 | 218 | .backButton { 219 | position: absolute; 220 | top: 0; 221 | right: 0; 222 | margin-top: 8px; 223 | margin-right: 8px; 224 | z-Index: 1; 225 | } 226 | 227 | .cutMergeButton { 228 | position: absolute; 229 | bottom: 0; 230 | left: 50%; 231 | transform-origin: center bottom; 232 | transform: translateX(-60%); 233 | /* display: block; */ 234 | } 235 | 236 | .changeModeButton { 237 | position: absolute; 238 | bottom: 0; 239 | left: 0; 240 | margin-left: 8px; 241 | } 242 | 243 | .textButton { 244 | font-family: 'Franchise', 'Roboto Condensed'; 245 | color: #ffffff; 246 | font-size: 30px; 247 | opacity: 0.5; 248 | -webkit-user-select:none; 249 | /* text-shadow: 1px 1px 80px rgba(0,0,0,0.6); */ 250 | } 251 | 252 | .noVideoText { 253 | position: absolute; 254 | top: 30%; 255 | left: 50%; 256 | transform-origin: center center; 257 | transform: translateX(-50%); 258 | z-Index: 1; 259 | } 260 | -------------------------------------------------------------------------------- /app/components/SceneGrid.css: -------------------------------------------------------------------------------- 1 | .grid { 2 | display: block; 3 | height: 100%; 4 | white-space: nowrap; 5 | border: 0; 6 | overflow: auto; 7 | margin: auto; 8 | outline: none; 9 | position: relative; 10 | } 11 | 12 | .gridItem { 13 | float: left; 14 | /*width: 168px;*/ 15 | /*padding: 8px;*/ 16 | /*background: #3b3b3b;*/ 17 | /* border: 0; */ 18 | margin: 0.5px; 19 | position: relative; 20 | outline: none; 21 | -webkit-user-select: none; 22 | background-repeat: no-repeat; 23 | /* background-size: cover; */ 24 | /* background-size: auto 100%; */ 25 | /* background-size: auto calc(100% + 20px); */ 26 | background-position: center; 27 | /* background-blend-mode: overlay; */ 28 | /* background-repeat: space; */ 29 | /* background-clip: padding-box; */ 30 | /* border: 4px solid rgba(0,0,0,0.1); */ 31 | border-style: solid; 32 | border-color: rgba(0,0,0,0.1); 33 | } 34 | 35 | .gridHeader { 36 | background: #3b3b3b; 37 | color: #eee; 38 | font-family: 'Open sans'; 39 | padding-left: 4px; 40 | overflow: hidden; 41 | position: relative; 42 | line-height: 0; 43 | } 44 | 45 | .gridHeaderImageAndText { 46 | float: left; 47 | transform-origin: 'left bottom'; 48 | vertical-align: baseline; 49 | } 50 | 51 | .gridHeaderImage { 52 | /* display: inline-block; */ 53 | position: absolute; 54 | } 55 | 56 | .gridHeaderText { 57 | display: inline-block; 58 | white-space: normal; 59 | color: #ff925e; 60 | } 61 | 62 | .gridHeaderTextName { 63 | font-size: 12px; 64 | font-weight: bold; 65 | color: #ffd3bf; 66 | letter-spacing: 0.3px; 67 | text-align: right; 68 | } 69 | 70 | .timelineWrapper { 71 | position: relative; 72 | background: rgba(255, 219, 204, 0.2); 73 | clear: both; 74 | } 75 | 76 | .timelineCut { 77 | position: absolute; 78 | background: #e85d22; 79 | height: 100%; 80 | } 81 | 82 | .timelineThumbIndicator { 83 | position: absolute; 84 | min-width: 1px; 85 | background: #ffdbcc; 86 | } 87 | 88 | .gridItemSelected { 89 | outline-style: solid; 90 | outline-color: #ff5006; 91 | background-color: #ff5006; 92 | /* box-shadow: 5px 0px 0px 0px #FF5006 */ 93 | } 94 | 95 | .sceneExpanded { 96 | outline-style: solid; 97 | outline-color: #ffd3bf; 98 | /* outline-color: #ff5006; */ 99 | outline-offset: -1px; 100 | /* background-color: #ff5006; */ 101 | /* box-shadow: 5px 0px 0px 0px #FF5006 */ 102 | } 103 | 104 | /* .gridItemSelected:after { 105 | content: ""; 106 | background-color: #FF5006; 107 | position: absolute; 108 | width: 5px; 109 | height: 100%; 110 | top: 0; 111 | right: 0; 112 | display: block; 113 | transform: translateX(10px); 114 | } */ 115 | 116 | .gridItem:hover { 117 | /* background: #eee; */ 118 | cursor: pointer; 119 | } 120 | 121 | .image { 122 | float: left; 123 | /*width: 168px;*/ 124 | /*padding: 8px;*/ 125 | /*background: #3b3b3b;*/ 126 | /*border: 0;*/ 127 | /*margin: 4px;*/ 128 | /* border-radius:8px; */ 129 | /*position: relative;*/ 130 | } 131 | 132 | .gridForPrinting { 133 | margin-left: -50px; 134 | } 135 | 136 | .frameNumber { 137 | position: absolute; 138 | top: 0; 139 | left: 0; 140 | background: #eee; 141 | border-radius: 2px; 142 | font-family: 'Open sans'; 143 | font-size: 10px; 144 | line-height: 10px; 145 | padding: 1px; 146 | color: #000000; 147 | transform-origin: left top; 148 | /*opacity: 0.5;*/ 149 | } 150 | 151 | .dragHandleButton { 152 | position: absolute; 153 | /* top: calc(50% - 28px); 154 | left: calc(50% - 72px); */ 155 | top: 50%; 156 | left: 50%; 157 | transform: translate(-50%, -50%) scale(0.95); 158 | /* width: 144px; */ 159 | /* height: 80%; */ 160 | opacity: 1; 161 | cursor: move; 162 | background-color: transparent; 163 | border: 0; 164 | outline: none; 165 | } 166 | 167 | .dragHandleIcon { 168 | position: absolute; 169 | /* top: calc(50% - 28px); 170 | left: calc(50% - 72px); */ 171 | top: 50%; 172 | left: 50%; 173 | transform: translate(-50%, -50%); 174 | width: 144px; 175 | opacity: 1; 176 | /* cursor: move; */ 177 | } 178 | 179 | .whileDragging { 180 | box-shadow: 0px 0px 80px 10px rgba(0, 0, 0, 0.5); 181 | z-index: 1000; /* has to be higher than dropzoneshow*/ 182 | } 183 | 184 | .hide { 185 | position: absolute; 186 | outline: none; 187 | top: 0; 188 | left: 50%; 189 | opacity: 0.5; 190 | background-color: transparent; 191 | } 192 | 193 | .inPoint { 194 | position: absolute; 195 | outline: none; 196 | bottom: 0; 197 | left: 0; 198 | opacity: 0.5; 199 | } 200 | 201 | /* .back { 202 | position: absolute; 203 | outline:none; 204 | bottom: 0; 205 | left: calc(50% - 24px - 48px); 206 | opacity: 0.5; 207 | } */ 208 | 209 | .save { 210 | position: absolute; 211 | outline: none; 212 | top: 0; 213 | right: 0; 214 | opacity: 0.5; 215 | } 216 | 217 | .textButton { 218 | font-family: 'Franchise'; 219 | color: #ffffff; 220 | font-size: 30px; 221 | opacity: 0.5; 222 | } 223 | 224 | /* .forward { 225 | position: absolute; 226 | outline:none; 227 | bottom: 0; 228 | left: calc(50% + 24px); 229 | opacity: 0.5; 230 | } */ 231 | 232 | .outPoint { 233 | position: absolute; 234 | outline: none; 235 | bottom: 0; 236 | right: 0; 237 | opacity: 0.5; 238 | } 239 | 240 | .dim { 241 | transition: opacity 0.3s; 242 | opacity: 0.2 !important; 243 | transition-delay: 0.5s; 244 | } 245 | 246 | .hoverButton { 247 | border: 0; 248 | cursor: pointer; 249 | background-color: transparent; 250 | outline: none; 251 | -webkit-user-select: none; 252 | } 253 | 254 | .opaque { 255 | opacity: 1 !important; 256 | } 257 | 258 | .lineBreak { 259 | clear: both; 260 | } 261 | 262 | .sheetTypeSwitchButton { 263 | position: fixed; 264 | transform-origin: center center; 265 | transform: translateY(-50%); 266 | top: 80%; 267 | left: 0; 268 | margin-top: 8px; 269 | padding-left: 8px; 270 | z-Index: 1; 271 | } 272 | -------------------------------------------------------------------------------- /test/e2e/e2e.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint no-restricted-syntax: ["error", "WithStatement", "BinaryExpression[operator='in']"] */ 2 | 3 | import { Application } from 'spectron'; 4 | import electronPath from 'electron'; 5 | import fakeDialog from 'spectron-fake-dialog'; 6 | import path from 'path'; 7 | import fs from 'fs'; 8 | import '../../internals/scripts/CheckBuildsExist'; 9 | 10 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; 11 | 12 | const delay = time => new Promise(resolve => setTimeout(resolve, time)); 13 | 14 | describe('main window', function spec() { 15 | beforeAll(async () => { 16 | this.app = new Application({ 17 | path: electronPath, 18 | args: [path.join(__dirname, '..', '..', 'app'), '--softreset'] 19 | // args: [path.join(__dirname, '..', '..', 'app'), '--softreset', '--debug'] 20 | }); 21 | fakeDialog.apply(this.app); 22 | console.log(await this.app.getSettings()); 23 | await delay(3000); 24 | return this.app.start(); 25 | }); 26 | 27 | afterAll(() => { 28 | if (this.app && this.app.isRunning()) { 29 | return this.app.stop(); 30 | } 31 | }); 32 | 33 | // prepare to store window title and handle for later use 34 | const windowObject = {}; 35 | 36 | it('should open the 4 windows', async () => { 37 | const { client } = this.app; 38 | const windowHandles = await client.windowHandles(); 39 | const windowNames = []; 40 | for (const windowHandleValue of windowHandles.value) { 41 | await client.window(windowHandleValue); 42 | const title = await client.getTitle(); 43 | // only add to array if it has a title 44 | // this will exclude debug windows 45 | if (title !== '') { 46 | windowNames.push(title) 47 | // store window title and handle for later use 48 | windowObject[title] = windowHandleValue 49 | } 50 | } 51 | console.log(windowNames.sort()); 52 | 53 | expect(windowNames).toEqual([ 54 | 'MoviePrint', 55 | 'MoviePrint_databaseWorker', 56 | 'MoviePrint_opencvWorker', 57 | 'MoviePrint_worker' 58 | ]); 59 | }); 60 | 61 | it("shouldn't have any logs in console of all windows", async () => { 62 | const { client } = this.app; 63 | Object.values(windowObject).map(async windowHandleValue => { 64 | await client.window(windowHandleValue); 65 | const logs = await client.getRenderProcessLogs(); 66 | // Print renderer process logs for MoviePrint renderer 67 | logs.forEach(log => { 68 | if (log.level === 'SEVERE' && 69 | log.message !== 'data:image/jpeg;base64, undefined - Failed to load resource: net::ERR_INVALID_URL') { 70 | expect(log.level).not.toEqual('SEVERE'); 71 | } 72 | }); 73 | }) 74 | }); 75 | 76 | it('should load a movie and get all 16 thumbs', async () => { 77 | const { client } = this.app; 78 | await client.window(windowObject['MoviePrint']); // focus main window 79 | const dragndropInput = '[type="file"]'; // selecting the input div via type 80 | const pathOfMovie = path.join(__dirname, '..', '..', 'resources', 'test_files', 'test_movie_1.mp4'); 81 | await client.chooseFile(dragndropInput, pathOfMovie); 82 | const val = await client.getValue(dragndropInput) 83 | console.log(val); 84 | client.waitForExist('[data-tid="thumbGridDiv"]', 5000); 85 | expect(await client.isExisting('[data-tid="thumbGridDiv"]')).toBe(true); 86 | expect(await client.isExisting('#thumb15')).toBe(true); 87 | // await client.browserWindow.capturePage().then((imageBuffer) => { 88 | // fs.writeFile('end of should load a movie and get all 16 thumbs.png', imageBuffer); 89 | // return undefined; 90 | // }).catch((err) => { 91 | // console.error(err); 92 | // }); 93 | }); 94 | 95 | it('should increase thumb count to 20', async () => { 96 | const { client } = this.app; 97 | // show settings menu 98 | await client.waitForExist('[data-tid="moreSettingsBtn"]', 3000); 99 | await client.element('[data-tid="moreSettingsBtn"]').click(); 100 | 101 | // move down to switch sliders to inputs 102 | await client.moveToObject('[data-tid="changeCachedFramesSizeDropdown"]'); 103 | await client.waitForVisible('[data-tid="showSlidersCheckbox"]', 3000); 104 | await client.element('[data-tid="showSlidersCheckbox"]').click(); 105 | 106 | // move up and change column count 107 | await client.moveToObject('[data-tid="columnCountInput"]'); 108 | await client.waitForVisible('[data-tid="columnCountInput"]', 3000); 109 | await client.setValue('[data-tid="columnCountInput"] input', 5); 110 | await client.keys('Enter'); 111 | await client.element('[data-tid="applyNewGridBtn"]').click(); 112 | 113 | await client.waitForExist('#thumb19', 3000); 114 | expect(await client.isExisting('#thumb19')).toBe(true); 115 | }); 116 | 117 | // it('should open a dialog', async () => { 118 | // const { client } = this.app; 119 | // fakeDialog.mock([ { method: 'showOpenDialogSync', value: ['faked.txt'] } ]) 120 | // 121 | // await client.click('[data-tid=openMoviesBtn]'); 122 | // const pathOfMovie = await client.getText('#return-value'); 123 | // console.log(pathOfMovie); 124 | // expect(await findCounter().getText()).toBe('0'); 125 | // }); 126 | 127 | // const findThumbGridDiv = () => this.app.client.element('[data-tid="thumbGridDiv"]'); 128 | // const thumbs = $$(".//*[contains(@class,'ThumbGrid__gridItem')]") 129 | // const findThumbs = () => this.app.client.elements('.//*[contains(@class,"ThumbGrid__gridItem")]'); 130 | // const thumbs = await findThumbs(); 131 | // console.log(thumbs); 132 | // console.log(thumbs.value.length); 133 | // await delay(1500); 134 | 135 | // const findCounter = () => this.app.client.element('[data-tid="counter"]'); 136 | // 137 | // const findButtons = async () => { 138 | // const { value } = await this.app.client.elements('[data-tclass="btn"]'); 139 | // return value.map(btn => btn.ELEMENT); 140 | // }; 141 | 142 | }); 143 | -------------------------------------------------------------------------------- /resources/weights/face_expression_model-weights_manifest.json: -------------------------------------------------------------------------------- 1 | [{"weights":[{"name":"dense0/conv0/filters","shape":[3,3,3,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0057930146946626555,"min":-0.7125408074435067}},{"name":"dense0/conv0/bias","shape":[32],"dtype":"float32"},{"name":"dense0/conv1/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006473719839956246,"min":-0.6408982641556684}},{"name":"dense0/conv1/pointwise_filter","shape":[1,1,32,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.010509579321917366,"min":-1.408283629136927}},{"name":"dense0/conv1/bias","shape":[32],"dtype":"float32"},{"name":"dense0/conv2/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005666389652326995,"min":-0.7252978754978554}},{"name":"dense0/conv2/pointwise_filter","shape":[1,1,32,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.010316079270605948,"min":-1.1760330368490781}},{"name":"dense0/conv2/bias","shape":[32],"dtype":"float32"},{"name":"dense0/conv3/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0063220320963392074,"min":-0.853474333005793}},{"name":"dense0/conv3/pointwise_filter","shape":[1,1,32,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.010322785377502442,"min":-1.4658355236053466}},{"name":"dense0/conv3/bias","shape":[32],"dtype":"float32"},{"name":"dense1/conv0/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0042531527724920535,"min":-0.5741756242864272}},{"name":"dense1/conv0/pointwise_filter","shape":[1,1,32,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.010653339647779278,"min":-1.1825207009035}},{"name":"dense1/conv0/bias","shape":[64],"dtype":"float32"},{"name":"dense1/conv1/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005166931012097527,"min":-0.6355325144879957}},{"name":"dense1/conv1/pointwise_filter","shape":[1,1,64,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.011478300188101974,"min":-1.3888743227603388}},{"name":"dense1/conv1/bias","shape":[64],"dtype":"float32"},{"name":"dense1/conv2/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006144821410085641,"min":-0.8479853545918185}},{"name":"dense1/conv2/pointwise_filter","shape":[1,1,64,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.010541967317169788,"min":-1.3809977185492421}},{"name":"dense1/conv2/bias","shape":[64],"dtype":"float32"},{"name":"dense1/conv3/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005769844849904378,"min":-0.686611537138621}},{"name":"dense1/conv3/pointwise_filter","shape":[1,1,64,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.010939095534530341,"min":-1.2689350820055196}},{"name":"dense1/conv3/bias","shape":[64],"dtype":"float32"},{"name":"dense2/conv0/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0037769308277204924,"min":-0.40790852939381317}},{"name":"dense2/conv0/pointwise_filter","shape":[1,1,64,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.01188667194516051,"min":-1.4382873053644218}},{"name":"dense2/conv0/bias","shape":[128],"dtype":"float32"},{"name":"dense2/conv1/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006497045825509464,"min":-0.8381189114907208}},{"name":"dense2/conv1/pointwise_filter","shape":[1,1,128,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.011632198913424622,"min":-1.3377028750438316}},{"name":"dense2/conv1/bias","shape":[128],"dtype":"float32"},{"name":"dense2/conv2/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005947182225246056,"min":-0.7969224181829715}},{"name":"dense2/conv2/pointwise_filter","shape":[1,1,128,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.011436844339557722,"min":-1.4524792311238306}},{"name":"dense2/conv2/bias","shape":[128],"dtype":"float32"},{"name":"dense2/conv3/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006665432686899222,"min":-0.8998334127313949}},{"name":"dense2/conv3/pointwise_filter","shape":[1,1,128,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.01283421422920975,"min":-1.642779421338848}},{"name":"dense2/conv3/bias","shape":[128],"dtype":"float32"},{"name":"dense3/conv0/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.004711699953266218,"min":-0.6737730933170692}},{"name":"dense3/conv0/pointwise_filter","shape":[1,1,128,256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.010955964817720302,"min":-1.3914075318504784}},{"name":"dense3/conv0/bias","shape":[256],"dtype":"float32"},{"name":"dense3/conv1/depthwise_filter","shape":[3,3,256,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.00554193468654857,"min":-0.7149095745647656}},{"name":"dense3/conv1/pointwise_filter","shape":[1,1,256,256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.016790372250126858,"min":-2.484975093018775}},{"name":"dense3/conv1/bias","shape":[256],"dtype":"float32"},{"name":"dense3/conv2/depthwise_filter","shape":[3,3,256,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006361540626077091,"min":-0.8142772001378676}},{"name":"dense3/conv2/pointwise_filter","shape":[1,1,256,256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.01777329678628959,"min":-1.7062364914838006}},{"name":"dense3/conv2/bias","shape":[256],"dtype":"float32"},{"name":"dense3/conv3/depthwise_filter","shape":[3,3,256,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006900275922289082,"min":-0.8625344902861353}},{"name":"dense3/conv3/pointwise_filter","shape":[1,1,256,256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.015449936717164282,"min":-1.9003422162112067}},{"name":"dense3/conv3/bias","shape":[256],"dtype":"float32"},{"name":"fc/weights","shape":[256,7],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.004834276554631252,"min":-0.7203072066400565}},{"name":"fc/bias","shape":[7],"dtype":"float32"}],"paths":["face_expression_model-shard1"]}] --------------------------------------------------------------------------------