├── .eslintrc.json ├── .gitignore ├── Credits.html ├── Readme.md ├── add_reg_keys.reg ├── app ├── aboutDialog.html ├── download_manager.js ├── import_webchimera.js ├── index.js ├── mainWindow.html ├── package.json ├── plugin_loader.html └── post_install_win.js ├── auto_updater.json ├── build ├── background.png ├── icon.icns ├── icon.ico ├── info.plist └── instal-splash.gif ├── gulpfile.js ├── icon.png ├── package.json ├── remove_reg_keys.reg └── src ├── components ├── Bottom.vue ├── Buffering.vue ├── ButtonWait.vue ├── CircleGraph.vue ├── CurrentProject.vue ├── ListItem.vue ├── LoginModal.vue ├── Modal.vue ├── NewTitleModal.vue ├── PathChooser.vue ├── PluginLoader.vue ├── Popover.vue ├── PreferencesModal.vue ├── ProjectDeletePopover.vue ├── ProjectIcon.vue ├── ProjectItem.vue ├── ProjectList.vue ├── ProjectPropertiesPopover.vue ├── ShiftModal.vue ├── Spinner.vue ├── SubFilter.vue ├── SubList.vue ├── TabItem.vue ├── TabPanel.vue ├── Tabs.vue ├── TimeInput.vue ├── TransInput.vue ├── UpdatePopup.vue ├── UrlPopover.vue ├── VProgress.vue ├── VideoControls.vue ├── VideoView.vue ├── VolumeControl.vue ├── WindowButtons.vue ├── aButton.vue ├── iButton.vue └── range.vue ├── directives ├── Dropdown.js ├── Dropzone.js ├── OpenFile.js ├── Popup.js └── Split.js ├── filters ├── escape.js └── time.js ├── fonts └── photon-entypo.woff ├── img ├── close-white.png ├── close.png ├── file-play.svg ├── file-text2.svg ├── maximize.png ├── minimize.png ├── ring-alt.svg ├── throbber.svg └── unmaximize.png ├── index.js ├── mainWindow.vue ├── mixins └── NonMovable.js ├── services └── native-resource.js ├── store ├── actions │ ├── common.js │ ├── index.js │ ├── ost.js │ ├── player.js │ ├── subfiles.js │ └── ui.js ├── common_mutations.js ├── index.js ├── middlewares.js ├── modules │ ├── ost.js │ ├── player.js │ ├── subfiles.js │ └── ui.js └── mutation-types.js ├── styles ├── buttons.scss ├── containers.scss ├── dialog.scss ├── footer.scss ├── header.scss ├── index.scss ├── list.scss ├── photon │ ├── bars.scss │ ├── base.scss │ ├── button-groups.scss │ ├── buttons.scss │ ├── docs.scss │ ├── forms.scss │ ├── grid.scss │ ├── icons.scss │ ├── images.scss │ ├── lists.scss │ ├── mixins.scss │ ├── navs.scss │ ├── normalize.scss │ ├── photon.scss │ ├── tables.scss │ ├── tabs.scss │ ├── utilities.scss │ └── variables.scss ├── popover.scss ├── project-list.scss ├── tabs.scss ├── trans-input.scss ├── update-popup.scss └── video-view.scss └── utils ├── MainMenu.js ├── SubParserWorker.js ├── debounce.js ├── dialog.js ├── srt.js └── time.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "parserOptions": { 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "indent": [ 14 | 0, 15 | "tab" 16 | ], 17 | "linebreak-style": [ 18 | 1, 19 | "unix" 20 | ], 21 | "quotes": [ 22 | 1, 23 | "single" 24 | ], 25 | "semi": [ 26 | 1, 27 | "never" 28 | ], 29 | "no-console": 0, 30 | "comma-dangle": 0 31 | }, 32 | "plugins": [ 33 | "html" 34 | ] 35 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | installers/ 2 | .DS_Store 3 | node_modules/ 4 | app/dist/ 5 | app/node_modules/ 6 | img/ 7 | npm-debug.log 8 | -------------------------------------------------------------------------------- /Credits.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

7 | Author:
8 | 9 | troorl <troorl@gmail.com>
10 |
11 | 12 | Subordination is based on:
13 | Electron <http://electron.atom.io>
14 | Vue.js <http://vuejs.org>
15 | Webchimera.js <https://github.com/RSATom/WebChimera.js>
16 | Photon <https://github.com/connors/photon> 17 |

