├── .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 | Close
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 | Download video module
15 | In order to load video files you need to download Webchimera video module
16 |
17 |
18 |
19 |
Downloading...
20 |
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 |
34 |
39 |
40 |
--------------------------------------------------------------------------------
/src/components/Buffering.vue:
--------------------------------------------------------------------------------
1 |
35 |
36 |
37 |
38 | Buffering
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/components/ButtonWait.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/components/CircleGraph.vue:
--------------------------------------------------------------------------------
1 |
70 |
71 |
72 |
73 |
74 |
82 |
83 |
93 |
94 |
99 | {{percent}}
100 |
101 |
102 | {{title}}
103 |
104 |
105 |
106 |
--------------------------------------------------------------------------------
/src/components/CurrentProject.vue:
--------------------------------------------------------------------------------
1 |
68 |
69 |
70 |
71 |
72 |
73 | Total
74 | {{item.value.items.length}}
75 | opening...
76 |
77 |
78 | Translated
79 | {{item.value.translated}}
80 |
81 |
82 | Properties
83 |
84 |
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/src/components/ListItem.vue:
--------------------------------------------------------------------------------
1 |
56 |
57 |
58 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/src/components/LoginModal.vue:
--------------------------------------------------------------------------------
1 |
59 |
60 |
61 |
62 |
67 |
68 | {{description}}
69 |
70 |
80 |
81 |
82 |
83 |
89 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/src/components/Modal.vue:
--------------------------------------------------------------------------------
1 |
78 |
79 |
80 |
86 |
87 |
--------------------------------------------------------------------------------
/src/components/NewTitleModal.vue:
--------------------------------------------------------------------------------
1 |
176 |
177 |
178 |
224 |
225 |
--------------------------------------------------------------------------------
/src/components/PathChooser.vue:
--------------------------------------------------------------------------------
1 |
56 |
57 |
58 |
59 | {{selectedDir}}
60 |
61 | Other...
62 |
63 |
64 |
--------------------------------------------------------------------------------
/src/components/PluginLoader.vue:
--------------------------------------------------------------------------------
1 |
42 |
43 |
44 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/src/components/Popover.vue:
--------------------------------------------------------------------------------
1 |
68 |
69 |
70 |
71 |
72 |
73 |
{{header}}
74 |
{{description}}
75 |
76 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/src/components/PreferencesModal.vue:
--------------------------------------------------------------------------------
1 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 | Sort projects by:
147 |
148 |
152 | {{option.text}}
153 |
154 |
155 |
156 |
157 |
165 |
166 |
167 | Default language:
168 |
169 |
173 | {{lang.name + ' [' + $key + ']'}}
174 |
175 |
176 |
177 |
178 |
179 | Automatically check for updates:
180 |
181 |
182 |
183 |
184 |
185 |
186 | Credentials:
187 |
188 | {{model.ost.verified ? 'Change...' : 'Login...'}}
189 |
190 |
191 |
192 |
193 | Use SSL:
194 |
195 |
196 |
197 |
198 |
199 |
204 |
205 |
206 |
215 |
216 |
217 |
218 |
--------------------------------------------------------------------------------
/src/components/ProjectDeletePopover.vue:
--------------------------------------------------------------------------------
1 |
63 |
64 |
65 |
83 |
84 |
--------------------------------------------------------------------------------
/src/components/ProjectIcon.vue:
--------------------------------------------------------------------------------
1 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/src/components/ProjectItem.vue:
--------------------------------------------------------------------------------
1 |
165 |
166 |
167 |
168 |
175 |
176 |
181 |
182 |
183 |
191 |
192 |
193 |
--------------------------------------------------------------------------------
/src/components/ProjectList.vue:
--------------------------------------------------------------------------------
1 |
74 |
75 |
76 |
84 |
85 |
86 |
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/src/components/ProjectPropertiesPopover.vue:
--------------------------------------------------------------------------------
1 |
141 |
142 |
143 |
144 |
152 |
153 | Result file name:
154 |
160 |
161 |
162 |
163 |
164 | Empty titles policy:
165 |
166 |
170 | {{option.text}}
171 |
172 |
173 |
174 |
175 |
176 | Language:
177 |
178 |
182 | {{option.name + ($key !== '0' ? ' [' + $key + ']' : '')}}
183 |
184 |
185 |
186 |
187 |
203 |
204 |
212 |
213 |
214 |
215 |
--------------------------------------------------------------------------------
/src/components/ShiftModal.vue:
--------------------------------------------------------------------------------
1 |
78 |
79 |
80 |
81 |
85 |
86 | When using negative values be careful, you can lose some titles at the beginning. Time is represented in millisecond (1000 ms = 1 s).
87 |
88 |
106 |
107 |
113 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/src/components/Spinner.vue:
--------------------------------------------------------------------------------
1 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/components/SubFilter.vue:
--------------------------------------------------------------------------------
1 |
58 |
59 |
60 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/src/components/SubList.vue:
--------------------------------------------------------------------------------
1 |
219 |
220 |
221 |
222 |
223 |
224 |
235 |
236 |
237 |
241 |
{{{item.value.text_orig | escape}}}
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
--------------------------------------------------------------------------------
/src/components/TabItem.vue:
--------------------------------------------------------------------------------
1 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/src/components/TabPanel.vue:
--------------------------------------------------------------------------------
1 |
53 |
54 |
55 |
56 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/src/components/Tabs.vue:
--------------------------------------------------------------------------------
1 |
39 |
40 |
41 |
42 |
49 | {{choice.text}}
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/src/components/TimeInput.vue:
--------------------------------------------------------------------------------
1 |
47 |
48 |
49 |
50 |
51 |
52 | .
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/src/components/TransInput.vue:
--------------------------------------------------------------------------------
1 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/components/UpdatePopup.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 |
50 |
51 |
--------------------------------------------------------------------------------
/src/components/UrlPopover.vue:
--------------------------------------------------------------------------------
1 |
57 |
58 |
59 |
89 |
90 |
--------------------------------------------------------------------------------
/src/components/VProgress.vue:
--------------------------------------------------------------------------------
1 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/components/VideoControls.vue:
--------------------------------------------------------------------------------
1 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
{{player.current_time | time}}
105 |
106 |
107 |
--------------------------------------------------------------------------------
/src/components/VideoView.vue:
--------------------------------------------------------------------------------
1 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
--------------------------------------------------------------------------------
/src/components/VolumeControl.vue:
--------------------------------------------------------------------------------
1 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
--------------------------------------------------------------------------------
/src/components/WindowButtons.vue:
--------------------------------------------------------------------------------
1 |
73 |
74 |
75 |
76 |
81 |
82 |
88 |
89 |
94 |
95 |
96 |
97 |
--------------------------------------------------------------------------------
/src/components/aButton.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/components/iButton.vue:
--------------------------------------------------------------------------------
1 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/components/range.vue:
--------------------------------------------------------------------------------
1 |
54 |
55 |
56 |
57 |
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 |
--------------------------------------------------------------------------------