18 | 19 | 20 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | ## Subordination 2 | 3 | Subordination is a desktop application for translating and editing subtitles. Currently only SRT format is supported. 4 | 5 | ### Building from source 6 | 7 | Subordination is an [Electron](http://electron.atom.io/) app. It's written in JavaScript with extensive use of [Vue.js](http://vuejs.org/), [Vuex](https://github.com/vuejs/vuex) and highly customised version of [Photon](http://photonkit.com/). Note that you need to have [npm](https://www.npmjs.com/) and [git](https://git-scm.com/) installed on you machine. First get the source code: 8 | 9 | ```bash 10 | git clone https://github.com/sunabozu/subordination.git 11 | cd subordination 12 | ``` 13 | 14 | Now install the dependencies for development and runtime. Note that the `webchimera.js` package may fail to install. It's a native module and npm will try to compile it from source, but it's not necessary, because Subordination loads its binary version separately. Just ignore all errors related to it. 15 | 16 | Also note that Subordination uses a project structure with two `package.json` files. [See more for details](https://github.com/electron-userland/electron-builder). 17 | 18 | ```bash 19 | cd app 20 | npm run prepare 21 | cd .. 22 | npm install 23 | ``` 24 | 25 | Now you can build and launch a debug version: 26 | 27 | ```bash 28 | npm run build-dev 29 | npm start 30 | ``` 31 | 32 | Or you can try to build a full-fledged binary. All the executables are stored inside the `installers` folder. 33 | 34 | ```bash 35 | npm run build-release 36 | npm run dist:osx 37 | npm run dist:win 38 | ``` 39 | 40 | ### A Linux version 41 | 42 | Currently Subordination is available only on Mac and Windows. The author doesn't use Linux on desktop and can't create anything decent for it. But there is no fundamental problem with it. All the components used in Subordinations can be run on Linux as well. If you want to contribute, please let me know, I'd gladly accept your pull requests. 43 | -------------------------------------------------------------------------------- /add_reg_keys.reg: -------------------------------------------------------------------------------- 1 | Windows Registry Editor Version 5.00 2 | [HKEY_CURRENT_USER\SOFTWARE\Classes\.srt] 3 | @="srt_auto_file" 4 | 5 | [HKEY_CURRENT_USER\SOFTWARE\Classes\srt_auto_file] 6 | 7 | [HKEY_CURRENT_USER\SOFTWARE\Classes\srt_auto_file\shell] 8 | 9 | [HKEY_CURRENT_USER\SOFTWARE\Classes\srt_auto_file\shell\open] 10 | 11 | [HKEY_CURRENT_USER\SOFTWARE\Classes\srt_auto_file\shell\open\command] 12 | @="\"@exe_path\" \"--processStart\" \"Subordination.exe\" \"-a=%1\"" 13 | -------------------------------------------------------------------------------- /app/aboutDialog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 23 | 24 | 25 | 26 |
27 |
28 |
Copyright © 2016 troorl
29 | 30 | 31 | 32 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /app/download_manager.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function (fileUrl, apiPath, callback) { 4 | const fs = require('fs') 5 | const request = require('request') 6 | 7 | let file = fs.createWriteStream(apiPath) 8 | let current_progress = 0 9 | 10 | const req = request({ 11 | method: 'GET', 12 | uri: fileUrl, 13 | timeout: 20000, 14 | followAllRedirects: false 15 | }) 16 | 17 | req.pipe(file) 18 | 19 | req.on('response', (data) => { 20 | callback({type: 'start', size: parseInt(data.headers['content-length'])}) 21 | }) 22 | 23 | req.on('data', (chunk) => { 24 | current_progress += chunk.length 25 | callback({type: 'progress', current: current_progress}) 26 | }) 27 | 28 | req.on('end', () => { 29 | callback({type: 'done'}) 30 | }) 31 | 32 | req.on('error', (err) => { 33 | callback({type: 'error', description: err}) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /app/import_webchimera.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function() { 4 | try { 5 | if(process.platform == 'darwin' || process.platform == 'win32') { 6 | const remote = require('electron').remote 7 | const path = require('path') 8 | const fs = require('fs') 9 | 10 | // check if the version is correct 11 | const data = fs.readFileSync(path.join(remote.app.getPath('appData'), remote.app.getName(), 'wc_version.txt'), 'utf8') 12 | console.log(data, remote.app.WC_VERSION) 13 | if(data != remote.app.WC_VERSION) { 14 | const err = `The version of Webchimera is wrong. Expected ${remote.app.WC_VERSION}, but got ${data}` 15 | // const notifier = require('node-notifier') 16 | // notifier.notify({title: 'Error', message: err}) 17 | throw new Error(err) 18 | } else { // success 19 | console.log('success') 20 | return require(path.join(remote.app.getPath('appData'), remote.app.getName(), 'webchimera.js')) 21 | } 22 | } else { // on linux we rely on the system version of libvlc 23 | return require('webchimera.js') 24 | } 25 | } catch(e) { 26 | console.error(e) 27 | return null 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/mainWindow.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Electron app 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "subordination", 3 | "productName": "Subordination", 4 | "version": "0.1.3", 5 | "author": "troorl ", 6 | "description": "A desktop application for translating and editing subtitles", 7 | "license": "MIT", 8 | "homepage": "http://subordination.cu.cc", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/sunabozu/subordination.git" 12 | }, 13 | "main": "index.js", 14 | "scripts": { 15 | "prepare": "npm i --no-scripts --force" 16 | }, 17 | "dependencies": { 18 | "electron-gh-releases": "^2.0.2", 19 | "electron-window-state": "^3.0.3", 20 | "iso-639-2": "^0.2.0", 21 | "opensubtitles-api": "^2.3.0" 22 | }, 23 | "optionalDependencies": { 24 | "unzip": "^0.1.11", 25 | "wcjs-renderer": "^0.1.10" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/plugin_loader.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | 13 |
14 |
15 | 16 |
17 | 21 | 22 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /app/post_install_win.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const {spawn} = require('child_process') 6 | 7 | 8 | const getUpdateDotExe = () => path.resolve(path.dirname(process.execPath), '..', 'update.exe') 9 | 10 | module.exports = { 11 | afterInstall: (app) => { 12 | const path_to_reg_file = path.join(path.dirname(process.execPath), 'add_reg_keys.reg') 13 | const updateDotExe = getUpdateDotExe() 14 | 15 | fs.readFile(path_to_reg_file, 'utf-8', (err, reg_file_content) => { 16 | reg_file_content = reg_file_content.replace('@exe_path', updateDotExe.replace(/([\\\s"])/g, '\\$1')) 17 | 18 | fs.writeFile(path_to_reg_file, reg_file_content, 'utf-8', () => { 19 | 20 | spawn('reg', ['import', path_to_reg_file], {detached: true}) 21 | .on('close', () => { 22 | // create shortcuts 23 | spawn(updateDotExe, ['--createShortcut', path.basename(process.execPath)], {detached: true}) 24 | .on('close', () => app.quit()) 25 | }) 26 | }) 27 | }) 28 | }, 29 | 30 | beforeUninstall: (app) => { 31 | const path_to_reg_file = path.join(path.dirname(app.getPath('exe')), 'remove_reg_keys.reg') 32 | spawn('reg', ['import', path_to_reg_file], {detached: true}) 33 | .on('close', () => { 34 | // delete shortcuts 35 | const updateDotExe = getUpdateDotExe(app) 36 | spawn(updateDotExe, ['--removeShortcut', path.basename(process.execPath)], {detached: true}) 37 | .on('close', () => app.quit()) 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /auto_updater.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://github.com/sunabozu/subordination/releases/download/v0.1.3/subordination-0.1.3-mac.zip" 3 | } 4 | -------------------------------------------------------------------------------- /build/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunabozu/subordination/0381e0dc573ba6ae4099232f0a409290e9735596/build/background.png -------------------------------------------------------------------------------- /build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunabozu/subordination/0381e0dc573ba6ae4099232f0a409290e9735596/build/icon.icns -------------------------------------------------------------------------------- /build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunabozu/subordination/0381e0dc573ba6ae4099232f0a409290e9735596/build/icon.ico -------------------------------------------------------------------------------- /build/info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleTypeRole 6 | Editor 7 | CFBundleDevelopmentRegion 8 | English 9 | CFBundleDocumentTypes 10 | 11 | 12 | CFBundleTypeExtensions 13 | 14 | srt 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /build/instal-splash.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunabozu/subordination/0381e0dc573ba6ae4099232f0a409290e9735596/build/instal-splash.gif -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const gulp = require('gulp') 4 | const gutil = require('gulp-util') 5 | // const babel = require('gulp-babel') 6 | const cached = require('gulp-cached') 7 | const remember = require('gulp-remember') 8 | const sass = require('gulp-sass') 9 | 10 | const webpack = require('webpack') 11 | const webpackTargetElectronRenderer = require('webpack-target-electron-renderer') 12 | 13 | // var vue = require('./gulp-simple-vue-templates') 14 | 15 | const source_js = ['src/**/*.{js,vue}', '!src/background/**/*.js'] 16 | // const source_bg_js = ['src/background/**/*.js'] 17 | const source_scss = ['src/styles/**/*scss'] 18 | const source_fonts = ['src/fonts/**/*'] 19 | const source_images = ['src/img/**/*'] 20 | const dest = 'app/dist' 21 | 22 | // const babel_ptions = { 23 | // plugins: ['syntax-async-generators'], 24 | // presets: ['stage-0', 'es2015-node5'] 25 | // } 26 | 27 | var wp_config = { 28 | entry: './src/index.js', 29 | output: { 30 | path: './app/dist', 31 | filename: 'bundle.js' 32 | }, 33 | 34 | module: { 35 | loaders: [ 36 | { 37 | test: /\.vue$/, 38 | loader: 'vue' 39 | }, 40 | { 41 | test: /\.js$/, 42 | loader: 'babel', 43 | exclude: /node_modules/ 44 | }, 45 | { 46 | test: /\.json$/, 47 | loader: 'json' 48 | } 49 | ] 50 | }, 51 | 52 | externals: [ 53 | { 54 | 'fs': 'require("fs")', 55 | 'path': 'require("path")', 56 | 'wcjs-renderer': 'require("wcjs-renderer")', 57 | './import_webchimera.js': 'require("./import_webchimera.js")', 58 | './post_install_win.js': 'require("./post_install_win.js")', 59 | 'node-notifier': 'require("node-notifier")', 60 | 'opensubtitles-api': 'require("opensubtitles-api")', 61 | 'iso-639-2': 'require("iso-639-2")', 62 | }, 63 | ], 64 | 65 | babel: { 66 | presets: ['es2015', 'stage-0'] 67 | }, 68 | 69 | devtool: 'eval-cheap-module-source-map', 70 | 71 | cache: true, 72 | } 73 | 74 | wp_config.target = webpackTargetElectronRenderer(wp_config) 75 | 76 | const sass_config = {} 77 | 78 | gulp.task('webpack_debug', function(callback) { 79 | webpack(wp_config).run(function(err, stats) { 80 | gutil.log('[webpack:build-dev]', stats.toString({ 81 | colors: true, 82 | chunks: false, 83 | })) 84 | callback() 85 | }) 86 | }) 87 | 88 | gulp.task('styles_debug', function() { 89 | console.log('running scss') 90 | return gulp.src('src' + '/styles/index.scss') 91 | .pipe(sass(sass_config).on('error', sass.logError)) 92 | .pipe(gulp.dest(dest + '/css')) 93 | }) 94 | 95 | gulp.task('fonts_debug', function() { 96 | return gulp.src(source_fonts) 97 | .pipe(cached('fonts')) 98 | .pipe(remember('fonts')) 99 | .pipe(gulp.dest(dest + '/fonts')) 100 | }) 101 | 102 | gulp.task('images_debug', function() { 103 | return gulp.src(source_images) 104 | .pipe(cached('images')) 105 | .pipe(remember('images')) 106 | .pipe(gulp.dest(dest + '/img')) 107 | }) 108 | 109 | gulp.task('watch', function(){ 110 | // gulp.watch(source_js, ['scripts_debug']) 111 | gulp.watch(source_js, ['webpack_debug']) 112 | // gulp.watch(source_bg_js, ['bg_scripts_debug']) 113 | gulp.watch(source_scss, ['styles_debug']) 114 | gulp.watch(source_fonts, ['fonts_debug']) 115 | gulp.watch(source_images, ['images_debug']) 116 | }) 117 | 118 | gulp.task('build-debug', ['webpack_debug', 'styles_debug', 'fonts_debug', 'images_debug']) 119 | gulp.task('watch-debug', ['watch', 'build-debug']) 120 | 121 | 122 | // RELEASE OPTIONS 123 | gulp.task('webpack-release-opts', function() { 124 | wp_config.devtool = undefined 125 | wp_config.plugins = [ 126 | new webpack.optimize.UglifyJsPlugin({ 127 | compress: { 128 | warnings: false, 129 | drop_console: true 130 | } 131 | }), 132 | 133 | new webpack.optimize.OccurrenceOrderPlugin() 134 | ] 135 | }) 136 | 137 | gulp.task('styles-release-opts', function() { 138 | sass_config.style = 'compressed' 139 | }) 140 | 141 | gulp.task('webpack', ['webpack-release-opts', 'webpack_debug']) 142 | gulp.task('styles', ['styles-release-opts', 'styles_debug']) 143 | 144 | gulp.task('build', ['webpack', 'styles', 'fonts_debug', 'images_debug']) 145 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunabozu/subordination/0381e0dc573ba6ae4099232f0a409290e9735596/icon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "prepare:osx": "npm i --ignore-scripts && node node_modules/node-sass/scripts/install.js && node node_modules/electron-prebuilt/install.js", 4 | "prepare:win": "npm i --ignore-scripts && node node_modules/node-sass/scripts/install.js && cd node_modules/electron-prebuilt && npm i --arch=ia32", 5 | "build-dev": "gulp build-debug", 6 | "watch-dev": "gulp watch-debug", 7 | "build-release": "gulp build", 8 | "package:osx": "electron-packager . --platform=darwin --arch=x64 --app-bundle-id='org.subordination' --osx-sign.identity='troorl' --prune --asar --overwrite", 9 | "postinstall": "install-app-deps", 10 | "clean": "rm -rf ./installers", 11 | "dist": "npm run build-release && npm run clean && npm run dist:osx && npm run dist:win", 12 | "dist:osx": "build --platform=darwin --arch=x64", 13 | "dist:win": "build --platform=win32 --arch=x64", 14 | "start": "APP_DEBUG=1 electron ./app", 15 | "start:win": "cmd /C \"set APP_DEBUG=1 && electron ./app", 16 | "publish": "npm run clean && npm run publish:osx && npm run publish:win", 17 | "publish:osx": "build --platform=darwin --arch=x64 --publish onTagOrDraft", 18 | "publish:win": "build --platform=win32 --publish onTagOrDraft" 19 | }, 20 | "build": { 21 | "productName": "Subordination", 22 | "app-bundle-id": "org.subordination", 23 | "app-category-type": "public.app-category.education", 24 | "app-copyright": "Copyright © 2016 troorl", 25 | "ignore": "app/node_modules/webchimera.js", 26 | "extend-info": "build/info.plist", 27 | "extraResources": [ 28 | "Credits.html", 29 | "icon.png", 30 | "add_reg_keys.reg", 31 | "remove_reg_keys.reg" 32 | ], 33 | "osx": { 34 | "identity": "troorl" 35 | }, 36 | "win": { 37 | "iconUrl": "https://raw.githubusercontent.com/sunabozu/subordination/master/build/icon.ico", 38 | "loadingGif": "build/instal-splash.gif", 39 | "remoteReleases": "https://github.com/sunabozu/subordination" 40 | } 41 | }, 42 | "directories": { 43 | "output": "./installers" 44 | }, 45 | "devDependencies": { 46 | "babel-core": "^6.8.0", 47 | "babel-loader": "^6.2.4", 48 | "babel-preset-es2015": "^6.6.0", 49 | "babel-preset-es2015-node5": "^1.1.0", 50 | "babel-preset-stage-0": "^6.5.0", 51 | "babel-runtime": "^6.6.1", 52 | "css-loader": "^0.23.1", 53 | "electron-builder": "^3.20.0", 54 | "electron-prebuilt": "^0.37.8", 55 | "electron-rebuild": "^1.1.3", 56 | "gulp": "^3.9.1", 57 | "gulp-babel": "^6.1.0", 58 | "gulp-cached": "^1.1.0", 59 | "gulp-remember": "^0.3.0", 60 | "gulp-replace": "^0.5.4", 61 | "gulp-sass": "^2.1.0", 62 | "gulp-sourcemaps": "^1.6.0", 63 | "html-minifier": "^2.1.2", 64 | "install": "^0.4.1", 65 | "json-loader": "^0.5.4", 66 | "node-loader": "^0.5.0", 67 | "style-loader": "^0.13.1", 68 | "template-html-loader": "0.0.3", 69 | "vue-html-loader": "^1.2.2", 70 | "vue-loader": "^8.3.1", 71 | "vue-style-loader": "^1.0.0", 72 | "webpack": "^1.13.0", 73 | "webpack-target-electron-renderer": "^0.3.0" 74 | }, 75 | "dependencies": { 76 | "vue": "^1.0.24", 77 | "vuex": "^0.6.3" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /remove_reg_keys.reg: -------------------------------------------------------------------------------- 1 | Windows Registry Editor Version 5.00 2 | [-HKEY_CURRENT_USER\SOFTWARE\Classes\.srt] 3 | 4 | [-HKEY_CURRENT_USER\SOFTWARE\Classes\srt_auto_file] 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/Bottom.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 40 | -------------------------------------------------------------------------------- /src/components/Buffering.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 41 | -------------------------------------------------------------------------------- /src/components/ButtonWait.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 37 | -------------------------------------------------------------------------------- /src/components/CircleGraph.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 106 | -------------------------------------------------------------------------------- /src/components/CurrentProject.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 89 | -------------------------------------------------------------------------------- /src/components/ListItem.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 67 | -------------------------------------------------------------------------------- /src/components/LoginModal.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 93 | -------------------------------------------------------------------------------- /src/components/Modal.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 87 | -------------------------------------------------------------------------------- /src/components/NewTitleModal.vue: -------------------------------------------------------------------------------- 1 | 176 | 177 | 225 | -------------------------------------------------------------------------------- /src/components/PathChooser.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 64 | -------------------------------------------------------------------------------- /src/components/PluginLoader.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 54 | -------------------------------------------------------------------------------- /src/components/Popover.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 80 | -------------------------------------------------------------------------------- /src/components/PreferencesModal.vue: -------------------------------------------------------------------------------- 1 | 139 | 140 | 218 | -------------------------------------------------------------------------------- /src/components/ProjectDeletePopover.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 84 | -------------------------------------------------------------------------------- /src/components/ProjectIcon.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 60 | -------------------------------------------------------------------------------- /src/components/ProjectItem.vue: -------------------------------------------------------------------------------- 1 | 165 | 166 | 193 | -------------------------------------------------------------------------------- /src/components/ProjectList.vue: -------------------------------------------------------------------------------- 1 | 74 | 75 | 95 | -------------------------------------------------------------------------------- /src/components/ProjectPropertiesPopover.vue: -------------------------------------------------------------------------------- 1 | 141 | 142 | 215 | -------------------------------------------------------------------------------- /src/components/ShiftModal.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 116 | -------------------------------------------------------------------------------- /src/components/Spinner.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 42 | -------------------------------------------------------------------------------- /src/components/SubFilter.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 68 | -------------------------------------------------------------------------------- /src/components/SubList.vue: -------------------------------------------------------------------------------- 1 | 219 | 220 | 249 | -------------------------------------------------------------------------------- /src/components/TabItem.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 49 | -------------------------------------------------------------------------------- /src/components/TabPanel.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 65 | -------------------------------------------------------------------------------- /src/components/Tabs.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 53 | -------------------------------------------------------------------------------- /src/components/TimeInput.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 56 | -------------------------------------------------------------------------------- /src/components/TransInput.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 47 | -------------------------------------------------------------------------------- /src/components/UpdatePopup.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 51 | -------------------------------------------------------------------------------- /src/components/UrlPopover.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 90 | -------------------------------------------------------------------------------- /src/components/VProgress.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 34 | -------------------------------------------------------------------------------- /src/components/VideoControls.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | 107 | -------------------------------------------------------------------------------- /src/components/VideoView.vue: -------------------------------------------------------------------------------- 1 | 118 | 119 | 128 | -------------------------------------------------------------------------------- /src/components/VolumeControl.vue: -------------------------------------------------------------------------------- 1 | 99 | 100 | 109 | -------------------------------------------------------------------------------- /src/components/WindowButtons.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 97 | -------------------------------------------------------------------------------- /src/components/aButton.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 15 | -------------------------------------------------------------------------------- /src/components/iButton.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 42 | -------------------------------------------------------------------------------- /src/components/range.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 58 | -------------------------------------------------------------------------------- /src/directives/Dropdown.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import Vue from 'vue' 4 | import {remote} from 'electron' 5 | 6 | 7 | export default { 8 | bind: function() { 9 | // console.log(this.el); 10 | this.el.addEventListener('click', (event) => { 11 | event.preventDefault() 12 | 13 | const elRect = this.el.getBoundingClientRect() 14 | const margin_top = process.platform == 'darwin' ? 4 : 0 15 | 16 | this.menu.popup(remote.getCurrentWindow(), 17 | Math.round(elRect.left), 18 | Math.round(elRect.top) + elRect.height + margin_top //not sure why I must add 4 19 | ) 20 | }) 21 | }, 22 | 23 | update: function(menu) { 24 | this.menu = menu 25 | // console.log(menu); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/directives/Dropzone.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | export default { 4 | bind() { 5 | this.el.classList.add('dropzone') 6 | 7 | this.initiate = e => { 8 | if(this.active) 9 | this.el.classList.add('dropzone-active') 10 | } 11 | 12 | this.end = e => { 13 | this.el.classList.remove('dropzone-active') 14 | } 15 | 16 | // console.log(this.el); 17 | this.el.addEventListener('drop', e => { 18 | if(!this.active) 19 | return false 20 | 21 | e.dataTransfer.items[0].webkitGetAsEntry().file(file => { 22 | this.callback([file.path]) 23 | console.log(file.path) 24 | }) 25 | 26 | this.end() 27 | return false 28 | }, false) 29 | 30 | this.el.addEventListener('dragenter', this.initiate) 31 | this.el.addEventListener('dragover', this.initiate) 32 | 33 | this.el.addEventListener('dragleave', this.end) 34 | 35 | this.el.addEventListener('dragend', this.end) 36 | 37 | this.el.addEventListener('dragexit', this.end) 38 | }, 39 | 40 | update(params) { 41 | this.callback = params.callback 42 | this.active = params.active 43 | console.log(params) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/directives/OpenFile.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import Vue from 'vue' 4 | 5 | export default { 6 | params: ['clientSize'], 7 | 8 | bind: function() { 9 | this.el.style.overflow = 'hidden' 10 | 11 | let clientWidth = this.el.clientWidth 12 | let clientHeight = this.el.clientHeight 13 | 14 | if(this.params.clientSize) { 15 | clientWidth = this.params.clientSize[0] 16 | clientHeight = this.params.clientSize[1] 17 | } 18 | 19 | this.el.innerHTML += `` 31 | 32 | const file_input = this.el.childNodes[this.el.childNodes.length - 1] 33 | 34 | file_input.addEventListener('change', (event) => { 35 | console.log(this.el, this.el.clientWidth); 36 | if(typeof this.callback === 'function') 37 | this.callback(event.target.files); 38 | }); 39 | }, 40 | 41 | update: function(callback) { 42 | this.callback = callback 43 | console.log(this.el, this.el.clientWidth); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/directives/Popup.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import Vue from 'vue' 4 | import {remote} from 'electron' 5 | 6 | const Menu = remote.require('menu'); 7 | const MenuItem = remote.require('menu-item'); 8 | 9 | export default { 10 | bind() { 11 | this.onRightClick = (e) => { 12 | setTimeout(() => { 13 | this.menu.popup(remote.getCurrentWindow()) 14 | }, 50) 15 | 16 | // e.stopPropagation() 17 | // e.preventDefault() 18 | } 19 | 20 | this.el.addEventListener('contextmenu', this.onRightClick) 21 | }, 22 | 23 | update(value) { 24 | if(!value) 25 | return 26 | 27 | this.menu = new Menu() 28 | for(let item of value) { 29 | this.menu.append(new MenuItem(item)) 30 | } 31 | }, 32 | 33 | unbind() { 34 | this.el.removeEventListener('contextmenu', this.onRightClick) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/directives/Split.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | export default { 4 | params: ['splitCallback'], 5 | 6 | bind() { 7 | this.splitters = [] 8 | console.log('main container', this.el.children) 9 | 10 | // this.last = this.el.children[this.el.children.length - 1] 11 | 12 | const onStart = (e) => { 13 | document.body.style.cursor = this.cursor 14 | 15 | const cRect = this.el.getBoundingClientRect() 16 | const initDim = e.target.prevPane.getBoundingClientRect()[this.dim] 17 | let newDim = 0 18 | 19 | // calculate total size of all pens minus the last one (or the first one in case of vertical position) 20 | let totalDim = 0 21 | this.splitters.map((item) => { 22 | totalDim += item.prevPane.getBoundingClientRect()[this.dim] 23 | }) 24 | 25 | const moveListener = (eMove) => { 26 | let diff = eMove[this.cursorDim] - e[this.cursorDim] 27 | 28 | if(this.opts.pos == 'vert') 29 | diff = 0 - diff 30 | 31 | newDim = initDim + diff 32 | 33 | if(newDim < this.opts.min[e.target.index]) { 34 | newDim = this.opts.min[e.target.index] 35 | } 36 | 37 | // the size of the stretched one 38 | const stretchedDim = cRect[this.dim] - (totalDim + diff) 39 | console.log(stretchedDim) 40 | if(stretchedDim < this.opts.min[this.opts.min.length - 1]) { 41 | console.log('limit') 42 | return 43 | } 44 | 45 | e.target.prevPane.style[this.dim] = newDim + 'px' 46 | 47 | // send signal with a slight delay 48 | setTimeout(() => { 49 | this.vm.$root.$broadcast('splitter-resize') 50 | }, 150) 51 | 52 | } 53 | 54 | const upListener = () => { 55 | // cleanup 56 | window.removeEventListener('mousemove', moveListener) 57 | window.removeEventListener('mouseup', upListener) 58 | 59 | // return default cursor 60 | document.body.style.cursor = 'auto' 61 | 62 | // notify about new size 63 | if(this.params.splitCallback) 64 | this.params.splitCallback(e.target.index, newDim) 65 | } 66 | 67 | window.addEventListener('mousemove', moveListener) 68 | window.addEventListener('mouseup', upListener) 69 | } 70 | 71 | for(let i = 1; i < this.el.children.length; i++) { 72 | const splitter = document.createElement('div') 73 | 74 | splitter.prevPane = this.el.children[i - 1] 75 | splitter.index = i - 1 76 | splitter.addEventListener('mousedown', onStart) 77 | this.splitters.push(splitter) 78 | 79 | this.el.children[i].insertBefore(splitter, null) 80 | } 81 | }, 82 | 83 | update(opts) { 84 | console.log('update', opts.init) 85 | 86 | this.opts = opts 87 | 88 | this.cl = 'spl spl-h' 89 | this.dim = 'width' 90 | this.cursorDim = 'clientX' 91 | this.cursor = 'col-resize' 92 | 93 | if(this.opts.pos == 'vert') { 94 | this.cl = 'spl spl-v' 95 | this.dim = 'height' 96 | this.cursorDim = 'clientY' 97 | this.cursor = 'row-resize' 98 | } 99 | 100 | for(let i = 0; i < this.splitters.length; i++) { 101 | if(this.opts.pos == 'vert') { 102 | this.splitters[i].prevPane.style.width = '100%' 103 | this.splitters[i].prevPane = this.el.children[i + 1] 104 | console.log('SIBLING', this.splitters[i].prevPane) 105 | } 106 | 107 | this.splitters[i].setAttribute('class', this.cl) 108 | this.splitters[i].prevPane.style[this.dim] = opts.init[i] + 'px' 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/filters/escape.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import Vue from 'vue' 4 | 5 | Vue.filter('escape', text => { 6 | if(!text) 7 | return text 8 | 9 | const entityMap = { 10 | '&': '&', 11 | '<': '<', 12 | '>': '>', 13 | '\"': '"', 14 | '\'': ''', 15 | '/': '/' 16 | } 17 | 18 | for(let key in entityMap) { 19 | text = text.replace(new RegExp(key, 'g'), entityMap[key]) 20 | } 21 | 22 | const font_re = /(<font color=(?:"){0,1}(#[\w]{6})(?:"){0,1}.*>.*</font>)/gim 23 | const i_re = /(<i>.*</i>)/gim 24 | const b_re = /(<b>.*</b>)/gim 25 | 26 | text = text.replace(font_re, '$1') 27 | text = text.replace(i_re, '$1') 28 | text = text.replace(b_re, '$1') 29 | text = text.replace(/\n/g, '
') 30 | 31 | return text 32 | }) 33 | -------------------------------------------------------------------------------- /src/filters/time.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import Vue from 'vue' 4 | import {ms2obj} from '../utils/time' 5 | 6 | 7 | Vue.filter('time', time => { 8 | let {hours, minutes, seconds} = ms2obj(time) 9 | 10 | return `${hours}:${minutes}:${seconds}` 11 | }) 12 | -------------------------------------------------------------------------------- /src/fonts/photon-entypo.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunabozu/subordination/0381e0dc573ba6ae4099232f0a409290e9735596/src/fonts/photon-entypo.woff -------------------------------------------------------------------------------- /src/img/close-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunabozu/subordination/0381e0dc573ba6ae4099232f0a409290e9735596/src/img/close-white.png -------------------------------------------------------------------------------- /src/img/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunabozu/subordination/0381e0dc573ba6ae4099232f0a409290e9735596/src/img/close.png -------------------------------------------------------------------------------- /src/img/file-play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/img/file-text2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/img/maximize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunabozu/subordination/0381e0dc573ba6ae4099232f0a409290e9735596/src/img/maximize.png -------------------------------------------------------------------------------- /src/img/minimize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunabozu/subordination/0381e0dc573ba6ae4099232f0a409290e9735596/src/img/minimize.png -------------------------------------------------------------------------------- /src/img/ring-alt.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/img/throbber.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 66 | 67 | 68 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/img/unmaximize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunabozu/subordination/0381e0dc573ba6ae4099232f0a409290e9735596/src/img/unmaximize.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const Vue = require('vue') 2 | const app = require('./mainWindow.vue') 3 | 4 | 5 | new Vue(app) 6 | -------------------------------------------------------------------------------- /src/mixins/NonMovable.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | This solves the https://github.com/electron/electron/issues/3009 issue 5 | Already fixed in https://github.com/electron/electron/pull/5557 6 | TODO: update to the latest Electron version 7 | */ 8 | 9 | const remote = require('electron').remote 10 | 11 | 12 | export default { 13 | methods: { 14 | setMovable() { 15 | this.win.setMovable(true) 16 | }, 17 | 18 | setUnmovable() { 19 | this.win.setMovable(false) 20 | } 21 | }, 22 | 23 | ready() { 24 | if(process.platform != 'darwin') // only needed in OS X 25 | return 26 | 27 | this.win = remote.getCurrentWindow() 28 | 29 | this.$el.addEventListener('mouseover', this.setUnmovable) 30 | this.$el.addEventListener('mouseout', this.setMovable) 31 | }, 32 | 33 | beforeDestroy() { 34 | this.$el.removeEventListener('mouseover', this.setUnmovable) 35 | this.$el.removeEventListener('mouseout', this.setMovable) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/services/native-resource.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | export default class Resource { 4 | baseUrl = null 5 | 6 | constructor(url) { 7 | this.baseUrl = url 8 | } 9 | 10 | queryString(params) { 11 | let result = '' 12 | Object.keys(params).forEach((key) => { 13 | result += `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}&` 14 | }) 15 | 16 | return result 17 | } 18 | 19 | get(params={}) { 20 | return new Promise((resolve, reject) => { 21 | if(!this.baseUrl) 22 | return reject('There is no URL') 23 | 24 | let body = this.queryString(params) 25 | 26 | const url = this.baseUrl + '?' + body 27 | body = null 28 | 29 | fetch(url, { 30 | body: body, 31 | }) 32 | .then( 33 | resp => resp.json() 34 | ) 35 | .then( 36 | data => resolve(data) 37 | ) 38 | .catch( 39 | err => reject(err) 40 | ) 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/store/actions/common.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import actions from '../actions' 4 | 5 | 6 | export default { 7 | loadState({actions, state, dispatch}, newState) { 8 | dispatch('LOAD_STATE', newState) 9 | 10 | // if(state.subfiles.current && state.subfiles.projects[state.subfiles.current] && state.subfiles.projects[state.subfiles.current].video_path) { 11 | // actions.setVideoFileForPlayer( 12 | // state.subfiles.projects[state.subfiles.current].video_path, 13 | // state.subfiles.projects[state.subfiles.current].video_position 14 | // ) 15 | // } 16 | 17 | console.log(state.subfiles.current) 18 | }, 19 | 20 | loadSubfile({dispatch, state}, items) { 21 | console.log('loading subfile') 22 | // dispatch('ADD_SUBTITLES', items) 23 | let translated = 0 24 | 25 | 26 | dispatch('ADD_SUBTITLES', items) 27 | 28 | for(let item of items) { 29 | // dispatch('ADD_SUBTITLE', item) 30 | 31 | if(item.text_trans !== '') 32 | translated++ 33 | } 34 | 35 | dispatch('SET_TRANSLATED_SUBTITLES_COUNT', translated) 36 | // dispatch('LOAD_SUBFILE', newSubfile) 37 | 38 | if(state.subfiles.projects[state.subfiles.current].video_path) { 39 | actions.player.setVideoFileForPlayer( 40 | {dispatch, state}, 41 | state.subfiles.projects[state.subfiles.current].video_path, 42 | state.subfiles.projects[state.subfiles.current].video_position 43 | ) 44 | } else { 45 | actions.player.unloadVideoFile({dispatch}) 46 | } 47 | }, 48 | 49 | setVideoFile({dispatch, state}, file_path) { 50 | console.log('subfile', state.subfiles.projects[state.subfiles.current]) 51 | actions.subfiles.setVideoFileForSubfile({dispatch}, file_path) 52 | actions.player.setVideoFileForPlayer( 53 | {dispatch, state}, 54 | file_path, 55 | state.subfiles.projects[state.subfiles.current].video_position 56 | ) 57 | }, 58 | 59 | closeProject({dispatch, state}) { 60 | if(!state.subfiles.current) 61 | return 62 | 63 | dispatch('CLEAR_SUBTITLES') 64 | dispatch('SET_CURRENT_PROJECT', null) 65 | 66 | actions.player.unloadVideoFile({dispatch}) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/store/actions/index.js: -------------------------------------------------------------------------------- 1 | import common from './common' 2 | import ost from './ost' 3 | import player from './player' 4 | import subfiles from './subfiles' 5 | import ui from './ui' 6 | 7 | export default { 8 | common, 9 | ost, 10 | player, 11 | subfiles, 12 | ui, 13 | } 14 | -------------------------------------------------------------------------------- /src/store/actions/ost.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import {remote} from 'electron' 4 | import Vue from 'vue' 5 | 6 | 7 | let OS = null // load dynamically 8 | // let instance = null 9 | let crypto = null // load dynamically 10 | 11 | function getInstance({username, password}, ssl) { 12 | if(!OS) 13 | OS = require('opensubtitles-api') 14 | 15 | return new OS({ 16 | // useragent: 'FileBot v4.5.6', 17 | useragent: 'Subordination v' + remote.app.getVersion(), 18 | username: username, 19 | password: password, 20 | ssl, 21 | }) 22 | } 23 | 24 | export default { 25 | verifyOstCredentials({state, dispatch}, credentials) { 26 | if(!crypto) 27 | crypto = require('crypto') 28 | 29 | credentials.password = crypto.createHash('md5').update(credentials.password).digest('hex') 30 | 31 | const promise = getInstance(credentials, state.ost.ssl).login() 32 | promise 33 | .then(resp => { 34 | console.log(resp) 35 | dispatch('SET_OST_CREDENTIALS', credentials) 36 | dispatch('SET_OST_VERIFIED', true) 37 | }) 38 | .catch(err => { 39 | console.log('error', err) 40 | // dispatch('SET_OST_VERIFIED', false) 41 | }) 42 | 43 | return promise 44 | }, 45 | 46 | uploadToOst({state, dispatch}, {sub_path, video_path, lang, imdbid}) { 47 | console.log(video_path, sub_path) 48 | if(!state.ost.verified) 49 | return 50 | 51 | return getInstance({username: state.ost.username, password: state.ost.password}) 52 | .upload({ 53 | path: video_path, 54 | subpath: sub_path, 55 | sublanguageid: lang, 56 | imdbid, 57 | }) 58 | 59 | // promise 60 | // .then(resp => { 61 | // if(resp.alreadyindb) 62 | // console.log(`IDSubtitle: ${resp.data.IDSubtitle}`) 63 | // console.log(`result: ${resp.status}, data: ${resp.data}`) 64 | // }) 65 | // .catch(err => { 66 | // console.log(`error: ${err}`) 67 | // }) 68 | 69 | // return promise 70 | }, 71 | 72 | setOstSll({dispatch}, enabled) { 73 | dispatch('SET_OST_SSL', enabled) 74 | }, 75 | 76 | getOstMetadata({state}, video_path) { 77 | if(!state.ost.verified) 78 | return 79 | 80 | return getInstance({username: state.ost.username, password: state.ost.password}) 81 | .identify({path: video_path, extended: true}) 82 | }, 83 | } 84 | -------------------------------------------------------------------------------- /src/store/actions/player.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import actions from '../actions' 4 | 5 | 6 | let wcjs = null 7 | 8 | // keep these two outside of the state 9 | let instance = null 10 | let canvas = null 11 | 12 | export default { 13 | initPlayer({dispatch, state}, canvas) { 14 | if(!canvas) { 15 | console.log('There is no canvas, cannot initialize') 16 | return 1 17 | } 18 | 19 | if(!wcjs) { // trying to load 20 | wcjs = require('wcjs-renderer') 21 | } 22 | 23 | const webchimera = require('./import_webchimera.js')() 24 | console.log(webchimera) 25 | 26 | if(!webchimera) { 27 | dispatch('PLUGINS_EXIST', false) 28 | console.log('There is no webchimera module in place') 29 | return 2 30 | } 31 | 32 | dispatch('PLUGINS_EXIST', true) 33 | 34 | instance = wcjs.init(canvas, null, null, webchimera) //, ['--no-playlist-autostart']) 35 | 36 | instance.onOpening = () => { 37 | dispatch('SET_PLAYER_STATE', 'opening') 38 | console.log('on opening') 39 | } 40 | 41 | instance.onBuffering = (percents) => { 42 | dispatch('SET_PLAYER_STATE', 'buffering') 43 | dispatch('SET_BUFFERING', percents) 44 | } 45 | }, 46 | 47 | mutePlayer({dispatch, state}, mute) { 48 | console.log(`mute ${state.player.muted} ${mute}`) 49 | 50 | dispatch('MUTE_PLAYER', mute) 51 | 52 | if(instance.mute != mute) 53 | instance.toggleMute() 54 | }, 55 | 56 | setVolume({dispatch, state}, volume) { 57 | dispatch('SET_VOLUME', volume) 58 | instance.volume = volume 59 | }, 60 | 61 | setVideoFileForPlayer({dispatch, state}, file_path, position) { 62 | console.log('state:', state) 63 | if(!instance) 64 | return 1 65 | 66 | const prefix = process.platform == 'win32' ? 'file:///' : 'file://' 67 | instance.play(`${prefix}${file_path}`) 68 | 69 | instance.volume = state.player.volume 70 | console.log('volume: ', instance.volume) 71 | actions.player.mutePlayer({dispatch, state}, state.player.muted) 72 | console.log(state.player.muted, instance.mute) 73 | 74 | // here is the point where video is loaded and ready 75 | // get the first frame 76 | instance.onPlaying = () => { // first playing 77 | instance.pause() 78 | 79 | // replace it with a regular onPlaying callback 80 | instance.onPlaying = () => { 81 | dispatch('SET_PLAYER_STATE', 'playing') 82 | console.log('playing') 83 | } 84 | } 85 | 86 | instance.onLengthChanged = (length) => { 87 | console.log('length changed', length) 88 | dispatch('SET_VIDEO_METADATA', 89 | 0, 90 | 0, 91 | length) 92 | } 93 | 94 | instance.onPaused = () => { // first paused 95 | dispatch('SET_PLAYER_STATE', 'paused') 96 | 97 | //restore saved position 98 | console.log('position', position) 99 | if(position) { 100 | actions.player.setVideoPosition({dispatch, state}, position) 101 | } 102 | 103 | instance.onTimeChanged = (time) => { 104 | dispatch('SET_CURRENT_TIME', time) 105 | } 106 | 107 | instance.onPositionChanged = (position) => { 108 | dispatch('SET_CURRENT_VIDEO_POSITION', position) 109 | // console.log('position changed', position) 110 | 111 | if(state.player.stopAfterPlay > 0 && state.player.current_time >= state.player.stopAfterPlay) { // stop right there if needed 112 | instance.pause() 113 | dispatch('STOP_AFTER_PLAY', 0) 114 | console.log('stop right there!') 115 | } 116 | } 117 | 118 | // replace it with a regular onPaused callback 119 | instance.onPaused = () => { 120 | dispatch('SET_PLAYER_STATE', 'paused') 121 | } 122 | 123 | instance.onStopped = () => { 124 | 125 | } 126 | 127 | instance.onEndReached = () => { 128 | dispatch('SET_PLAYER_STATE', 'ended') 129 | instance.pause() 130 | } 131 | } 132 | 133 | // get frame data - width and height 134 | let onFrameReady = instance.onFrameReady 135 | instance.onFrameReady = (frame) => { 136 | console.log('metadata', frame.width, frame.height, instance.length) 137 | dispatch('SET_VIDEO_METADATA', 138 | frame.width, 139 | frame.height, 140 | 0.0) 141 | 142 | instance.onFrameReady = onFrameReady 143 | } 144 | 145 | if(state.subfiles.current) 146 | instance.subtitles.load(state.subfiles.projects[state.subfiles.current].path) 147 | }, 148 | 149 | unloadVideoFile({dispatch}) { 150 | if(!instance) 151 | return 1 152 | 153 | instance.stop() 154 | 155 | dispatch('RESET_VIDEO') 156 | 157 | // reset imdbid 158 | dispatch('CHANGE_PROJECT_PROPERTIES', {imdbid: null}) 159 | }, 160 | 161 | setVideoTime({dispatch, state}, time) { // jump to some particular time position 162 | if(!instance) 163 | return 1 164 | 165 | actions.player.setVideoPosition({dispatch, state}, time / state.player.metadata.length) 166 | console.log(`time: ${time}/${state.player.metadata.length}`) 167 | }, 168 | 169 | setVideoPosition({dispatch, state}, position) { 170 | if(!instance) 171 | return 1 172 | 173 | if(0 >= position >= 1) 174 | return 175 | 176 | if(state.player.state.ended) 177 | instance.play() 178 | 179 | instance.position = position 180 | dispatch('SET_CURRENT_VIDEO_POSITION', position) 181 | dispatch('SET_CURRENT_TIME', instance.time) 182 | console.log('changed position', position) 183 | }, 184 | 185 | // setSubfile: 'SET_SUBFILE', 186 | setSubfile({state}) { 187 | if(state.subfiles.current) 188 | instance.subtitles.load(state.subfiles.projects[state.subfiles.current].path) 189 | }, 190 | 191 | toggleVideo({state}) { 192 | if(!instance || !wcjs) 193 | return 1 194 | 195 | if(state.player.state.ended) 196 | instance.play() 197 | else 198 | instance.togglePause() 199 | }, 200 | 201 | play() { 202 | instance.play() 203 | }, 204 | 205 | pause() { 206 | instance.pause() 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/store/actions/subfiles.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import path from 'path' 4 | import actions from '../actions' 5 | import {ms2string} from '../../utils/time' 6 | import parser from '../../utils/SubParserWorker' 7 | 8 | 9 | export default { 10 | // return promise 11 | openSubfile({dispatch, state}, file_path) { 12 | return new Promise((resolve, reject) => { 13 | let already_exists = false 14 | for(let key in state.subfiles.projects) { 15 | console.log(key) 16 | if(state.subfiles.projects[key].path == file_path) { 17 | already_exists = true 18 | break 19 | } 20 | } 21 | 22 | if(already_exists) { 23 | console.log('project already exists') 24 | reject(`The project already exists`) 25 | return 26 | } 27 | 28 | if(state.subfiles.current) 29 | dispatch('CLEAR_SUBTITLES') 30 | 31 | console.log('adding new project') 32 | dispatch('ADD_PROJECT', path.basename(file_path), { 33 | translated: 0, 34 | export_name: path.basename(file_path), 35 | export_path: null, 36 | export_mode: 0, // 0: replace with original, 1: ignore 37 | path: file_path, 38 | video_path: null, 39 | lang: null, 40 | imdbid: null, 41 | video_position: 0.0, 42 | scroll: 0, 43 | current_index: 0, 44 | items: [], 45 | filter: 'all', 46 | open_date: Date.now(), 47 | }) 48 | 49 | console.log('set current project') 50 | 51 | dispatch('SET_CURRENT_PROJECT', path.basename(file_path)) 52 | 53 | console.log('execute parsing') 54 | 55 | actions.subfiles.parseSubfile({dispatch}, file_path) 56 | .catch(err => { // rollback of an error 57 | console.error(err) 58 | actions.common.closeProject({dispatch, state}) 59 | actions.subfiles.deleteProject({dispatch}, path.basename(file_path)) 60 | 61 | reject(err) 62 | }) 63 | }) 64 | }, 65 | 66 | deleteProject({dispatch}, name) { 67 | setTimeout(() => { // need to be async, otherwise causes an exception 68 | dispatch('DELETE_PROJECT', name) 69 | }, 1) 70 | }, 71 | 72 | setCurrentProject({dispatch}, name) { 73 | dispatch('CLEAR_SUBTITLES') 74 | dispatch('SET_CURRENT_PROJECT', name) 75 | }, 76 | 77 | changeProjectProperties({dispatch}, props) { 78 | dispatch('CHANGE_PROJECT_PROPERTIES', props) 79 | }, 80 | 81 | changeProjectExportName({dispatch}, export_name) { 82 | dispatch('CHANGE_PROJECT_EXPORT_NAME', export_name) 83 | }, 84 | 85 | changeProjectExportPath({dispatch}, export_path) { 86 | dispatch('CHANGE_PROJECT_EXPORT_PATH', export_path) 87 | }, 88 | 89 | // return promise 90 | parseSubfile({dispatch, state}, file_path) { 91 | console.log('parsing') 92 | 93 | return new Promise((resolve, reject) => { 94 | parser(file_path, (error, result) => { 95 | console.log(result); 96 | 97 | if(error) { 98 | console.log(error) 99 | return reject(error) 100 | } 101 | 102 | // for(let item of result) { 103 | // dispatch('ADD_SUBTITLE', item) 104 | // } 105 | dispatch('ADD_SUBTITLES', result) 106 | 107 | }) 108 | }) 109 | }, 110 | 111 | makeItemActive({dispatch, state}, index) { 112 | dispatch('MAKE_SUBTITLE_ACTIVE', index) 113 | }, 114 | 115 | moveItem({dispatch, state}, direction) { 116 | // const currentIndex = state.subfile.items.indexOf(item) 117 | 118 | if(direction === 'next' && state.subfiles.projects[state.subfiles.current].current_index < state.subfiles.projects[state.subfiles.current].items.length - 1) { 119 | dispatch('MAKE_SUBTITLE_ACTIVE', state.subfiles.projects[state.subfiles.current].current_index + 1) 120 | return 1 // success 121 | } 122 | if(direction === 'prev' && state.subfiles.projects[state.subfiles.current].current_index > 0) { 123 | dispatch('MAKE_SUBTITLE_ACTIVE', state.subfiles.projects[state.subfiles.current].current_index - 1) 124 | return 1 // success 125 | } 126 | }, 127 | 128 | updateSubtitle({dispatch, state}, index, value) { 129 | dispatch('UPDATE_SUBTITLE', index, value) 130 | }, 131 | 132 | updateSubtitleFull({dispatch}, index, value) { 133 | dispatch('UPDATE_SUBTITLE_FULL', index, value) 134 | }, 135 | 136 | deleteCurrentSubtitle({dispatch, state}) { 137 | if(!state.subfiles.projects[state.subfiles.current] || state.subfiles.projects[state.subfiles.current].current_index < 0) 138 | return 139 | 140 | // TODO make it choose current index when it's possible 141 | // const index = state.subfiles.projects[state.subfiles.current].current_index 142 | 143 | // delete 144 | dispatch('DELETE_SUBTITLE', state.subfiles.projects[state.subfiles.current].current_index) 145 | 146 | // try to make next title active 147 | // if fails, try to move backward 148 | if(state.subfiles.projects[state.subfiles.current].current_index < state.subfiles.projects[state.subfiles.current].items.length) 149 | dispatch('MAKE_SUBTITLE_ACTIVE', state.subfiles.projects[state.subfiles.current].current_index) 150 | else 151 | actions.subfiles.moveItem('prev') 152 | }, 153 | 154 | reindexSubfile({dispatch, state}, start_from) { 155 | if(!state.subfiles.projects[state.subfiles.current]) 156 | return 157 | 158 | const array = (JSON.parse(JSON.stringify(state.subfiles.projects[state.subfiles.current].items))) 159 | 160 | if(!start_from || start_from >= array.length) 161 | start_from = 0 162 | 163 | for(let i = start_from; i < array.length; i++) { 164 | if(array[i] && array[i].number != i + 1) 165 | // dispatch('RENUMBER_SUBTITLE', i - 1, i) 166 | console.log(array[i].number) 167 | array[i].number = i + 1 168 | } 169 | 170 | dispatch('ADD_SUBTITLES', array) 171 | }, 172 | 173 | // policy: 0 - remove, 1 - shift to the beginning 174 | shiftSubtitles({dispatch, state}, shift, policy) { 175 | if(!state.subfiles.projects[state.subfiles.current]) { 176 | return 177 | } 178 | 179 | const array = (JSON.parse(JSON.stringify(state.subfiles.projects[state.subfiles.current].items))) 180 | let start = 0 181 | let end = 0 182 | 183 | for(let i = 0; i < array.length; i++) { 184 | start = array[i].time_markers.start + shift 185 | end = array[i].time_markers.end + shift 186 | 187 | if(start < 0) { 188 | if(policy === 0) { 189 | array.splice(i, 1) 190 | i-- 191 | continue 192 | } else { 193 | end -= start 194 | start = 0 195 | } 196 | } 197 | 198 | array[i].time_markers.start = start 199 | array[i].time_markers.end = end 200 | 201 | array[i].time = ms2string( 202 | array[i].time_markers.start, 203 | array[i].time_markers.end 204 | ) 205 | } 206 | 207 | dispatch('ADD_SUBTITLES', array) 208 | }, 209 | 210 | setDefaultExportPath({dispatch, state}, path) { 211 | dispatch('SET_DEFAULT_EXPORT_PATH', path) 212 | }, 213 | 214 | setDefaultLanguage({dispatch, state}, lang) { 215 | dispatch('SET_DEFAULT_LANGUAGE', lang) 216 | }, 217 | 218 | copyOriginalSubitem({dispatch, state}, index) { 219 | dispatch('UPDATE_SUBTITLE', index, state.subfiles.projects[state.subfiles.current].items[index].text_orig) 220 | }, 221 | 222 | setSubfileFilter({dispatch, state}, filter) { 223 | dispatch('SET_SUBFILE_FILTER', filter) 224 | }, 225 | 226 | setVideoFileForSubfile({dispatch, state}, file_path) { 227 | dispatch('SET_VIDEO_FILE', file_path) 228 | }, 229 | 230 | clearVideoFile({dispatch}) { 231 | dispatch('CLEAR_VIDEO_FILE') 232 | }, 233 | } 234 | -------------------------------------------------------------------------------- /src/store/actions/ui.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | export default { 4 | setLeftPanelWidth({actions, dispatch, state}, width) { 5 | dispatch('SET_LEFT_PANEL_WIDTH', width) 6 | }, 7 | 8 | setCentralPanelWidth({actions, dispatch, state}, width) { 9 | dispatch('SET_CENTRAL_PANEL_WIDTH', width) 10 | }, 11 | 12 | setTransInputHeight({actions, dispatch, state}, height) { 13 | dispatch('SET_TRANS_INPUT_HEIGHT', height) 14 | }, 15 | 16 | setProjectSortOrder({actions, dispatch, state}, projectSortOrder) { 17 | dispatch('SET_PROJECT_SORT_ORDER', projectSortOrder) 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /src/store/common_mutations.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import * as types from './mutation-types' 3 | 4 | 5 | export default { 6 | [types.LOAD_STATE](state, newState) { 7 | try { 8 | Object.assign(state.ui, newState.ui) 9 | } catch(e) { 10 | console.log(e) 11 | state.success = false 12 | } 13 | 14 | try { 15 | Object.assign(state.player, newState.player) 16 | } catch(e) { 17 | console.error(e) 18 | state.success = false 19 | } 20 | 21 | try { 22 | Object.assign(state.ost, newState.ost) 23 | } catch(e) { 24 | console.error(e) 25 | state.success = false 26 | } 27 | 28 | try { 29 | Object.assign(state.subfiles, newState.subfiles) 30 | // Vue.set(state, 'subfiles', newState.subfiles) 31 | } catch(e) { 32 | console.error(e) 33 | state.success = false 34 | } 35 | 36 | // state.success = true // if we loaded everything without errors 37 | }, 38 | 39 | // [types.LOAD_SUBFILE](state, items) { 40 | // try { 41 | // // Object.assign(state.subfiles.items[state.subfiles.current].items, items) 42 | // // Vue.set(state.subfiles.items[state.subfiles.current].items, items) 43 | // for(let item of items) { 44 | // dispatch('ADD_SUBTITLE', item.current) 45 | // } 46 | // } catch(e) { 47 | // console.error(e) 48 | // } 49 | // } 50 | } 51 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import Vue from 'vue' 4 | import Vuex from 'vuex' 5 | 6 | // import {commonActions} from './actions' 7 | 8 | // import common from './mutations' 9 | import common from './common_mutations' 10 | import ui from './modules/ui' 11 | import subfiles from './modules/subfiles' 12 | // import {subfile, subfileMutations, subfileActions} from './modules/subfile' 13 | import player from './modules/player' 14 | import ost from './modules/ost' 15 | 16 | import middlewares from './middlewares' 17 | 18 | 19 | Vue.use(Vuex) 20 | 21 | export default new Vuex.Store({ 22 | strict: true, 23 | 24 | modules: { 25 | ost, 26 | ui, 27 | player, 28 | subfiles, 29 | }, 30 | 31 | mutations: common, 32 | middlewares: [middlewares], 33 | }) 34 | -------------------------------------------------------------------------------- /src/store/middlewares.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import {remote} from 'electron' 4 | 5 | 6 | const setEdited = function(edited) { 7 | try { 8 | const win = remote.getCurrentWindow() 9 | 10 | if(win) 11 | win.setDocumentEdited(edited) 12 | } catch(e) { 13 | 14 | } 15 | } 16 | 17 | export default { 18 | onInit(state, store) { 19 | 20 | }, 21 | 22 | onMutation(mutation, state, store) { 23 | switch(mutation.type) { 24 | case 'CHANGE_PROJECT_EXPORT_NAME': 25 | case 'CHANGE_PROJECT_EXPORT_PATH': 26 | case 'ADD_SUBTITLE': 27 | case 'UPDATE_SUBTITLE': 28 | case 'UPDATE_SUBTITLE_FULL': 29 | case 'INSERT_SUBTITLE': 30 | case 'DELETE_SUBTITLE': 31 | case 'RENUMBER_SUBTITLE': 32 | case 'SET_SUBTITLE_TIME': 33 | const win = remote.getCurrentWindow() 34 | 35 | if(!win.isDocumentEdited()) { 36 | setEdited(true) 37 | console.log('edited', mutation.type) 38 | } 39 | break 40 | 41 | case 'SET_CURRENT_PROJECT': 42 | case 'CLEAR_SUBTITLES': 43 | setEdited(false) 44 | console.log('not edited', mutation.type) 45 | break 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/store/modules/ost.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import * as types from '../mutation-types' 4 | 5 | 6 | 7 | export default { 8 | state: { 9 | username: '', 10 | password: '', 11 | 12 | ssl: false, 13 | 14 | verified: false, 15 | }, 16 | 17 | // mutations 18 | mutations: { 19 | [types.SET_OST_CREDENTIALS](ost, {username, password}) { 20 | ost.username = username 21 | ost.password = password 22 | }, 23 | 24 | [types.SET_OST_SSL](ost, enabled) { 25 | ost.ssl = enabled 26 | }, 27 | 28 | [types.SET_OST_VERIFIED](ost, value) { 29 | ost.verified = value 30 | }, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/store/modules/player.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | 4 | // import debounce from 'lodash/function/debounce' 5 | import Vue from 'vue' 6 | import * as types from '../mutation-types' 7 | import debounce from '../../utils/debounce' 8 | 9 | 10 | export default { 11 | state: { 12 | plugins_exist: false, 13 | 14 | volume: 50, 15 | muted: false, 16 | 17 | // videofile_path: null, 18 | subfile_path: null, 19 | current_time: 0, 20 | // current_position: 0.0, 21 | buffering: 100, 22 | 23 | // just a temporary marker 24 | stopAfterPlay: 0, 25 | 26 | state: { 27 | stopped: true, 28 | opening: false, 29 | buffering: false, 30 | playing: false, 31 | paused: false, 32 | ended: false, 33 | }, 34 | 35 | metadata: { 36 | width: 0, 37 | height: 0, 38 | length: 0, 39 | } 40 | }, 41 | 42 | // mutations 43 | mutations: { 44 | [types.PLUGINS_EXIST](player, exist) { 45 | console.log('pugins exist', player.plugins_exist) 46 | if(typeof exist == 'boolean') 47 | player.plugins_exist = exist 48 | 49 | console.log('pugins exist', player.plugins_exist) 50 | }, 51 | 52 | [types.MUTE_PLAYER](player, mute) { 53 | player.muted = mute 54 | }, 55 | 56 | [types.SET_VOLUME](player, volume) { 57 | player.volume = volume 58 | }, 59 | 60 | [types.SET_VIDEO_METADATA](player, width, height, length) { 61 | if(width > 0 && height > 0) { 62 | player.metadata.width = width 63 | player.metadata.height = height 64 | } 65 | 66 | if(length > 0) 67 | player.metadata.length = length 68 | }, 69 | 70 | // [types.SET_SUBFILE]({player}, file_path) { 71 | // if(file_path) 72 | // player.subfile_path = file_path 73 | // 74 | // if(instance && player.subfile_path) { 75 | // instance.subtitles.load(player.subfile_path) 76 | // } 77 | // }, 78 | 79 | [types.SET_CURRENT_TIME](player, time) { 80 | player.current_time = time // milliseconds 81 | }, 82 | 83 | // [types.SET_CURRENT_POSITION]({player}, position) { 84 | // player.current_position = position // % 85 | // }, 86 | 87 | [types.SET_BUFFERING](player, percents) { 88 | player.buffering = percents 89 | }, 90 | 91 | [types.SET_PLAYER_STATE](player, newState) { 92 | switch (newState) { 93 | case 'opening': 94 | player.state.opening = true 95 | player.state.stopped = false 96 | player.state.ended = false 97 | break 98 | case 'buffering': 99 | player.state.buffering = true 100 | player.state.opening = false 101 | player.state.ended = false 102 | break 103 | case 'playing': 104 | player.state.playing = true 105 | player.state.paused = false 106 | player.state.ended = false 107 | break 108 | case 'paused': 109 | player.state.paused = true 110 | player.state.playing = false 111 | player.state.ended = false 112 | break 113 | case 'ended': 114 | player.state.ended = true 115 | player.state.playing = false 116 | player.state.paused = true 117 | } 118 | }, 119 | 120 | [types.RESET_VIDEO](player) { 121 | player.state.stopped = true 122 | player.state.playing = false 123 | player.state.paused = false 124 | player.state.buffering = false 125 | player.state.ended = false 126 | 127 | Object.assign(player.metadata, {width: 0, height: 0, length: 0}) 128 | 129 | // player.videofile_path = null 130 | player.current_time = 0 131 | // player.current_position = 0 132 | player.buffering = 100 133 | }, 134 | 135 | [types.STOP_AFTER_PLAY](player, time) { 136 | player.stopAfterPlay = time 137 | } 138 | 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/store/modules/subfiles.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import {remote} from 'electron' 4 | // import path from 'path' 5 | import {ms2string} from '../../utils/time' 6 | import Vue from 'vue' 7 | import * as types from '../mutation-types' 8 | 9 | 10 | export default { 11 | state: { 12 | current: null, 13 | projects: {}, 14 | export_path_default: remote.app.getPath('documents'), 15 | default_lang: 'eng', 16 | }, 17 | 18 | mutations: { 19 | // project 20 | [types.ADD_PROJECT](subfiles, name, subfile) { 21 | Vue.set(subfiles.projects, name, subfile) 22 | // subfiles.projects.push(subfile) 23 | }, 24 | 25 | [types.SET_CURRENT_PROJECT](subfiles, name) { 26 | subfiles.current = name 27 | // subfiles.current = index 28 | if(name && subfiles.projects[subfiles.current]) 29 | subfiles.projects[subfiles.current].open_date = Date.now() 30 | }, 31 | 32 | [types.DELETE_PROJECT](subfiles, name) { 33 | console.log(name, subfiles.projects[name]) 34 | // delete subfiles.projects[name] 35 | Vue.delete(subfiles.projects, name) 36 | }, 37 | 38 | [types.CHANGE_PROJECT_PROPERTIES](subfiles, props) { 39 | if(subfiles.projects[subfiles.current]) 40 | Object.assign(subfiles.projects[subfiles.current], props) 41 | }, 42 | 43 | [types.CHANGE_PROJECT_EXPORT_NAME](subfiles, export_name) { 44 | subfiles.projects[subfiles.current].export_name = export_name 45 | }, 46 | 47 | [types.CHANGE_PROJECT_EXPORT_PATH](subfiles, export_path) { 48 | subfiles.projects[subfiles.current].export_path = export_path 49 | }, 50 | 51 | // subfile 52 | [types.ADD_SUBTITLE](subfiles, subtitle) { 53 | subfiles.projects[subfiles.current].items.push(subtitle) 54 | }, 55 | 56 | [types.SET_TRANSLATED_SUBTITLES_COUNT](subfiles, translated) { 57 | Vue.set(subfiles.projects[subfiles.current], 'translated', translated) 58 | }, 59 | 60 | [types.ADD_SUBTITLES](subfiles, subtitles) { 61 | subfiles.projects[subfiles.current].items = subtitles 62 | // console.log(subfiles.projects[subfiles.current].items); 63 | }, 64 | 65 | [types.MAKE_SUBTITLE_ACTIVE](subfiles, index) { 66 | if(subfiles.projects[subfiles.current].current_index == index || index < 0 || index > subfiles.projects[subfiles.current].items.length - 1) 67 | return 68 | 69 | subfiles.projects[subfiles.current].current_index = index 70 | }, 71 | 72 | [types.UPDATE_SUBTITLE](subfiles, index, value) { 73 | // console.log(index, value); 74 | if(subfiles.projects[subfiles.current].items[index].text_trans === '' && value !== '') 75 | subfiles.projects[subfiles.current].translated++ 76 | else { 77 | if(value === '' && subfiles.projects[subfiles.current].items[index].text_trans !== '' 78 | && subfiles.projects[subfiles.current].translated > 0) 79 | subfiles.projects[subfiles.current].translated-- 80 | } 81 | 82 | subfiles.projects[subfiles.current].items[index].text_trans = value 83 | }, 84 | 85 | [types.UPDATE_SUBTITLE_FULL](subfiles, index, value) { 86 | Object.assign(subfiles.projects[subfiles.current].items[index], value) 87 | }, 88 | 89 | [types.INSERT_SUBTITLE](subfiles, subtitle, index) { 90 | if(!subfiles.projects[subfiles.current]) 91 | return 92 | 93 | subfiles.projects[subfiles.current].items.splice(index, 0, subtitle) 94 | }, 95 | 96 | [types.DELETE_SUBTITLE](subfiles, index) { 97 | subfiles.projects[subfiles.current].items.splice(index, 1) 98 | }, 99 | 100 | [types.RENUMBER_SUBTITLE](subfiles, index, number) { 101 | subfiles.projects[subfiles.current].items[index].number = number 102 | }, 103 | 104 | [types.SET_SUBTITLE_TIME](subfiles, index, start, end) { 105 | subfiles.projects[subfiles.current].items[index].time_markers.start = start 106 | subfiles.projects[subfiles.current].items[index].time_markers.end = end 107 | 108 | // let result = ms2obj(subfiles.projects[subfiles.current].items[index].time_markers.start, true) 109 | // let time_start = `${result.hours}:${result.minutes}:${result.seconds}.${result.milliseconds}` 110 | // result = ms2obj(subfiles.projects[subfiles.current].items[index].time_markers.end, true) 111 | // let time_end = `${result.hours}:${result.minutes}:${result.seconds}.${result.milliseconds}` 112 | // subfiles.projects[subfiles.current].items[index].time = `${time_start} --> ${time_end}` 113 | subfiles.projects[subfiles.current].items[index].time = ms2string( 114 | subfiles.projects[subfiles.current].items[index].time_markers.start, 115 | subfiles.projects[subfiles.current].items[index].time_markers.end 116 | ) 117 | }, 118 | 119 | [types.SET_DEFAULT_EXPORT_PATH](subfiles, path) { 120 | if(typeof(path) === 'string') 121 | subfiles.export_path_default = path 122 | }, 123 | 124 | [types.SET_DEFAULT_LANGUAGE](subfiles, lang) { 125 | subfiles.default_lang = lang 126 | }, 127 | 128 | [types.CLEAR_SUBTITLES](subfiles) { 129 | if(subfiles.current && subfiles.projects[subfiles.current]) 130 | subfiles.projects[subfiles.current].items = [] 131 | }, 132 | 133 | [types.SET_SUBFILE_FILTER](subfiles, filter) { 134 | subfiles.projects[subfiles.current].filter = filter 135 | }, 136 | 137 | [types.SET_VIDEO_FILE](subfiles, file_path) { 138 | Vue.set(subfiles.projects[subfiles.current], 'video_path', file_path) 139 | }, 140 | 141 | [types.CLEAR_VIDEO_FILE](subfiles) { 142 | console.log('clear') 143 | Vue.set(subfiles.projects[subfiles.current], 'video_path', null) 144 | Vue.set(subfiles.projects[subfiles.current], 'video_position', 0.0) 145 | }, 146 | 147 | [types.SET_CURRENT_VIDEO_POSITION](subfiles, position) { 148 | Vue.set(subfiles.projects[subfiles.current], 'video_position', position) 149 | }, 150 | 151 | [types.SET_SCROLL_POSITION](subfiles, position) { 152 | Vue.set(subfiles.projects[subfiles.current], 'scroll', position) 153 | }, 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/store/modules/ui.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import * as types from '../mutation-types' 3 | 4 | 5 | export default { 6 | state: { 7 | leftPanelWidth: 250, 8 | centralPanelWidth: 300, 9 | videoViewHeight: 400, 10 | transInputHeight: 100, 11 | 12 | projectSortOrder: 0, // 0 - by export name, 1 - by usage 13 | 14 | autoUpdates: 1, 15 | }, 16 | 17 | // mutations 18 | mutations: { 19 | [types.SET_LEFT_PANEL_WIDTH](ui, width) { 20 | ui.leftPanelWidth = width 21 | console.log(width) 22 | }, 23 | 24 | [types.SET_CENTRAL_PANEL_WIDTH](ui, width) { 25 | ui.centralPanelWidth = width 26 | console.log(width) 27 | }, 28 | 29 | [types.SET_TRANS_INPUT_HEIGHT](ui, height) { 30 | ui.transInputHeight = height 31 | console.log(height) 32 | }, 33 | 34 | [types.SET_PROJECT_SORT_ORDER](ui, projectSortOrder) { 35 | ui.projectSortOrder = projectSortOrder 36 | }, 37 | 38 | [types.SET_AUTO_UPDATES](ui, autoUpdates) { 39 | console.log(autoUpdates) 40 | ui.autoUpdates = autoUpdates 41 | }, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/store/mutation-types.js: -------------------------------------------------------------------------------- 1 | // common 2 | export const LOAD_STATE = 'LOAD_STATE' 3 | export const LOAD_SUBFILE = 'LOAD_SUBFILE' 4 | 5 | // ui 6 | export const SET_LEFT_PANEL_WIDTH = 'SET_LEFT_PANEL_WIDTH' 7 | export const SET_CENTRAL_PANEL_WIDTH = 'SET_CENTRAL_PANEL_WIDTH' 8 | export const SET_TRANS_INPUT_HEIGHT = 'SET_TRANS_INPUT_HEIGHT' 9 | export const SET_PROJECT_SORT_ORDER = 'SET_PROJECT_SORT_ORDER' 10 | export const SET_AUTO_UPDATES = 'SET_AUTO_UPDATES' 11 | 12 | // subfiles 13 | export const ADD_PROJECT = 'ADD_PROJECT' 14 | export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT' 15 | export const DELETE_PROJECT = 'DELETE_PROJECT' 16 | export const CHANGE_PROJECT_PROPERTIES = 'CHANGE_PROJECT_PROPERTIES' 17 | export const CHANGE_PROJECT_EXPORT_NAME = 'CHANGE_PROJECT_EXPORT_NAME' 18 | export const CHANGE_PROJECT_EXPORT_PATH = 'CHANGE_PROJECT_EXPORT_PATH' 19 | export const SET_SUBFILE_FILTER = 'SET_SUBFILE_FILTER' 20 | export const ADD_SUBTITLE = 'ADD_SUBTITLE' 21 | export const SET_TRANSLATED_SUBTITLES_COUNT = 'SET_TRANSLATED_SUBTITLES_COUNT' 22 | export const ADD_SUBTITLES = 'ADD_SUBTITLES' 23 | export const CLEAR_SUBTITLES = 'CLEAR_SUBTITLES' 24 | export const MAKE_SUBTITLE_ACTIVE = 'MAKE_SUBTITLE_ACTIVE' 25 | export const UPDATE_SUBTITLE = 'UPDATE_SUBTITLE' 26 | export const UPDATE_SUBTITLE_FULL = 'UPDATE_SUBTITLE_FULL' 27 | export const INSERT_SUBTITLE = 'INSERT_SUBTITLE' 28 | export const DELETE_SUBTITLE = 'DELETE_SUBTITLE' 29 | export const RENUMBER_SUBTITLE = 'RENUMBER_SUBTITLE' 30 | export const SET_SUBTITLE_TIME = 'SET_SUBTITLE_TIME' 31 | export const SET_DEFAULT_EXPORT_PATH = 'SET_DEFAULT_EXPORT_PATH' 32 | export const SET_DEFAULT_LANGUAGE = 'SET_DEFAULT_LANGUAGE' 33 | export const SET_VIDEO_FILE = 'SET_VIDEO_FILE' 34 | export const SET_CURRENT_VIDEO_POSITION = 'SET_CURRENT_VIDEO_POSITION' 35 | export const CLEAR_VIDEO_FILE = 'CLEAR_VIDEO_FILE' 36 | export const SET_SCROLL_POSITION = 'SET_SCROLL_POSITION' 37 | 38 | // player 39 | // export const SET_CANVAS = 'SET_CANVAS' 40 | export const PLUGINS_EXIST = 'PLUGINS_EXIST' 41 | export const MUTE_PLAYER = 'MUTE_PLAYER' 42 | export const SET_VOLUME = 'SET_VOLUME' 43 | export const SET_VIDEO_METADATA = 'SET_VIDEO_METADATA' 44 | export const SET_SUBFILE = 'SET_SUBFILE' 45 | export const SET_CURRENT_TIME = 'SET_CURRENT_TIME' 46 | export const SET_CURRENT_POSITION = 'SET_CURRENT_POSITION' 47 | export const SET_BUFFERING = 'SET_BUFFERING' 48 | export const SET_PLAYER_STATE = 'SET_PLAYER_STATE' 49 | export const RESET_VIDEO = 'RESET_VIDEO' 50 | export const STOP_AFTER_PLAY = 'STOP_AFTER_PLAY' 51 | 52 | 53 | // opensubtitles 54 | export const SET_OST_CREDENTIALS = 'SET_OST_CREDENTIALS' 55 | export const SET_OST_SSL = 'SET_OST_SSL' 56 | export const SET_OST_VERIFIED = 'SET_OST_VERIFIED' 57 | -------------------------------------------------------------------------------- /src/styles/buttons.scss: -------------------------------------------------------------------------------- 1 | .btn:disabled, textarea:disabled, input:disabled { 2 | // background: $border-color; 3 | // color: $toolbar-border-color; 4 | // 5 | // & span { 6 | // color: $toolbar-border-color; 7 | // } 8 | opacity: .4; 9 | } 10 | 11 | // textarea:disabled, input[type=text]:disabled, input[type=password]:disabled, input[type=range]:disabled { 12 | // // background: var(--window-color); 13 | // opacity: .4; 14 | // } 15 | 16 | .btn-group.tabs .btn { 17 | padding: 2px 8px; 18 | 19 | &.active { 20 | background-image: var(--tab-linear-gradiend); 21 | font-weight: 300; 22 | } 23 | } 24 | 25 | input[type=range] { 26 | -webkit-appearance: none; 27 | 28 | &::-webkit-slider-runnable-track { 29 | height: 3px; 30 | border: none; 31 | } 32 | 33 | &::-webkit-slider-thumb { 34 | -webkit-appearance: none; 35 | border: var(--border-dark); 36 | height: 16px; 37 | width: 16px; 38 | border-radius: 50%; 39 | background: white; 40 | margin-top: -6px; 41 | } 42 | 43 | &:focus { 44 | outline: none; 45 | } 46 | } 47 | 48 | label.disabled { 49 | color: var(--dark-border-color); 50 | } 51 | 52 | .form-footer { 53 | display: flex; 54 | justify-content: flex-end; 55 | align-items: center; 56 | width: 100%; 57 | margin-top: 10px; 58 | 59 | & button { 60 | margin-left: 10px; 61 | } 62 | } 63 | 64 | input[type="text"], input[type="password"] { 65 | min-height: 20px; 66 | padding-top: 0; 67 | padding-bottom: 0; 68 | } 69 | 70 | body[platform=darwin] .btn.btn-dark { 71 | -webkit-filter: brightness(1.3); 72 | background: var(--left-panel-bg); 73 | border-color: var(--left-panel-bg); 74 | 75 | &:enabled:active { 76 | // background: lighten($left-panel-color, 3%); 77 | background: var(--left-panel-bg); 78 | -webkit-filter: brightness(1.2); 79 | } 80 | 81 | & .icon { 82 | color: var(--active-unfocused-color); 83 | } 84 | 85 | &:enabled:active .icon { 86 | color: var(--default-color); 87 | -webkit-filter: brightness(0.5); 88 | } 89 | } 90 | 91 | .btn.btn-dark { 92 | margin-right: 0; 93 | } 94 | 95 | .btn-wait { 96 | display: flex; 97 | align-items: center; 98 | } 99 | 100 | .btn-wait svg { 101 | margin-right: 2px; 102 | } 103 | 104 | .btn-micro.btn-default { 105 | padding: 1px; 106 | height: 16px; 107 | 108 | & .icon { 109 | font-size: calc(var(--font-size-default) - 2px); 110 | margin: 0; 111 | } 112 | } 113 | 114 | .spinner-circle { 115 | stroke: var(--active-color); 116 | } 117 | 118 | .volume { 119 | position: fixed; 120 | height: 24px; 121 | width: 80px; 122 | border: 1px var(--border-color) solid; 123 | display: flex; 124 | align-items: center; 125 | padding: 0; 126 | // transform: rotate(270deg); 127 | background: var(--window-color); 128 | box-shadow: var(--volume-shadow); 129 | 130 | & input[type=range] { 131 | width: 100%; 132 | } 133 | } 134 | 135 | .volume-transition { 136 | transition: all .3s ease; 137 | // max-height: 48px; 138 | } 139 | 140 | .volume-enter, .volume-leave { 141 | // opacity: .5; 142 | transform: scaleY(0); 143 | // max-height: 1px; 144 | } 145 | 146 | body[platform=win32] .toolbar .btn { 147 | // padding-top: 0; 148 | // padding-bottom: 0; 149 | margin: 0; 150 | border: 0; 151 | background-color: var(--window-color); 152 | transition: all .1s ease; 153 | 154 | &:hover { 155 | -webkit-filter: brightness(.9); 156 | } 157 | 158 | & .icon { 159 | width: 20px; 160 | height: 20px; 161 | font-size: 20px; 162 | } 163 | } 164 | 165 | // titlebar controls (for Windows only) 166 | .btn-group.btn-win { 167 | min-width: 138px; 168 | } 169 | 170 | .btn.btn-win { 171 | width: 46px; 172 | height: 28px; 173 | background-color: var(--window-color); 174 | } 175 | 176 | .btn.btn-win:after { 177 | background-repeat: no-repeat; 178 | background-position: center; 179 | content: ' '; 180 | display: block; 181 | left: 0; 182 | top: 0; 183 | width: 100%; 184 | height: 100%; 185 | z-index: 1; 186 | } 187 | 188 | body[inactive=true] .btn.btn-win:after { 189 | opacity: .35; 190 | } 191 | 192 | body[inactive=true] .btn:hover.btn-win:after { 193 | opacity: 1; 194 | } 195 | 196 | .btn-group.btn-win { 197 | margin-right: 0; 198 | } 199 | 200 | .btn.btn-win.btn-minimize:after { 201 | background-image: url("../img/minimize.png"); 202 | } 203 | 204 | .btn.btn-win.btn-maximize:after { 205 | background-image: url("../img/maximize.png"); 206 | } 207 | 208 | .btn.btn-win.btn-unmaximize:after { 209 | background-image: url("../img/unmaximize.png"); 210 | } 211 | 212 | .btn.btn-win.btn-close:after { 213 | background-image: url("../img/close.png"); 214 | } 215 | 216 | .btn.btn-win:hover.btn-close:after { 217 | background-image: url("../img/close-white.png"); 218 | opacity: 1; 219 | } 220 | 221 | .btn.btn-close.btn-win:hover { 222 | background-color: #da3d27; 223 | } 224 | -------------------------------------------------------------------------------- /src/styles/containers.scss: -------------------------------------------------------------------------------- 1 | html { 2 | // min-height: 100%; 3 | position: relative; 4 | } 5 | 6 | body { 7 | background: var(--window-color); 8 | // display: flex; 9 | // flex-grow: 1; 10 | 11 | & * { 12 | cursor: inherit; 13 | } 14 | } 15 | 16 | body[platform=win32] { 17 | background: #0078d7; 18 | padding: 1px; 19 | } 20 | 21 | .main-container { 22 | display: flex; 23 | flex-direction: row; 24 | // flex-grow: 1; 25 | animation: fade-in .3s ease; 26 | width: 100%; 27 | height: 100%; 28 | // position: absolute; 29 | // top: 0; 30 | // bottom: 0; //for footer 31 | // left: 0; 32 | // right: 0; 33 | // overflow: hidden; 34 | } 35 | 36 | body[platform=win32] .main-container { 37 | // border: 1px solid #0078d7; 38 | // margin: 1px; 39 | } 40 | 41 | body[platform=win32][maximized=true] { 42 | padding: 0; 43 | } 44 | 45 | body[platform=win32][inactive=true] { 46 | background: var(--window-color); 47 | } 48 | 49 | @keyframes fade-in { 50 | from {opacity: 0} 51 | to {opacity: 1} 52 | } 53 | 54 | .panel { 55 | position: relative; 56 | display: flex; 57 | flex-direction: column; 58 | } 59 | 60 | .left-panel { 61 | text-overflow: ellipsis !important; 62 | min-width: 130px; 63 | background: var(--left-panel-bg); 64 | color: var(--left-panel-color); 65 | // padding: 0 10px 0 10px; 66 | // flex-grow: 1; 67 | 68 | &.mini { 69 | padding: 0 5px 0 5px; 70 | } 71 | 72 | & div { 73 | display: flex; 74 | } 75 | } 76 | 77 | body[platform=win32] .left-panel { 78 | // border-right: var(--border-thick); 79 | -webkit-filter: invert(85%); 80 | } 81 | 82 | .central-panel { 83 | // max-width: 400px; 84 | // min-width: 190px; 85 | // flex: 1000 0 0; 86 | // width: 300px; 87 | // order: 1; 88 | // flex-grow: 1; 89 | } 90 | 91 | .right-panel { 92 | min-width: 344px; 93 | // item:nth-child(2); 94 | // order: 2; 95 | flex-grow: 1; 96 | } 97 | 98 | .right-container { 99 | display: flex; 100 | flex-direction: column; 101 | flex-grow: 1; 102 | } 103 | 104 | // body[platform=win32] .right-panel { 105 | // min-width: 343px; 106 | // } 107 | 108 | .form-panel { 109 | flex-direction: column; 110 | background: var(--panel-color); 111 | padding: 15px; 112 | border-radius: var(--default-border-radius); 113 | border: var(--border-standard); 114 | } 115 | 116 | .dropzone { 117 | transition: .3s all; 118 | } 119 | 120 | .dropzone-active { 121 | opacity: 1; 122 | background: green; 123 | // -webkit-filter: invert(1); 124 | -webkit-filter: grayscale(100%) sepia(100%) hue-rotate(90deg); 125 | } 126 | 127 | .spl { 128 | position: absolute; 129 | // background: red; 130 | // opacity: .4; 131 | opacity: 0; 132 | } 133 | 134 | .spl-h { 135 | top: 0; 136 | left: -3px; 137 | height: 100%; 138 | width: 6px; 139 | cursor: col-resize; 140 | } 141 | 142 | .spl-v { 143 | left: 0; 144 | top: -3px; 145 | width: 100%; 146 | height: 6px; 147 | cursor: row-resize; 148 | } 149 | -------------------------------------------------------------------------------- /src/styles/dialog.scss: -------------------------------------------------------------------------------- 1 | .dialog { 2 | 3 | display: flex; 4 | position: relative; 5 | flex-direction: column; 6 | border: 1px solid rgba(0, 0, 0, 0.3); 7 | border-top: 0; 8 | border-radius: 2px; 9 | box-shadow: 0 4px 9px rgba(0, 0, 0, 0.3); 10 | padding: 15px; 11 | background: var(--window-color); 12 | 13 | & div { 14 | display: flex; 15 | } 16 | 17 | & .form-group { 18 | flex-direction: row; 19 | align-content: center; 20 | align-items: center; 21 | justify-content: space-between; 22 | flex-grow: 1; 23 | } 24 | 25 | & .form-control { 26 | width: auto; 27 | margin-left: 4px; 28 | } 29 | 30 | & select { 31 | flex-grow: 0; 32 | } 33 | 34 | & label { 35 | margin-bottom: 0px; 36 | } 37 | } 38 | 39 | .dialog:before { 40 | position: fixed; 41 | top: 0; 42 | left: 0; 43 | right: 0; 44 | bottom: 0; 45 | content: ""; 46 | z-index: -1; 47 | } 48 | 49 | @keyframes fadeIn { 50 | from { 51 | opacity: 0; 52 | } 53 | to { 54 | opacity: 0.5; 55 | } 56 | } 57 | 58 | .modal-transition { 59 | position: fixed; 60 | display: flex; 61 | left: 0; 62 | right: 0; 63 | margin-left: auto; 64 | margin-right: auto; 65 | z-index: 1000; 66 | width: min-content; 67 | 68 | transition: all .25s ease; 69 | top: 0px; 70 | } 71 | 72 | .modal-enter, .modal-leave { 73 | transform: translateY(-100%); 74 | } 75 | -------------------------------------------------------------------------------- /src/styles/footer.scss: -------------------------------------------------------------------------------- 1 | footer { 2 | position: absolute; 3 | bottom: 0; 4 | width: 100%; 5 | height: 20px; 6 | min-height: 20px; 7 | -webkit-app-region: drag; 8 | } 9 | -------------------------------------------------------------------------------- /src/styles/header.scss: -------------------------------------------------------------------------------- 1 | header.toolbar-header { 2 | width: 100%; 3 | height: var(--header-height); 4 | padding-top: var(--header-padding-top); 5 | // padding-left: 70px; 6 | // padding-right: 5px; 7 | min-height: var(--header-height); 8 | -webkit-app-region: drag; 9 | 10 | & .btn-default { 11 | position: relative; 12 | } 13 | } 14 | 15 | header button, .btn, .btn-default, .btn-group { 16 | -webkit-app-region: no-drag; 17 | } 18 | 19 | .left-panel header.toolbar-header { 20 | display: flex; 21 | justify-content: flex-end; 22 | padding-left: 10px; 23 | padding-right: 10px; 24 | background: transparent; 25 | border-color: transparent; 26 | box-shadow: inset 0 1px 0 transparent; 27 | transition: all .2s; 28 | 29 | & .toolbar-actions { 30 | padding-right: 0; 31 | } 32 | } 33 | 34 | body[platform=win32] .left-panel header.toolbar-header { 35 | padding-right: 0px; 36 | padding-left: 0px; 37 | justify-content: flex-start; 38 | } 39 | 40 | body[platform=win32] header .toolbar-actions { 41 | padding-left: 0px; 42 | padding-right: 0px; 43 | margin-left: 0px; 44 | } 45 | 46 | .mini header.toolbar-header { 47 | min-height: 75px; 48 | padding-top: 35px; 49 | justify-content: center; 50 | 51 | & .toolbar-actions { 52 | padding-left:0; 53 | 54 | & .btn { 55 | margin-left: 0; 56 | } 57 | } 58 | } 59 | 60 | .central-panel header.toolbar-header { 61 | display: flex; 62 | justify-content: center; 63 | } 64 | 65 | body[platform=win32] header .btn { 66 | box-shadow: none; 67 | height: 28px; 68 | } 69 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import 'photon/photon.scss'; 2 | 3 | @import "buttons.scss"; 4 | @import 'dialog.scss'; 5 | @import 'containers.scss'; 6 | @import 'header.scss'; 7 | @import 'footer.scss'; 8 | @import 'list.scss'; 9 | @import 'popover.scss'; 10 | @import 'project-list.scss'; 11 | @import 'tabs.scss'; 12 | @import 'trans-input.scss'; 13 | @import 'update-popup.scss'; 14 | @import 'video-view.scss'; 15 | -------------------------------------------------------------------------------- /src/styles/list.scss: -------------------------------------------------------------------------------- 1 | .list-group { 2 | flex-grow: 1; 3 | overflow-y: scroll; 4 | background: #fff; 5 | border-right: var(--border-thick); 6 | } 7 | 8 | .list-group-item { 9 | display: flex; 10 | padding-left: 0; 11 | height: 85px; 12 | 13 | &>div { 14 | flex-grow: 1; 15 | } 16 | 17 | & p { 18 | cursor: default; 19 | } 20 | } 21 | 22 | .list-item-header { 23 | display: flex; 24 | justify-content: space-between; 25 | } 26 | 27 | .list-item-indicator { 28 | width: 5px; 29 | min-width: 5px; 30 | margin-right: 10px; 31 | margin-top: -10px; 32 | margin-bottom: -10px; 33 | } 34 | 35 | .light { 36 | color: var(--gray-color); 37 | // font-size: $font-size-default - 2; 38 | // font: small-caption; 39 | font-size: calc(var(--font-size-default) - 2px); 40 | opacity: .5; 41 | // z-index: 42 | cursor: default; 43 | } 44 | 45 | .active .light { 46 | color: var(--gray-color); 47 | -webkit-filter: brightness(1.2); 48 | } 49 | 50 | .ready .light { 51 | color: var(--trans-color); 52 | } 53 | -------------------------------------------------------------------------------- /src/styles/photon/bars.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Bars.css 3 | // -------------------------------------------------- 4 | 5 | .toolbar { 6 | min-height: var(--toolbar-min-height); 7 | @include clearfix; 8 | } 9 | 10 | body[platform=darwin] .toolbar { 11 | box-shadow: inset 0 1px 0 #f5f4f5; 12 | // @include linear-gradient(#e8e6e8, #d1cfd1); 13 | background: var(--window-color-active-toolbar); 14 | } 15 | 16 | body[platform=darwin][inactive=true] .central-panel .toolbar, body[platform=darwin][inactive=true] .right-panel .toolbar { 17 | background: var(--window-color-inactive-toolbar); 18 | } 19 | 20 | body[platform=win32] .toolbar { 21 | background: var(--window-color); 22 | } 23 | 24 | .toolbar-header { 25 | border-bottom: var(--border-dark); 26 | 27 | .title { 28 | margin-top: 1px; 29 | } 30 | } 31 | 32 | .toolbar-footer { 33 | border-top: var(--border-dark); 34 | -webkit-app-region: drag; 35 | } 36 | 37 | // Simple centered title to go in the toolbar 38 | .title { 39 | margin: 0; 40 | font-size: 12px; 41 | font-weight: 400; 42 | text-align: center; 43 | color: #555; 44 | cursor: default; 45 | } 46 | 47 | // Borderless toolbar for the clean look 48 | .toolbar-borderless { 49 | border-top: 0; 50 | border-bottom: 0; 51 | } 52 | 53 | // Buttons in toolbars 54 | .toolbar-actions { 55 | margin-top: var(--header-margin-top); 56 | margin-bottom: var(--header-margin-bottom); 57 | padding-right: 3px; 58 | padding-left: 3px; 59 | padding-bottom: 3px; 60 | -webkit-app-region: drag; 61 | @include clearfix; 62 | 63 | > .btn, 64 | > .btn-group { 65 | margin-left: 4px; 66 | margin-right: 4px; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/styles/photon/base.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Base styles 3 | // -------------------------------------------------- 4 | 5 | * { 6 | // cursor: default; 7 | -webkit-user-drag: text; 8 | -webkit-user-select: none; 9 | -webkit-box-sizing: border-box; 10 | box-sizing: border-box; 11 | } 12 | 13 | html, body { 14 | height: 100%; 15 | width: 100%; 16 | padding: 0; 17 | margin: 0; 18 | overflow: hidden; 19 | } 20 | 21 | body { 22 | // font-family: $font-family-default; 23 | // font-size: $font-size-default; 24 | // line-height: $line-height-default; 25 | // font: caption; 26 | // font-family: BlinkMacSystemFont; 27 | // font: menu; 28 | font-family: var(--default-font-family); 29 | font-size: var(--default-font-size); 30 | // font-size: medium; 31 | color: WindowText; 32 | background-color: transparent; 33 | } 34 | 35 | hr { 36 | margin: 15px 0; 37 | overflow: hidden; 38 | background: transparent; 39 | border: 0; 40 | border-bottom: var(--border-standard); 41 | } 42 | 43 | // Typography 44 | h1, h2, h3, h4, h5, h6 { 45 | margin-top: 20px; 46 | margin-bottom: 10px; 47 | font-weight: 500; 48 | white-space: nowrap; 49 | overflow: hidden; 50 | text-overflow: ellipsis; 51 | } 52 | 53 | h1 { font-size: 36px; } 54 | h2 { font-size: 30px; } 55 | h3 { font-size: 24px; } 56 | h4 { font-size: 18px; } 57 | h5 { font-size: 14px; } 58 | h6 { font-size: 12px; } 59 | 60 | // Basic app structure 61 | .window { 62 | position: absolute; 63 | top: 0; 64 | right: 0; 65 | bottom: 0; 66 | left: 0; 67 | display: flex; 68 | flex-direction: column; 69 | background-color: var(--chrome-color); 70 | } 71 | 72 | .window-content { 73 | position: relative; 74 | overflow-y: auto; 75 | display: flex; 76 | flex: 1; 77 | } 78 | -------------------------------------------------------------------------------- /src/styles/photon/button-groups.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Button-groups.css 3 | // Adapted from Bootstrap's button-groups.less (https://github.com/twbs/bootstrap/blob/master/less/button-groups.less) 4 | // -------------------------------------------------- 5 | 6 | // Button groups 7 | .btn-group { 8 | position: relative; 9 | display: inline-block; 10 | vertical-align: middle; // match .btn alignment given font-size hack above 11 | -webkit-app-region: no-drag; 12 | 13 | .btn { 14 | position: relative; 15 | float: left; 16 | 17 | // Bring the "active" button to the front 18 | &:focus, 19 | &:active{ 20 | z-index: 2; 21 | } 22 | 23 | &.active { 24 | z-index: 3; 25 | } 26 | } 27 | } 28 | 29 | // Prevent double borders when buttons are next to each other 30 | .btn-group { 31 | .btn + .btn, 32 | .btn + .btn-group, 33 | .btn-group + .btn, 34 | .btn-group + .btn-group { 35 | margin-left: -1px; 36 | } 37 | 38 | > .btn:first-child { 39 | border-top-right-radius: 0; 40 | border-bottom-right-radius: 0; 41 | } 42 | 43 | > .btn:last-child { 44 | border-top-left-radius: 0; 45 | border-bottom-left-radius: 0; 46 | } 47 | 48 | > .btn:not(:first-child):not(:last-child) { 49 | border-radius: 0; 50 | } 51 | 52 | .btn + .btn { 53 | border-left: var(--border-dark); 54 | } 55 | 56 | .btn + .btn.active { 57 | // border-left: 0; 58 | } 59 | 60 | // Selected state 61 | .active { 62 | color: #fff; 63 | border: 1px solid transparent; 64 | background-color: var(--active-color); 65 | background-image: none; 66 | } 67 | 68 | // Invert the icon in the active button 69 | .active .icon { 70 | color: #fff; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/styles/photon/buttons.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Buttons.css 3 | // -------------------------------------------------- 4 | 5 | .btn { 6 | display: inline-block; 7 | padding: 3px 8px; 8 | margin-bottom: 0; 9 | // font-size: $font-size-default; 10 | line-height: 1.4; 11 | text-align: center; 12 | white-space: nowrap; 13 | vertical-align: middle; 14 | cursor: default; 15 | background-image: none; 16 | border: 1px solid transparent; 17 | border-radius: var(--default-border-radius); 18 | box-shadow: 0 1px 1px rgba(0,0,0,.06); 19 | -webkit-app-region: no-drag; 20 | 21 | &:focus { 22 | outline: none; 23 | box-shadow: none; 24 | } 25 | } 26 | 27 | body[platform=darwin][inactive=true] .toolbar .btn, 28 | body[platform=darwin][inactive=true] .tabs .btn.active { 29 | opacity: .5; 30 | } 31 | 32 | .btn-mini { 33 | padding: 0px 6px; 34 | padding-left: 15px; 35 | padding-right: 15px; 36 | } 37 | 38 | .btn-large { 39 | padding: 6px 12px; 40 | } 41 | 42 | .btn-form { 43 | padding-right: 20px; 44 | padding-left: 20px; 45 | } 46 | 47 | // Normal buttons 48 | .btn-default, .btn-white { 49 | color: var(--gray-color); 50 | background: var(--btn-default-bg); 51 | 52 | &:enabled:active { 53 | background-color: #ddd; 54 | background-image: none; 55 | } 56 | } 57 | 58 | body[platform=darwin] .btn-default, body[platform=darwin] .btn-white { 59 | border-top-color: var(--dark-border-color); 60 | border-right-color: var(--dark-border-color); 61 | border-bottom-color: var(--darker-bottom-border-color); 62 | border-left-color: var(--dark-border-color); 63 | } 64 | 65 | // Button variations 66 | body[platform=darwin] .btn-primary { 67 | color: #fff; 68 | border-color: var(--primary-color); 69 | border-bottom-color: darken(#388df8, 15%); 70 | text-shadow: 0 1px 1px rgba(0,0,0,.1); 71 | font-weight: 300; 72 | } 73 | 74 | // For primary buttons 75 | .btn-primary { 76 | background: var(--btn-primary-bg); 77 | 78 | &:enabled:active { 79 | background: var(--btn-primary-bg); 80 | -webkit-filter: brightness(0.8); 81 | } 82 | } 83 | 84 | body[platform=darwin][inactive=true] .btn-primary { 85 | color: var(--gray-color); 86 | border-top-color: var(--dark-border-color); 87 | border-right-color: var(--dark-border-color); 88 | border-bottom-color: var(--darker-bottom-border-color); 89 | border-left-color: var(--dark-border-color); 90 | background: var(--btn-default-bg); 91 | } 92 | 93 | // Icons in buttons 94 | .btn .icon { 95 | float: left; 96 | width: 14px; 97 | height: 14px; 98 | margin-top: 1px; 99 | margin-bottom: 1px; 100 | color: #737475; 101 | font-size: 14px; 102 | line-height: 1; 103 | } 104 | 105 | // Add the margin next to the icon if there is text in the button too 106 | .btn .icon-text { 107 | margin-right: 5px; 108 | } 109 | 110 | // This utility class add a down arrow icon to the button 111 | .btn-dropdown { 112 | font-size: calc(var(--font-size-default) - 1px); 113 | 114 | &:after { 115 | font-family: "photon-entypo"; 116 | margin-left: 5px; 117 | content: '\e873'; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/styles/photon/forms.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Forms.css 3 | // Adapted from Bootstrap's forms.less (https://github.com/twbs/bootstrap/blob/master/less/forms.less) 4 | // -------------------------------------------------- 5 | 6 | label { 7 | display: inline-block; 8 | margin-bottom: 5px; 9 | white-space: nowrap; 10 | overflow: hidden; 11 | text-overflow: ellipsis; 12 | cursor: default !important; 13 | } 14 | 15 | input[type="search"] { 16 | box-sizing: border-box; 17 | } 18 | 19 | input[type="radio"], 20 | input[type="checkbox"] { 21 | margin: 4px 0 0; 22 | line-height: normal; 23 | } 24 | 25 | body[platform*="darwin"] input[type="radio"], 26 | body[platform*="darwin"] input[type="checkbox"] { 27 | // transform: scale(1.2); 28 | margin-right: 2px; 29 | -webkit-appearance:none; 30 | position: relative; 31 | outline: 0; 32 | color: default; 33 | background: white; 34 | border: var(--border-dark); 35 | border-radius: var(--default-border-radius); 36 | width: 13px; 37 | height: 13px; 38 | font-size: 11px; 39 | } 40 | 41 | body[platform*="darwin"] input[type="checkbox"]:checked { 42 | background: var(--btn-primary-bg); 43 | border-width: 0; 44 | } 45 | 46 | body[platform*="darwin"] input[type="checkbox"]:checked:before, 47 | body[platform=darwin][inactive=true] input[type="checkbox"]:checked:before { 48 | position:absolute; 49 | top: 0; 50 | left: 0; 51 | width: 100%; 52 | height: 100%; 53 | text-align: center; 54 | color: white; 55 | content: '✓'; 56 | } 57 | 58 | body[platform=darwin][inactive=true] input[type="checkbox"]:checked:before { 59 | background: white; 60 | color: WindowText; 61 | border: var(--border-dark); 62 | border-radius: var(--default-border-radius); 63 | } 64 | 65 | .form-control { 66 | display: inline-block; 67 | width: 100%; 68 | min-height: 25px; 69 | padding: var(--padding-less); 70 | // font-size: var(--font-size-default); 71 | line-height: $line-height-default; 72 | background-color: var(--chrome-color); 73 | border: var(--border-standard); 74 | border-radius: var(--default-border-radius); 75 | outline: none; 76 | -webkit-app-region: no-drag; 77 | // transition: box-shadow .3s linear; 78 | 79 | &:focus { 80 | border-color: var(--focus-input-color); 81 | box-shadow: var(--box-shadow-focus); 82 | animation: .2s linear mac-input-outline; 83 | } 84 | } 85 | 86 | @keyframes mac-input-outline { 87 | from { 88 | border-color: var(--focus-input-color); 89 | box-shadow: var(--box-shadow-focus-anim); 90 | } 91 | to { 92 | border-color: var(--focus-input-color); 93 | box-shadow: var(--box-shadow-focus); 94 | } 95 | } 96 | 97 | input[type="number"].form-control { 98 | padding: 1px; 99 | text-align: right; 100 | max-width: 50px; 101 | } 102 | 103 | // Reset height for `textarea`s 104 | textarea { 105 | height: auto; 106 | resize: none; 107 | } 108 | 109 | // Form groups 110 | // 111 | // Designed to help with the organization and spacing of vertical forms. For 112 | // horizontal forms, use the predefined grid classes. 113 | 114 | .form-group { 115 | flex-direction: column; 116 | margin-bottom: 10px; 117 | } 118 | 119 | // Checkboxes and radios 120 | // 121 | // Indent the labels to position radios/checkboxes as hanging controls. 122 | 123 | .radio, 124 | .checkbox { 125 | position: relative; 126 | display: block; 127 | margin-top: 10px; 128 | margin-bottom: 10px; 129 | 130 | label { 131 | padding-left: 20px; 132 | margin-bottom: 0; 133 | // font-weight: normal; 134 | } 135 | } 136 | 137 | .radio input[type="radio"], 138 | .radio-inline input[type="radio"], 139 | .checkbox input[type="checkbox"], 140 | .checkbox-inline input[type="checkbox"] { 141 | position: absolute; 142 | margin-left: -20px; 143 | margin-top: 4px; 144 | } 145 | 146 | // Form actions 147 | .form-actions .btn { 148 | margin-right: 10px; 149 | 150 | &:last-child { 151 | margin-right: 0; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/styles/photon/grid.scss: -------------------------------------------------------------------------------- 1 | // 2 | // The Grid.css 3 | // -------------------------------------------------- 4 | 5 | .pane-group { 6 | position: absolute; 7 | top: 0; 8 | right: 0; 9 | bottom: 0; 10 | left: 0; 11 | display: flex; 12 | } 13 | 14 | .pane { 15 | position: relative; 16 | overflow-y: auto; 17 | flex: 1; 18 | border-left: var(--border-standard); 19 | 20 | &:first-child { 21 | border-left: 0; 22 | } 23 | } 24 | 25 | .pane-sm { 26 | max-width: 220px; 27 | min-width: 150px; 28 | } 29 | 30 | .pane-mini { 31 | width: 80px; 32 | flex: none; 33 | } 34 | 35 | .pane-one-fourth { 36 | width: 25%; 37 | flex: none; 38 | } 39 | 40 | .pane-one-third { 41 | width: 33.3%; 42 | flex: none; 43 | } 44 | -------------------------------------------------------------------------------- /src/styles/photon/images.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Images.scss 3 | // -------------------------------------------------- 4 | 5 | img { 6 | -webkit-user-drag: text; 7 | } 8 | 9 | .img-circle { 10 | border-radius: 50%; 11 | } 12 | 13 | .img-rounded { 14 | border-radius: var(--default-border-radius); 15 | } 16 | -------------------------------------------------------------------------------- /src/styles/photon/lists.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Lists.scss 3 | // -------------------------------------------------- 4 | 5 | // List groups 6 | // These are to be used when shows list items that contain 7 | // more substanstial amounts of information. (headings, images, text, etc.) 8 | 9 | .list-group { 10 | width: 100%; 11 | list-style: none; 12 | margin: 0; 13 | padding: 0; 14 | 15 | 16 | * { 17 | margin: 0; 18 | white-space: nowrap; 19 | overflow: hidden; 20 | text-overflow: ellipsis; 21 | } 22 | } 23 | 24 | .list-group-item { 25 | padding: 10px; 26 | // font-size: $font-size-default; 27 | color: #414142; 28 | border-top: var(--border-standard); 29 | 30 | &:first-child { 31 | border-top: 0; 32 | } 33 | 34 | &:focus { 35 | outline: none; 36 | } 37 | 38 | &.active, &:focus { 39 | background: var(--active-color-list); 40 | } 41 | 42 | // &.active .list-item-indicator, 43 | // &.selected .list-item-indicator { 44 | // background-color: $active-unfocused-color; 45 | // } 46 | 47 | &.active .list-item-indicator, 48 | &:focus .list-item-indicator, 49 | // `.selected` is deprecated. Use `.active` instead. 50 | &.selected:focus .list-item-indicator { 51 | background-color: var(--active-color); 52 | } 53 | } 54 | 55 | .list-group-header { 56 | padding: 10px; 57 | } 58 | 59 | // Media objects in lists 60 | .media-object { 61 | margin-top: 3px; 62 | } 63 | 64 | .media-object.pull-left { 65 | margin-right: 10px 66 | } 67 | 68 | .media-object.pull-right { 69 | margin-left: 10px 70 | } 71 | 72 | .media-body { 73 | overflow: hidden; 74 | } 75 | -------------------------------------------------------------------------------- /src/styles/photon/mixins.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Mixins 3 | // -------------------------------------------------- 4 | 5 | // General 6 | // -------------------------------------------------- 7 | 8 | // Clearfix 9 | // Source: http://nicolasgallagher.com/micro-clearfix-hack/ 10 | // 11 | // For modern browsers 12 | // 1. The space content is one way to avoid an Opera bug when the 13 | // contenteditable attribute is included anywhere else in the document. 14 | // Otherwise it causes space to appear at the top and bottom of elements 15 | // that are clearfixed. 16 | // 2. The use of `table` rather than `block` is only necessary if using 17 | // `:before` to contain the top-margins of child elements. 18 | @mixin clearfix() { 19 | &:before, 20 | &:after { 21 | display: table; // 2 22 | content: " "; // 1 23 | } 24 | &:after { 25 | clear: both; 26 | } 27 | } 28 | 29 | // Box shadow 30 | @mixin box-shadow($shadow...) { 31 | -webkit-box-shadow: $shadow; 32 | box-shadow: $shadow; 33 | } 34 | 35 | // Gradients 36 | 37 | // From top to bottom 38 | @mixin linear-gradient($color-from, $color-to) { 39 | // background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%,$color-from), color-stop(100%,$color-to)); // Chrome, Safari4+ 40 | background-image: -webkit-linear-gradient(top, $color-from 0%, $color-to 100%); // Chrome10+, Safari5.1+ 41 | // background-image: linear-gradient(to bottom, $color-from 0%, $color-to 100%); // W3C 42 | } 43 | 44 | // From left to right 45 | @mixin split-linear-gradient($color-from, $color-to) { 46 | background-color: $color-from; // Old browsers 47 | background-image: -webkit-gradient(linear, left top, right top, color-stop(50%,$color-from), color-stop(50%,$color-to)); // Chrome, Safari4+ 48 | background-image: -webkit-linear-gradient(left, $color-from 50%, $color-to 50%); // Chrome10+, Safari5.1+ 49 | background-image: linear-gradient(to right, $color-from 50%, $color-to 50%); // W3C 50 | } 51 | 52 | // From bottom left to top right 53 | @mixin directional-gradient($color-from, $color-to, $deg: 45deg) { 54 | background-color: $color-from; // Old browsers 55 | background-image: -webkit-gradient(linear, left bottom, right top, color-stop(0%,$color-from), color-stop(100%,$color-to)); // Chrome, Safari4+ 56 | background-image: -webkit-linear-gradient($deg, $color-from 0%, $color-to 100%); // Chrome10+, Safari5.1+ 57 | background-image: -moz-linear-gradient($deg, $color-from 0%, $color-to 100%); // FF3.6+ 58 | background-image: linear-gradient($deg, $color-from 0%, $color-to 100%); // W3C 59 | } 60 | -------------------------------------------------------------------------------- /src/styles/photon/navs.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Navs.scss 3 | // -------------------------------------------------- 4 | 5 | .nav-group { 6 | font-size: 14px; 7 | } 8 | 9 | .nav-group-item { 10 | padding: 2px 10px 2px 25px; 11 | display: block; 12 | color: var(--gray-color); 13 | text-decoration: none; 14 | white-space: nowrap; 15 | overflow: hidden; 16 | text-overflow: ellipsis; 17 | 18 | &:active, 19 | &.active { 20 | background-color: #dcdfe1; 21 | } 22 | 23 | .icon { 24 | width: 19px; // Prevents a one pixel cutoff 25 | height: 18px; 26 | float: left; 27 | color: #737475; 28 | margin-top: -3px; 29 | margin-right: 7px; 30 | font-size: 18px; 31 | text-align: center; 32 | } 33 | } 34 | 35 | .nav-group-title { 36 | margin: 0; 37 | padding: 10px 10px 2px; 38 | font-size: 12px; 39 | font-weight: 500; 40 | color: var(--gray-color); 41 | -webkit-filter: brightness(1.2); 42 | } 43 | -------------------------------------------------------------------------------- /src/styles/photon/normalize.scss: -------------------------------------------------------------------------------- 1 | // Based on normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css 2 | // This normalize aims to reduce the number of rules and focus on Chrome. 3 | 4 | // 5 | // 1. Normalize vertical alignment of `progress` in Chrome. 6 | // 7 | 8 | audio, 9 | canvas, 10 | progress, 11 | video { 12 | vertical-align: baseline; // 1 13 | } 14 | 15 | // 16 | // Prevent modern browsers from displaying `audio` without controls. 17 | // 18 | 19 | audio:not([controls]) { 20 | display: none; 21 | } 22 | 23 | // Links 24 | // ========================================================================== 25 | 26 | // 27 | // Improve readability of focused elements when they are also in an 28 | // active/hover state. 29 | // 30 | 31 | a:active, 32 | a:hover { 33 | outline: 0; 34 | } 35 | 36 | // Text-level semantics 37 | // ========================================================================== 38 | 39 | // 40 | // Address styling not present in IE 8/9/10/11, Safari, and Chrome. 41 | // 42 | 43 | abbr[title] { 44 | border-bottom: 1px dotted; 45 | } 46 | 47 | // 48 | // Address style set to `bolder` in Chrome. 49 | // 50 | 51 | b, 52 | strong { 53 | font-weight: bold; 54 | } 55 | 56 | // 57 | // Address styling not present in Chrome. 58 | // 59 | 60 | dfn { 61 | font-style: italic; 62 | } 63 | 64 | // 65 | // Address variable `h1` font-size and margin within `section` and `article` 66 | // contexts in Chrome. 67 | // 68 | 69 | h1 { 70 | font-size: 2em; 71 | margin: 0.67em 0; 72 | } 73 | 74 | // 75 | // Address inconsistent and variable font size. 76 | // 77 | 78 | small { 79 | font-size: 80%; 80 | } 81 | 82 | // 83 | // Prevent `sub` and `sup` affecting `line-height`. 84 | // 85 | 86 | sub, 87 | sup { 88 | font-size: 75%; 89 | line-height: 0; 90 | position: relative; 91 | vertical-align: baseline; 92 | } 93 | 94 | sup { 95 | top: -0.5em; 96 | } 97 | 98 | sub { 99 | bottom: -0.25em; 100 | } 101 | 102 | // Grouping content 103 | // ========================================================================== 104 | 105 | // 106 | // Contain overflow. 107 | // 108 | 109 | pre { 110 | overflow: auto; 111 | } 112 | 113 | // 114 | // Address odd `em`-unit font size rendering. 115 | // 116 | 117 | code, 118 | kbd, 119 | pre, 120 | samp { 121 | font-family: monospace, monospace; 122 | font-size: 1em; 123 | } 124 | 125 | // Forms 126 | // ========================================================================== 127 | 128 | // 129 | // Known limitation: by default, Chrome allows very limited 130 | // styling of `select`, unless a `border` property is set. 131 | // 132 | 133 | // 134 | // 1. Correct color not being inherited. 135 | // Known issue: affects color of disabled elements. 136 | // 2. Correct font properties not being inherited. 137 | // 3. Resets margin 138 | // 139 | 140 | button, 141 | input, 142 | optgroup, 143 | select, 144 | textarea { 145 | color: inherit; // 1 146 | font: inherit; // 2 147 | margin: 0; // 3 148 | } 149 | 150 | // 151 | // Fix the cursor style for Chrome's increment/decrement buttons. For certain 152 | // `font-size` values of the `input`, it causes the cursor style of the 153 | // decrement button to change from `default` to `text`. 154 | // 155 | 156 | input[type="number"]::-webkit-inner-spin-button, 157 | input[type="number"]::-webkit-outer-spin-button { 158 | height: auto; 159 | } 160 | 161 | // 162 | // 1. Address `appearance` set to `searchfield` in Chrome. 163 | // 2. Address `box-sizing` set to `border-box` in Chrome. 164 | // 165 | 166 | input[type="search"] { 167 | -webkit-appearance: textfield; // 1 168 | box-sizing: content-box; // 2 169 | } 170 | 171 | // 172 | // Remove inner padding and search cancel button in Chrome on OS X. 173 | // 174 | 175 | input[type="search"]::-webkit-search-cancel-button, 176 | input[type="search"]::-webkit-search-decoration { 177 | -webkit-appearance: none; 178 | } 179 | 180 | // 181 | // Define consistent border, margin, and padding. 182 | // 183 | 184 | fieldset { 185 | border: 1px solid #c0c0c0; 186 | margin: 0 2px; 187 | padding: 0.35em 0.625em 0.75em; 188 | } 189 | 190 | // 191 | // 1. Correct `color` not being inherited in IE 8/9/10/11. 192 | // 2. Remove padding so people aren't caught out if they zero out fieldsets. 193 | // 194 | 195 | legend { 196 | border: 0; // 1 197 | padding: 0; // 2 198 | } 199 | 200 | // Tables 201 | // ========================================================================== 202 | 203 | // 204 | // Remove most spacing between table cells. 205 | // 206 | 207 | table { 208 | border-collapse: collapse; 209 | border-spacing: 0; 210 | } 211 | 212 | td, 213 | th { 214 | padding: 0; 215 | } 216 | -------------------------------------------------------------------------------- /src/styles/photon/photon.scss: -------------------------------------------------------------------------------- 1 | // Variables 2 | @import "variables.scss"; 3 | 4 | // Mixins 5 | @import "mixins.scss"; 6 | 7 | // Normalize, Base, & Utilities CSS 8 | @import "normalize.scss"; 9 | @import "base.scss"; 10 | @import "utilities.scss"; 11 | 12 | // Components 13 | @import "buttons.scss"; 14 | @import "button-groups.scss"; 15 | @import "bars.scss"; 16 | @import "forms.scss"; 17 | @import "grid.scss"; 18 | @import "images.scss"; 19 | @import "lists.scss"; 20 | @import "navs.scss"; 21 | @import "icons.scss"; 22 | @import "tables.scss"; 23 | @import "tabs.scss"; 24 | -------------------------------------------------------------------------------- /src/styles/photon/tables.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Navs.scss 3 | // -------------------------------------------------- 4 | 5 | table { 6 | width: 100%; 7 | border: 0; 8 | border-collapse: separate; 9 | font-size: 12px; 10 | text-align: left; 11 | } 12 | 13 | thead { 14 | background-color: #f5f5f4; 15 | } 16 | 17 | tbody { 18 | background-color: #fff; 19 | } 20 | 21 | .table-striped tr:nth-child(even) { 22 | background-color: #f5f5f4; 23 | } 24 | 25 | tr:active, 26 | .table-striped tr:active:nth-child(even) { 27 | color: #fff; 28 | background-color: var(--active-color); 29 | } 30 | 31 | thead tr:active { 32 | color: var(--gray-color); 33 | background-color: #f5f5f4; 34 | } 35 | 36 | th { 37 | font-weight: normal; 38 | border-right: var(--border-standard); 39 | border-bottom: var(--border-standard); 40 | } 41 | 42 | th, 43 | td { 44 | padding: 2px 15px; 45 | white-space: nowrap; 46 | overflow: hidden; 47 | text-overflow: ellipsis; 48 | 49 | &:last-child { 50 | border-right: 0; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/styles/photon/tabs.scss: -------------------------------------------------------------------------------- 1 | // Tabs 2 | 3 | .tab-group { 4 | margin-top: -1px; 5 | display: flex; 6 | border-top: 1px solid #989698; 7 | border-bottom: 1px solid #989698; 8 | } 9 | 10 | .tab-item { 11 | position: relative; 12 | flex: 1; 13 | padding: 3px; 14 | font-size: 12px; 15 | text-align: center; 16 | border-left: 1px solid #989698; 17 | @include linear-gradient(#b8b6b8, #b0aeb0); 18 | 19 | &:first-child { 20 | border-left: 0; 21 | } 22 | 23 | &.active { 24 | @include linear-gradient(#d4d2d4, #cccacc); 25 | } 26 | 27 | .icon-close-tab { 28 | position: absolute; 29 | top: 50%; 30 | left: 5px; 31 | width: 15px; 32 | height: 15px; 33 | font-size: 15px; 34 | line-height: 15px; 35 | text-align: center; 36 | color: #666; 37 | opacity: 0; 38 | transition: opacity .1s linear, background-color .1s linear; 39 | border-radius: 3px; 40 | transform: translateY(-50%); 41 | z-index: 10; 42 | } 43 | 44 | &:after { 45 | position: absolute; 46 | top: 0; 47 | right: 0; 48 | bottom: 0; 49 | left: 0; 50 | content: ""; 51 | background-color: rgba(0,0,0,.08); 52 | opacity: 0; 53 | transition: opacity .1s linear; 54 | z-index: 1; 55 | } 56 | 57 | // Okay, I know... this is nuts but... 58 | &:hover:not(.active):after { 59 | opacity: 1; 60 | } 61 | 62 | &:hover .icon-close-tab { 63 | opacity: 1; 64 | } 65 | 66 | .icon-close-tab:hover { 67 | background-color: rgba(0,0,0,.08); 68 | } 69 | } 70 | 71 | .tab-item-fixed { 72 | flex: none; 73 | padding: 3px 10px; 74 | } 75 | -------------------------------------------------------------------------------- /src/styles/photon/utilities.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Utilities styles 3 | // -------------------------------------------------- 4 | 5 | // Utility classes 6 | .selectable-text { 7 | cursor: text; 8 | -webkit-user-select: text; 9 | } 10 | 11 | // Text alignment 12 | .text-center { 13 | text-align: center; 14 | } 15 | 16 | .text-right { 17 | text-align: right; 18 | } 19 | 20 | .text-left { 21 | text-align: left; 22 | } 23 | 24 | // Floats 25 | .pull-left { 26 | float: left; 27 | } 28 | 29 | .pull-right { 30 | float: right; 31 | } 32 | 33 | // Padding 34 | .padded { 35 | padding: var(--padding); 36 | } 37 | 38 | .padded-less { 39 | padding: var(--padding-less); 40 | } 41 | 42 | .padded-more { 43 | padding: var(--padding-more); 44 | } 45 | 46 | // Vertical Padding 47 | .padded-vertically { 48 | padding-top: var(--padding); 49 | padding-bottom: var(--padding); 50 | } 51 | 52 | .padded-vertically-less { 53 | padding-top: var(--padding-less); 54 | padding-bottom: var(--padding-less); 55 | } 56 | 57 | .padded-vertically-more { 58 | padding-top: var(--padding-more); 59 | padding-bottom: var(--padding-more); 60 | } 61 | 62 | // Horizontal Padding 63 | .padded-horizontally { 64 | padding-right: var(--padding); 65 | padding-left: var(--padding); 66 | } 67 | 68 | .padded-horizontally-less { 69 | padding-right: var(--padding-less); 70 | padding-left: var(--padding-less); 71 | } 72 | 73 | .padded-horizontally-more { 74 | padding-right: var(--padding-more); 75 | padding-left: var(--padding-more); 76 | } 77 | 78 | // Padding top 79 | .padded-top { 80 | padding-top: var(--padding); 81 | } 82 | 83 | .padded-top-less { 84 | padding-top: var(--padding-less); 85 | } 86 | 87 | .padded-top-more { 88 | padding-top: var(--padding-more); 89 | } 90 | 91 | // Padding bottom 92 | .padded-bottom { 93 | padding-bottom: var(--padding); 94 | } 95 | 96 | .padded-bottom-less { 97 | padding-bottom: var(--padding-less); 98 | } 99 | 100 | .padded-bottom-more { 101 | padding-bottom: var(--padding-more); 102 | } 103 | 104 | // Set the background-color to set a sidebar back a bit. 105 | .sidebar { 106 | background-color: #f5f5f4; 107 | } 108 | 109 | // Allow the window to be dragged around the desktop by any element in the application. 110 | .draggable { 111 | -webkit-app-region: drag; 112 | } 113 | 114 | // within draggable regions, allow specific elements to be exempted. 115 | .not-draggable { 116 | -webkit-app-region: no-drag; 117 | } 118 | 119 | // Clearfix 120 | .clearfix { 121 | @include clearfix(); 122 | } 123 | -------------------------------------------------------------------------------- /src/styles/photon/variables.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Variables 3 | // -------------------------------------------------- 4 | 5 | // OS X 6 | :root body[platform=darwin] { 7 | --window-color: #ececec; 8 | --window-color-active-toolbar: linear-gradient(#e7e7e7, #c9c9c9); 9 | --window-color-inactive-toolbar: linear-gradient(#f3f3f3, #fbfbfb); 10 | --panel-color: #e2e2e2; 11 | 12 | --active-color: #2f55d2; 13 | --active-color-list: var(--window-color); 14 | --primary-color: #388df8; 15 | --chrome-color: #fff; 16 | --gray-color: #333; 17 | 18 | --btn-default-bg: linear-gradient(#fcfcfc, #f1f1f1); 19 | --btn-primary-bg: linear-gradient(#6eb4f7, #1a82fb); 20 | 21 | --left-panel-bg: #323333; 22 | --left-panel-color: var(--dark-border-color); 23 | --project-item-filter: brightness(.8); 24 | --project-item-active-filter: brightness(1.1); 25 | --project-item-bg: #464847; 26 | --project-item-active-color: var(--dark-border-color); 27 | --project-item-color: var(--dark-border-color); 28 | --graph-bg-stroke: #3e4040; 29 | --default-color: #fff; 30 | --active-unfocused-color: #dcdcdc; 31 | --trans-color: var(--active-color); 32 | 33 | --border-color: #ddd; 34 | --border-standard: 1px solid var(--border-color); 35 | --border-thick: 2px solid var(--border-color); 36 | 37 | --dark-border-color: #c2c0c2; 38 | --border-dark: 1px var(--dark-border-color) solid; 39 | 40 | --darker-bottom-border-color: #a19fa1; 41 | --toolbar-border-color: #939293; 42 | --tab-color-bottom: #462af7; 43 | 44 | --tab-linear-gradiend: linear-gradient(#706f70, #666566); 45 | 46 | --default-border-radius: 4px; 47 | 48 | --padding: 10px; 49 | --padding-mini: 3px; 50 | --padding-less: 5px; 51 | --padding-more: 20px; 52 | 53 | --focus-input-color: #6db3fd; 54 | --popover-shadow: 2px 2px 20px rgba(0,0,0,0.2); 55 | --volume-shadow: 0 -1px 5px rgba(0,0,0,0.1); 56 | --box-shadow-focus: 3px 3px 0 var(--focus-input-color), 57 | -3px -3px 0 var(--focus-input-color), 58 | -3px 3px 0 var(--focus-input-color), 59 | 3px -3px 0 var(--focus-input-color); 60 | --box-shadow-focus-anim: 10px 10px 0 var(--focus-input-color), 61 | -10px -10px 0 var(--focus-input-color), 62 | -10px 10px 0 var(--focus-input-color), 63 | 10px -10px 0 var(--focus-input-color); 64 | 65 | --font-size-default: 13px; 66 | --default-font-family: BlinkMacSystemFont; 67 | --default-font-size: 13px; 68 | 69 | --header-height: 40px; 70 | --header-margin-top: 4px; 71 | --header-margin-bottom: 3px; 72 | --header-padding-top: 5px; 73 | --toolbar-min-height: 22px; 74 | } 75 | 76 | // Windows 77 | :root body[platform=win32] { 78 | --window-color: #f0f0f0; 79 | --panel-color: none; 80 | 81 | --active-color: #cde3f8; 82 | --active-color-list: #cde3f8; 83 | --primary-color: #388df8; 84 | --chrome-color: #fff; 85 | --gray-color: #333; 86 | 87 | --btn-default-bg: #cccccc; 88 | --btn-primary-bg: #cccccc; 89 | 90 | --left-panel-bg: var(--window-color); 91 | --left-panel-color: white; 92 | --project-item-filter: brightness(1.1); 93 | --project-item-active-filter: brightness(.9); 94 | --project-item-bg: #ededed; 95 | --project-item-active-color: #000; 96 | --project-item-color: var(--darker-bottom-border-color); 97 | --graph-bg-stroke: #3e4040; 98 | --default-color: #fff; 99 | --active-unfocused-color: #dcdcdc; 100 | --trans-color: #0078d7; 101 | 102 | --border-color: #ddd; 103 | --border-standard: 1px solid var(--border-color); 104 | --border-thick: 2px solid var(--border-color); 105 | 106 | --dark-border-color: #c2c0c2; 107 | --border-dark: 1px var(--dark-border-color) solid; 108 | 109 | --darker-bottom-border-color: #a19fa1; 110 | --toolbar-border-color: #939293; 111 | --tab-color-bottom: #462af7; 112 | 113 | --tab-linear-gradiend: linear-gradient(#706f70, #666566); 114 | 115 | --default-border-radius: 0px; 116 | 117 | --padding: 10px; 118 | --padding-mini: 3px; 119 | --padding-less: 5px; 120 | --padding-more: 20px; 121 | 122 | --focus-input-color: #0078d7; 123 | --popover-shadow: 2px 2px 20px rgba(0,0,0,0.2); 124 | --volume-shadow: 0 -1px 5px rgba(0,0,0,0.1); 125 | --box-shadow-focus: 2px 2px 0 var(--focus-input-color), 126 | -2px -2px 0 var(--focus-input-color), 127 | -2px 2px 0 var(--focus-input-color), 128 | 2px -2px 0 var(--focus-input-color); 129 | --box-shadow-focus-anim: 2px 2px 0 var(--focus-input-color), 130 | -2px -2px 0 var(--focus-input-color), 131 | -2px 2px 0 var(--focus-input-color), 132 | 2px -2px 0 var(--focus-input-color); 133 | 134 | --font-size-default: 13px; 135 | --default-font-family: "Segoe UI"; 136 | --default-font-size: 13px; 137 | 138 | --header-height: 29px; 139 | --header-margin-top: 0px; 140 | --header-margin-bottom: 0px; 141 | --header-padding-top: 0px; 142 | --toolbar-min-height: 28px; 143 | } 144 | 145 | // Try to use the system's font on whatever platform the user is on. 146 | // $font-family-default: system, -apple-system, ".SFNSDisplay-Regular", "Helvetica Neue", Helvetica, "Segoe UI", sans-serif !default; 147 | // $font-size-default: 13px !default; 148 | $font-weight: 500 !default; 149 | $font-weight-bold: 700 !default; 150 | $font-weight-light: 300 !default; 151 | $line-height-default: 1.6 !default; 152 | 153 | 154 | // Colors 155 | // -------------------------------------------------- 156 | 157 | // Main colors 158 | // $primary-color: #388df8 !default; 159 | // $chrome-color: #fff !default; 160 | // $window-color: #ececec !default; 161 | // $window-color-active-toolbar: linear-gradient(#e7e7e7, #c9c9c9); 162 | // $window-color-inactive-toolbar: linear-gradient(#f3f3f3, #fbfbfb); 163 | // $panel-color: #e2e2e2 !default; 164 | // $left-panel-color: #323333; 165 | 166 | // Buttons 167 | // $btn-default-bg: linear-gradient(#fcfcfc, #f1f1f1); 168 | // $btn-primary-bg: linear-gradient(#6eb4f7, #1a82fb); 169 | // $btn-primary-bg-pressed: linear-gradient(darken(#6eb4f7, 10%), darken(#1a82fb, 10%)); 170 | 171 | // Copy 172 | // $gray-color: #333 !default; 173 | 174 | // Borders 175 | // $border-color: #ddd !default; 176 | // $dark-border-color: #c2c0c2 !default; 177 | // $darker-bottom-border-color: #a19fa1 !default; 178 | // $toolbar-border-color: #939293 !default; 179 | 180 | // Action colors 181 | // $default-color: #fff !default; 182 | $positive-color: #34c84a !default; 183 | $negative-color: #fc605b !default; 184 | $warning-color: #fdbc40 !default; 185 | 186 | // Shades 187 | // $dark-color: #57acf5 !default; 188 | 189 | // Focus and active colors 190 | // $active-color: #2f55d2; 191 | // $active-color: #0295ff; 192 | // $active-color: #fe6358; 193 | // $tab-color-bottom: #462af7; 194 | // $tab-linear-gradiend: linear-gradient(#706f70, #666566); 195 | 196 | // $active-unfocused-color: #dcdcdc; 197 | // $focus-input-color: #6db3fd !default; 198 | 199 | // Other 200 | // -------------------------------------------------- 201 | 202 | // Border radius 203 | // $default-border-radius: 4px; 204 | 205 | // Padding 206 | // $padding: 10px; 207 | // $padding-mini: 3px; 208 | // $padding-less: 5px; 209 | // $padding-more: 20px; 210 | -------------------------------------------------------------------------------- /src/styles/popover.scss: -------------------------------------------------------------------------------- 1 | .popover { 2 | position: relative; 3 | box-sizing: border-box; 4 | flex-direction: column; 5 | padding: var(--padding); 6 | border: var(--border-dark); 7 | background: var(--window-color); 8 | color: WindowText; 9 | border-radius: var(--default-border-radius); 10 | 11 | & h1, & h2 { 12 | font: menu; 13 | font-weight: bold; 14 | margin: 0; 15 | margin-bottom: 3px; 16 | } 17 | 18 | & h2 { 19 | font-weight: normal; 20 | opacity: .5; 21 | } 22 | 23 | & hr { 24 | height: 1px; 25 | background: var(--dark-border-color); 26 | border: 0; 27 | margin: 10px -10px 10px -10px; 28 | // margin-top: 5px; 29 | // margin-bottom: 5px; 30 | // margin-left: -10px; 31 | // margin-right: -10px; 32 | } 33 | } 34 | 35 | .popover, .arrow { 36 | box-shadow: var(--popover-shadow); 37 | } 38 | 39 | .arrow { 40 | content: ''; 41 | display: block; 42 | position: absolute; 43 | width: 15px; 44 | height: 15px; 45 | border: var(--border-dark); 46 | box-shadow: none; 47 | background: var(--window-color); 48 | transform: rotate(45deg); 49 | // z-index: -1; 50 | } 51 | 52 | .popover.arrow-top:before, 53 | .popover.arrow-top:after { 54 | bottom: 100%; 55 | left: 20px; 56 | margin-bottom: -7px; 57 | border-bottom: 0; 58 | border-right: 0; 59 | } 60 | 61 | .popover.arrow-bottom:before, 62 | .popover.arrow-bottom:after { 63 | top: 100%; 64 | left: 20px; 65 | margin-top: -7px; 66 | border-top: 0; 67 | border-left: 0; 68 | } 69 | 70 | .popover.arrow-right:before, 71 | .popover.arrow-right:after { 72 | top: 40px; 73 | left: 100%; 74 | margin-left: -7px; 75 | border-bottom: 0; 76 | border-left: 0; 77 | } 78 | 79 | .arrow { 80 | top: 20px; 81 | left: 0; 82 | margin-left: -9px; 83 | border-top: 0; 84 | border-right: 0; 85 | } 86 | 87 | .popover-transition { 88 | position: fixed; 89 | top: 0; 90 | left: 0; 91 | transition: all .3s ease; 92 | z-index: 1000; 93 | } 94 | 95 | .popover-enter, .popover-leave { 96 | opacity: 0; 97 | } 98 | -------------------------------------------------------------------------------- /src/styles/project-list.scss: -------------------------------------------------------------------------------- 1 | .graph { 2 | display: none; 3 | margin: auto; 4 | position: relative; 5 | text-align: center; 6 | 7 | & text { 8 | fill: var(--active-unfocused-color); 9 | text-anchor: middle; 10 | alignment-baseline: central; 11 | font-weight: 100; 12 | } 13 | } 14 | 15 | .circle-graph, .circle-graph-top { 16 | fill: transparent; 17 | stroke: var(--graph-bg-stroke); 18 | } 19 | 20 | .circle-graph-top { 21 | stroke: var(--active-color); 22 | } 23 | 24 | .current-project, .project-expanded { 25 | display: flex; 26 | flex-direction: column; 27 | } 28 | 29 | project-list { 30 | overflow-y: scroll; 31 | overflow-x: hidden; 32 | } 33 | 34 | body[platform=darwin] project-list { 35 | font-weight: 300; 36 | } 37 | 38 | body[platform=win32] project-list { 39 | &::-webkit-scrollbar { 40 | width: 0px; 41 | } 42 | } 43 | 44 | .project-expanded span { 45 | font: small-caption; 46 | color: var(--dark-border-color); 47 | -webkit-filter: brightness(.7); 48 | } 49 | 50 | .project-desc { 51 | justify-content: space-between; 52 | margin-bottom: 5px; 53 | } 54 | 55 | .project-item { 56 | flex-direction: column; 57 | padding: 0 10px 5px 10px; 58 | transition: all .2 linear; 59 | color: var(--project-item-color); 60 | -webkit-filter: var(--project-item-filter); 61 | 62 | &.active { 63 | color: var(--project-item-active-color); 64 | background: var(--project-item-bg); 65 | padding-top: 5px; 66 | margin-bottom: 5px; 67 | -webkit-filter: var(--project-item-active-filter); 68 | 69 | & .project-icon { 70 | fill: var(--project-item-active-color); 71 | } 72 | } 73 | 74 | & .project-expanded { 75 | margin-left: 22px; 76 | } 77 | 78 | & .project-icon { 79 | margin-right: 5px; 80 | min-width: 16px; 81 | height: 16px; 82 | fill: var(--project-item-color); 83 | } 84 | 85 | // &:hover:active { 86 | // background: lighten($left-panel-color, 5%); 87 | // } 88 | // 89 | // &:hover { 90 | // background: lighten($left-panel-color, 10%); 91 | // } 92 | 93 | & .project-item-header:active { 94 | transform: scale(0.98); 95 | opacity: 0.6; 96 | } 97 | 98 | & span.icon { 99 | margin-right: 5px; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/styles/tabs.scss: -------------------------------------------------------------------------------- 1 | .tab-panel { 2 | position: relative; 3 | margin-top: 12px; 4 | } 5 | 6 | body[platform=win32] .tab-panel { 7 | margin-top: 24px; 8 | } 9 | 10 | .tab-panel .btn-group.tabs { 11 | position: absolute; 12 | display: flex; 13 | } 14 | 15 | body[platform=darwin] .tab-panel .btn-group.tabs { 16 | left: 50%; 17 | right: 50%; 18 | top: -12px; 19 | justify-content: center; 20 | } 21 | 22 | body[platform=win32] .tab-panel .btn-group.tabs { 23 | left: -0px; 24 | top: -25px; 25 | justify-content: left; 26 | } 27 | 28 | .tab-content { 29 | display: flex; 30 | flex-direction: column; 31 | margin-top: 12px; 32 | margin-bottom: 16px; 33 | } 34 | -------------------------------------------------------------------------------- /src/styles/trans-input.scss: -------------------------------------------------------------------------------- 1 | #trans-input { 2 | // width: 100%; 3 | // height: 100%; 4 | // resize: none; 5 | min-height: 50px; 6 | border: 0; 7 | flex-grow: 1; 8 | } 9 | 10 | #trans-input:focus { 11 | outline: none; 12 | } 13 | 14 | #trans-input-container { 15 | display: flex; 16 | position: relative; 17 | width: 100%; 18 | flex-direction: column; 19 | // flex-grow: 1; 20 | } 21 | -------------------------------------------------------------------------------- /src/styles/update-popup.scss: -------------------------------------------------------------------------------- 1 | .update-popup { 2 | // position: absolute; 3 | // bottom: 0; 4 | // left: 0; 5 | // width: 250px; 6 | // padding: 10px; 7 | // background: var(--window-color); 8 | // border: var(--border-standard); 9 | // border-radius: var(--default-border-radius); 10 | // display: flex; 11 | // flex-direction: column; 12 | 13 | & label { 14 | text-overflow: none; 15 | white-space: normal; 16 | } 17 | } 18 | 19 | .central-popup-transition { 20 | position: fixed; 21 | transition: all .5s cubic-bezier(.06,.68,.77,1.45); 22 | z-index: 1000; 23 | } 24 | 25 | .central-popup-enter, .central-popup-leave { 26 | transform: translateY(-200%); 27 | opacity: .5; 28 | } 29 | -------------------------------------------------------------------------------- /src/styles/video-view.scss: -------------------------------------------------------------------------------- 1 | .video-container { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: flex-end; 5 | position: relative; 6 | width: 100%; 7 | background: var(--window-color); 8 | min-height: 150px; 9 | flex-grow: 1; 10 | } 11 | 12 | .video-container canvas { 13 | width: 100%; 14 | height: 100%; 15 | } 16 | 17 | .video-box { 18 | position: absolute; 19 | } 20 | 21 | .video-controls { 22 | display: flex; 23 | flex-grow: 1; 24 | border-top: var(--border-thick); 25 | border-bottom: var(--border-thick); 26 | height: 24px; 27 | box-shadow: inset 0 1px 0 #f5f4f5; 28 | min-width: 338px; 29 | 30 | & button { 31 | border-radius: 0; 32 | border-left: 0; 33 | border-bottom: 0; 34 | border-top: 0; 35 | padding: 2px 8px; 36 | } 37 | 38 | & input[type="range"] { 39 | flex-grow: 1; 40 | align-self: center; 41 | margin-left: 5px; 42 | margin-right: 5px; 43 | } 44 | 45 | & label { 46 | // width: 100px; 47 | align-self: center; 48 | margin-bottom: 0; 49 | margin-right: 5px; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/utils/SubParserWorker.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import fs from 'fs' 4 | import {str_to_dict} from './srt' 5 | 6 | 7 | export default function(path, callback) { 8 | const stats = fs.statSync(path) 9 | if(!stats.isFile()) 10 | return callback(`This file can't be open`) 11 | 12 | if(stats['size'] > 2000000) 13 | return callback(`The file is too big`) 14 | 15 | let result = [] 16 | 17 | fs.readFile(path, 'utf8', (error, data) => { 18 | if(error) { 19 | callback(error) 20 | return 21 | } 22 | 23 | // const parser = str_to_dict(data) 24 | // parser.next() 25 | // 26 | // for(let item of parser) { 27 | // result.push(item) 28 | // } 29 | // 30 | // if(result.length < 5) // a subfile must contain at least 5 titles 31 | // return callback(`This is not a valid SubRip file`) 32 | // 33 | // callback(null, result) 34 | 35 | str_to_dict(data, (item, done) => { 36 | result.push(item) 37 | 38 | if(done) { 39 | if(result.length < 5) 40 | return callback(`This is not a valid SubRip file`) 41 | 42 | callback(null, result) 43 | } 44 | }) 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /src/utils/debounce.js: -------------------------------------------------------------------------------- 1 | // debounces events 2 | export default function debounce(callback, wait=0) { 3 | let timeout = 0 4 | 5 | return (...args) => { 6 | if(timeout === 0) { 7 | timeout = setTimeout(() => { 8 | callback(...args) 9 | timeout = 0 10 | }, wait) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/dialog.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // import {remote} from 'electron' 4 | const dialog = require('electron').remote.dialog 5 | const remote = require('electron').remote 6 | 7 | 8 | const showMsg = function(type, msg, detail) { 9 | const win = remote.getCurrentWindow() 10 | 11 | try { 12 | dialog.showMessageBox(win, { 13 | type, 14 | buttons: ['Ok'], 15 | defaultId: 0, 16 | title: 'Error', 17 | message: msg, 18 | detail: detail, 19 | }) 20 | } catch(e) { 21 | console.log(e) 22 | } 23 | } 24 | 25 | export default { 26 | warning(msg, detail) { 27 | showMsg('error', msg, detail) 28 | }, 29 | 30 | error(msg, detail) { 31 | showMsg('error', msg, detail) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/srt.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const p_number = /^(\d+)[\r\n]*$/m 4 | const p_number_new = /^(\d+)$/ 5 | const p_time = /^(\d{1,2}):(\d{1,2}):(\d{1,2}),(\d{1,3}) --> (\d{1,2}):(\d{1,2}):(\d{1,2}),(\d{1,3})\s*/ 6 | const p_time_item = /(\d{1,2}):(\d{1,2}):(\d{1,2}),(\d{1,3})/ 7 | 8 | 9 | function __time_to_milliseconds(hours, minutes, seconds, milliseconds) { 10 | hours = parseInt(hours) 11 | minutes = parseInt(minutes) 12 | seconds = parseInt(seconds) 13 | milliseconds = parseInt(milliseconds) 14 | 15 | return milliseconds + (seconds + minutes * 60 + hours * 3600) * 1000 16 | } 17 | 18 | function parse_time(match) { 19 | const start = __time_to_milliseconds( 20 | match[1], 21 | match[2], 22 | match[3], 23 | match[4] 24 | ) 25 | const end = __time_to_milliseconds( 26 | match[5], 27 | match[6], 28 | match[7], 29 | match[8] 30 | ) 31 | return {start, end} 32 | } 33 | 34 | export function str_to_dict(content, callback) { 35 | const lines = content.replace(/\r/g, '').split('\n') 36 | // yield lines.length //return the number of lines 37 | 38 | if(lines.length > 0) { 39 | const contains_digit = /.*(\d+).*/.exec(lines[0]) 40 | 41 | if(contains_digit) { 42 | lines[0] = contains_digit[1] 43 | } 44 | } 45 | 46 | let current_line = 0 47 | let current = null //current item we need to yield return 48 | let new_title = false 49 | 50 | for(let line of lines) { 51 | current_line++ 52 | 53 | if(line === '') { 54 | new_title = false 55 | continue 56 | } 57 | 58 | if(p_number.exec(line)) { 59 | if(current) { 60 | // yield {current, current_line} //return 61 | // yield current 62 | callback(current) 63 | } 64 | 65 | current = { 66 | number: parseInt(line), 67 | time: '', 68 | time_markers: '', 69 | text_orig: '', 70 | text_trans: '', 71 | } 72 | new_title = true 73 | continue 74 | } 75 | 76 | const match = p_time.exec(line) 77 | if(match) { 78 | current.time = line 79 | current.time_markers = parse_time(match) 80 | continue 81 | } 82 | 83 | if(new_title) { 84 | if(current.text_orig != '') 85 | current.text_orig += '\n' 86 | current.text_orig += line 87 | } 88 | } 89 | 90 | // yield {current, current_line} //return the last item 91 | // yield current 92 | callback(current, 'done') 93 | } 94 | 95 | export function dict_to_srt(subtitles, empty_titles_policy) { 96 | let result = '' 97 | 98 | for(let subtitle of subtitles) { 99 | if(empty_titles_policy === 1 && subtitle.text_trans == '') // ignore 100 | continue 101 | 102 | result += subtitle.number + '\n' 103 | result += subtitle.time + '\n' 104 | 105 | if(subtitle.text_trans == '') { // replace with original 106 | result += subtitle.text_orig 107 | } else { 108 | result += subtitle.text_trans 109 | } 110 | 111 | result += '\n\n' 112 | } 113 | 114 | return result 115 | } 116 | 117 | // function parser (stream, callback) { 118 | // let current_line = 0 119 | // let current = null //current item we need to yield return 120 | // let new_title = false 121 | // 122 | // stream.on('line', (line) => { 123 | // current_line++ 124 | // 125 | // if(current_line == 1) { 126 | // const contains_digit = /.*(\d+).*/.exec(line) 127 | // 128 | // if(contains_digit) { 129 | // line = contains_digit[1] 130 | // } 131 | // } 132 | // 133 | // // console.log('==>', line) 134 | // 135 | // if(line === '') { 136 | // new_title = false 137 | // return 138 | // } 139 | // 140 | // if(p_number_new.exec(line)) { 141 | // if(current) { 142 | // // stream.pause() 143 | // callback({current, current_line}) //return previous item 144 | // // stream.resume() 145 | // } 146 | // 147 | // current = { 148 | // number: parseInt(line), 149 | // time: '', 150 | // time_markers: '', 151 | // text_orig: '' 152 | // } 153 | // new_title = true 154 | // return 155 | // } 156 | // 157 | // const match = p_time.exec(line) 158 | // if(match) { 159 | // current.time = line 160 | // current.time_markers = parse_time(match) 161 | // return 162 | // } 163 | // 164 | // if(new_title) { 165 | // if(current.text_orig != '') 166 | // current.text_orig += '\n' 167 | // current.text_orig += line 168 | // } 169 | // }); 170 | // 171 | // stream.on('close', () => { 172 | // console.log('EOF'); 173 | // return callback({current, current_line, done: true}) 174 | // }) 175 | // } 176 | -------------------------------------------------------------------------------- /src/utils/time.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function pad(number) { 4 | return (number < 10 ? '0' : '') + number 5 | } 6 | 7 | export function ms2obj(time, ms) { 8 | const result = {} 9 | 10 | if(ms) // calculate milliseconds if needed 11 | result.milliseconds = pad(parseInt(time % 1000)) 12 | 13 | let x = time / 1000 14 | 15 | result.seconds = pad(parseInt(x % 60)) 16 | x /= 60 17 | result.minutes = pad(parseInt(x % 60)) 18 | x /= 60 19 | result.hours = pad(parseInt(x % 24)) 20 | 21 | return result 22 | } 23 | 24 | function add_zerrows(data, length) { 25 | data = data.toString() 26 | const diff = length - data.length 27 | if(diff > 0) { 28 | for(let i = 0; i < diff; i++) { 29 | data += '0' 30 | } 31 | } 32 | 33 | return data 34 | } 35 | 36 | export function ms2string(start, end) { 37 | let result = ms2obj(start, true) 38 | let time_start = `${add_zerrows(result.hours, 2)}:${add_zerrows(result.minutes, 2)}:${add_zerrows(result.seconds, 2)},${add_zerrows(result.milliseconds, 3)}` 39 | result = ms2obj(end, true) 40 | let time_end = `${add_zerrows(result.hours, 2)}:${add_zerrows(result.minutes, 2)}:${add_zerrows(result.seconds, 2)},${add_zerrows(result.milliseconds, 3)}` 41 | 42 | return `${time_start} --> ${time_end}` 43 | } 44 | --------------------------------------------------------------------------------