├── .editorconfig ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── Open_In_BeyondPlayer_Chrome ├── Open_In_BeyondPlayer_Chrome.pem ├── README.md ├── background.html ├── background.js ├── common.js ├── icon.png ├── icon128.png ├── icon16.png ├── icon48.png ├── manifest.json └── options.html ├── README.md ├── icon_lite.icns ├── icon_pro.icns ├── jsconfig.json ├── scripts ├── 0_build_mas.sh ├── 1_packageAppStore.sh ├── build_dev.sh ├── build_lite.sh ├── build_pro.sh ├── change_dep.sh ├── child.plist ├── copy_mpv_deps.sh ├── dist │ ├── ffmpeg │ ├── ffprobe │ ├── libavcodec.58.dylib │ ├── libavdevice.58.dylib │ ├── libavfilter.7.dylib │ ├── libavformat.58.dylib │ ├── libavutil.56.dylib │ ├── libjpeg.9.dylib │ ├── libmpv.1.dylib │ ├── libogg.0.dylib │ ├── libopus.0.dylib │ ├── libpostproc.55.dylib │ ├── libswresample.3.dylib │ ├── libswscale.5.dylib │ ├── libtheoradec.1.dylib │ ├── libtheoraenc.1.dylib │ ├── libvorbis.0.dylib │ ├── libvorbisenc.2.dylib │ └── libx264.155.dylib ├── extend-info-lite.plist ├── extend-info-pro.plist ├── i18n_linter ├── install_ffmpeg.sh ├── install_mpv.sh ├── install_mpvjs.sh ├── lite.parent.plist ├── loginhelper.plist └── parent.plist └── src ├── .eslintignore ├── .eslintrc ├── .prettierrc ├── README.md ├── app ├── assets │ ├── css │ │ ├── popup.css │ │ ├── react-search-input.css │ │ └── styles.css │ ├── documentation │ │ ├── vid_tutorial.html │ │ └── vids │ │ │ ├── advanced.mp4 │ │ │ └── essential.mp4 │ └── fonts │ │ ├── FiraSans-Regular.ttf │ │ ├── FiraSansCondensed-Regular.ttf │ │ ├── Merriweather-Regular.ttf │ │ ├── Oswald-Regular.ttf │ │ ├── TitilliumWeb-Bold.ttf │ │ ├── TitilliumWeb-Regular.ttf │ │ └── TitilliumWeb-SemiBold.ttf ├── i18n │ ├── en.json │ ├── index.js │ ├── ru.json │ ├── uk.json │ ├── zh-CN.json │ └── zh-TW.json ├── index.html ├── inter_op_webpane.js ├── inter_op_youtube.js ├── inter_op_youtube_browser.js ├── js │ ├── Components │ │ ├── BaseSubtitleSettingsPane.jsx │ │ ├── Common.less │ │ ├── DictionaryItem.jsx │ │ ├── DictionaryPane.jsx │ │ ├── DownloadSubtitleDialog.jsx │ │ ├── ExportAnkiDialog │ │ │ ├── AnkiVideoCard.jsx │ │ │ ├── AnkiVideoCard.module.less │ │ │ ├── ExportAnkiDialog.jsx │ │ │ ├── ExportAnkiDialog.less │ │ │ ├── ExportAnkiVidLib.jsx │ │ │ ├── ExportAnkiVidLib.module.less │ │ │ └── index.js │ │ ├── LineItem.jsx │ │ ├── LineItemList.jsx │ │ ├── Loader │ │ │ ├── Loader.jsx │ │ │ ├── Loader.module.less │ │ │ ├── Loader.spec.js │ │ │ └── index.js │ │ ├── LocalSubtitleSettingsPane.jsx │ │ ├── MRUItem.jsx │ │ ├── MRUPane.jsx │ │ ├── MessageDialog.jsx │ │ ├── MessagePane.jsx │ │ ├── OpenMediaPane.jsx │ │ ├── PlayerControls.jsx │ │ ├── PlayerControls.less │ │ ├── PlayerSubtitle.jsx │ │ ├── PopupPane.less │ │ ├── ProgressPane.jsx │ │ ├── PromptDialog.jsx │ │ ├── ReactList.jsx │ │ ├── SearchInput.jsx │ │ ├── SettingsPane │ │ │ ├── SettingsPane.jsx │ │ │ ├── index.js │ │ │ └── sections │ │ │ │ ├── ExternalDictionary.jsx │ │ │ │ ├── Language.jsx │ │ │ │ ├── LoopCount.jsx │ │ │ │ ├── MiscSection.jsx │ │ │ │ ├── PlayerSubtitleColor.jsx │ │ │ │ ├── Socks5Proxy.jsx │ │ │ │ ├── SubtitleFont.jsx │ │ │ │ ├── Voices.jsx │ │ │ │ ├── WordBehavior.jsx │ │ │ │ └── hooks.js │ │ ├── SubControls.jsx │ │ ├── SubControls.less │ │ ├── SubtitlePane.jsx │ │ ├── SwitchesPane │ │ │ ├── SwitchesPane.jsx │ │ │ └── SwitchesPane.less │ │ ├── TagEditor │ │ │ ├── Input.jsx │ │ │ ├── Suggestions.jsx │ │ │ ├── Tag.jsx │ │ │ └── TagEditor.jsx │ │ ├── TagEditorDialog.jsx │ │ ├── TitlePane.jsx │ │ ├── TunerPane.jsx │ │ ├── TunerPane.less │ │ ├── VidItem.jsx │ │ ├── VidItem.module.less │ │ ├── VidLibPane.jsx │ │ ├── VidLibPane.module.less │ │ ├── VidLineItem.jsx │ │ ├── WebPane.jsx │ │ ├── WebSourceItem.jsx │ │ ├── WindowButtons.jsx │ │ ├── WindowButtonsStyles.js │ │ ├── WordDefintionEditorPane.jsx │ │ ├── WordItem.jsx │ │ ├── WordListPane.jsx │ │ ├── WordNotifier │ │ │ ├── WordNotifierComponent.js │ │ │ ├── WordNotifierItem.js │ │ │ ├── constants.js │ │ │ ├── helpers.js │ │ │ ├── notification.css │ │ │ ├── utils.js │ │ │ └── validators.js │ │ ├── YouTubeBrowser.jsx │ │ ├── YouTubeIFramePlayer.jsx │ │ └── YouTubeSubtitleSettingsPane.jsx │ ├── Containers │ │ ├── App.jsx │ │ └── App.less │ ├── Images │ │ ├── back.jpg │ │ ├── book.svg │ │ ├── equalizer.svg │ │ ├── loop.svg │ │ ├── pause.svg │ │ ├── play.svg │ │ ├── pm_auto_pause.svg │ │ ├── pm_auto_repeat.svg │ │ ├── pm_repeat.svg │ │ ├── pm_sequence.svg │ │ ├── sub.png │ │ ├── switches.svg │ │ └── tutorial.svg │ ├── Model │ │ ├── Caption │ │ │ ├── addic7ed.js │ │ │ ├── index.js │ │ │ └── opensubtitles.js │ │ ├── Dictionary │ │ │ └── index.js │ │ ├── Export │ │ │ ├── destinations │ │ │ │ └── anki │ │ │ │ │ ├── AnkiFileDestination.js │ │ │ │ │ └── index.js │ │ │ ├── index.js │ │ │ ├── readme.md │ │ │ └── sources │ │ │ │ ├── VidLibExportSourceAdapter.js │ │ │ │ └── __tests__ │ │ │ │ └── VidLibExportSourceAdapter.spec.js │ │ ├── FFmpegHelper │ │ │ └── index.js │ │ ├── KUtils │ │ │ └── index.js │ │ ├── LRCHelper │ │ │ └── index.js │ │ ├── MRUFiles │ │ │ └── index.js │ │ ├── MansonryLayout │ │ │ └── index.js │ │ ├── PlayMode.js │ │ ├── Settings │ │ │ └── index.js │ │ ├── SubExtractor │ │ │ └── index.js │ │ ├── SubStore │ │ │ └── index.js │ │ ├── Subtitle │ │ │ ├── index.js │ │ │ ├── parse.js │ │ │ ├── parseTimestamps.js │ │ │ ├── resync.js │ │ │ ├── stringify.js │ │ │ ├── stringifyVtt.js │ │ │ ├── toMS.js │ │ │ ├── toSrtTime.js │ │ │ └── toVttTime.js │ │ ├── TagSearch │ │ │ └── index.js │ │ ├── VidLib │ │ │ ├── VidConverter.js │ │ │ └── index.js │ │ ├── WebSearch │ │ │ └── index.js │ │ ├── WordBook │ │ │ └── index.js │ │ ├── YoutubeSubtitle │ │ │ └── index.js │ │ └── youtube-subtitle-converter │ │ │ ├── gen.js │ │ │ ├── index.js │ │ │ ├── parser.js │ │ │ └── util.js │ └── renderer.jsx ├── main.js └── static │ └── youtube.html ├── art ├── closed-caption-logo-small.png ├── closed-caption-logo.png ├── manage_subtitle.psd ├── noun_1650297_cc.png ├── settings-512.png ├── settings_1.png ├── sub.png ├── subitle_1.png ├── subitle_1.svg └── subtitle.png ├── babel.config.js ├── etc └── lookup │ └── osx-lookup ├── jest.config.js ├── oss-attribution └── attribution.txt ├── package-lock.json ├── package.json └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | [*] 4 | charset = utf-8 5 | indent_size = 4 6 | indent_style = space 7 | insert_final_newline = true 8 | max_line_length = 150 9 | trim_trailing_whitespace = true 10 | [*.scss] 11 | indent_size = 2 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | app/build 2 | src/app/build 3 | src/node_modules 4 | art/* 5 | release-builds 6 | mas-builds/ 7 | test/* 8 | video/* 9 | build/* 10 | src/app/main_packed.js 11 | bundle_report.html 12 | src/app/main_packed.js 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${workspaceRoot}/src/app/main_packed.js", 9 | "stopOnEntry": false, 10 | "args": [], 11 | "cwd": "${workspaceRoot}/src", 12 | "runtimeExecutable": "${workspaceRoot}/src/node_modules/.bin/electron", 13 | "runtimeArgs": [".", "--enable-logging"], 14 | "env": {}, 15 | "externalConsole": false, 16 | "sourceMaps": false, 17 | "outDir": null 18 | }, 19 | { 20 | "name": "Attach", 21 | "type": "node", 22 | "request": "attach", 23 | "port": 5858, 24 | "sourceMaps": false, 25 | "outDir": null 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": [{ "directory": "./src", "!cwd": true }], 3 | "eslint.nodePath": "./src/node_modules", 4 | "jest.rootPath": "src", 5 | "debug.javascript.usePreview": false // TODO: will be removed after we upgrade to Electron 6.x or newer 6 | } 7 | -------------------------------------------------------------------------------- /Open_In_BeyondPlayer_Chrome/Open_In_BeyondPlayer_Chrome.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCpS0szuRMxbQwa 3 | 46CJ5bJVreXoPOQ35Hw0R6nFPUTLQCm2Y/yLZPNdvHfoVTHq5IjEGs4/farEtItI 4 | +HO5eQdhNWoa6XKM1xRJgisIsVEHksds0cKIaM+ToYkuyX+AQvXrIZcGZtR8dujh 5 | 0yk8I4dFcHwjxVWlnlG/KTENJg7TJcLpGNE3uqXjWq+R3AqV9MWQ/zutLULLY6wT 6 | fZtAH2jefDu7yQEyBrAx4vNQhSygfnQJeyKunu8Le/sUt9eFngl67g041Y076RhU 7 | ZD640rVESy84S/uaXWVv8NQasSB/VpuTNk7Gf8F1zo0UtD3XYk3+FVTnIyE3P5qx 8 | px54c/MTAgMBAAECggEATlYaYsEpie3pM4oP7687cZ0JrQh8HAbcmD5u5udebWRv 9 | yMeEhSX93OqIW5gMhKIiTQgQVt26BtJ6PxZESlcgHJGgWvqZ+GTPrOIQMrIGCiiX 10 | ohl/2CoTnz8YoO85UerqK1a8MQilG0DUqyZQRtUz02Rk9RJrersDHZ8cZPau1nG6 11 | a1eoMo5XbL61wL3pkbo1XL4AtOpXe63m1ACCqi+JdRX1pxY+kbMkVx8CeksTQ2Oj 12 | HGkMoPZD3YHFBncIpBmdb6El1iiIiOUi5N5nHsH7dRFh88vCW8KuQZ8ufQA55nW+ 13 | 7KQ16JCvxizi/msTPzEEaepT4HFg5VHZ4j4ngyftJQKBgQDYdmwFgu/7jmPAiImy 14 | PTb34Hd4W1TTw/DQCiBsW5IMmNqgzgYltYg00jV/IOvUT2dR8dtzXscWGF9xjvjC 15 | XAE7H27YJ0lhZfS+VfMpturiWYRRXctQvUk2uSlhwAIj7O2jlBTFnS/wT/iatzpV 16 | w6SC2KT4mGY0enNVHT6KmeySBQKBgQDIN1IJygjz8XGa2xSkTM6CSL2B+zhZgKKS 17 | ed9mW4qBhBNVo3hyQq7pb1esGfmcIj98FLFxEiKKm6ZRnF6qGEZ2skGVN6aDFgJM 18 | kCTG+qURqxkQTMOVdIX5B8hG+7ZPhv7dj+9s2YPzR2cn6anTH1nLx6qAJzQCcIU/ 19 | 7Vvaj96ENwKBgGisIyIrJfcD7NKuc79gAJOu1La7m3JnnqxLKVCcmyxCQf5egfR8 20 | Kug3+iyGc+OPnguvI4pPe4AAuy6Dj2EU8ndvhL87iC10Cvx7PYGfdUeNOAHMlENv 21 | tNakhRFCswZCTMu8EKtajlLrqPDPx4Kvf37SWjvoHgwkZl7zLEoDkrUtAoGAMJVz 22 | +9ohyAg7uAcXgDL/HZBHJCZw6w8S5BZcxnrKJlmFU4+iZ5+U0CJrlOCMuH17CEIB 23 | ON3csePJPR6DviS73Iuu7GWfq0mI70k/E2W47oulPlZSU479/4sK52anO68XY25M 24 | /A3gPgWCm6XQxuFhqdheoFBjB4CEZRnU4zlsFosCgYB7EpDKZj8fLr89yJ4PCoXH 25 | 54OwNaC/ZfKQkFjj0JyOhgeIExl1YRuK/NHKNG6EAxdMZXGvk0y653p960Qj3/mO 26 | GiFlPiElJNKj26Zn83FVr440yO5yRZ/3RYkN+uv7NpjeVd9P8ekHJJ1ykIpxUDzI 27 | JUy9PId9XOR4/B6JgxUcyA== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /Open_In_BeyondPlayer_Chrome/README.md: -------------------------------------------------------------------------------- 1 | # Open In BeyondPlayer (Chrome) 2 | -------------------------------------------------------------------------------- /Open_In_BeyondPlayer_Chrome/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Open_In_BeyondPlayer_Chrome/background.js: -------------------------------------------------------------------------------- 1 | import { updateBrowserAction, openInBeyondPlayer } from "./common.js"; 2 | 3 | let showForPages = ["https://*.youtube.com/*"]; 4 | 5 | updateBrowserAction(); 6 | 7 | [ 8 | ["page", "pageUrl"], 9 | ["link", "linkUrl"], 10 | ["video", "srcUrl"] 11 | ].forEach(([item, linkType]) => { 12 | chrome.contextMenus.create({ 13 | title: `Open in BeyondPlayer Pro`, 14 | id: `open${item}inbeyondplayer`, 15 | contexts: [item], 16 | documentUrlPatterns: showForPages, 17 | onclick: (info, tab) => { 18 | console.log("info:" + JSON.stringify(info)); 19 | openInBeyondPlayer(tab.id, info[linkType]); 20 | } 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /Open_In_BeyondPlayer_Chrome/common.js: -------------------------------------------------------------------------------- 1 | export function openInBeyondPlayer(tabId, url) { 2 | const baseURL = `beyondplayer://open?`; 3 | const params = [`url=${encodeURIComponent(url)}`]; 4 | const code = ` 5 | var link = document.createElement('a'); 6 | link.href='${baseURL}${params.join("&")}'; 7 | document.body.appendChild(link); 8 | link.click(); 9 | `; 10 | chrome.tabs.executeScript(tabId, { code }); 11 | } 12 | 13 | export function updateBrowserAction() { 14 | chrome.browserAction.setPopup({ popup: "" }); 15 | chrome.browserAction.onClicked.addListener(() => { 16 | // get active window 17 | chrome.tabs.query({ currentWindow: true, active: true }, tabs => { 18 | if (tabs.length === 0) return; 19 | // TODO: filter url 20 | const tab = tabs[0]; 21 | if (tab.id === chrome.tabs.TAB_ID_NONE) return; 22 | openInBeyondPlayer(tab.id, tab.url); 23 | }); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /Open_In_BeyondPlayer_Chrome/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/Open_In_BeyondPlayer_Chrome/icon.png -------------------------------------------------------------------------------- /Open_In_BeyondPlayer_Chrome/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/Open_In_BeyondPlayer_Chrome/icon128.png -------------------------------------------------------------------------------- /Open_In_BeyondPlayer_Chrome/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/Open_In_BeyondPlayer_Chrome/icon16.png -------------------------------------------------------------------------------- /Open_In_BeyondPlayer_Chrome/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/Open_In_BeyondPlayer_Chrome/icon48.png -------------------------------------------------------------------------------- /Open_In_BeyondPlayer_Chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | 4 | "name": "Open In BeyondPlayer Pro", 5 | "description": "Open videos in BeyondPlayer Pro.", 6 | "version": "2.2.0", 7 | "options_page": "options.html", 8 | "background": { 9 | "page": "background.html" 10 | }, 11 | "browser_action": { 12 | "default_icon": "icon.png", 13 | "default_title": "Open In BeyondPlayer Pro" 14 | }, 15 | "permissions": ["tabs", "activeTab", "contextMenus", "storage"], 16 | "icons": { 17 | "16": "icon16.png", 18 | "48": "icon48.png", 19 | "128": "icon128.png" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Open_In_BeyondPlayer_Chrome/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Options 8 | 26 | 27 | 28 |
29 |

Please visit circleapps.co for more about BeyondPlayer Pro.

30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![image](https://user-images.githubusercontent.com/44904628/153615541-ddd54ca1-aed6-4834-9ea6-d785691f4ea6.png) 2 | 3 | 4 | BeyondPlayer (originally called Source Player) is an open source video player for English learner. It supports almost all common video types and playback functionalities. 5 | 6 | What makes it unique is its dedicated features for English study, such as: 7 | 8 | - View definition in pop-up dictionary 9 | - Drag and select to loop lines 10 | - Save video clip to clip library 11 | - Highly configurable web dictionaries 12 | - YouTube player and browser 13 | - Collect word to word book and word list 14 | - Blur out hardcoded subtitle 15 | - Automatically hide subtitle 16 | - Automatically download subtitles 17 | 18 | See [Product Page](https://circleapps.co/) or [BeyondPlayer Wiki](https://github.com/circleapps/beyondplayer/wiki) for more about this app. 19 | 20 | For the moment, it supports MacOS only, but I hope it will support Windows soon. 21 | 22 | --- 23 | 24 | # Install dependencies 25 | 26 | ``` 27 | # Before installing the dependencies, make sure you have nodejs (12.x), yarn, xcode installed. 28 | 29 | cd {PROJECT_ROOT}/scripts 30 | ./install_mpvjs.sh 31 | ``` 32 | 33 | # Debug 34 | 35 | ``` 36 | cd {PROJECT_ROOT}/src 37 | npm run dev 38 | ``` 39 | 40 | Then Press F5 in Visual Studio Code 41 | 42 | # Build 43 | 44 | ## Dev Build 45 | 46 | ``` 47 | # Before building it, make sure you have electron-packager installed globally (by npm install electron-packager -g) 48 | 49 | cd {PROJECT_ROOT}/scripts 50 | ./build_dev.sh 51 | ``` 52 | 53 | If everything goes as planned, you will find the dev build under {PROJECT_ROOT}/build. 54 | 55 | --- 56 | 57 | # License 58 | 59 | BeyondPlayer is based on [mpv.js](https://github.com/Kagami/mpv.js/) and [Electron](https://electronjs.org/), for complete open source softwares used by this app, see 60 | [Open Source Software Attribution](https://github.com/circleapps/sourceplayer/wiki/Open-Source-Software-Attribution) 61 | 62 | --- 63 | 64 | # Acknowlegement 65 | 66 | BeyondPlayer leveraged [OpenSubtitle API](https://opensubtitles.org) for subtitle searching and downloading. 67 | -------------------------------------------------------------------------------- /icon_lite.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/icon_lite.icns -------------------------------------------------------------------------------- /icon_pro.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/icon_pro.icns -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules/", "src/node_modules/"], 3 | "typeAcquisition": { 4 | "include": ["PRO_VERSION"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /scripts/0_build_mas.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ $1 == lite ]] ; then 4 | APP_NAME="BeyondPlayer Lite" 5 | BOUNDLE_ID="co.circleapps.sourceplayerlite" 6 | else 7 | APP_NAME="BeyondPlayer Pro" 8 | BOUNDLE_ID="co.circleapps.sourceplayer" 9 | fi 10 | 11 | echo $APP_NAME 12 | 13 | set -ex 14 | cd "$( dirname "${BASH_SOURCE[0]}" )" 15 | #rm -rf dist 16 | #mkdir dist 17 | 18 | copy_deps() { 19 | local dep=$1 20 | local depname=$(basename $dep) 21 | [[ -e dist/$depname ]] || install -m755 $dep dist 22 | otool -L $dep | awk '/\/usr\/local.*\.dylib /{print $1}' | while read lib; do 23 | local libname=$(basename $lib) 24 | [[ $depname = $libname ]] && continue 25 | echo $libname 26 | install_name_tool -change $lib @loader_path/$libname dist/$depname 27 | [[ -e dist/$libname ]] && continue 28 | install -m755 $lib dist 29 | copy_deps $lib 30 | done 31 | } 32 | 33 | #set +x 34 | #copy_deps /usr/local/lib/libmpv.1.dylib 35 | #set -x 36 | 37 | # See . 38 | install_name_tool -change /System/Library/Frameworks/CoreImage.framework/Versions/A/CoreImage /System/Library/Frameworks/QuartzCore.framework/Versions/A/Frameworks/CoreImage.framework/Versions/A/CoreImage dist/libavfilter.7.dylib 39 | install_name_tool -change /usr/local/opt/mpv/lib/libmpv.1.dylib '@loader_path/libmpv.1.dylib' ../src/node_modules/mpv.js/build/Release/mpvjs.node 40 | 41 | chmod +x dist/*.dylib 42 | cp -r dist/* ../src/node_modules/mpv.js/build/Release/ 43 | 44 | rm -f ../src/app/build/renderer.js.map 45 | 46 | 47 | if [[ $1 == "lite" ]] ; then 48 | npm run lite --prefix ../src 49 | else 50 | npm run prod --prefix ../src 51 | fi 52 | 53 | 54 | if [[ $1 == "lite" ]] ; then 55 | 56 | electron-packager ../src "$APP_NAME" \ 57 | --overwrite \ 58 | --asar.unpackDir=node_modules \ 59 | --ignore=app/js --ignore=webpack.config.js --ignore=app/main.js --ignore=jest.config.js --ignore=babel.config.js --ignore=etc \ 60 | --platform=mas --arch=x64 --out=../build/mas --icon=../icon_lite.icns --app-bundle-id="$BOUNDLE_ID" --electron-version=5.0.13 \ 61 | --extend-info extend-info-lite.plist 62 | 63 | else 64 | 65 | electron-packager ../src "$APP_NAME" \ 66 | --overwrite \ 67 | --asar.unpackDir="{node_modules,etc}" \ 68 | --ignore=app/js --ignore=webpack.config.js --ignore=app/main.js --ignore=jest.config.js --ignore=babel.config.js \ 69 | --platform=mas --arch=x64 --out=../build/mas --icon=../icon_pro.icns --app-bundle-id="$BOUNDLE_ID" --electron-version=5.0.13 \ 70 | --extend-info extend-info-pro.plist 71 | 72 | fi 73 | -------------------------------------------------------------------------------- /scripts/build_dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | cd "$( dirname "${BASH_SOURCE[0]}" )" 5 | #rm -rf dist 6 | #mkdir dist 7 | 8 | copy_deps() { 9 | local dep=$1 10 | local depname=$(basename $dep) 11 | [[ -e dist/$depname ]] || install -m755 $dep dist 12 | otool -L $dep | awk '/\/usr\/local.*\.dylib /{print $1}' | while read lib; do 13 | local libname=$(basename $lib) 14 | [[ $depname = $libname ]] && continue 15 | echo $libname 16 | install_name_tool -change $lib @loader_path/$libname dist/$depname 17 | [[ -e dist/$libname ]] && continue 18 | install -m755 $lib dist 19 | copy_deps $lib 20 | done 21 | } 22 | 23 | #set +x 24 | #copy_deps /usr/local/lib/libmpv.1.dylib 25 | #set -x 26 | 27 | # See . 28 | install_name_tool -change /System/Library/Frameworks/CoreImage.framework/Versions/A/CoreImage /System/Library/Frameworks/QuartzCore.framework/Versions/A/Frameworks/CoreImage.framework/Versions/A/CoreImage dist/libavfilter.7.dylib 29 | install_name_tool -change /usr/local/opt/mpv/lib/libmpv.1.dylib '@loader_path/libmpv.1.dylib' ../src/node_modules/mpv.js/build/Release/mpvjs.node 30 | 31 | chmod +x dist/*.dylib 32 | cp -r dist/* ../src/node_modules/mpv.js/build/Release/ 33 | 34 | rm -f ../src/app/build/renderer.js.map 35 | 36 | npm run dev_no_watch --prefix ../src 37 | 38 | electron-packager ../src --overwrite --ignore=app/js --platform=darwin --arch=x64 --out=../build/dev --icon=../icon_pro.icns --prune \ 39 | --electron-version=5.0.13 --extend-info extend-info-pro.plist -------------------------------------------------------------------------------- /scripts/build_lite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sh ./0_build_mas.sh lite 4 | sh ./1_packageAppStore.sh lite -------------------------------------------------------------------------------- /scripts/build_pro.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sh ./0_build_mas.sh 4 | sh ./1_packageAppStore.sh -------------------------------------------------------------------------------- /scripts/change_dep.sh: -------------------------------------------------------------------------------- 1 | change_deps() { 2 | local dep=$1 3 | local depname=$(basename $dep) 4 | echo $depname 5 | # [[ -e dist/$depname ]] || install -m755 $dep dist 6 | otool -L $dep | awk '/\/usr\/local.*\.dylib /{print $1}' | while read lib; do 7 | local libname=$(basename $lib) 8 | [[ $depname = $libname ]] && continue 9 | echo $libname 10 | install_name_tool -change $lib @loader_path/$libname dist/$depname 11 | # [[ -e dist/$libname ]] && continue 12 | # install -m755 $lib dist 13 | done 14 | } 15 | 16 | change_deps $1 -------------------------------------------------------------------------------- /scripts/child.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.inherit 8 | 9 | 10 | -------------------------------------------------------------------------------- /scripts/copy_mpv_deps.sh: -------------------------------------------------------------------------------- 1 | copy_deps() { 2 | local dep=$1 3 | local depname=$(basename $dep) 4 | [[ -e dist/$depname ]] || install -m755 $dep dist 5 | otool -L $dep | awk '/\/usr\/local.*\.dylib /{print $1}' | while read lib; do 6 | local libname=$(basename $lib) 7 | [[ $depname = $libname ]] && continue 8 | echo $libname 9 | install_name_tool -change $lib @loader_path/$libname dist/$depname 10 | [[ -e dist/$libname ]] && continue 11 | install -m755 $lib dist 12 | copy_deps $lib 13 | done 14 | } 15 | 16 | set +x 17 | copy_deps /usr/local/lib/libmpv.1.dylib 18 | set -x -------------------------------------------------------------------------------- /scripts/dist/ffmpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/scripts/dist/ffmpeg -------------------------------------------------------------------------------- /scripts/dist/ffprobe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/scripts/dist/ffprobe -------------------------------------------------------------------------------- /scripts/dist/libavcodec.58.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/scripts/dist/libavcodec.58.dylib -------------------------------------------------------------------------------- /scripts/dist/libavdevice.58.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/scripts/dist/libavdevice.58.dylib -------------------------------------------------------------------------------- /scripts/dist/libavfilter.7.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/scripts/dist/libavfilter.7.dylib -------------------------------------------------------------------------------- /scripts/dist/libavformat.58.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/scripts/dist/libavformat.58.dylib -------------------------------------------------------------------------------- /scripts/dist/libavutil.56.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/scripts/dist/libavutil.56.dylib -------------------------------------------------------------------------------- /scripts/dist/libjpeg.9.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/scripts/dist/libjpeg.9.dylib -------------------------------------------------------------------------------- /scripts/dist/libmpv.1.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/scripts/dist/libmpv.1.dylib -------------------------------------------------------------------------------- /scripts/dist/libogg.0.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/scripts/dist/libogg.0.dylib -------------------------------------------------------------------------------- /scripts/dist/libopus.0.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/scripts/dist/libopus.0.dylib -------------------------------------------------------------------------------- /scripts/dist/libpostproc.55.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/scripts/dist/libpostproc.55.dylib -------------------------------------------------------------------------------- /scripts/dist/libswresample.3.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/scripts/dist/libswresample.3.dylib -------------------------------------------------------------------------------- /scripts/dist/libswscale.5.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/scripts/dist/libswscale.5.dylib -------------------------------------------------------------------------------- /scripts/dist/libtheoradec.1.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/scripts/dist/libtheoradec.1.dylib -------------------------------------------------------------------------------- /scripts/dist/libtheoraenc.1.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/scripts/dist/libtheoraenc.1.dylib -------------------------------------------------------------------------------- /scripts/dist/libvorbis.0.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/scripts/dist/libvorbis.0.dylib -------------------------------------------------------------------------------- /scripts/dist/libvorbisenc.2.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/scripts/dist/libvorbisenc.2.dylib -------------------------------------------------------------------------------- /scripts/dist/libx264.155.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/scripts/dist/libx264.155.dylib -------------------------------------------------------------------------------- /scripts/i18n_linter: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const assert = require("assert").strict; 4 | const { difference, keys } = require("../src/node_modules/lodash/lodash"); 5 | const en = require("../src/app/i18n/en.json"); 6 | const codes = ['uk', 'zh-CN', 'zh-TW', 'ru']; 7 | 8 | codes.forEach(code=>{ 9 | const translation = require(`../src/app/i18n/${code}.json`); 10 | const diff = difference(keys(en), keys(translation)); 11 | 12 | assert(diff.length === 0, `en i18n file has more keys than the ${code} one, keys = ${diff}`); 13 | }) 14 | -------------------------------------------------------------------------------- /scripts/install_ffmpeg.sh: -------------------------------------------------------------------------------- 1 | brew tap circleapps/ffmpeg 2 | brew install circleapps/ffmpeg/ffmpeg --with-disable-securetransport -------------------------------------------------------------------------------- /scripts/install_mpv.sh: -------------------------------------------------------------------------------- 1 | brew tap circleapps/mpv 2 | brew install circleapps/mpv/mpv -------------------------------------------------------------------------------- /scripts/install_mpvjs.sh: -------------------------------------------------------------------------------- 1 | cd ../src 2 | rm -rf node_modules 3 | npm install 4 | cd ../scripts 5 | cp -r dist/* ../src/node_modules/mpv.js/build/Release/ 6 | install_name_tool -change /usr/local/opt/mpv/lib/libmpv.1.dylib '@loader_path/libmpv.1.dylib' ../src/node_modules/mpv.js/build/Release/mpvjs.node -------------------------------------------------------------------------------- /scripts/lite.parent.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | G9SLV5C872.co.circleapps.sourceplayerlite 9 | com.apple.security.network.client 10 | 11 | com.apple.security.files.user-selected.read-only 12 | 13 | 14 | -------------------------------------------------------------------------------- /scripts/loginhelper.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | -------------------------------------------------------------------------------- /scripts/parent.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | G9SLV5C872.co.circleapps.sourceplayer 9 | com.apple.security.network.client 10 | 11 | com.apple.security.network.server 12 | 13 | com.apple.security.files.user-selected.read-only 14 | 15 | com.apple.security.files.user-selected.read-write 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "babel-eslint", 4 | "parserOptions": { 5 | "sourceType": "module", 6 | "allowImportExportEverywhere": false, 7 | "codeFrame": false 8 | }, 9 | "extends": ["airbnb", "prettier"], 10 | "env": { 11 | "browser": true, 12 | "es6": true, 13 | "jest": true 14 | }, 15 | "globals": { 16 | "PRO_VERSION": "readonly" 17 | }, 18 | "rules": { 19 | "max-len": ["error", { "code": 150 }], 20 | "lines-between-class-members": ["error", "always", { "exceptAfterSingleLine": true }], 21 | "prefer-promise-reject-errors": ["off"], 22 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 23 | "react/prop-types": ["warn"], 24 | "no-return-assign": ["off"], 25 | "import/prefer-default-export": "off", 26 | "no-shadow": "warn", 27 | "consistent-return": "warn", 28 | "no-restricted-syntax": "off", 29 | "no-await-in-loop": "off", 30 | "react/require-default-props": "off", 31 | "no-underscore-dangle": ["warn", { "allowAfterThis": false }], 32 | "react/prefer-stateless-function": "warn", 33 | "global-require": "off", 34 | "no-unused-expressions": [ 35 | "warn", 36 | { 37 | "allowShortCircuit": false, 38 | "allowTernary": false 39 | } 40 | ], 41 | "quotes": [ 42 | 2, 43 | "single", 44 | { 45 | "avoidEscape": true 46 | } 47 | ], 48 | "jsx-a11y/anchor-is-valid": "off", 49 | "indent": ["error", 4, { "SwitchCase": 1 }], 50 | "react/jsx-indent": ["error", 4], 51 | "jsx-quotes": ["error", "prefer-double"], 52 | "comma-dangle": ["error", "never"], 53 | "func-style": "off", 54 | "react/forbid-prop-types": [ 55 | "error", 56 | { 57 | "forbid": ["any"] 58 | } 59 | ], 60 | "jsx-a11y/no-static-element-interactions": "off", 61 | "jsx-a11y/click-events-have-key-events": "off", 62 | "jsx-a11y/role-has-required-aria-props": "off", 63 | "react/jsx-one-expression-per-line": "off", 64 | "react/jsx-props-no-spreading": "warn", 65 | "react/destructuring-assignment": "warn", 66 | "react/jsx-boolean-value": "warn", 67 | "react/jsx-closing-bracket-location": "off", 68 | "react/jsx-curly-spacing": "warn", 69 | "react/jsx-indent-props": "off", 70 | "react/jsx-key": "warn", 71 | "react/jsx-max-props-per-line": [1, { "maximum": 3 }], 72 | "react/jsx-no-bind": "off", 73 | "react/jsx-no-literals": "off", 74 | "react/jsx-pascal-case": "warn", 75 | "import/extensions": "off", 76 | "react/jsx-sort-prop-types": "off", 77 | "react/jsx-sort-props": "off", 78 | "react/jsx-wrap-multilines": "error", 79 | "react/no-multi-comp": "off", 80 | "react/no-set-state": "off", 81 | "react/prefer-es6-class": "warn", 82 | "react/self-closing-comp": "warn", 83 | 84 | "react/sort-comp": [ 85 | "error", 86 | { 87 | "order": ["static-variables", "static-methods", "lifecycle", "render", "everything-else"] 88 | } 89 | ], 90 | "react/sort-prop-types": "warn" 91 | }, 92 | "settings": { 93 | "import/core-modules": ["electron"] 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 150, 3 | "useTabs": false, 4 | "tabWidth": 4, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": true 10 | } 11 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | This README would normally document whatever steps are necessary to get your application up and running. 4 | 5 | ### What is this repository for? 6 | 7 | - Quick summary 8 | - Version 9 | - [Learn Markdown](https://bitbucket.org/tutorials/markdowndemo) 10 | 11 | ### How do I get set up? 12 | 13 | - Summary of set up 14 | - Configuration 15 | - Dependencies 16 | - Database configuration 17 | - How to run tests 18 | - Deployment instructions 19 | 20 | ### Contribution guidelines 21 | 22 | - Writing tests 23 | - Code review 24 | - Other guidelines 25 | 26 | ### Who do I talk to? 27 | 28 | - Repo owner or admin 29 | - Other community or team contact 30 | -------------------------------------------------------------------------------- /src/app/assets/css/react-search-input.css: -------------------------------------------------------------------------------- 1 | .k-search-input { 2 | padding: 10px 10px; 3 | height: 52px; 4 | position: relative; 5 | color: white; 6 | } 7 | 8 | .k-search-input::before { 9 | content: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABS2lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxMzggNzkuMTU5ODI0LCAyMDE2LzA5LzE0LTAxOjA5OjAxICAgICAgICAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIi8+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgo8P3hwYWNrZXQgZW5kPSJyIj8+IEmuOgAAAUxJREFUOI2N0z1LnUEQBeDHj86oWKSxFqJom8Y0CoqYFMYirY1YCX4hFqKQCLERNJVFChttgkVUIiiIWouFaKXkF3ixuRrSRLR45w3LRa/3wDIHds/Z2ZnZqqmZWQmaMI6PaEUVrvALyygoQW3C32MjTFJ0xBrFMDbTzeqIfdgJ8T560YBX6MYW6vEDn0oNGrCOGnxFPw5wiz84xiBm4klraE4NRvAaR5jHQ+k7A0vYjqzGUoOB4N/KiHOsRPyQGrwJfvqCGE4i5pr/RVTB7Sn+pQaXwd9WIMzP/E4NdoJPyKpcDpMRc41qfJdNWDcWyoinZRNaxGpqUMQQ7jGHPfTIBqcOXfgpayNcS2qQj/KerJ0bsqnseyKDYohbcBgZF9Iu7MbmF5zhDn9xgUW0oRPnaA+TxvQzwQ0+x3oOPSE+RrHUoBIU8E72Vx4eAfJWQ43lt2BiAAAAAElFTkSuQmCC'); 10 | display: block; 11 | position: absolute; 12 | width: 15px; 13 | z-index: 3; 14 | height: 15px; 15 | font-size: 20px; 16 | top: 11px; 17 | left: 16px; 18 | line-height: 32px; 19 | opacity: 0.6; 20 | } 21 | 22 | .k-search-input > input { 23 | width: 100%; 24 | font-size: 18px; 25 | border: none; 26 | line-height: 22px; 27 | padding: 5px 10px 5px 25px; 28 | height: 32px; 29 | position: relative; 30 | background-color: rgb(46, 46, 46); 31 | border-radius: 6px; 32 | } 33 | 34 | .k-search-input > span { 35 | position: absolute; 36 | right: 15px; 37 | top: 5px; 38 | padding: 10px; 39 | cursor: pointer; 40 | visibility: hidden; 41 | } 42 | 43 | .k-search-input > input:focus { 44 | outline: none; 45 | } 46 | -------------------------------------------------------------------------------- /src/app/assets/documentation/vid_tutorial.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41 | 42 | 43 |
44 |

BeyondPlayer Video Tutorial

45 |
46 |

BeyondPlayer Essential

47 |
48 |
49 | 52 |
53 |
54 |
55 |
56 |

BeyondPlayer Advanced (Pro Only Features)

57 |
58 |
59 | 62 |
63 |
64 |
65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /src/app/assets/documentation/vids/advanced.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/src/app/assets/documentation/vids/advanced.mp4 -------------------------------------------------------------------------------- /src/app/assets/documentation/vids/essential.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/src/app/assets/documentation/vids/essential.mp4 -------------------------------------------------------------------------------- /src/app/assets/fonts/FiraSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/src/app/assets/fonts/FiraSans-Regular.ttf -------------------------------------------------------------------------------- /src/app/assets/fonts/FiraSansCondensed-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/src/app/assets/fonts/FiraSansCondensed-Regular.ttf -------------------------------------------------------------------------------- /src/app/assets/fonts/Merriweather-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/src/app/assets/fonts/Merriweather-Regular.ttf -------------------------------------------------------------------------------- /src/app/assets/fonts/Oswald-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/src/app/assets/fonts/Oswald-Regular.ttf -------------------------------------------------------------------------------- /src/app/assets/fonts/TitilliumWeb-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/src/app/assets/fonts/TitilliumWeb-Bold.ttf -------------------------------------------------------------------------------- /src/app/assets/fonts/TitilliumWeb-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/src/app/assets/fonts/TitilliumWeb-Regular.ttf -------------------------------------------------------------------------------- /src/app/assets/fonts/TitilliumWeb-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/src/app/assets/fonts/TitilliumWeb-SemiBold.ttf -------------------------------------------------------------------------------- /src/app/i18n/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | let loadedLanguage; 4 | const util = require('util'); 5 | 6 | module.exports = new i18n(); 7 | 8 | function i18n() {} 9 | 10 | i18n.prototype.init = function(locale) { 11 | if (fs.existsSync(path.join(__dirname, locale + '.json'))) { 12 | loadedLanguage = JSON.parse(fs.readFileSync(path.join(__dirname, locale + '.json'), 'utf8')); 13 | } else { 14 | loadedLanguage = JSON.parse(fs.readFileSync(path.join(__dirname, 'en.json'), 'utf8')); 15 | } 16 | }; 17 | 18 | i18n.prototype.t = function(phrase) { 19 | let translation = loadedLanguage[phrase]; 20 | if (translation === undefined) { 21 | translation = phrase; 22 | } 23 | return translation; 24 | }; 25 | 26 | i18n.prototype.tf = function(phrase, ...args) { 27 | let translation = loadedLanguage[phrase]; 28 | if (translation === undefined) { 29 | translation = phrase; 30 | } 31 | return util.format(translation, ...args); 32 | }; 33 | -------------------------------------------------------------------------------- /src/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BeyondPlayer 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 17 | 18 | -------------------------------------------------------------------------------- /src/app/inter_op_webpane.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer } = require('electron'); 2 | 3 | document.addEventListener( 4 | 'contextmenu', 5 | function(e) { 6 | e = e || window.event; 7 | var selection = window.getSelection(); 8 | if (selection) { 9 | var msg = { 10 | text: selection.toString(), 11 | x: e.clientX, 12 | y: e.clientY 13 | }; 14 | ipcRenderer.sendToHost('right-click-selection', msg); 15 | } 16 | }, 17 | false 18 | ); 19 | 20 | document.addEventListener( 21 | 'mouseup', 22 | function(e) { 23 | e = e || window.event; 24 | var selection = window.getSelection(); 25 | if (selection) { 26 | ipcRenderer.sendToHost('change-selection', selection.toString()); 27 | } 28 | }, 29 | false 30 | ); 31 | -------------------------------------------------------------------------------- /src/app/inter_op_youtube.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer } = require('electron'); 2 | 3 | ipcRenderer.on('videoId', function(event, data) { 4 | global.videoId = data; 5 | }); 6 | 7 | ipcRenderer.on('loadVideo', function(event, data) { 8 | global.loadVideo(data); 9 | }); 10 | 11 | ipcRenderer.on('playVideo', function(event, data) { 12 | global.player.playVideo(); 13 | }); 14 | 15 | ipcRenderer.on('pauseVideo', function(event, data) { 16 | global.player.pauseVideo(); 17 | }); 18 | 19 | ipcRenderer.on('startProgress', function(event, data) { 20 | global.startProgress(); 21 | }); 22 | 23 | ipcRenderer.on('seek', function(event, time) { 24 | global.player.seekTo(time); 25 | }); 26 | 27 | ipcRenderer.on('speed', function(event, speed) { 28 | global.player.setPlaybackRate(speed); 29 | }); 30 | 31 | ipcRenderer.on('checkSubtitleAndAd', function(event) { 32 | global.checkSubtitleAndAd(); 33 | }); 34 | 35 | ipcRenderer.on('openSubtitle', function(event) { 36 | global.openSubtitle(); 37 | }); 38 | 39 | ipcRenderer.on('checkAndHideControls', function(event, delay, hideControls) { 40 | global.checkAndHideControls(); 41 | }); 42 | 43 | ipcRenderer.on('hideSubtitle', function(event) { 44 | global.hideSubtitle(); 45 | }); 46 | 47 | global.sendToHost = (channel, data) => { 48 | ipcRenderer.sendToHost(channel, data); 49 | }; 50 | -------------------------------------------------------------------------------- /src/app/inter_op_youtube_browser.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer } = require('electron'); 2 | const getVideoId = require('get-video-id'); 3 | 4 | document.addEventListener('DOMContentLoaded', function() { 5 | document.addEventListener( 6 | 'click', 7 | function(e) { 8 | let a; 9 | if (e.target.nodeNmae == 'A') { 10 | a = e.target; 11 | } else { 12 | a = e.target.closest('a'); 13 | } 14 | if (!a) return true; 15 | 16 | let path = a.href; 17 | let id; 18 | 19 | if (!path.includes('/user/')) { 20 | id = getVideoId(path).id; 21 | } 22 | 23 | if (id) { 24 | //e.preventDefault(); 25 | //e.stopPropagation(); 26 | setTimeout(function() { 27 | ipcRenderer.sendToHost('click-video', path); 28 | }, 100); 29 | return false; 30 | } else { 31 | return true; 32 | } 33 | }, 34 | true 35 | ); 36 | 37 | setInterval(() => { 38 | if (document.activeElement && document.activeElement.nodeName == 'INPUT') { 39 | ipcRenderer.sendToHost('is-focus', true); 40 | } else { 41 | ipcRenderer.sendToHost('is-focus', false); 42 | } 43 | }, 1000); 44 | 45 | document.ondragover = document.ondrop = ev => { 46 | ev.preventDefault(); 47 | ev.stopPropagation(); 48 | }; 49 | 50 | document.body.ondrop = ev => { 51 | ev.preventDefault(); 52 | ev.stopPropagation(); 53 | }; 54 | }); 55 | 56 | ipcRenderer.on('pause', function(event) { 57 | var video = document.getElementsByTagName('video')[0]; 58 | if (video) { 59 | setTimeout(() => { 60 | video.pause(); 61 | }, 5000); 62 | } 63 | }); 64 | 65 | global.sendToHost = (channel, data) => { 66 | ipcRenderer.sendToHost(channel, data); 67 | }; 68 | -------------------------------------------------------------------------------- /src/app/js/Components/BaseSubtitleSettingsPane.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { remote } from 'electron'; 3 | 4 | export default class BaseSubtitleSettingsPane extends React.Component { 5 | simulateClickElement(e) { 6 | const offset = this.getOffset(e); 7 | offset.x += 10; 8 | offset.y += 10; 9 | const wc = remote.getCurrentWindow().webContents; 10 | wc.sendInputEvent({ type: 'mouseDown', x: offset.x, y: offset.y, button: 'left', clickCount: 1 }); 11 | wc.sendInputEvent({ type: 'mouseUp', x: offset.x, y: offset.y, button: 'left', clickCount: 1 }); 12 | } 13 | 14 | getOffset(el) { 15 | let x = 0; 16 | let y = 0; 17 | 18 | while (el && !isNaN(el.offsetLeft) && !isNaN(el.offsetTop)) { 19 | x += el.offsetLeft - el.scrollLeft; 20 | y += el.offsetTop - el.scrollTop; 21 | el = el.offsetParent; 22 | } 23 | 24 | return { y, x }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/js/Components/Common.less: -------------------------------------------------------------------------------- 1 | .byp-button { 2 | outline: none; 3 | border: none; 4 | background-color: transparent; 5 | text-align: center; 6 | display: inline-block; 7 | } 8 | 9 | .byp-toolbar-button { 10 | width: 39px; 11 | height: 100%; 12 | font-size: 22px; 13 | border: none; 14 | outline: none; 15 | background-color: rgba(39, 39, 39, 0); 16 | color: rgb(240, 240, 240); 17 | padding: 0; 18 | } 19 | 20 | .byp-toolbar-button.byp-active { 21 | background-color: rgba(19, 19, 19, 0.75); 22 | } 23 | 24 | .byp-icon { 25 | display: inline-block; 26 | height: 24px; 27 | width: 24px; 28 | background-size: cover; 29 | } 30 | 31 | .byp-anim { 32 | -webkit-animation: rotation 2s infinite linear; 33 | } 34 | 35 | @-webkit-keyframes rotation { 36 | to { 37 | -webkit-transform: rotate(0deg); 38 | } 39 | from { 40 | -webkit-transform: rotate(359deg); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/js/Components/DictionaryItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Dictionary from '../Model/Dictionary'; 3 | 4 | export default class DictionaryItem extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.audio = null; 8 | this.onClickPlaySound = this.onClickPlaySound.bind(this); 9 | this.state = { isPaused: true }; 10 | } 11 | 12 | onClickPlaySound(evt) { 13 | this.play(); 14 | } 15 | 16 | play() { 17 | Dictionary.pronounce(this.props.data.word); 18 | } 19 | 20 | render() { 21 | return ( 22 |
23 |

{this.props.data.name}

24 |
25 | {this.props.data.definition.map((line, i) => ( 26 |
{line}
27 | ))} 28 |
29 |
30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/js/Components/DictionaryPane.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DictionaryItem from './DictionaryItem.jsx'; 3 | 4 | export default class DictionaryPane extends React.Component { 5 | render() { 6 | return ( 7 |
8 | {this.props.data.map((item, i) => ( 9 | 10 | ))} 11 |
12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/js/Components/ExportAnkiDialog/AnkiVideoCard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Card, Tooltip, Checkbox } from 'antd'; 4 | import { card, thumb, text, checkbox } from './AnkiVideoCard.module.less'; 5 | 6 | const AnkiVideoCard = ({ id, frontPreview, frontText, checked, onSelectVideo }) => ( 7 | onSelectVideo(id, !checked)}> 8 | 9 | 10 | 11 | {frontText} 12 | 13 | 14 | ); 15 | 16 | AnkiVideoCard.propTypes = { 17 | checked: PropTypes.bool.isRequired, 18 | frontPreview: PropTypes.string.isRequired, 19 | frontText: PropTypes.string.isRequired, 20 | id: PropTypes.string.isRequired, 21 | onSelectVideo: PropTypes.func.isRequired 22 | }; 23 | export { AnkiVideoCard }; 24 | -------------------------------------------------------------------------------- /src/app/js/Components/ExportAnkiDialog/AnkiVideoCard.module.less: -------------------------------------------------------------------------------- 1 | .card { 2 | height: 150px; 3 | width: 150px !important; 4 | padding: 0 !important; 5 | margin: 5px; 6 | position: relative; 7 | cursor: pointer; 8 | 9 | * { 10 | cursor: pointer; 11 | } 12 | } 13 | 14 | .text { 15 | display: inline-block; 16 | white-space: nowrap; 17 | overflow: hidden; 18 | width: 100%; 19 | padding: 5px; 20 | text-overflow: ellipsis; 21 | } 22 | 23 | .thumb { 24 | max-width: 100%; 25 | max-height: 120px; 26 | } 27 | 28 | .checkbox { 29 | position: absolute; 30 | right: 5px; 31 | bottom: 5px; 32 | } 33 | -------------------------------------------------------------------------------- /src/app/js/Components/ExportAnkiDialog/ExportAnkiDialog.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { remote } from 'electron'; 3 | import { Button, Tabs, Modal, Spin, notification } from 'antd'; 4 | import { ExportAnkiVidLib } from './ExportAnkiVidLib.jsx'; 5 | import { AnkiFileDestination } from '../../Model/Export/destinations/anki'; 6 | 7 | const i18n = remote.require('./i18n'); 8 | 9 | export class ExportAnkiDialog extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | this.activeTab = 'ankiVidLib'; 13 | this.state = { exporting: false }; 14 | } 15 | 16 | render() { 17 | const { onClose } = this.props; 18 | const { exporting } = this.state; 19 | 20 | return ( 21 | 31 | {i18n.t('close')} 32 | , 33 | 36 | ]}> 37 | 38 |
39 | (this.currentSource = activeTab)}> 40 | 41 |
42 | (this.ankiVidLib = e)} /> 43 |
44 |
45 |
46 |
47 |
48 |
49 | ); 50 | } 51 | 52 | onExport = () => { 53 | this[this.activeTab].getParams().then(async params => { 54 | const { deckName, source, frontTemplate, backTemplate } = params; 55 | const defaultPath = `${deckName.replace(/\s/g, '').toLowerCase()}.anki.apkg`; 56 | const file = remote.dialog.showSaveDialog(remote.getCurrentWindow(), { defaultPath }); 57 | 58 | if (file) { 59 | this.setState(() => ({ exporting: true })); 60 | 61 | new AnkiFileDestination({ deckName, file, frontTemplate, backTemplate }) 62 | .export(source) 63 | .then(() => { 64 | notification.success({ 65 | placement: 'topRight', 66 | message: i18n.t('anki.export.dialog.success') 67 | }); 68 | }) 69 | .catch(() => { 70 | notification.error({ 71 | placement: 'topRight', 72 | message: i18n.t('anki.export.dialog.fail') 73 | }); 74 | }) 75 | .finally(() => { 76 | this.setState(() => ({ exporting: false })); 77 | }); 78 | } 79 | }); 80 | }; 81 | } 82 | -------------------------------------------------------------------------------- /src/app/js/Components/ExportAnkiDialog/ExportAnkiDialog.less: -------------------------------------------------------------------------------- 1 | .anki-export-modal { 2 | .ant-modal-body { 3 | overflow: scroll; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/app/js/Components/ExportAnkiDialog/ExportAnkiVidLib.module.less: -------------------------------------------------------------------------------- 1 | .hasError > :global(.ant-input), 2 | .hasError > :global(.ant-card) { 3 | border: 1px solid #f5222d; 4 | } 5 | 6 | .cardsContainer { 7 | max-height: 250px; 8 | min-height: 250px; 9 | overflow: scroll; 10 | } 11 | 12 | .selectorLink.selectorLink { 13 | padding-right: 0; 14 | } 15 | -------------------------------------------------------------------------------- /src/app/js/Components/ExportAnkiDialog/index.js: -------------------------------------------------------------------------------- 1 | export { ExportAnkiDialog } from './ExportAnkiDialog.jsx'; 2 | -------------------------------------------------------------------------------- /src/app/js/Components/Loader/Loader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Modal from 'react-modal'; 3 | import PropTypes from 'prop-types'; 4 | import MessageDialog from '../MessageDialog.jsx'; 5 | import { root } from './Loader.module.less'; 6 | 7 | /** 8 | * A default wait popup dialog 9 | * @param {Object} params 10 | * @param {boolean} params.isOpen - a proxy option to the modal dialog, shows/hides dialog 11 | * @param {function} params.onAfterOpen - a proxy option to the modal dialog, on after open callback 12 | * @param {string=} params.message - a popup message 13 | */ 14 | const Loader = ({ isOpen = true, onAfterOpen = () => {}, message = '' }) => ( 15 | 16 | 17 | 18 | ); 19 | 20 | Loader.propTypes = { 21 | isOpen: PropTypes.bool, 22 | message: PropTypes.string, 23 | onAfterOpen: PropTypes.func 24 | }; 25 | 26 | export { Loader }; 27 | -------------------------------------------------------------------------------- /src/app/js/Components/Loader/Loader.module.less: -------------------------------------------------------------------------------- 1 | .root { 2 | top: 50%; 3 | left: 50%; 4 | right: auto; 5 | bottom: auto; 6 | margin-right: -50%; 7 | transform: translate(-50%; -50%); 8 | } 9 | -------------------------------------------------------------------------------- /src/app/js/Components/Loader/Loader.spec.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import React from 'react'; 3 | import Modal from 'react-modal'; 4 | import { render, screen } from '@testing-library/react'; 5 | import { Loader } from './Loader.jsx'; 6 | 7 | describe('Loader specs', () => { 8 | beforeAll(() => { 9 | const modalRoot = document.createElement('div'); 10 | 11 | modalRoot.setAttribute('id', 'modal-root'); 12 | document.body.appendChild(modalRoot); 13 | 14 | Modal.setAppElement(modalRoot); 15 | }); 16 | 17 | it('should correctly render a message', () => { 18 | const message = 'Test message'; 19 | render(); 20 | 21 | expect(screen.queryByText(message)).not.toBeNull(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/js/Components/Loader/index.js: -------------------------------------------------------------------------------- 1 | export { Loader } from './Loader.jsx'; 2 | -------------------------------------------------------------------------------- /src/app/js/Components/MRUItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import path from 'path-extra'; 4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 5 | import { faYoutube } from '@fortawesome/free-brands-svg-icons'; 6 | import { faHdd } from '@fortawesome/free-solid-svg-icons'; 7 | 8 | export default class MRUItem extends React.Component { 9 | handleOnClickOpen = e => { 10 | if (_.isString(this.props.file)) { 11 | this.props.onOpenRecentFile(this.props.file); 12 | } else { 13 | this.props.onOpenRecentURL(this.props.file.url); 14 | } 15 | }; 16 | 17 | getTitle = mruItem => { 18 | if (_.isString(mruItem)) { 19 | return path.basename(mruItem); 20 | } else { 21 | return mruItem.title; 22 | } 23 | }; 24 | 25 | render() { 26 | return ( 27 |
28 | {_.isString(this.props.file) ? ( 29 | 30 | ) : ( 31 | 32 | )} 33 | {this.getTitle(this.props.file)} 34 |
35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/js/Components/MRUPane.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MRUItem from './MRUItem.jsx'; 3 | import MRUFiles from '../Model/MRUFiles'; 4 | 5 | export default class MRUPane extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { 10 | mru: [] 11 | }; 12 | 13 | MRUFiles.load(files => { 14 | this.setState({ mru: files }); 15 | }); 16 | } 17 | 18 | render() { 19 | return ( 20 |
21 |
22 |
23 | {' '} 24 | Open Movie File ...{' '} 25 |
26 |
27 | {' '} 28 | Open Youtube Video ...{' '} 29 |
30 |
31 |
32 |
33 | {this.state.mru.map((file, i) => ( 34 | 40 | ))} 41 |
42 |
43 |
44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/js/Components/MessageDialog.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Loader from 'react-loader-spinner'; 3 | 4 | export default class MessageDialog extends React.Component { 5 | render() { 6 | return ( 7 |
8 |
(this.message = ref)}> 9 | {this.props.message} 10 |
11 |
12 | 13 |
14 |
15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/js/Components/MessagePane.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import lerp from 'lerp'; 3 | 4 | export default class MessagePane extends React.Component { 5 | static ANIM_TIME = 300; 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | opacity: 0, 10 | show: false 11 | }; 12 | this.start = 0; 13 | } 14 | 15 | showMessage(message, ms) { 16 | this.setState({ opacity: 1, show: true }, () => { 17 | this.refs.message.innerHTML = message; 18 | this.playAnim = false; 19 | clearTimeout(this.timer); 20 | if (ms) { 21 | this.timer = setTimeout(() => { 22 | this.hide(); 23 | }, ms); 24 | } 25 | }); 26 | } 27 | 28 | hide() { 29 | this.start = 0; 30 | this.playAnim = true; 31 | window.requestAnimationFrame(this.hideUpdate); 32 | } 33 | 34 | hideUpdate = timestamp => { 35 | if (!this.playAnim) return; 36 | 37 | if (!this.start) this.start = timestamp; 38 | let progress = timestamp - this.start; 39 | 40 | this.setState({ 41 | opacity: lerp(1, 0, progress / MessagePane.ANIM_TIME) 42 | }); 43 | 44 | if (progress < MessagePane.ANIM_TIME) { 45 | window.requestAnimationFrame(this.hideUpdate); 46 | } else { 47 | this.start = 0; 48 | this.setState({ show: false }); 49 | } 50 | }; 51 | 52 | render() { 53 | return ( 54 |
55 |

56 | Message form 57 |

58 |
59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/app/js/Components/OpenMediaPane.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { faYoutube } from '@fortawesome/free-brands-svg-icons'; 4 | import { faHdd, faCompass } from '@fortawesome/free-solid-svg-icons'; 5 | import { remote } from 'electron'; 6 | 7 | const i18n = remote.require('./i18n'); 8 | 9 | export default class OpenMediaPane extends React.Component { 10 | render() { 11 | return ( 12 |
13 |
14 | 17 |
{i18n.t('open.file')}
18 |
19 | {PRO_VERSION ? ( 20 |
21 | 24 |
{i18n.t('open.youtube.video')}
25 |
26 | ) : null} 27 |
28 | 31 |
{i18n.t('tutorial')}
32 |
33 |
34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/js/Components/PlayerControls.less: -------------------------------------------------------------------------------- 1 | .byp-player-controls-container { 2 | height: 62px; 3 | margin-bottom: 4px; 4 | width: 100%; 5 | display: flex; 6 | flex-direction: column; 7 | z-index: 10; 8 | } 9 | 10 | .byp-player-controls-toolbar { 11 | border-radius: 5px; 12 | margin: 0px 0px 2px 15px; 13 | background-color: rgba(102, 102, 102, 0.355); 14 | } 15 | 16 | .byp-player-controls { 17 | height: 42px; 18 | width: 100%; 19 | display: flex; 20 | background-color: rgba(75, 75, 75, 0); 21 | padding: 0 10px 4px 10px; 22 | } 23 | 24 | .byp-seek { 25 | height: 20px; 26 | padding: 0px 7px 0px 7px; 27 | outline: none; 28 | } 29 | 30 | .byp-timer-current { 31 | height: 42px; 32 | flex: 1; 33 | font-family: 'k-font-numbers', sans-serif; 34 | font-size: 15px; 35 | padding: 6px 23px 8px 12px; 36 | color: rgba(255, 255, 255, 0.5); 37 | } 38 | 39 | .byp-timer-duration { 40 | height: 40px; 41 | padding: 11px 15px 11px 3px; 42 | color: rgb(211, 211, 211); 43 | } 44 | 45 | .byp-seek input { 46 | height: 100%; 47 | width: 100%; 48 | } 49 | 50 | .byp-loop-counter { 51 | display: inline-block; 52 | width: 14px; 53 | height: 14px; 54 | font-size: 10px; 55 | line-height: 100%; 56 | top: 0px; 57 | left: 12px; 58 | text-align: center; 59 | position: relative; 60 | background-color: #127bc8; 61 | padding-top: 1px; 62 | padding-left: 1px; 63 | 64 | border-radius: 7px; 65 | color: white; 66 | /*text-shadow: rgb(243, 243, 243) 0px 0px 2px;*/ 67 | } 68 | 69 | .byp-player-button { 70 | width: 43px; 71 | height: 40px; 72 | font-size: 22px; 73 | border: none; 74 | outline: none; 75 | background-color: rgba(39, 39, 39, 0); 76 | color: rgb(240, 240, 240); 77 | text-align: center; 78 | display: flex; 79 | align-items: center; 80 | justify-content: center; 81 | } 82 | 83 | .byp-icon { 84 | &-play { 85 | background-image: url('../Images/play.svg'); 86 | width: 32px; 87 | height: 32px; 88 | } 89 | &-pause { 90 | background-image: url('../Images/pause.svg'); 91 | width: 32px; 92 | height: 32px; 93 | } 94 | 95 | &-equalizer { 96 | background-image: url('../Images/equalizer.svg'); 97 | height: 22px; 98 | width: 22px; 99 | } 100 | 101 | &-pm-normal { 102 | background-image: url('../Images/pm_sequence.svg'); 103 | width: 29px; 104 | height: 29px; 105 | } 106 | 107 | &-pm-auto-repeat { 108 | background-image: url('../Images/pm_auto_repeat.svg'); 109 | height: 29px; 110 | width: 29px; 111 | } 112 | 113 | &-pm-auto-pause { 114 | background-image: url('../Images/pm_auto_pause.svg'); 115 | height: 29px; 116 | width: 29px; 117 | } 118 | 119 | &-loop { 120 | background-image: url('../Images/loop.svg'); 121 | width: 26px; 122 | height: 26px; 123 | margin-top: 4px; 124 | } 125 | 126 | &-book { 127 | background-image: url('../Images/book.svg'); 128 | margin-top: 5px; 129 | height: 20px; 130 | width: 20px; 131 | } 132 | 133 | &-switches { 134 | background-image: url('../Images/switches.svg'); 135 | height: 23px; 136 | width: 23px; 137 | } 138 | } 139 | 140 | .byp-player-button.byp-active { 141 | margin: 0px; 142 | padding: 0px; 143 | background-color: rgba(26, 26, 26); 144 | } 145 | -------------------------------------------------------------------------------- /src/app/js/Components/PopupPane.less: -------------------------------------------------------------------------------- 1 | .popup-pane() { 2 | position: absolute; 3 | bottom: 52px; 4 | left: 27px; 5 | width: 320px; 6 | height: 340px; 7 | background-color: rgb(59, 59, 59); 8 | z-index: 1; 9 | cursor: default; 10 | padding: 20px 28px; 11 | color: white; 12 | border: 1px solid #e6edec; 13 | border-radius: 6px; 14 | z-index: 11; 15 | } 16 | 17 | .popup-pane-after-and-before(@offset) { 18 | top: 100%; 19 | left: @offset; 20 | border: solid transparent; 21 | content: ' '; 22 | height: 0; 23 | width: 0; 24 | position: absolute; 25 | pointer-events: none; 26 | z-index: 11; 27 | } 28 | 29 | .popup-pane-after() { 30 | border-color: rgba(61, 61, 61, 0); 31 | border-top-color: #3d3d3d; 32 | border-width: 10px; 33 | margin-left: -10px; 34 | } 35 | 36 | .popup-pane-before() { 37 | border-color: rgba(230, 237, 236, 0); 38 | border-top-color: #e6edec; 39 | border-width: 12px; 40 | margin-left: -12px; 41 | } 42 | -------------------------------------------------------------------------------- /src/app/js/Components/ProgressPane.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Loader from 'react-loader-spinner'; 3 | 4 | export default class ProgressPane extends React.Component { 5 | render() { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/js/Components/PromptDialog.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { remote } from 'electron'; 3 | 4 | const i18n = remote.require('./i18n'); 5 | 6 | export default class PromptDialog extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | url: this.props.defaultValue 12 | }; 13 | } 14 | 15 | componentDidMount() { 16 | this.refs.urlInput.select(); 17 | } 18 | 19 | handlePromptOk = () => { 20 | this.props.onPromptOk(this.state.url); 21 | }; 22 | 23 | handlePromptCancel = () => { 24 | this.props.onPromptCancel(this.state.url); 25 | }; 26 | 27 | handleUrlChange = v => { 28 | this.setState({ 29 | url: v.target.value 30 | }); 31 | }; 32 | 33 | render() { 34 | return ( 35 |
36 |
37 | 38 | 39 |
40 |
41 | 45 | 49 |
50 |
51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/js/Components/SearchInput.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export default class SearchInput extends Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | searchTerm: this.props.value || '' 9 | }; 10 | this.updateSearch = this.updateSearch.bind(this); 11 | } 12 | 13 | componentWillReceiveProps(nextProps) { 14 | if (typeof nextProps.value !== 'undefined' && nextProps.value !== this.props.value) { 15 | const e = { 16 | target: { 17 | value: nextProps.value 18 | } 19 | }; 20 | this.updateSearch(e); 21 | } 22 | } 23 | 24 | render() { 25 | const { 26 | onChange, 27 | onClickClear, 28 | sortResults, 29 | throttle, 30 | filterKeys, 31 | value, 32 | fuzzy, 33 | flex, 34 | inputClassName, 35 | inputStyle, 36 | ...inputProps 37 | } = this.props; // eslint-disable-line no-unused-vars 38 | inputProps.type = inputProps.type || 'search'; 39 | inputProps.value = this.state.searchTerm; 40 | inputProps.onChange = this.updateSearch; 41 | inputProps.placeholder = inputProps.placeholder || 'Search'; 42 | 43 | return ( 44 |
45 | {' '} 46 | 47 | × 48 | 49 |
50 | ); 51 | } 52 | 53 | handleClickClear = () => { 54 | this.props.onClickClear(); 55 | this.refs.clear.style.visibility = 'hidden'; 56 | }; 57 | 58 | updateSearch(e) { 59 | const searchTerm = e.target.value; 60 | this.setState( 61 | { 62 | searchTerm: searchTerm 63 | }, 64 | () => { 65 | this.refs.clear.style.visibility = searchTerm ? 'visible' : 'hidden'; 66 | if (this._throttleTimeout) { 67 | clearTimeout(this._throttleTimeout); 68 | } 69 | 70 | this._throttleTimeout = setTimeout(() => this.props.onChange(searchTerm), this.props.throttle); 71 | } 72 | ); 73 | } 74 | } 75 | 76 | SearchInput.defaultProps = { 77 | onChange() {}, 78 | onClickClear() {}, 79 | fuzzy: false, 80 | flex: false, 81 | throttle: 200 82 | }; 83 | 84 | SearchInput.propTypes = { 85 | onChange: PropTypes.func, 86 | onClickClear: PropTypes.func, 87 | sortResults: PropTypes.bool, 88 | fuzzy: PropTypes.bool, 89 | flex: PropTypes.bool, 90 | throttle: PropTypes.number, 91 | flex: PropTypes.bool, 92 | filterKeys: PropTypes.oneOf([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), 93 | value: PropTypes.string 94 | }; 95 | -------------------------------------------------------------------------------- /src/app/js/Components/SettingsPane/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './SettingsPane.jsx'; 2 | -------------------------------------------------------------------------------- /src/app/js/Components/SettingsPane/sections/ExternalDictionary.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { onValueChange } from './hooks'; 3 | import Settings from '../../../Model/Settings'; 4 | 5 | const ExternalDictionary = ({ i18n, onChange, value }) => { 6 | const onChangeExternalDictionary = onValueChange(Settings.SKEY_EXT_DIC, onChange); 7 | 8 | return ( 9 |
10 | 13 |
14 |
15 | 25 |
26 |
27 | 37 |
38 |
39 |
40 | ); 41 | }; 42 | 43 | export { ExternalDictionary }; 44 | -------------------------------------------------------------------------------- /src/app/js/Components/SettingsPane/sections/Language.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { onValueChange } from './hooks'; 3 | import Settings from '../../../Model/Settings'; 4 | 5 | const languages = [ 6 | { 7 | key: 'en', 8 | name: 'English' 9 | }, 10 | { 11 | key: 'zh-CN', 12 | name: '简体中文' 13 | }, 14 | { 15 | key: 'zh-TW', 16 | name: '傳統中文' 17 | }, 18 | { 19 | key: 'uk', 20 | name: 'Українська' 21 | }, 22 | { 23 | key: 'ru', 24 | name: 'Русский' 25 | } 26 | ]; 27 | const Language = ({ i18n, onChange, value }) => { 28 | const onChangeLanguage = onValueChange(Settings.UI_LNG, onChange); 29 | 30 | return ( 31 |
32 | 35 |
36 | 43 |
44 |
45 | ); 46 | }; 47 | 48 | export { Language }; 49 | -------------------------------------------------------------------------------- /src/app/js/Components/SettingsPane/sections/LoopCount.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { onValueChange } from './hooks'; 3 | import Settings from '../../../Model/Settings'; 4 | 5 | const LOOP_COUNTS = [ 6 | { 7 | name: '2', 8 | key: 2 9 | }, 10 | { 11 | name: '3', 12 | key: 3 13 | }, 14 | { 15 | name: '4', 16 | key: 4 17 | }, 18 | { 19 | name: '5', 20 | key: 5 21 | }, 22 | { 23 | name: '6', 24 | key: 6 25 | }, 26 | { 27 | name: '7', 28 | key: 7 29 | }, 30 | { 31 | name: '8', 32 | key: 8 33 | }, 34 | { 35 | name: '9', 36 | key: 9 37 | } 38 | ]; 39 | 40 | const LoopCount = ({ i18n, onChange, value }) => { 41 | const onChangeLoopCount = onValueChange(Settings.SKEY_SINGLE_LINE_LOOP_COUNT, onChange); 42 | 43 | return ( 44 |
45 | 48 |
49 | 56 |
57 |
58 | ); 59 | }; 60 | 61 | export { LoopCount }; 62 | -------------------------------------------------------------------------------- /src/app/js/Components/SettingsPane/sections/MiscSection.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { onValueChange, extractors } from './hooks'; 3 | import Settings from '../../../Model/Settings'; 4 | 5 | const { checked } = extractors; 6 | 7 | const MiscSection = ({ i18n, onChange, fastForwardPause, fixedSubtitlePosition, displaySubtitleBackground, wordNotification, autoPauseResume }) => { 8 | const onToggleFastForwardPause = onValueChange(Settings.SKEY_PWFF, onChange, checked); 9 | const onToggleFixedSubtitlePosition = onValueChange(Settings.SKEY_FSP, onChange, checked); 10 | const onToggleDisplaySubtitleBackground = onValueChange(Settings.SKEY_DSB, onChange, checked); 11 | const onToggleWordNotification = onValueChange(Settings.SKEY_DWN, onChange, checked); 12 | 13 | return ( 14 |
15 | 18 |
19 |
{i18n.t('pause.playback.when.fast.forward')}
20 | 21 |
22 |
23 |
{i18n.t('fixed.subtitle.position')}
24 | 25 |
26 |
27 |
{i18n.t('display.subtitle.background')}
28 | 29 |
30 |
31 |
{i18n.t('enable.word.notification')}
32 | 33 |
34 |
35 | ); 36 | }; 37 | 38 | export { MiscSection }; 39 | -------------------------------------------------------------------------------- /src/app/js/Components/SettingsPane/sections/PlayerSubtitleColor.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { onValueChange } from './hooks'; 3 | import Settings from '../../../Model/Settings'; 4 | 5 | const PlayerSubtitleColor = ({ i18n, value, onChange }) => { 6 | const onChangeColor = onValueChange(Settings.SKEY_PLAYER_SUB_COLOR, onChange); 7 | 8 | return ( 9 |
10 | 13 |
14 | {Settings.COLORS.map((color, index) => ( 15 |
16 | 26 |
27 | ))} 28 |
29 |
30 | ); 31 | }; 32 | 33 | export { PlayerSubtitleColor }; 34 | -------------------------------------------------------------------------------- /src/app/js/Components/SettingsPane/sections/Socks5Proxy.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { onValueChange, extractors } from './hooks'; 3 | import Settings from '../../../Model/Settings'; 4 | 5 | const Socks5Proxy = ({ i18n, onChange, port, ipAddress, enabled }) => { 6 | const onChangePort = onValueChange(Settings.SKEY_SOCKS5_PORT, onChange); 7 | const onToggleEnabled = onValueChange(Settings.SKEY_ENABLE_SOCKS5, onChange, extractors.checked); 8 | const onChangeIpAddress = onValueChange(Settings.SKEY_SOCKS5_IP, onChange); 9 | 10 | return ( 11 |
12 | 15 |
16 | 21 |
22 |
23 | 28 |
29 |
30 | 34 |
35 |
    36 |
  • {i18n.t('hint.proxy.1')}
  • 37 |
  • {i18n.t('hint.proxy.2')}
  • 38 |
39 |
40 | ); 41 | }; 42 | 43 | export { Socks5Proxy }; 44 | -------------------------------------------------------------------------------- /src/app/js/Components/SettingsPane/sections/SubtitleFont.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { shell } from 'electron'; 3 | import { onValueChange } from './hooks'; 4 | import Settings from '../../../Model/Settings'; 5 | 6 | const SubtitleFont = ({ i18n, onChange, value }) => { 7 | const onChangeFont = onValueChange(Settings.SKEY_PLAYER_SUB_FONT, onChange); 8 | const onCredit = useCallback(credit => { 9 | return () => shell.openExternal(credit); 10 | }, []); 11 | 12 | return ( 13 |
14 | 17 |
18 | {Settings.FONTS.map((font, index) => ( 19 |
20 | 33 |
34 | ))} 35 |
36 |
37 | ); 38 | }; 39 | 40 | export { SubtitleFont }; 41 | -------------------------------------------------------------------------------- /src/app/js/Components/SettingsPane/sections/Voices.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from 'react'; 2 | import Settings from '../../../Model/Settings'; 3 | import Dictionary from '../../../Model/Dictionary'; 4 | 5 | const Voices = ({ i18n, onChange, value }) => { 6 | const [voices, setVoices] = useState([ 7 | { 8 | name: i18n.t('voice.off'), 9 | key: 'Off' 10 | }, 11 | { 12 | name: i18n.t('system.voice'), 13 | key: '' 14 | } 15 | ]); 16 | 17 | useEffect(() => { 18 | const processVoices = speechVoices => { 19 | const items = speechVoices.map(({ name, lang }) => ({ name: `${name} [${lang}]`, key: name })); 20 | 21 | setVoices([...voices, ...items]); 22 | }; 23 | 24 | const speechVoices = window.speechSynthesis.getVoices(); 25 | 26 | if (speechVoices.length) { 27 | processVoices(speechVoices); 28 | } else { 29 | window.speechSynthesis.onvoiceschanged = () => { 30 | window.speechSynthesis.onvoiceschanged = undefined; 31 | processVoices(window.speechSynthesis.getVoices()); 32 | }; 33 | } 34 | }, [setVoices]); 35 | 36 | const onChangeVoice = useCallback( 37 | ({ target }) => { 38 | const { value } = target; 39 | 40 | if (!value) { 41 | Dictionary.pronounceDefault('Hello, this is the default system voice.'); 42 | } else if (value !== 'Off') { 43 | Dictionary.pronounce(`Hello, this is ${value}. I hope you like my voice.`, value); 44 | } 45 | 46 | onChange(Settings.SKEY_VOICE, value); 47 | }, 48 | [onChange] 49 | ); 50 | 51 | return ( 52 |
53 | 56 |
57 | 64 |
65 |
66 | {i18n.t('hint.install.voice.before.change')} 67 |
68 |
69 |
70 | ); 71 | }; 72 | 73 | export { Voices }; 74 | -------------------------------------------------------------------------------- /src/app/js/Components/SettingsPane/sections/WordBehavior.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { onValueChange } from './hooks'; 3 | import Settings from '../../../Model/Settings'; 4 | 5 | const WordBehavior = ({ i18n, onChange, value }) => { 6 | const onChangeWordBehavior = onValueChange(Settings.SKEY_CWB, onChange); 7 | 8 | return ( 9 |
10 | 13 |
14 |
15 | 25 |
26 |
27 | 37 |
38 |
39 | 50 |
51 |
52 |
53 | ); 54 | }; 55 | 56 | export { WordBehavior }; 57 | -------------------------------------------------------------------------------- /src/app/js/Components/SettingsPane/sections/hooks.js: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | 3 | const value = target => target.value; 4 | const checked = target => target.checked; 5 | const extractors = { value, checked }; 6 | 7 | const onValueChange = (key, onChange, extractor = value) => { 8 | return useCallback(({ target }) => onChange(key, extractor(target)), [onChange]); 9 | }; 10 | 11 | export { onValueChange, extractors }; 12 | -------------------------------------------------------------------------------- /src/app/js/Components/SubControls.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { remote } from 'electron'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | import { faEllipsisV } from '@fortawesome/free-solid-svg-icons'; 5 | import './Common.less'; 6 | import './SubControls.less'; 7 | 8 | const i18n = remote.require('./i18n'); 9 | const { Menu, MenuItem } = remote; 10 | 11 | export default class SubControls extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | const { onOpenYouTubeVideo, onOpenFile, onSettings } = this.props; 16 | 17 | this.menu = new Menu(); 18 | this.menu.append( 19 | new remote.MenuItem({ 20 | label: i18n.t('open.file'), 21 | click: () => { 22 | onOpenFile(); 23 | } 24 | }) 25 | ); 26 | if (PRO_VERSION) { 27 | this.menu.append( 28 | new MenuItem({ 29 | label: i18n.t('open.youtube.video'), 30 | click: () => { 31 | onOpenYouTubeVideo(); 32 | } 33 | }) 34 | ); 35 | } 36 | this.menu.append(new MenuItem({ type: 'separator' })); 37 | this.menu.append( 38 | new MenuItem({ 39 | role: 'recentDocuments', 40 | label: i18n.t('open.recent'), 41 | submenu: [ 42 | { 43 | label: i18n.t('clear.recent'), 44 | click() { 45 | remote.app.clearRecentDocuments(); 46 | } 47 | } 48 | ] 49 | }) 50 | ); 51 | this.menu.append(new MenuItem({ type: 'separator' })); 52 | this.menu.append( 53 | new MenuItem({ 54 | label: i18n.t('settings'), 55 | click: () => { 56 | onSettings(); 57 | } 58 | }) 59 | ); 60 | this.menu.append( 61 | new MenuItem({ 62 | label: i18n.t('quit'), 63 | click: () => { 64 | remote.app.quit(); 65 | } 66 | }) 67 | ); 68 | } 69 | 70 | render() { 71 | return ( 72 |
73 | 76 |
77 | ); 78 | } 79 | 80 | handleClickMenu = e => { 81 | e.stopPropagation(); 82 | this.menu.popup({ 83 | x: e.clientX - 180, 84 | y: e.clientY 85 | }); 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /src/app/js/Components/SubControls.less: -------------------------------------------------------------------------------- 1 | .byp-sub-controls { 2 | position: absolute; 3 | height: 100%; 4 | right: 2px; 5 | margin: 0; 6 | text-align: right; 7 | display: inline-block; 8 | z-index: 10; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/js/Components/SubtitlePane.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LineItemList from './LineItemList.jsx'; 3 | import SearchInput from './SearchInput.jsx'; 4 | import { remote } from 'electron'; 5 | import { breakSentence } from '../Model/KUtils'; 6 | import LocalSubtitleSettingsPane from './LocalSubtitleSettingsPane.jsx'; 7 | import YouTubeSubtitleSettingsPane from './YouTubeSubtitleSettingsPane.jsx'; 8 | 9 | const i18n = remote.require('./i18n'); 10 | 11 | export default class SubtitlePane extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | this.handleClickToggle = this.handleClickToggle.bind(this); 15 | this.onSearchUpdated = this.onSearchUpdated.bind(this); 16 | this.state = { 17 | searchTerm: '', 18 | searchTermWords: [], 19 | filterMode: true, 20 | showSettings: this.props.subtitles.length > 1 ? false : true, 21 | ccList: [] 22 | }; 23 | 24 | this.list = null; 25 | } 26 | 27 | scrollToSub(index) { 28 | this.list.scrollToSub(index); 29 | } 30 | 31 | updateSubtitleFontSize = () => { 32 | this.list.updateFontSize(); 33 | }; 34 | 35 | handleClickToggle(e) { 36 | this.setState({ showSettings: !this.state.showSettings }); 37 | } 38 | 39 | showSettings(v) { 40 | this.setState({ showSettings: v }); 41 | } 42 | 43 | onSearchUpdated(term) { 44 | var words = breakSentence(term); 45 | this.setState({ searchTerm: term, searchTermWords: words }); 46 | } 47 | 48 | handleFocusSearch = () => { 49 | this.props.onStartFilterSubtitle(); 50 | }; 51 | 52 | handleClickClear = () => { 53 | this.setState({ searchTerm: '', searchTermWords: [] }, () => { 54 | if (this.props.lineIndex != -1) { 55 | this.list.scrollToSub(this.props.lineIndex); 56 | } 57 | }); 58 | }; 59 | 60 | containsElement(el) {} 61 | 62 | setCCList = ccList => { 63 | this.setState({ ccList }); 64 | }; 65 | 66 | render() { 67 | const filteredLines = this.props.filterBySearchTerm 68 | ? this.props.lines.filter(line => line.text.toLowerCase().includes(this.state.searchTerm.toLowerCase())) 69 | : this.props.lines; 70 | 71 | return ( 72 |
73 | 81 | 82 | (this.list = ref)} 87 | searchTerm={this.state.searchTerm} 88 | searchTermWords={this.state.searchTermWords} 89 | /> 90 | {this.state.showSettings ? ( 91 | this.props.isLocal ? ( 92 | 93 | ) : ( 94 | 95 | ) 96 | ) : null} 97 |
102 | 103 |
104 |
105 | ); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/app/js/Components/SwitchesPane/SwitchesPane.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { remote } from 'electron'; 3 | import './SwitchesPane.less'; 4 | import { Switch } from 'antd'; 5 | 6 | const i18n = remote.require('./i18n'); 7 | 8 | export default class SwitchesPane extends React.Component { 9 | render() { 10 | const { fullScreen, skipNoDialogueClips, onToggleFullScreen, onToggleSkipNoDialogueClips } = this.props; 11 | return ( 12 |
(this.container = r)}> 13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 | ); 23 | } 24 | 25 | containsElement = element => { 26 | return this.container.contains(element); 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/app/js/Components/SwitchesPane/SwitchesPane.less: -------------------------------------------------------------------------------- 1 | @import '../PopupPane.less'; 2 | 3 | .byp-switches-pane { 4 | .popup-pane; 5 | height: 125px; 6 | left: 89px; 7 | width: 290px; 8 | } 9 | 10 | .byp-switches-pane:after, 11 | .byp-switches-pane:before { 12 | .popup-pane-after-and-before(40%); 13 | } 14 | 15 | .byp-switches-pane:after { 16 | .popup-pane-after; 17 | } 18 | 19 | .byp-switches-pane:before { 20 | .popup-pane-before; 21 | } 22 | 23 | .byp-switches-pane > div { 24 | display: flex; 25 | align-items: center; 26 | justify-content: center; 27 | height: 45px; 28 | } 29 | 30 | .byp-switches-pane-prefix { 31 | display: inline-block; 32 | width: 220px; 33 | padding-right: 15px; 34 | text-align: left; 35 | } 36 | -------------------------------------------------------------------------------- /src/app/js/Components/TagEditor/Input.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const SIZER_STYLES = { 4 | position: 'absolute', 5 | width: 0, 6 | height: 0, 7 | visibility: 'hidden', 8 | overflow: 'scroll', 9 | whiteSpace: 'pre' 10 | }; 11 | 12 | const STYLE_PROPS = ['fontSize', 'fontFamily', 'fontWeight', 'fontStyle', 'letterSpacing']; 13 | 14 | class Input extends React.Component { 15 | constructor(props) { 16 | super(props); 17 | this.state = { inputWidth: null }; 18 | } 19 | 20 | componentDidMount() { 21 | const { autoresize, autofocus } = this.props; 22 | 23 | if (autoresize) { 24 | this.copyInputStyles(); 25 | this.updateInputWidth(); 26 | } 27 | 28 | if (autofocus) { 29 | this.input.focus(); 30 | } 31 | } 32 | 33 | componentDidUpdate() { 34 | this.updateInputWidth(); 35 | } 36 | 37 | render() { 38 | const { inputAttributes, inputEventHandlers, query, placeholder, expandable, listboxId, selectedIndex } = this.props; 39 | const { inputWidth } = this.state; 40 | 41 | return ( 42 |
43 | { 47 | this.input = c; 48 | }} 49 | value={query} 50 | placeholder={placeholder} 51 | role="combobox" 52 | aria-autocomplete="list" 53 | aria-label={placeholder} 54 | aria-owns={listboxId} 55 | aria-activedescendant={selectedIndex > -1 ? `${listboxId}-${selectedIndex}` : null} 56 | aria-expanded={expandable} 57 | style={{ width: inputWidth }} 58 | /> 59 |
{ 61 | this.sizer = c; 62 | }} 63 | style={SIZER_STYLES}> 64 | {query || placeholder} 65 |
66 |
67 | ); 68 | } 69 | 70 | copyInputStyles() { 71 | const inputStyle = window.getComputedStyle(this.input); 72 | 73 | STYLE_PROPS.forEach(prop => { 74 | this.sizer.style[prop] = inputStyle[prop]; 75 | }); 76 | } 77 | 78 | updateInputWidth() { 79 | let inputWidth; 80 | const { autoresize } = this.props; 81 | const { inputWidth: stateInputWidth } = this.state; 82 | 83 | if (autoresize) { 84 | // scrollWidth is designed to be fast not accurate. 85 | // +2 is completely arbitrary but does the job. 86 | inputWidth = Math.ceil(this.sizer.scrollWidth) + 2; 87 | } 88 | 89 | if (inputWidth !== stateInputWidth) { 90 | this.setState({ inputWidth }); 91 | } 92 | } 93 | } 94 | 95 | module.exports = Input; 96 | -------------------------------------------------------------------------------- /src/app/js/Components/TagEditor/Suggestions.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | function escapeForRegExp(query) { 4 | return query.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&'); 5 | } 6 | 7 | function markIt(input, query) { 8 | let result = input; 9 | 10 | if (query) { 11 | const regex = RegExp(escapeForRegExp(query), 'gi'); 12 | 13 | result = input.replace(regex, '$&'); 14 | } 15 | 16 | return { 17 | __html: result 18 | }; 19 | } 20 | 21 | function filterSuggestions(query, suggestions, length, suggestionsFilter) { 22 | let filter = suggestionsFilter; 23 | 24 | if (!filter) { 25 | const regex = new RegExp(`(?:^|\\s)${escapeForRegExp(query)}`, 'i'); 26 | filter = item => regex.test(item.name); 27 | } 28 | 29 | return suggestions.filter(item => filter(item, query)).slice(0, length); 30 | } 31 | 32 | class Suggestions extends React.Component { 33 | constructor(props) { 34 | super(props); 35 | 36 | const { query, suggestions, maxSuggestionsLength, suggestionsFilter } = this.props; 37 | 38 | this.state = { 39 | options: filterSuggestions(query, suggestions, maxSuggestionsLength, suggestionsFilter) 40 | }; 41 | } 42 | 43 | // eslint-disable-next-line camelcase 44 | UNSAFE_componentWillReceiveProps(newProps) { 45 | this.setState({ 46 | options: filterSuggestions(newProps.query, newProps.suggestions, newProps.maxSuggestionsLength, newProps.suggestionsFilter) 47 | }); 48 | } 49 | 50 | render() { 51 | const { expandable, selectedIndex, listboxId, query } = this.props; 52 | const { options: stateOptions } = this.state; 53 | 54 | if (!expandable || !stateOptions.length) { 55 | return null; 56 | } 57 | 58 | const options = stateOptions.map((item, i) => { 59 | const key = `${listboxId}-${i}`; 60 | const classNames = []; 61 | 62 | if (selectedIndex === i) { 63 | classNames.push('is-active'); 64 | } 65 | 66 | if (item.disabled) { 67 | classNames.push('is-disabled'); 68 | } 69 | 70 | return ( 71 |
  • 78 | 79 |
  • 80 | ); 81 | }); 82 | 83 | return ( 84 |
    85 |
      86 | {options} 87 |
    88 |
    89 | ); 90 | } 91 | 92 | handleMouseDown(item, e) { 93 | // focus is shifted on mouse down but calling preventDefault prevents this 94 | e.preventDefault(); 95 | 96 | // eslint-disable-next-line react/destructuring-assignment 97 | this.props.addTag(item); 98 | } 99 | } 100 | 101 | module.exports = Suggestions; 102 | -------------------------------------------------------------------------------- /src/app/js/Components/TagEditor/Tag.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const Tag = ({ classNames, tag, onDelete }) => ( 4 | 7 | ); 8 | 9 | module.exports = Tag; 10 | -------------------------------------------------------------------------------- /src/app/js/Components/TagEditorDialog.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TagEditor from './TagEditor/TagEditor.jsx'; 3 | import VidLib from '../Model/VidLib'; 4 | import { remote } from 'electron'; 5 | const i18n = remote.require('./i18n'); 6 | 7 | export default class TagEditorDialog extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | tags: this.props.tags.map(tag => { 12 | return { name: tag }; 13 | }), 14 | suggestions: VidLib.getAllTags().map(tag => { 15 | return { name: tag }; 16 | }) 17 | }; 18 | } 19 | 20 | handleDelete = i => { 21 | const tags = this.state.tags.slice(0); 22 | tags.splice(i, 1); 23 | this.setState({ tags }); 24 | }; 25 | 26 | handleAddition = tag => { 27 | const tags = [].concat(this.state.tags, tag); 28 | this.setState({ tags }); 29 | }; 30 | 31 | handleClickOK = () => { 32 | let query = this.tagEditor.getQuery(); 33 | if (query) { 34 | const tags = [].concat(this.state.tags, { name: query }); 35 | this.setState({ tags }, () => { 36 | this.props.onClickOK( 37 | this.state.tags.map(tag => { 38 | return tag.name; 39 | }) 40 | ); 41 | }); 42 | } else { 43 | this.props.onClickOK( 44 | this.state.tags.map(tag => { 45 | return tag.name; 46 | }) 47 | ); 48 | } 49 | }; 50 | 51 | handleAddRecentTag = tag => { 52 | return e => { 53 | this.handleAddition({ name: tag }); 54 | }; 55 | }; 56 | 57 | render() { 58 | const { tags, suggestions } = this.state; 59 | return ( 60 |
    61 |
    62 |
    {i18n.t('editing.tags')}
    63 |
    64 | { 72 | this.tagEditor = r; 73 | }} 74 | /> 75 |
    76 | {this.props.recentTags.reverse().map((tag, i) => ( 77 | 80 | ))} 81 |
    82 |
    83 | 87 | 91 |
    92 |
    93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/app/js/Components/TitlePane.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class TitlePane extends React.Component { 4 | render() { 5 | return
    {this.props.title}
    ; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/app/js/Components/TunerPane.less: -------------------------------------------------------------------------------- 1 | @import url('./PopupPane.less'); 2 | 3 | .byp-tuner-pane { 4 | .popup-pane; 5 | } 6 | 7 | .byp-tuner-pane:after, 8 | .byp-tuner-pane:before { 9 | .popup-pane-after-and-before(42%); 10 | } 11 | 12 | .byp-tuner-pane:after { 13 | .popup-pane-after; 14 | } 15 | 16 | .byp-tuner-pane:before { 17 | .popup-pane-before; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/js/Components/VidItem.module.less: -------------------------------------------------------------------------------- 1 | .videoNum { 2 | position: absolute; 3 | display: inline-block; 4 | top: 3px; 5 | left: 3px; 6 | color: rgb(235, 235, 235); 7 | font-size: 12px; 8 | background-color: rgba(107, 107, 107, 0.151); 9 | padding: 0 10 0 10; 10 | border-radius: 2px; 11 | border: none; 12 | width: 22px; 13 | text-align: center; 14 | } 15 | -------------------------------------------------------------------------------- /src/app/js/Components/VidLibPane.module.less: -------------------------------------------------------------------------------- 1 | .icon { 2 | width: 29px; 3 | height: 29px; 4 | 5 | &Normal { 6 | margin-top: 5px; 7 | background-image: url('../Images/pm_sequence.svg'); 8 | } 9 | 10 | &AutoRepeat { 11 | margin-top: 5px; 12 | background-image: url('../Images/pm_auto_repeat.svg'); 13 | } 14 | 15 | &AutoPause { 16 | margin-top: 5px; 17 | background-image: url('../Images/pm_auto_pause.svg'); 18 | } 19 | } 20 | 21 | .toolbar { 22 | height: 52px; 23 | width: 100%; 24 | padding-left: 10px; 25 | text-align: right; 26 | position: relative; 27 | background-color: rgb(40, 40, 40); 28 | } 29 | 30 | .toolbarSeparator { 31 | width: 1px; 32 | height: 18px; 33 | border: none; 34 | outline: none; 35 | margin-top: 8px; 36 | background-color: rgb(94, 92, 92); 37 | } 38 | 39 | .toolbarButton { 40 | width: 35px; 41 | height: 100%; 42 | font-size: 22px; 43 | border: none; 44 | outline: none; 45 | background-color: rgba(39, 39, 39, 0); 46 | color: white; 47 | 48 | text-align: center; 49 | display: flex; 50 | align-items: center; 51 | justify-content: center; 52 | } 53 | 54 | .loopCounter { 55 | display: inline-block; 56 | width: 16px; 57 | height: 16px; 58 | font-size: 10px; 59 | line-height: 100%; 60 | top: -5px; 61 | left: 13px; 62 | text-align: center; 63 | position: relative; 64 | background-color: #127bc8; 65 | padding-top: 2px; 66 | padding-left: 0px; 67 | 68 | border-radius: 8px; 69 | color: white; 70 | } 71 | -------------------------------------------------------------------------------- /src/app/js/Components/VidLineItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { select } from '../Model/KUtils'; 3 | 4 | export default class VidLineItem extends React.PureComponent { 5 | static CHARS_PER_LINE = 38; 6 | static ROW_HEIGHT = 25; 7 | static ROW_PADDING = 16; 8 | 9 | constructor(props) { 10 | super(props); 11 | this.progressBar = null; 12 | } 13 | 14 | handleClickWord = e => { 15 | e.stopPropagation(); 16 | var word = e.target.innerText; 17 | //e.target.style["user-select"] = "text"; 18 | select(e.target); 19 | this.props.onClickWord(word, e.target); 20 | select(null); 21 | //e.target.style["user-select"] = "none"; 22 | }; 23 | 24 | updateProgress = percent => { 25 | this.progressBar.style.height = percent + '%'; 26 | }; 27 | 28 | buildClassName = () => { 29 | var cn = 'k-vid-line-item-words'; 30 | if (this.props.currentIndex === this.props.line.index && this.props.highlighted) { 31 | cn += ' k-selected'; 32 | } 33 | if (this.props.line.index % 2 == 0) { 34 | cn += ' k-even'; 35 | } else { 36 | cn += ' k-odd'; 37 | } 38 | return cn; 39 | }; 40 | 41 | handleMouseDown = e => { 42 | this.dragStartPos = [e.screenX, e.screenY]; 43 | }; 44 | 45 | handleMouseMove = e => { 46 | this.dragEndPos = [e.screenX, e.screenY]; 47 | }; 48 | 49 | handleMouseUp = e => { 50 | if (!this.dragStartPos) return; 51 | 52 | if (this.dragStartPos[0] == this.dragEndPos[0] && this.dragStartPos[1] == this.dragEndPos[1]) { 53 | if (e.target.nodeName != 'SPAN') { 54 | this.props.onClickLine(this.props.line.index, true); 55 | } 56 | } 57 | }; 58 | 59 | handleMouseLeave = e => {}; 60 | 61 | render() { 62 | const line = this.props.line; 63 | return ( 64 |
    (this.element = ref)} 67 | onMouseDown={this.handleMouseDown} 68 | onMouseMove={this.handleMouseMove} 69 | onMouseUp={this.handleMouseUp} 70 | onMouseLeave={this.handleMouseLeave} 71 | data-index={line.index} 72 | onContextMenu={this.props.onContextMenu} 73 | style={{ height: line.height }}> 74 |
    (this.progressBar = ref)} 77 | style={{ 78 | height: this.props.currentIndex > line.index ? '100%' : '0%', 79 | backgroundColor: this.props.highlighted ? 'rgb(9, 140, 228)' : 'rgb(40, 40, 40)' 80 | }}>
    81 |
    82 | {line.words.map((word, i) => 83 | word == '\n' ? ( 84 |
    85 | ) : ( 86 | = line.highlightStart && i <= line.highlightEnd ? ' k-highlighter' : ''} 88 | key={i} 89 | onClick={this.handleClickWord}> 90 | {' '} 91 | {word}{' '} 92 | 93 | ) 94 | )} 95 |
    96 |
    97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/app/js/Components/WebSourceItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { remote } from 'electron'; 3 | const i18n = remote.require('./i18n'); 4 | 5 | export default class WebSourceItem extends React.Component { 6 | handleChangeHomeUrl = e => { 7 | this.props.onChangeHomeUrl(this.props.index, e.target.value); 8 | }; 9 | 10 | handleChangeSearchUrl = e => { 11 | this.props.onChangeSearchUrl(this.props.index, e.target.value); 12 | }; 13 | 14 | handleChangeName = e => { 15 | this.props.onChangeName(this.props.index, e.target.value); 16 | }; 17 | 18 | handleChangeEnabled = e => { 19 | this.props.onChangeEnabled(this.props.index, e.target.checked); 20 | }; 21 | 22 | handleChangeSeparator = e => { 23 | this.props.onChangeSeparator(this.props.index, e.target.value); 24 | }; 25 | 26 | collectValue() { 27 | return { 28 | name: this.state.name, 29 | homeUrl: this.state.homeUrl, 30 | searchUrl: this.state.searchUrl, 31 | enabled: this.state.enabled 32 | }; 33 | } 34 | 35 | render() { 36 | return ( 37 |
    38 |
    39 | {i18n.t('web.source.name')} 40 |
    41 |
    42 | 43 |
    44 |
    45 | {i18n.t('web.source.search.url')} 46 |
    47 |
    48 | 49 |
    50 |
    51 | {i18n.t('web.source.home.url')} 52 |
    53 |
    54 | 55 |
    56 |
    57 | {i18n.t('web.source.separator')} 58 |
    59 |
    60 | 61 |
    62 | 63 |
    64 | 70 |
    71 |
    72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/app/js/Components/WordDefintionEditorPane.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { faUndo, faTimes } from '@fortawesome/free-solid-svg-icons'; 4 | import Dictionary from '../Model/Dictionary'; 5 | import { remote } from 'electron'; 6 | const i18n = remote.require('./i18n'); 7 | 8 | export default class WordDefinitionEditorPane extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | definition: props.definition 13 | }; 14 | } 15 | 16 | handleChange = e => { 17 | this.setState({ 18 | definition: e.target.value 19 | }); 20 | }; 21 | 22 | handleOK = e => { 23 | this.props.onOK(this.props.word, this.state.definition); 24 | }; 25 | 26 | handleCancel = e => { 27 | this.props.onCancel(); 28 | }; 29 | 30 | handleClickDelete = e => { 31 | if (confirm(i18n.t('confirm.delete.word.annotation'))) { 32 | this.props.onDeleteWordDefinition(this.props.word); 33 | } 34 | }; 35 | 36 | handleClickReload = e => { 37 | Dictionary.lookup(this.props.word, str => { 38 | this.setState({ 39 | definition: str 40 | }); 41 | }); 42 | }; 43 | 44 | render() { 45 | return ( 46 |
    47 |
    48 | {this.props.word} 49 | {this.props.isEditing ? ( 50 |
    51 | 58 | 64 |
    65 | ) : null} 66 |
    67 | 71 |
    72 | 76 | 80 |
    81 |
    82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/app/js/Components/WordItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LineItem from './LineItem.jsx'; 3 | import { select } from '../Model/KUtils'; 4 | import WordBookInstance from '../Model/WordBook'; 5 | 6 | export default class WordItem extends React.PureComponent { 7 | constructor(props) { 8 | super(props); 9 | this.element = null; 10 | } 11 | 12 | handleClickWord = e => { 13 | e.stopPropagation(); 14 | var word = e.target.innerText; 15 | select(e.target); 16 | this.props.onClickWord(word, e.target); 17 | select(null); 18 | }; 19 | 20 | handleClickLine = e => { 21 | this.props.onSelect(this.props.word); 22 | }; 23 | 24 | handleContextMenu = e => { 25 | this.props.onContextMenuOnWord(e); 26 | this.props.onSelect(this.props.word); 27 | }; 28 | 29 | handleClickLineOnLineItem = (index, target) => { 30 | this.props.onClickLineOnLineItem(index, this.props.word); 31 | }; 32 | 33 | highlight(flag) { 34 | if (flag) { 35 | this.element.classList.add('selected'); 36 | } else { 37 | this.element.classList.remove('selected'); 38 | } 39 | } 40 | 41 | handleClickRemove = () => { 42 | this.props.onRemoveWord(this.props.word); 43 | }; 44 | 45 | handleClickSearchInDictionary = () => { 46 | this.props.onSearchInDictionary(this.props.word); 47 | }; 48 | 49 | handleClickSearchInWeb = () => { 50 | this.props.onSearchInWeb(this.props.word); 51 | }; 52 | 53 | handleClickEditNote = () => { 54 | this.props.onEditNote(this.props.word); 55 | }; 56 | 57 | trimDef(def) { 58 | const MAX = 200; 59 | if (def.length > MAX) { 60 | return def.substring(0, MAX - 3) + '...'; 61 | } 62 | return def; 63 | } 64 | 65 | render() { 66 | const def = WordBookInstance.getWordDefinition(this.props.word); 67 | const textStyle = def ? 'underline' : 'inherit'; 68 | return ( 69 |
    70 |
    (this.element = ref)} 73 | onClick={this.handleClickLine} 74 | onContextMenu={this.handleContextMenu}> 75 | 76 | {this.props.word}{' '} 77 | 78 | {this.props.selected ? ( 79 |
    80 | 83 | 86 | 89 |
    90 | ) : null} 91 | {this.props.selected ? : null} 92 |
    93 |
    94 | {this.props.wordRelatedLines.map((line, i) => ( 95 | 105 | ))} 106 |
    107 |
    108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/app/js/Components/WordNotifier/WordNotifierItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import lerp from 'lerp'; 3 | import ReactTimeout from 'react-timeout'; 4 | 5 | class WordNotifierItem extends React.Component { 6 | static ANIM_TIME = 300; 7 | static REMOVE_TIME = 10000; 8 | 9 | constructor(props) { 10 | super(props); 11 | this.rootDOM = React.createRef(); 12 | this.state = { 13 | height: 0, 14 | opacity: 1 15 | }; 16 | } 17 | 18 | componentDidMount() { 19 | this.resetHeight(); 20 | if (!this.props.pause) { 21 | this.countTimeRemove(); 22 | } 23 | } 24 | 25 | countTimeRemove() { 26 | this.timer = this.props.setTimeout(() => { 27 | //this.hide(); 28 | this.props.onSelfRemove(this.props.notification); 29 | }, WordNotifierItem.REMOVE_TIME); 30 | } 31 | 32 | componentDidUpdate(preProps) { 33 | if (preProps.pause != this.props.pause) { 34 | if (this.props.pause) { 35 | clearTimeout(this.timer); 36 | } else { 37 | this.countTimeRemove(); 38 | } 39 | } 40 | } 41 | 42 | resetHeight() { 43 | this.setState({ height: this.rootDOM.current.scrollHeight }); 44 | } 45 | 46 | truncate(content) { 47 | const MAX = 120; 48 | if (content.length > MAX) { 49 | content = content.substring(0, MAX - 3) + '...'; 50 | } 51 | var lines = content.split('\n'); 52 | 53 | return lines.map((line, index) =>
    {line}
    ); 54 | } 55 | 56 | onClickClose = e => { 57 | e.stopPropagation(); 58 | e.preventDefault(); 59 | this.props.onClickClose(this.props.notification); 60 | }; 61 | 62 | onClickEdit = e => { 63 | e.stopPropagation(); 64 | e.preventDefault(); 65 | this.props.onClickEdit(this.props.notification); 66 | }; 67 | 68 | hide() { 69 | this.start = 0; 70 | this.playAnim = true; 71 | window.requestAnimationFrame(this.hideUpdate); 72 | } 73 | 74 | hideUpdate = timestamp => { 75 | if (!this.playAnim) return; 76 | 77 | if (!this.start) this.start = timestamp; 78 | let progress = timestamp - this.start; 79 | 80 | this.setState({ 81 | opacity: lerp(1, 0, progress / WordNotifierItem.ANIM_TIME) 82 | }); 83 | 84 | if (progress < WordNotifierItem.ANIM_TIME) { 85 | window.requestAnimationFrame(this.hideUpdate); 86 | } else { 87 | this.start = 0; 88 | this.props.onSelfRemove(this.props.notification); 89 | } 90 | }; 91 | 92 | render() { 93 | const { notification } = this.props; 94 | let { childElementStyle } = this.state; 95 | 96 | let fontSize = 14; 97 | 98 | if (this.props.containerSize > 600) { 99 | fontSize = 20; 100 | } 101 | 102 | const toolbar = ( 103 |
    104 | 105 | 106 |
    107 | ); 108 | 109 | return ( 110 |
    119 |
    120 |
    121 | {toolbar} 122 |

    {notification.word}

    123 |
    {this.truncate(notification.definition)}
    124 |
    125 |
    126 |
    127 | ); 128 | } 129 | } 130 | 131 | export default ReactTimeout(WordNotifierItem); 132 | -------------------------------------------------------------------------------- /src/app/js/Components/WordNotifier/constants.js: -------------------------------------------------------------------------------- 1 | export const NOTIFICATION_BASE_CLASS = 'notification-item'; 2 | 3 | export const CONTAINER = { 4 | BOTTOM_LEFT: 'bottom-left', 5 | BOTTOM_RIGHT: 'bottom-right', 6 | BOTTOM_CENTER: 'bottom-center', 7 | TOP_LEFT: 'top-left', 8 | TOP_RIGHT: 'top-right', 9 | TOP_CENTER: 'top-center' 10 | }; 11 | 12 | export const INSERTION = { 13 | TOP: 'top', 14 | BOTTOM: 'bottom' 15 | }; 16 | 17 | export const NOTIFICATION_TYPE = { 18 | SUCCESS: 'success', 19 | DANGER: 'danger', 20 | INFO: 'info', 21 | DEFAULT: 'default', 22 | WARNING: 'warning' 23 | }; 24 | 25 | export const NOTIFICATION_STAGE = { 26 | // used for both sliding and animation at the same time 27 | SLIDING_ANIMATION_EXIT: 'SLIDING_ANIMATION_EXIT', 28 | 29 | // used by API call to remove notification 30 | MANUAL_REMOVAL: 'REMOVAL' 31 | }; 32 | 33 | export const REMOVAL = { 34 | TIMEOUT: 1, 35 | CLICK: 2, 36 | TOUCH: 3, 37 | MANUAL: 4 38 | }; 39 | 40 | export const ERROR = { 41 | // dismiss icon option 42 | DISMISS_ICON_CLASS: 'className property of dismissIcon option is required', 43 | DISMISS_ICON_CONTENT: 'content property of dismissIcon option is required', 44 | DISMISS_ICON_STRING: 'className property of dismissIcon option must be a String', 45 | DISMISS_ICON_INVALID: 'content property of dismissIcon option must be a valid React element', 46 | 47 | // animations 48 | ANIMATION_IN: 'animationIn option must be an array', 49 | ANIMATION_OUT: 'animationOut option must be an array', 50 | 51 | // dismiss 52 | DISMISS_REQUIRED: 'duration property of dismiss option is required', 53 | DISMISS_NUMBER: 'duration property of dismiss option must be a Number', 54 | DISMISS_POSITIVE: 'duration property of dismiss option must be a positive Number', 55 | 56 | // title 57 | TITLE_STRING: 'title option must be a String.', 58 | 59 | // message 60 | MESSAGE_REQUIRED: 'message option is required', 61 | MESSAGE_STRING: 'message option must be a String', 62 | 63 | // type 64 | TYPE_REQUIRED: 'type option is required', 65 | TYPE_STRING: 'type option must be a String', 66 | TYPE_NOT_EXISTENT: 'type option not found', 67 | 68 | // container 69 | CONTAINER_REQUIRED: 'container option is required', 70 | CONTAINER_STRING: 'container option must be a String', 71 | 72 | // dismissable 73 | DISMISSABLE_CLICK_BOOL: 'click property of dismissable option must be a Boolean', 74 | DISMISSABLE_TOUCH_BOOL: 'touch property of dismissable option must be a Boolean', 75 | 76 | // width 77 | WIDTH_NUMBER: 'width option must be a Number', 78 | 79 | // insert 80 | INSERT_STRING: 'insert option must be a String', 81 | 82 | // transition 83 | TRANSITION_DURATION_NUMBER: 'duration property of transition option must be a Number', 84 | TRANSITION_CUBICBEZIER_NUMBER: 'cubicBezier property of transition option must be a String', 85 | TRANSITION_DELAY_NUMBER: 'delay property of transition option must be a Number', 86 | 87 | // custom types 88 | TYPE_NOT_FOUND: 'custom type not found' 89 | }; 90 | 91 | export const BREAKPOINT = 768; 92 | -------------------------------------------------------------------------------- /src/app/js/Components/WordNotifier/helpers.js: -------------------------------------------------------------------------------- 1 | export function getCubicBezierTransition(duration = 500, cubicBezier = 'linear', delay = 0, property = 'height') { 2 | return `${duration}ms ${property} ${cubicBezier} ${delay}ms`; 3 | } 4 | 5 | export function getRandomId() { 6 | return Math.random() 7 | .toString(36) 8 | .substr(2, 9); 9 | } 10 | 11 | export function getNotificationOptions(options) { 12 | const notification = options; 13 | const { id } = notification; 14 | 15 | notification.id = id || getRandomId(); 16 | 17 | return notification; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/js/Components/WordNotifier/notification.css: -------------------------------------------------------------------------------- 1 | .notification-container-root { 2 | position: absolute; 3 | top: 55px; 4 | bottom: 18%; 5 | right: 5px; 6 | z-index: 8000; 7 | margin-bottom: -20px; 8 | width: 25%; 9 | max-width: 350px; 10 | overflow-y: hidden; 11 | } 12 | 13 | .notification-container { 14 | position: absolute; 15 | top: 0; 16 | /* 17 | display: flex; 18 | flex-direction: row; 19 | flex-wrap: wrap-reverse; 20 | align-content: flex-start; 21 | */ 22 | width: 100%; 23 | pointer-events: auto; 24 | } 25 | 26 | .notification-default { 27 | background-color: rgba(255, 255, 255, 0.767); 28 | } 29 | 30 | .notification-title { 31 | color: black; 32 | font-weight: bold; 33 | font-size: 120%; 34 | } 35 | .notification-message { 36 | color: rgb(0, 0, 0); 37 | } 38 | 39 | .notification-close span { 40 | color: rgb(0, 0, 0); 41 | } 42 | 43 | .notification-item-root, 44 | .notification-title, 45 | .notification-message, 46 | .notification-item { 47 | font-family: Arial, Helvetica, sans-serif; 48 | } 49 | 50 | .notification-item { 51 | position: relative; 52 | } 53 | 54 | .notification-toolbar { 55 | font-size: 15px; 56 | position: absolute; 57 | right: 10px; 58 | top: 0; 59 | color: rgb(3, 3, 3); 60 | visibility: hidden; 61 | } 62 | .notification-toolbar span { 63 | padding-left: 5px; 64 | } 65 | 66 | .notification-item:hover .notification-toolbar { 67 | visibility: visible; 68 | } 69 | 70 | .nc-center { 71 | top: 50%; 72 | left: 50%; 73 | position: fixed; 74 | z-index: 8000; 75 | pointer-events: all; 76 | } 77 | 78 | .nc-box { 79 | left: -50%; 80 | position: relative; 81 | transform: translateY(-50%); 82 | } 83 | 84 | .notification-item { 85 | display: flex; 86 | position: relative; 87 | border-radius: 3px; 88 | cursor: pointer; 89 | /*box-shadow: 1px 3px 4px rgba(0, 0, 0, 0.2);*/ 90 | } 91 | 92 | .notification-container-top-right .notification-item-root, 93 | .notification-container-bottom-right .notification-item-root { 94 | margin-left: auto; 95 | } 96 | 97 | .notification-container-top-left .notification-item-root, 98 | .notification-container-bottom-left .notification-item-root { 99 | margin-right: auto; 100 | } 101 | 102 | .notification-item-root { 103 | width: 100%; 104 | margin-bottom: 5px; 105 | } 106 | 107 | .notification-title { 108 | font-weight: bold; 109 | margin-top: 5px; 110 | margin-bottom: 5px; 111 | } 112 | 113 | .notification-message { 114 | max-width: calc(100% - 15px); 115 | line-height: 150%; 116 | word-wrap: break-word; 117 | margin-bottom: 0; 118 | margin-top: 0; 119 | } 120 | 121 | .notification-invisible { 122 | visibility: hidden; 123 | max-width: 375px; 124 | } 125 | 126 | .notification-visible { 127 | visibility: visible; 128 | } 129 | 130 | .notification-content { 131 | padding: 8px 15px; 132 | display: inline-block; 133 | width: 100%; 134 | } 135 | -------------------------------------------------------------------------------- /src/app/js/Components/WordNotifier/utils.js: -------------------------------------------------------------------------------- 1 | export function cssWidth(width) { 2 | return width ? `${width}px` : undefined; 3 | } 4 | 5 | export function isNullOrUndefined(prop) { 6 | return prop === null || prop === undefined; 7 | } 8 | 9 | export function isString(object) { 10 | return typeof object === 'string'; 11 | } 12 | 13 | export function isNumber(object) { 14 | return typeof object === 'number'; 15 | } 16 | 17 | export function isBoolean(object) { 18 | return typeof object === 'boolean'; 19 | } 20 | 21 | export function isArray(object) { 22 | return !isNullOrUndefined(object) && object.constructor === Array; 23 | } 24 | -------------------------------------------------------------------------------- /src/app/js/Containers/App.less: -------------------------------------------------------------------------------- 1 | .ant-form-item { 2 | margin-bottom: 8px !important; 3 | } 4 | 5 | /* antd beyond overrides*/ 6 | .beyond-form { 7 | .ant-row { 8 | margin-bottom: 10px; 9 | } 10 | 11 | .ant-select { 12 | width: 100%; 13 | } 14 | 15 | .form-label { 16 | text-align: right; 17 | padding-right: 15px; 18 | } 19 | } 20 | 21 | .ant-modal-body, 22 | .ant-modal-header { 23 | padding: 14px; 24 | } 25 | 26 | .byp-center-container { 27 | position: absolute; 28 | height: 100%; 29 | width: 100%; 30 | z-index: 9; 31 | pointer-events: none; 32 | background-image: url('../Images/back.jpg'); 33 | background-size: cover; 34 | } 35 | 36 | .byp-clear-background { 37 | background-image: none; 38 | } 39 | -------------------------------------------------------------------------------- /src/app/js/Images/back.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/src/app/js/Images/back.jpg -------------------------------------------------------------------------------- /src/app/js/Images/book.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/js/Images/equalizer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/js/Images/loop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/js/Images/pause.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/js/Images/play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/js/Images/pm_auto_pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | macOS icon 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/app/js/Images/pm_auto_repeat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | macOS icon 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/app/js/Images/pm_repeat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | macOS icon 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/app/js/Images/pm_sequence.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | macOS icon 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/app/js/Images/sub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/src/app/js/Images/sub.png -------------------------------------------------------------------------------- /src/app/js/Images/switches.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/js/Images/tutorial.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/js/Model/Caption/addic7ed.js: -------------------------------------------------------------------------------- 1 | const addic7ed = require('addic7ed-api'); 2 | 3 | const download = async (item, path) => { 4 | return addic7ed.download(item.subInfo, path); 5 | }; 6 | 7 | const transform = (query, items) => 8 | items.map(item => ({ 9 | name: `${query}.${item.distribution}.${item.team}`, 10 | subInfo: item, 11 | extention: '', 12 | source: 'addic7ed', 13 | size: '', 14 | score: 0, 15 | download 16 | })); 17 | 18 | const textSearch = async (query, language, limit) => { 19 | const splitQuery = query.match(/s([0-9]{1,2})\s*e([0-9]{1,2}.*)/i); 20 | 21 | if (!splitQuery) { 22 | console.log(`Addic7ed: Can't parse ${query}...`); 23 | return []; 24 | } 25 | 26 | let serie = query.replace(splitQuery[0], ''); 27 | serie = serie.replace(/\./g, ' '); 28 | const season = parseInt(splitQuery[1], 10); 29 | const episode = parseInt(splitQuery[2], 10); 30 | 31 | let items = []; 32 | 33 | try { 34 | items = await addic7ed.search(serie, season, episode, language); 35 | 36 | if (!items) { 37 | console.log('Addic7ed: Nothing found...'); 38 | return []; 39 | } 40 | return transform(query, items); 41 | } catch (e) { 42 | alert('Unable to retrieve data from addic7ed:' + e); 43 | } 44 | 45 | return []; 46 | }; 47 | 48 | //TODO remove it 49 | const fileSearch = async () => {}; 50 | 51 | export default { 52 | textSearch, 53 | fileSearch, 54 | download 55 | }; 56 | -------------------------------------------------------------------------------- /src/app/js/Model/Caption/index.js: -------------------------------------------------------------------------------- 1 | const Promise = require('bluebird'); 2 | import addic7ed from './addic7ed'; 3 | import opensubtitles from './opensubtitles'; 4 | import path from 'path-extra'; 5 | import fs from 'fs-extra'; 6 | 7 | class Caption { 8 | sources; 9 | constructor() { 10 | this.sources = [opensubtitles, addic7ed]; 11 | } 12 | 13 | getLocalSubtitles = movieFile => { 14 | let dirName = path.dirname(movieFile); 15 | let idxFilePath = path.replaceExt(movieFile, '.kidx'); 16 | console.log(`idxFilePath: ${idxFilePath}`); 17 | 18 | if (!fs.existsSync(idxFilePath)) { 19 | fs.closeSync(fs.openSync(idxFilePath, 'w')); 20 | } 21 | let subFiles = fs 22 | .readFileSync(idxFilePath) 23 | .toString() 24 | .split('\n'); 25 | 26 | subFiles = subFiles.filter(subFile => subFile); 27 | 28 | subFiles = subFiles.map(subFile => { 29 | if (!subFile.startsWith('/')) { 30 | return path.join(dirName, subFile); 31 | } 32 | return subFile; 33 | }); 34 | return subFiles; 35 | }; 36 | 37 | searchByQuery = async (query, language = 'eng', limit = 10) => { 38 | if (language == 'eng') { 39 | query = query.replace(/[^\x00-\x7F]/g, ''); 40 | query = query.replace(/\.\.\./g, '.'); 41 | query = query.replace(/\.\./g, '.'); 42 | query = query.replace(/^\./g, ''); 43 | } 44 | 45 | const checkSources = this.sources.map(source => { 46 | try { 47 | return source.textSearch(query, language, limit); 48 | } catch (e) { 49 | alert(e); 50 | return []; 51 | } 52 | }); 53 | 54 | let all = await Promise.all(checkSources); 55 | let results = []; 56 | for (let r of all) { 57 | results = results.concat(r); 58 | } 59 | return results; 60 | }; 61 | 62 | searchByFiles(files, language = 'eng', limit = 10) { 63 | const opensubtitlesRef = opensubtitles.fileSearch(files, language, limit); 64 | 65 | return { 66 | on(event, callback) { 67 | switch (event) { 68 | case 'completed': 69 | default: 70 | // First promise which is resolved should return its results 71 | Promise.race([opensubtitlesRef]).then(results => callback(results)); 72 | 73 | return this; 74 | } 75 | } 76 | }; 77 | } 78 | 79 | download = async (item, source, filename) => { 80 | switch (source) { 81 | case 'opensubtitles': 82 | return await opensubtitles.download(item, filename); 83 | case 'addic7ed': 84 | return await addic7ed.download(item, filename); 85 | } 86 | }; 87 | } 88 | 89 | export default new Caption(); 90 | -------------------------------------------------------------------------------- /src/app/js/Model/Caption/opensubtitles.js: -------------------------------------------------------------------------------- 1 | import OS from 'opensubtitles-api'; 2 | import { head } from 'lodash'; 3 | import zlib from 'zlib'; 4 | import fse from 'fs-extra'; 5 | import bluebird from 'bluebird'; 6 | import iconvlite from 'iconv-lite'; 7 | import got from 'got'; 8 | 9 | const zlibUnzip = bluebird.promisify(zlib.unzip); 10 | 11 | const OpenSubtitles = new OS({ 12 | useragent: 'EMPlayer v1', 13 | ssl: true 14 | }); 15 | 16 | const download = async (item, path) => { 17 | const response = await got(item.downloadUrl, { encoding: null }); 18 | const unzipped = await zlibUnzip(response.body); 19 | const subtitleContent = iconvlite.decode(unzipped, item.encoding); 20 | await fse.writeFile(path, subtitleContent, 'utf8'); 21 | }; 22 | 23 | const transform = items => 24 | items.map(({ filename, url, encoding, score }) => ({ 25 | score, 26 | download, 27 | encoding, 28 | name: filename, 29 | downloadUrl: url, 30 | extention: '', 31 | source: 'opensubtitles', 32 | size: '' 33 | })); 34 | 35 | const textSearch = async (query, language, limit) => { 36 | const options = { 37 | sublanguageid: language, 38 | limit, 39 | query, 40 | gzip: true 41 | }; 42 | 43 | let items = []; 44 | 45 | try { 46 | items = await OpenSubtitles.search(options); 47 | 48 | if (!items) { 49 | console.log(`Opensubtitles: Nothing found...`); 50 | return []; 51 | } 52 | 53 | const firstItem = head(Object.keys(items)); // firstItem is selected language: obj[language] 54 | const results = items[firstItem]; 55 | 56 | if (!results) return []; 57 | 58 | return transform(results); 59 | } catch (e) { 60 | alert('Unable to retrieve data from OpenSubtitle: ' + e); 61 | } 62 | return items; 63 | }; 64 | 65 | const fileSearch = async (files, language, limit) => { 66 | const subtitleReferences = files.map(async file => { 67 | const info = await OpenSubtitles.identify({ 68 | path: file.path, 69 | extend: true 70 | }); 71 | 72 | const options = { 73 | limit, 74 | sublanguageid: language, 75 | hash: info.moviehash, 76 | filesize: info.moviebytesize, 77 | path: file.path, 78 | filename: file.filename, 79 | imdbid: null, 80 | gzip: true 81 | }; 82 | 83 | if (info && info.metadata && info.metadata.imdbid) { 84 | options['imdbid'] = info.metadata.imdbid; 85 | } 86 | 87 | const result = await OpenSubtitles.search(options); 88 | const firstItem = head(Object.keys(result)); 89 | const subtitle = result[firstItem]; 90 | 91 | return { 92 | file, 93 | subtitle 94 | }; 95 | }); 96 | 97 | const downloadedReferences = await Promise.all(subtitleReferences); 98 | const subtitleResults = downloadedReferences.filter(({ subtitle }) => subtitle !== undefined); 99 | 100 | return subtitleResults; 101 | }; 102 | 103 | export default { 104 | textSearch, 105 | fileSearch, 106 | download 107 | }; 108 | -------------------------------------------------------------------------------- /src/app/js/Model/Dictionary/index.js: -------------------------------------------------------------------------------- 1 | import say from 'say'; 2 | import path from 'path-extra'; 3 | import { exec } from 'child_process'; 4 | import open from 'open'; 5 | import { remote } from 'electron'; 6 | import Settings from '../Settings'; 7 | 8 | class Dictionary { 9 | lookup = async (word, callback) => { 10 | var appPath = remote.app.getAppPath(); 11 | var exePath = path.join(appPath, 'etc', 'lookup', 'osx-lookup').replace('app.asar', 'app.asar.unpacked'); 12 | exec(`"${exePath}" "${word}"`, (err, stdout, stderr) => { 13 | if (err) { 14 | console.log('not able to execute osx-dictionary:' + err); 15 | callback(null); 16 | return; 17 | } 18 | 19 | console.log(`stdout: ${stdout}`); 20 | console.log(`stderr: ${stderr}`); 21 | 22 | callback(stdout, stderr); 23 | 24 | /* 25 | var items = JSON.parse(stdout); 26 | items = items.filter(item=> item.definition); 27 | items.forEach(item => { 28 | item.definition = this.parseDefinition(item.definition) 29 | }); 30 | callback(items); 31 | */ 32 | }); 33 | }; 34 | 35 | parseDefinition(item) { 36 | var lines = item.split(/\s+(\d+|•)\s+/); 37 | console.log(lines); 38 | return lines; 39 | } 40 | 41 | showDicWindow = word => { 42 | if (Settings.getSettings(Settings.SKEY_EXT_DIC) == Settings.EXD_APPLE_DIC) { 43 | open(`dict://${word}`); 44 | } else if (Settings.getSettings(Settings.SKEY_EXT_DIC) == Settings.EXD_EUDIC) { 45 | open(`eudic://dict/${word}`); 46 | } 47 | }; 48 | 49 | pronounce = (word, voice) => { 50 | say.stop(); 51 | if (voice) { 52 | say.speak(word, voice); 53 | } else { 54 | var voiceFromSetttings = Settings.getSettings(Settings.SKEY_VOICE); 55 | if (voiceFromSetttings) { 56 | say.speak(word, voiceFromSetttings); 57 | } else if (voiceFromSetttings != 'Off') { 58 | say.speak(word); 59 | } 60 | } 61 | }; 62 | 63 | pronounceDefault = word => { 64 | say.stop(); 65 | say.speak(word); 66 | }; 67 | } 68 | 69 | export default new Dictionary(); 70 | -------------------------------------------------------------------------------- /src/app/js/Model/Export/destinations/anki/AnkiFileDestination.js: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import AnkiExport from 'anki-apkg-export'; 3 | 4 | function interpolateTemplate(params) { 5 | const names = Object.keys(params); 6 | const values = Object.values(params); 7 | 8 | return new Function(...names, `return \`${this}\`;`)(...values); 9 | } 10 | 11 | class AnkiFileDestination { 12 | constructor({ file, deckName, frontTemplate, backTemplate }) { 13 | this.file = file; 14 | this.deckName = deckName; 15 | this.backTemplate = backTemplate.replace(/{{/g, '${').replace(/}}/g, '}'); 16 | this.frontTemplate = frontTemplate.replace(/{{/g, '${').replace(/}}/g, '}'); 17 | } 18 | 19 | export = async source => { 20 | const apkgDeck = new AnkiExport(this.deckName); 21 | const backTemplate = interpolateTemplate.bind(this.backTemplate); 22 | const frontTemplate = interpolateTemplate.bind(this.frontTemplate); 23 | const isIncludeVideo = this.backTemplate.includes('frontVideo') || this.frontTemplate.includes('frontVideo'); 24 | const isIncludePreview = this.backTemplate.includes('frontPreview') || this.frontTemplate.includes('frontPreview'); 25 | 26 | for (const item of source) { 27 | const { id, frontVideo, frontPreview } = item; 28 | 29 | if (isIncludeVideo) { 30 | const frontVideoContent = await fs.readFile(frontVideo); 31 | apkgDeck.addMedia(`${id}.mp4`, frontVideoContent); 32 | 33 | item.frontVideo = `${id}.mp4`; 34 | } 35 | 36 | if (isIncludePreview) { 37 | const frontVideoContent = await fs.readFile(frontPreview); 38 | apkgDeck.addMedia(`${id}.png`, frontVideoContent); 39 | 40 | item.frontPreview = `${id}.png`; 41 | } 42 | 43 | apkgDeck.addCard(frontTemplate(item), backTemplate(item)); 44 | } 45 | 46 | const zipFile = await apkgDeck.save(); 47 | 48 | await fs.writeFile(this.file, zipFile, 'binary'); 49 | }; 50 | } 51 | 52 | export { AnkiFileDestination }; 53 | -------------------------------------------------------------------------------- /src/app/js/Model/Export/destinations/anki/index.js: -------------------------------------------------------------------------------- 1 | export { AnkiFileDestination } from './AnkiFileDestination'; 2 | -------------------------------------------------------------------------------- /src/app/js/Model/Export/index.js: -------------------------------------------------------------------------------- 1 | export { VidLibExportSourceAdapter } from './sources/VidLibExportSourceAdapter'; 2 | export { AnkiFileDestination } from './destinations/anki'; 3 | -------------------------------------------------------------------------------- /src/app/js/Model/Export/readme.md: -------------------------------------------------------------------------------- 1 | This folder contains two entities: sources and destinations; 2 | 3 | Source can be a vidlib or for example a word book. 4 | Destination can be anki or Apple Notes. 5 | 6 | Source should be a generator which returns source items 7 | Source item structure is: 8 | 9 | ``` 10 | { 11 | id, 12 | frontVideo?, // absolute video file path 13 | frontPreview?, // absolute image file path 14 | frontText, // front card text 15 | backText // back card text, 16 | tags?: [] 17 | } 18 | ``` 19 | 20 | Destination is a class which has an export method with a source as a param. 21 | This approach allows us to combine different sources and different destinations. 22 | -------------------------------------------------------------------------------- /src/app/js/Model/Export/sources/VidLibExportSourceAdapter.js: -------------------------------------------------------------------------------- 1 | const lines2Text = lines => 2 | lines 3 | .reduce((result, line) => { 4 | return `${result}${line.text}\n`; 5 | }, '') 6 | .replace(/\n$/, ''); 7 | 8 | const vid2SourceItem = viItem => { 9 | const { id, thumbnail: frontPreview, vid: frontVideo, lines = [], lines2 = [], tags } = viItem; 10 | 11 | const frontText = lines2Text(lines); 12 | const backText = lines2Text(lines2); 13 | 14 | return { 15 | id, 16 | frontVideo, 17 | frontPreview, 18 | frontText, 19 | backText, 20 | tags 21 | }; 22 | }; 23 | 24 | class VidLibExportSourceAdapter { 25 | constructor({ vidLib, filter = () => true }) { 26 | this.vidLib = vidLib; 27 | this.filter = filter; 28 | } 29 | 30 | *[Symbol.iterator]() { 31 | const ids = this.vidLib.retrieveAll(); 32 | 33 | for (let index = 0; index < ids.length; index += 1) { 34 | const sourceItem = this.vidLib.genVidInfo(ids[index]); 35 | const resultItem = vid2SourceItem(sourceItem); 36 | 37 | if (this.filter(resultItem)) { 38 | yield resultItem; 39 | } 40 | } 41 | } 42 | 43 | static getOutputFields() { 44 | return ['id', 'frontVideo', 'frontPreview', 'frontText', 'tags']; 45 | } 46 | } 47 | 48 | export { VidLibExportSourceAdapter }; 49 | -------------------------------------------------------------------------------- /src/app/js/Model/Export/sources/__tests__/VidLibExportSourceAdapter.spec.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | import { VidLibExportSourceAdapter } from '../VidLibExportSourceAdapter'; 3 | 4 | describe('VidLibExportSourceAdapter specs', () => { 5 | const createVidLibItem = () => ({ 6 | id: faker.random.alphaNumeric(), 7 | thumbnail: faker.system.filePath(), 8 | vid: faker.system.filePath(), 9 | tags: [], 10 | lines: [{ text: faker.lorem.sentence() }] 11 | }); 12 | 13 | const createVidLib = items => ({ 14 | retrieveAll: () => items.map(item => item.id), 15 | genVidInfo: id => { 16 | if (id === items[0].id) { 17 | return items[0]; 18 | } else if (id === items[1].id) { 19 | return items[1]; 20 | } 21 | 22 | throw new Error(); 23 | } 24 | }); 25 | 26 | const expectMatchObject = (resultItem, vidItem) => { 27 | expect(resultItem).toMatchObject({ 28 | id: vidItem.id, 29 | frontVideo: vidItem.vid, 30 | frontPreview: vidItem.thumbnail, 31 | frontText: vidItem.lines[0].text 32 | }); 33 | }; 34 | 35 | it('should convert vidLib items to the source items', () => { 36 | const vidLibItems = [createVidLibItem(), createVidLibItem()]; 37 | const vidLib = createVidLib(vidLibItems); 38 | 39 | const vidAdapter = new VidLibExportSourceAdapter({ vidLib }); 40 | const result = [...vidAdapter]; 41 | 42 | expect(result.length).toBe(2); 43 | expectMatchObject(result[0], vidLibItems[0]); 44 | expectMatchObject(result[1], vidLibItems[1]); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/app/js/Model/FFmpegHelper/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import util from 'util'; 3 | import log from 'electron-log'; 4 | import FFmpeg from 'fluent-ffmpeg'; 5 | import { remote } from 'electron'; 6 | 7 | const isDev = require('electron-is-dev'); 8 | const exec = util.promisify(require('child_process').exec); 9 | 10 | export const getFFmpegExePath = exeName => { 11 | let exePath; 12 | if (isDev) { 13 | const appPath = path.join(remote.app.getAppPath(), '..'); 14 | exePath = path.join(appPath, 'scripts', 'dist', exeName); 15 | } else { 16 | const appPath = remote.app.getAppPath(); 17 | exePath = path.join(appPath, 'node_modules', 'mpv.js', 'build', 'Release', exeName); 18 | } 19 | return exePath.replace('app.asar', 'app.asar.unpacked'); 20 | }; 21 | 22 | export const initFFmpegPath = () => { 23 | const ffmpegPath = getFFmpegExePath('ffmpeg'); 24 | const ffprobePath = getFFmpegExePath('ffprobe'); 25 | 26 | log.info(`Init ffmpeg path:${ffmpegPath}`); 27 | log.info(`Init ffprobe path:${ffprobePath}`); 28 | 29 | FFmpeg.setFfmpegPath(ffmpegPath); 30 | FFmpeg.setFfprobePath(ffprobePath); 31 | }; 32 | 33 | export const convertToSrt = async subtitleFile => { 34 | const baseName = path.basename(subtitleFile); 35 | const tempFile = path.join(remote.app.getPath('temp'), `${baseName}.srt`); 36 | const command = `"${getFFmpegExePath('ffmpeg')}" -y -i "${subtitleFile}" "${tempFile}"`; 37 | 38 | log.info(`Execute Command: ${command}`); 39 | 40 | try { 41 | const r = await exec(command); 42 | if (r.error) throw r.error; 43 | } catch (e) { 44 | log.error(`error on converting ${subtitleFile} to srt file: ${e}`); 45 | return ''; 46 | } 47 | 48 | return tempFile; 49 | }; 50 | -------------------------------------------------------------------------------- /src/app/js/Model/LRCHelper/index.js: -------------------------------------------------------------------------------- 1 | import parser from 'lrc-parser'; 2 | 3 | class LRCHelper { 4 | parseLRC = str => { 5 | const lrc = parser(str); 6 | lrc.scripts.forEach(line => { 7 | line.start = line.start * 1000; 8 | line.end = line.end * 1000; 9 | }); 10 | return lrc.scripts; 11 | }; 12 | } 13 | 14 | export default new LRCHelper(); 15 | -------------------------------------------------------------------------------- /src/app/js/Model/MRUFiles/index.js: -------------------------------------------------------------------------------- 1 | import storage from 'electron-json-storage'; 2 | import _ from 'lodash'; 3 | 4 | class MRUFiles { 5 | KEY = 'mru'; 6 | MAX_COUNT = 20; 7 | files = []; 8 | 9 | load = callback => { 10 | storage.get(this.KEY, (error, data) => { 11 | if (error) throw error; 12 | 13 | if (_.isEmpty(data)) { 14 | this.files = []; 15 | } else { 16 | this.files = data; 17 | } 18 | callback(this.files); 19 | }); 20 | }; 21 | 22 | add = file => { 23 | if (!this.files || _.isEmpty(this.files)) { 24 | this.files = []; 25 | } 26 | 27 | let index = this.files.findIndex(item => { 28 | if (_.isString(item)) { 29 | return item == file; 30 | } else { 31 | return item.url == file.url; 32 | } 33 | }); 34 | 35 | if (index != -1) { 36 | this.files.splice(index, 1); 37 | this.files.unshift(file); 38 | } else { 39 | this.files.unshift(file); 40 | if (this.files.length > this.MAX_COUNT) { 41 | this.files.pop(); 42 | } 43 | } 44 | 45 | console.log(this.files); 46 | 47 | storage.set(this.KEY, this.files, function(error) { 48 | if (error) throw error; 49 | }); 50 | }; 51 | } 52 | 53 | export default new MRUFiles(); 54 | -------------------------------------------------------------------------------- /src/app/js/Model/MansonryLayout/index.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | 3 | export default class MansonryLayout extends EventEmitter { 4 | options; 5 | container; 6 | 7 | columnCount; 8 | columnHeights = void 0; 9 | 10 | persist = void 0; // packing new elements, or all elements? 11 | 12 | nodes = void 0; 13 | nodesWidths = void 0; 14 | nodesHeights = void 0; 15 | 16 | packed; 17 | 18 | constructor(options) { 19 | super(); 20 | this.options = options; 21 | this.container = this.options.container.nodeType ? this.options.container : document.querySelector(this.options.container); 22 | this.packed = this.options.packed.indexOf('data-') === 0 ? this.options.packed : 'data-' + this.options.packed; 23 | this.size = this.options.size; 24 | this.position = this.options.position !== false; 25 | } 26 | 27 | selectors = { 28 | all: () => { 29 | return this.toArray(this.container.children); 30 | }, 31 | new: () => { 32 | return this.toArray(this.container.children).filter(node => { 33 | return !node.hasAttribute('' + this.packed); 34 | }); 35 | } 36 | }; 37 | 38 | runSeries = functions => { 39 | functions.forEach(function(func) { 40 | return func(); 41 | }); 42 | }; 43 | 44 | toArray = input => { 45 | return Array.prototype.slice.call(input); 46 | }; 47 | 48 | fillArray = length => { 49 | return Array.apply(null, Array(length)).map(function() { 50 | return 0; 51 | }); 52 | }; 53 | 54 | setupColumns = () => { 55 | this.columnCount = Math.floor(this.container.clientWidth / (this.size.columnWidth + this.size.gap)); 56 | this.columnHeights = this.fillArray(this.columnCount); 57 | 58 | this.margin = (this.container.clientWidth - this.columnCount * this.size.columnWidth - (this.columnCount - 1) * this.size.gap) / 2.0; 59 | }; 60 | 61 | setupNodes = () => { 62 | this.nodes = this.selectors[this.persist ? 'new' : 'all'](); 63 | }; 64 | 65 | setupNodesDimensions = () => { 66 | if (this.nodes.length === 0) return; 67 | 68 | this.nodesWidths = this.nodes.map(function(element) { 69 | return element.clientWidth; 70 | }); 71 | this.nodesHeights = this.nodes.map(function(element) { 72 | return element.clientHeight; 73 | }); 74 | }; 75 | 76 | setupNodesStyles = () => { 77 | this.nodes.forEach((element, index) => { 78 | //alert(Math.min.apply(Math, this.columnHeights)); 79 | //const columnIndex = this.columnHeights.indexOf(Math.min.apply(Math, this.columnHeights)); 80 | const columnIndex = index % this.columnHeights.length; 81 | 82 | element.style.position = 'absolute'; 83 | 84 | const nodeTop = this.columnHeights[columnIndex] + 'px'; 85 | 86 | //alert(columnIndex) 87 | const nodeLeft = this.margin + columnIndex * this.size.columnWidth + columnIndex * this.size.gap + 'px'; 88 | 89 | //alert(nodeLeft); 90 | 91 | element.style.top = nodeTop; 92 | element.style.left = nodeLeft; 93 | element.setAttribute(this.packed, ''); 94 | 95 | // ignore nodes with no width and/or height 96 | const nodeWidth = this.nodesWidths[index]; 97 | const nodeHeight = this.nodesHeights[index]; 98 | 99 | if (nodeWidth && nodeHeight) { 100 | this.columnHeights[columnIndex] += nodeHeight + this.size.vgap; 101 | } 102 | }); 103 | }; 104 | 105 | // API 106 | pack = () => { 107 | this.persist = false; 108 | this.runSeries(this.setup.concat(this.run)); 109 | return this.emit('pack'); 110 | }; 111 | 112 | update = () => { 113 | this.persist = true; 114 | this.runSeries(this.run); 115 | return this.emit('update'); 116 | }; 117 | 118 | setup = [this.setupColumns]; 119 | run = [this.setupNodes, this.setupNodesDimensions, this.setupNodesStyles]; 120 | } 121 | -------------------------------------------------------------------------------- /src/app/js/Model/PlayMode.js: -------------------------------------------------------------------------------- 1 | export default Object.freeze({ 2 | NORMAL: 0, 3 | AUTO_REPEAT: 1, 4 | AUTO_PAUSE: 2 5 | }); 6 | -------------------------------------------------------------------------------- /src/app/js/Model/SubExtractor/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import Ffmpeg from 'fluent-ffmpeg'; 4 | 5 | const fileExists = require('file-exists').sync; 6 | 7 | const streamToString = (stream, enc) => { 8 | let str = ''; 9 | return new Promise((resolve, reject) => { 10 | stream.on('data', data => { 11 | str += typeof enc === 'string' ? data.toString(enc) : data.toString(); 12 | }); 13 | 14 | stream.on('end', () => resolve(str)); 15 | stream.on('error', reject); 16 | }); 17 | }; 18 | 19 | export const subtitleExtractor = (filePath, outputDir, progressCallback) => { 20 | const dir = outputDir || path.dirname(filePath); 21 | const name = path.basename(filePath, path.extname(filePath)); 22 | const srtPath = language => { 23 | const languageSuffix = language ? `.${language}` : ''; 24 | return path.join(dir, `${name + languageSuffix}.srt`); 25 | }; 26 | return new Promise((resolve, reject) => 27 | Ffmpeg({ source: filePath }).ffprobe(async (err, { streams }) => { 28 | if (err) { 29 | return reject(err); 30 | } 31 | 32 | const subtitles = streams.filter( 33 | ({ codec_type, codec_name }) => codec_type === 'subtitle' && !codec_name.match(/.*_pgs_*.|dvd_subtitle/i) 34 | ); 35 | const result = []; 36 | 37 | try { 38 | for (const subtitleItem of subtitles) { 39 | const { index, tags = {} } = subtitleItem; 40 | const language = tags.language || tags.LANGUAGE || index; 41 | // const title = tags.title || ''; 42 | let text = await new Promise((subtitleResolve, subtitleReject) => { 43 | let subtitleText; 44 | 45 | const stream = Ffmpeg({ source: filePath }) 46 | .outputOptions(`-map 0:${index}`) 47 | .format('srt') 48 | .on('error', subtitleReject) 49 | .on('end', () => subtitleResolve(subtitleText)) 50 | .pipe(undefined, { end: true }); 51 | 52 | subtitleText = streamToString(stream); 53 | }); 54 | 55 | text = text.replace(/\<[^>]*\>/g, ''); 56 | let subtitlePath = srtPath(language); 57 | for (let i = 2; fileExists(subtitlePath); i += 1) { 58 | subtitlePath = language ? srtPath(language + i) : srtPath(i); 59 | } 60 | fs.writeFileSync(subtitlePath, text, { encoding: 'utf-8' }); 61 | const item = { 62 | number: index, 63 | path: subtitlePath, 64 | language 65 | }; 66 | result.push(item); 67 | setTimeout(() => { 68 | progressCallback(item, result.length - 1); 69 | }, 0); 70 | } 71 | } catch (error) { 72 | return reject(error); 73 | } 74 | 75 | resolve(result); 76 | }) 77 | ); 78 | }; 79 | -------------------------------------------------------------------------------- /src/app/js/Model/SubStore/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path-extra'; 2 | import { remote, shell } from 'electron'; 3 | import md5 from 'md5'; 4 | import fs from 'fs-extra'; 5 | const i18n = remote.require('./i18n'); 6 | import { deleteFolderRecursiveSync, parseLinesFromFile } from '../KUtils'; 7 | 8 | class SubStore { 9 | basePath; 10 | 11 | constructor() { 12 | this.basePath = path.join(remote.app.getPath('userData'), 'subtitles'); 13 | fs.ensureDirSync(this.basePath); 14 | } 15 | 16 | getStorePath(filePath) { 17 | var hash = md5(filePath); 18 | console.log(`hash: ${hash}`); 19 | return path.join(this.basePath, hash); 20 | } 21 | 22 | createStoreFolder(filePath) { 23 | var folderPath = this.getStorePath(filePath); 24 | fs.ensureDirSync(folderPath); 25 | } 26 | 27 | saveTimePos = (movieUrl, timePos, timeOffset, secondTimeOffset, subtitle, secondSubtitle) => { 28 | const storePath = this.getStorePath(movieUrl); 29 | fs.ensureDirSync(storePath); 30 | 31 | const timePosFilePath = this.getStoreTimePosFile(movieUrl); 32 | 33 | const obj = { 34 | timePos, 35 | subtitle, 36 | secondSubtitle, 37 | timeOffset, 38 | secondTimeOffset 39 | }; 40 | fs.writeFileSync(timePosFilePath, JSON.stringify(obj)); 41 | }; 42 | 43 | getStoreTimePosFile = movieUrl => { 44 | let basename = path.basename(movieUrl); 45 | let storePath = this.getStorePath(movieUrl); 46 | console.log(`storePath: ${storePath}`); 47 | let timePosFilePath = path.join(storePath, basename + '.tps'); 48 | console.log(`timePosFilePath: ${timePosFilePath}`); 49 | return timePosFilePath; 50 | }; 51 | 52 | getLocalTimePos = movieUrl => { 53 | if (movieUrl.startsWith('http')) return { timePos: 0, subtitle: '', timeOffset: 0 }; 54 | 55 | let timePosFilePath = this.getStoreTimePosFile(movieUrl); 56 | 57 | if (!fs.existsSync(timePosFilePath)) { 58 | fs.closeSync(fs.openSync(timePosFilePath, 'w')); 59 | } 60 | let obj = {}; 61 | try { 62 | obj = JSON.parse(fs.readFileSync(timePosFilePath).toString()); 63 | } catch (e) {} 64 | 65 | if (!obj.timePos) { 66 | obj.timePos = 0; 67 | } 68 | if (!obj.subtitle) { 69 | obj.subtitle = ''; 70 | } 71 | if (!obj.secondSubtitle) { 72 | obj.secondSubtitle = ''; 73 | } 74 | if (!obj.timeOffset) { 75 | obj.timeOffset = 0; 76 | } 77 | if (!obj.secondTimeOffset) { 78 | obj.secondTimeOffset = 0; 79 | } 80 | return obj; 81 | }; 82 | 83 | clearSubtitles = (movieUrl, callback) => { 84 | if (!movieUrl || movieUrl.startsWith('http')) return; 85 | 86 | let storePath = this.getStorePath(movieUrl); 87 | if (fs.existsSync(storePath)) { 88 | var result = confirm(i18n.t('confirm.clear.subtitles')); 89 | if (result) { 90 | deleteFolderRecursiveSync(storePath); 91 | if (callback) callback(); 92 | } 93 | } 94 | }; 95 | 96 | getLocalSubtitles = movieUrl => { 97 | if (movieUrl.startsWith('http')) return []; 98 | 99 | let storePath = this.getStorePath(movieUrl); 100 | if (!fs.existsSync(storePath)) { 101 | fs.mkdirpSync(storePath); 102 | } 103 | 104 | let subFiles = fs 105 | .readdirSync(storePath) 106 | .filter(file => { 107 | var lower = file.toLocaleLowerCase(); 108 | return lower.endsWith('srt') || lower.endsWith('lrc'); 109 | }) 110 | .map(file => path.join(storePath, file)); 111 | 112 | subFiles.unshift('None'); 113 | return subFiles; 114 | }; 115 | 116 | addExternalSubtitle = (movieUrl, subtitleFileName) => { 117 | this.createStoreFolder(movieUrl); 118 | let storePath = path.join(this.getStorePath(movieUrl), path.basename(subtitleFileName)); 119 | fs.copySync(subtitleFileName, storePath); 120 | }; 121 | 122 | revealSubtitleFolder = movieUrl => { 123 | if (!movieUrl || movieUrl.startsWith('http')) return; 124 | 125 | let storePath = this.getStorePath(movieUrl); 126 | if (fs.existsSync(storePath)) { 127 | shell.openItem(storePath); 128 | } 129 | }; 130 | } 131 | 132 | export default new SubStore(); 133 | -------------------------------------------------------------------------------- /src/app/js/Model/Subtitle/index.js: -------------------------------------------------------------------------------- 1 | export { default as toMS } from './toMS'; 2 | export { default as toSrtTime } from './toSrtTime'; 3 | export { default as toVttTime } from './toVttTime'; 4 | export { default as parse } from './parse'; 5 | export { default as stringify } from './stringify'; 6 | export { default as stringifyVtt } from './stringifyVtt'; 7 | export { default as resync } from './resync'; 8 | export { default as parseTimestamps } from './parseTimestamps'; 9 | -------------------------------------------------------------------------------- /src/app/js/Model/Subtitle/parse.js: -------------------------------------------------------------------------------- 1 | import parseTimestamps from './parseTimestamps'; 2 | /** 3 | * Parse a SRT or WebVTT string. 4 | * @param {String} srtOrVtt 5 | * @return {Array} subtitles 6 | */ 7 | 8 | export default function parse(srtOrVtt) { 9 | if (!srtOrVtt) return []; 10 | 11 | const source = srtOrVtt 12 | .trim() 13 | .concat('\n') 14 | .replace(/\r\n/g, '\n') 15 | .replace(/\\N/g, '\n') 16 | .replace(/\n{3,}/g, '\n\n') 17 | .replace(/^WEBVTT.*\n(?:.*: .*\n)*\n/, '') 18 | .replace(//g, '') 19 | .replace(/<\/?b>/g, '') 20 | .replace(/<\/font>/g, '') 21 | .split('\n'); 22 | 23 | let lines = source.reduce( 24 | (captions, row, index) => { 25 | const caption = captions[captions.length - 1]; 26 | 27 | if (!caption.index) { 28 | if (/^-?\d+$/.test(row)) { 29 | caption.index = parseInt(row, 10); 30 | return captions; 31 | } 32 | } 33 | 34 | if (!caption.hasOwnProperty('start')) { 35 | let timeObj; 36 | try { 37 | timeObj = parseTimestamps(row); 38 | } catch (ex) { 39 | console.log('Unable to parse row: ' + row); 40 | console.log('ex:' + ex); 41 | timeObj = { start: 0, end: 0 }; 42 | } 43 | Object.assign(caption, timeObj); 44 | return captions; 45 | } 46 | 47 | if (row === '') { 48 | delete caption.index; 49 | if (index !== source.length - 1) { 50 | captions.push({}); 51 | } 52 | } else { 53 | row = row.replace(/\{\\.+?\}/g, ''); 54 | caption.text = caption.text ? caption.text + '\n' + row : row; 55 | } 56 | 57 | return captions; 58 | }, 59 | [{}] 60 | ); 61 | 62 | let prevStart, prevEnd; 63 | lines = lines.reduce((result, line, index) => { 64 | if (index != 0) { 65 | if (line.start == prevStart && line.end == prevEnd) { 66 | result[result.length - 1].text += '\n' + line.text; 67 | } else { 68 | result.push(line); 69 | } 70 | } else { 71 | result.push(line); 72 | } 73 | prevStart = line.start; 74 | prevEnd = line.end; 75 | return result; 76 | }, []); 77 | 78 | return lines; 79 | } 80 | -------------------------------------------------------------------------------- /src/app/js/Model/Subtitle/parseTimestamps.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | import toMS from './toMS'; 6 | 7 | /** 8 | * Timestamp regex 9 | * @type {RegExp} 10 | */ 11 | 12 | const RE = /^((?:\d{2,}:)?\d{2}:\d{2}[,.]\d{2}\d?) --> ((?:\d{2,}:)?\d{2}:\d{2}[,.]\d{2}\d?)(?: (.*))?$/; 13 | 14 | /** 15 | * parseTimestamps 16 | * @param value 17 | * @returns {{start: Number, end: Number}} 18 | */ 19 | 20 | export default function parseTimestamps(value) { 21 | const match = RE.exec(value); 22 | const cue = { 23 | start: toMS(match[1]), 24 | end: toMS(match[2]) 25 | }; 26 | if (match[3]) { 27 | cue.settings = match[3]; 28 | } 29 | return cue; 30 | } 31 | -------------------------------------------------------------------------------- /src/app/js/Model/Subtitle/resync.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | import toMS from './toMS'; 6 | 7 | /** 8 | * Resync the given subtitles. 9 | * @param captions 10 | * @param time 11 | * @returns {Array|*} 12 | */ 13 | 14 | export default function resync(captions, time) { 15 | return captions.map(caption => { 16 | const start = toMS(caption.start) + time; 17 | const end = toMS(caption.end) + time; 18 | 19 | return Object.assign({}, caption, { 20 | start, 21 | end 22 | }); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /src/app/js/Model/Subtitle/stringify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | import toSrtTime from './toSrtTime'; 6 | 7 | /** 8 | * Stringify the given array of captions. 9 | * @param {Array} captions 10 | * @return {String} srt 11 | */ 12 | 13 | export default function stringify(captions) { 14 | return ( 15 | captions 16 | .map((caption, index) => { 17 | return (index > 0 ? '\n' : '') + [index + 1, `${toSrtTime(caption.start)} --> ${toSrtTime(caption.end)}`, caption.text].join('\n'); 18 | }) 19 | .join('\n') + '\n' 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/app/js/Model/Subtitle/stringifyVtt.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | import toVttTime from './toVttTime'; 6 | 7 | /** 8 | * Stringify the given array of captions to WebVTT format. 9 | * @param {Array} captions 10 | * @return {String} webVtt 11 | */ 12 | 13 | export default function stringifyVtt(captions) { 14 | return ( 15 | 'WEBVTT\n\n' + 16 | captions 17 | .map((caption, index) => { 18 | return ( 19 | (index > 0 ? '\n' : '') + 20 | [ 21 | index + 1, 22 | `${toVttTime(caption.start)} --> ${toVttTime(caption.end)}${caption.settings ? ' ' + caption.settings : ''}`, 23 | caption.text 24 | ].join('\n') 25 | ); 26 | }) 27 | .join('\n') + 28 | '\n' 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/app/js/Model/Subtitle/toMS.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Return the given SRT timestamp as milleseconds. 3 | * @param {string|number} timestamp 4 | * @returns {number} milliseconds 5 | */ 6 | 7 | export default function toMS(timestamp) { 8 | if (!isNaN(timestamp)) { 9 | return timestamp; 10 | } 11 | 12 | const match = timestamp.match(/^(?:(\d{2,}):)?(\d{2}):(\d{2})[,.](\d{2}\d?)$/); 13 | 14 | if (!match) { 15 | throw new Error('Invalid SRT or VTT time format: "' + timestamp + '"'); 16 | } 17 | 18 | const hours = match[1] ? parseInt(match[1], 10) * 3600000 : 0; 19 | const minutes = parseInt(match[2], 10) * 60000; 20 | const seconds = parseInt(match[3], 10) * 1000; 21 | const milliseconds = parseInt(match[4], 10); 22 | 23 | return hours + minutes + seconds + milliseconds; 24 | } 25 | -------------------------------------------------------------------------------- /src/app/js/Model/Subtitle/toSrtTime.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | import zeroFill from 'zero-fill'; 6 | 7 | /** 8 | * Return the given milliseconds as SRT timestamp. 9 | * @param timestamp 10 | * @returns {string} 11 | */ 12 | 13 | export default function toSrtTime(timestamp) { 14 | if (isNaN(timestamp)) { 15 | return timestamp; 16 | } 17 | 18 | const date = new Date(0, 0, 0, 0, 0, 0, timestamp); 19 | 20 | const hours = zeroFill(2, date.getHours()); 21 | const minutes = zeroFill(2, date.getMinutes()); 22 | const seconds = zeroFill(2, date.getSeconds()); 23 | const ms = timestamp - (hours * 3600000 + minutes * 60000 + seconds * 1000); 24 | 25 | return `${hours}:${minutes}:${seconds},${zeroFill(3, ms)}`; 26 | } 27 | -------------------------------------------------------------------------------- /src/app/js/Model/Subtitle/toVttTime.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | import zeroFill from 'zero-fill'; 6 | 7 | /** 8 | * Return the given milliseconds as WebVTT timestamp. 9 | * @param timestamp 10 | * @returns {string} 11 | */ 12 | 13 | export default function toVttTime(timestamp) { 14 | if (isNaN(timestamp)) { 15 | return timestamp; 16 | } 17 | 18 | const date = new Date(0, 0, 0, 0, 0, 0, timestamp); 19 | 20 | const hours = zeroFill(2, date.getHours()); 21 | const minutes = zeroFill(2, date.getMinutes()); 22 | const seconds = zeroFill(2, date.getSeconds()); 23 | const ms = timestamp - (hours * 3600000 + minutes * 60000 + seconds * 1000); 24 | 25 | return `${hours}:${minutes}:${seconds}.${zeroFill(3, ms)}`; 26 | } 27 | -------------------------------------------------------------------------------- /src/app/js/Model/TagSearch/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra'; 2 | 3 | export default class TagSearch { 4 | filePath; 5 | index; 6 | 7 | constructor(filePath) { 8 | this.filePath = filePath; 9 | } 10 | 11 | load = () => { 12 | if (fs.existsSync(this.filePath)) { 13 | this.index = fs.readJSONSync(this.filePath); 14 | } else { 15 | this.index = {}; 16 | } 17 | }; 18 | 19 | getAllTags = () => { 20 | return Object.keys(this.index); 21 | }; 22 | 23 | getAllTagCounts = () => { 24 | let tags = Object.keys(this.index); 25 | let counts = []; 26 | tags.forEach(value => { 27 | let count = this.index[value].length; 28 | if (count > 0) { 29 | counts.push({ value, count }); 30 | } 31 | }); 32 | return counts; 33 | }; 34 | 35 | add = (tag, id, save = false) => { 36 | let idList = []; 37 | if (this.index.hasOwnProperty(tag)) { 38 | idList = this.index[tag]; 39 | } else { 40 | this.index[tag] = idList; 41 | } 42 | if (idList.indexOf(id) === -1) { 43 | idList.push(id); 44 | } 45 | if (save) { 46 | this.save(); 47 | } 48 | }; 49 | 50 | addAll = (tags, id, save = true) => { 51 | tags.forEach(tag => { 52 | this.add(tag, id); 53 | }); 54 | if (save) { 55 | this.save(); 56 | } 57 | }; 58 | 59 | remove = (tag, id, save = false) => { 60 | if (this.index.hasOwnProperty(tag)) { 61 | let idList = this.index[tag]; 62 | let index = idList.indexOf(id); 63 | if (index !== -1) { 64 | idList.splice(index, 1); 65 | } 66 | if (idList.length == 0) { 67 | delete this.index[tag]; 68 | } 69 | if (save) { 70 | this.save(); 71 | } 72 | } 73 | }; 74 | 75 | removeAll = (tags, id) => { 76 | tags.forEach(tag => { 77 | this.remove(tag, id); 78 | }); 79 | this.save(); 80 | }; 81 | 82 | findIdsWithTag = tag => { 83 | if (this.index.hasOwnProperty(tag)) { 84 | return this.index[tag]; 85 | } 86 | return []; 87 | }; 88 | 89 | findIdsWithTags = tags => { 90 | let prevSet; 91 | tags.forEach(tag => { 92 | let ids = this.findIdsWithTag(tag); 93 | let set = new Set(ids); 94 | if (prevSet) { 95 | let intersect = new Set(); 96 | prevSet.forEach(x => { 97 | if (set.has(x)) { 98 | intersect.add(x); 99 | } 100 | }); 101 | prevSet = intersect; 102 | } else { 103 | prevSet = set; 104 | } 105 | }); 106 | if (prevSet) { 107 | return Array.from(prevSet); 108 | } 109 | return []; 110 | }; 111 | 112 | save = () => { 113 | fs.writeJSONSync(this.filePath, this.index); 114 | }; 115 | } 116 | -------------------------------------------------------------------------------- /src/app/js/Model/VidLib/VidConverter.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import util from 'util'; 3 | import { remote } from 'electron'; 4 | import fs from 'fs-extra'; 5 | import path from 'path-extra'; 6 | import { getFFmpegExePath } from '../FFmpegHelper'; 7 | 8 | const log = require('electron-log'); 9 | 10 | const exec = util.promisify(require('child_process').exec); 11 | 12 | class VidConverter { 13 | convert = async (vidPath, startTime, endTime, aid, outPath) => { 14 | // ffmpeg -fflags +genpts -i Girl\,\ Interrupted\ 1999\ DvDrip\[Eng\]-greenbud1969.avi -ss 00:01:30.0 -acodec copy -vcodec copy -t 00:00:30.0 out.mp4 15 | const tempFile = path.join(remote.app.getPath('temp'), 'tempvid.mp4'); 16 | if (fs.existsSync(tempFile)) { 17 | fs.removeSync(tempFile); 18 | } 19 | try { 20 | // const commandExtract = `ffmpeg -ss ${startTime} -to ${endTime} -fflags +genpts -i "${vidPath}" -acodec copy -vcodec copy -strict -2 "${tempFile}"` 21 | const commandExtract = `"${getFFmpegExePath( 22 | 'ffmpeg' 23 | )}" -ss ${startTime} -to ${endTime} -fflags +genpts -i "${vidPath}" -acodec aac -vcodec copy -map 0:v:0 -map 0:a:${aid - 24 | 1} -strict -2 -b:a 128k "${tempFile}"`; 25 | const commandScale = `"${getFFmpegExePath( 26 | 'ffmpeg' 27 | )}" -i "${tempFile}" -vf scale=370:-2 -acodec aac -af "aresample=async=1000" -max_muxing_queue_size 1999 "${outPath}"`; 28 | 29 | let r = await exec(commandExtract); 30 | if (r.error) { 31 | log.error(`error on extract: ${r.error}`); 32 | return false; 33 | } 34 | r = await exec(commandScale); 35 | 36 | if (r.error) { 37 | log.error(`error on scale: ${r.error}`); 38 | return false; 39 | } 40 | } catch (e) { 41 | log.error(`error on generating movie clip: ${e}`); 42 | return false; 43 | } 44 | 45 | return true; 46 | }; 47 | 48 | genThumbnail = async (vidPath, time, outPath) => { 49 | const commandThumbnail = `"${getFFmpegExePath('ffmpeg')}" -i "${vidPath}" -ss ${time} -vframes 1 "${outPath}"`; 50 | log.log(commandThumbnail); 51 | const { error } = await exec(commandThumbnail); 52 | if (error) { 53 | log.error(`error on gen thumbnail: ${error}`); 54 | return false; 55 | } 56 | return true; 57 | }; 58 | } 59 | 60 | export default new VidConverter(); 61 | -------------------------------------------------------------------------------- /src/app/js/Model/YoutubeSubtitle/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra'; 2 | import log from 'electron-log'; 3 | import { remote } from 'electron'; 4 | import { parseAutoGen, parseCC } from '../youtube-subtitle-converter'; 5 | import { formatMs } from '../KUtils'; 6 | 7 | class YoutubeSubtitle { 8 | getSubtitleFromUrl = async (url, videoId, lang) => { 9 | let lines = []; 10 | if (lang.startsWith('cc_')) { 11 | lines = await this.getCCFromId(videoId, lang.replace('cc_', '')); 12 | } else { 13 | lines = await this.getAutoSubFromUrl(url, lang); 14 | } 15 | return lines; 16 | }; 17 | 18 | downloadSubtitleFromUrl = async (url, videoId, lang) => { 19 | const lines = await this.getSubtitleFromUrl(url, videoId, lang); 20 | const text = this.convertLinesToSrt(lines); 21 | const filePath = remote.dialog.showSaveDialog(remote.getCurrentWindow(), { defaultPath: `YouTubeSubtitle.${lang}.srt` }); 22 | await fs.writeFile(filePath, text); 23 | return filePath; 24 | }; 25 | 26 | convertLinesToSrt = lines => { 27 | const result = lines.reduce((text, line, index) => { 28 | let temp = `${index + 1}\n`; 29 | temp += `${formatMs(line.start)} --> ${formatMs(line.end)}\n`; 30 | temp += `${line.text}\n\n`; 31 | return text + temp; 32 | }, ''); 33 | return result; 34 | }; 35 | 36 | getAutoSubFromUrl = async (url, lang) => { 37 | let lines = []; 38 | const text = await this.sendAutoGenerateRequest(url, lang); 39 | if (text) { 40 | lines = parseAutoGen(text); 41 | } 42 | return lines; 43 | }; 44 | 45 | getCCFromId = async (videoId, lang) => { 46 | const text = await this.sendCCRequest(videoId, lang); 47 | let lines = []; 48 | if (text) { 49 | lines = parseCC(text); 50 | } 51 | return lines; 52 | }; 53 | 54 | sendAutoGenerateRequest = async (url, lang) => { 55 | let requestUrl = url; 56 | if (!requestUrl.includes(`lang=${lang}`)) { 57 | requestUrl += `&tlang=${lang}`; 58 | } 59 | const text = await this.requestText(requestUrl); 60 | return text; 61 | }; 62 | 63 | sendCCRequest = async (videoId, lang) => { 64 | const url = `http://video.google.com/timedtext?lang=${lang}&v=${videoId}`; 65 | log.info(`getCCFromId:${url}`); 66 | 67 | const text = await this.requestText(url); 68 | return text; 69 | }; 70 | 71 | ccListParser = text => { 72 | const trackList = new DOMParser().parseFromString(text, 'text/xml').getElementsByTagName('track'); 73 | return Array.from(trackList).map(trackElement => ({ 74 | key: `cc_${trackElement.getAttribute('lang_code')}`, 75 | name: `${trackElement.getAttribute('lang_translated')} [cc]` 76 | })); 77 | }; 78 | 79 | getCCListFromId = async videoId => { 80 | const listUrl = `https://video.google.com/timedtext?hl=en&v=${videoId}&type=list`; 81 | log.info(`listUrl:${listUrl}`); 82 | const text = this.requestText(listUrl); 83 | if (text) { 84 | return this.ccListParser(text); 85 | } 86 | return []; 87 | }; 88 | 89 | requestText = async url => { 90 | return new Promise(resolve => { 91 | const xhr = new XMLHttpRequest(); 92 | xhr.addEventListener('load', () => { 93 | if (xhr.status === 200) { 94 | resolve(xhr.responseText); 95 | } else { 96 | resolve(''); 97 | } 98 | }); 99 | xhr.addEventListener('error', () => { 100 | log.error(xhr.statusText); 101 | resolve(''); 102 | }); 103 | xhr.open('GET', url); 104 | xhr.send(); 105 | }); 106 | }; 107 | } 108 | 109 | export default new YoutubeSubtitle(); 110 | -------------------------------------------------------------------------------- /src/app/js/Model/youtube-subtitle-converter/gen.js: -------------------------------------------------------------------------------- 1 | import he from 'he'; 2 | import { breakLine } from '../KUtils'; 3 | 4 | const genCCSub = body => { 5 | const lineObjs = body.slice(2).filter(lineObj => lineObj.length > 2); 6 | const subtitles = lineObjs.map(([tagName, { t, d }, text], i) => { 7 | const startTime = parseInt(t); 8 | const endTime = parseInt(d) + startTime; 9 | 10 | return { 11 | text, 12 | start: startTime, 13 | end: endTime, 14 | sentences: breakLine(text), 15 | index: i 16 | }; 17 | }, ''); 18 | 19 | return subtitles; 20 | }; 21 | 22 | const genTranscript = body => { 23 | const lineObjs = body.slice(2).filter(lineObj => lineObj.length > 2); 24 | const subtitles = lineObjs.map(([tagName, { start, dur }, text], i) => { 25 | const startTime = parseFloat(start) * 1000; 26 | const endTime = parseFloat(dur) * 1000 + startTime; 27 | var sub = { 28 | start: startTime, 29 | end: endTime, 30 | text: text, 31 | sentences: breakLine(he.decode(text)), 32 | index: i 33 | }; 34 | return sub; 35 | }, ''); 36 | 37 | return subtitles; 38 | }; 39 | 40 | const genSub = body => { 41 | const lineObjs = body.slice(2).filter(lineObj => lineObj.length > 2); 42 | //console.log("len:" + lineObjs.length); 43 | 44 | let subtitles = lineObjs.map(([tagName, { t, d }, ...texts], index) => { 45 | const startTime = parseInt(t); 46 | let words = texts 47 | .map(text => { 48 | if (text[0] === 's') { 49 | return text[2].trim(); 50 | } else { 51 | return text; 52 | } 53 | }) 54 | .filter(w => w); 55 | const content = words.join(' '); 56 | var sentences = breakLine(content); 57 | 58 | var sub = { 59 | start: startTime, 60 | sentences: sentences, 61 | text: content, 62 | index: index 63 | }; 64 | 65 | return sub; 66 | }, ''); 67 | 68 | let pEnd = 0; 69 | for (let i = subtitles.length - 1; i >= 0; i--) { 70 | var sub = subtitles[i]; 71 | if (pEnd != 0) { 72 | sub.end = pEnd - 1; 73 | } 74 | pEnd = sub.start; 75 | } 76 | 77 | return subtitles; 78 | }; 79 | 80 | const gen = json => { 81 | if (json[0] === 'timedtext' && json[1].format === '3') { 82 | if (json[2][0] === 'body') { 83 | // CC srt sub 84 | return genCCSub(json[2]); 85 | } 86 | // auto srt sub without youtube-like styling 87 | return genSub(json[3]); 88 | } else if (json[0] === 'transcript') { 89 | return genTranscript(json); 90 | } else { 91 | throw Error('only timedtext with format 3 or transcript expected.'); 92 | } 93 | }; 94 | 95 | export default gen; 96 | -------------------------------------------------------------------------------- /src/app/js/Model/youtube-subtitle-converter/index.js: -------------------------------------------------------------------------------- 1 | import { breakLine } from '../KUtils'; 2 | import gen from './gen'; 3 | import parse from './parser'; 4 | 5 | export function parseCC(xmlStr) { 6 | if (!xmlStr) { 7 | return []; 8 | } else { 9 | return gen(parse(xmlStr)); 10 | } 11 | } 12 | 13 | export function parseAutoGen(jsonStr) { 14 | if (!jsonStr) { 15 | return []; 16 | } 17 | return parseJson(jsonStr); 18 | } 19 | 20 | const reducer = (content, seg) => { 21 | return content + seg.utf8; 22 | }; 23 | 24 | const parseJson = jsonStr => { 25 | jsonStr = jsonStr.replace(/\\n/g, ' '); 26 | const json = JSON.parse(jsonStr); 27 | const events = json.events; 28 | 29 | const lines = events 30 | .map(event => { 31 | const start = event.tStartMs; 32 | // const durationMs = event.dDurationMs; 33 | // const end = start + durationMs; 34 | // duration from YouTube is not correct, calculate it by ourself 35 | if (event.segs) { 36 | if (event.segs.length == 1 && !event.segs[0].utf8.trim()) return null; 37 | 38 | const text = event.segs.reduce(reducer, ''); 39 | const sentences = breakLine(text); 40 | return { start, sentences, text }; 41 | } 42 | return null; 43 | }) 44 | .filter(line => line != null); 45 | 46 | let lastStart = 0; 47 | for (let index = lines.length - 1; index >= 0; index -= 1) { 48 | const line = lines[index]; 49 | line.index = index; 50 | if (!lastStart) { 51 | line.end = line.start + 99999; 52 | } else { 53 | line.end = lastStart - 1; 54 | } 55 | lastStart = line.start; 56 | } 57 | 58 | return lines; 59 | }; 60 | -------------------------------------------------------------------------------- /src/app/js/Model/youtube-subtitle-converter/parser.js: -------------------------------------------------------------------------------- 1 | const isDocumentNode = nodeType => nodeType === 9; 2 | 3 | const isTextNode = nodeType => nodeType === 3; 4 | 5 | const reduceAttr = attributes => { 6 | const attrs = Array.from(attributes); 7 | 8 | const props = attrs.reduce((acc, { nodeName, nodeValue }) => { 9 | acc[nodeName] = nodeValue; 10 | return acc; 11 | }, {}); 12 | 13 | return props; 14 | }; 15 | 16 | const xmlToJson = ({ nodeType, childNodes, nodeName, nodeValue, attributes }) => { 17 | if (isDocumentNode(nodeType)) { 18 | return xmlToJson(childNodes[0]); 19 | } 20 | 21 | if (isTextNode(nodeType)) { 22 | return nodeValue; 23 | } 24 | 25 | if (nodeName === undefined) { 26 | return; 27 | } 28 | 29 | const props = reduceAttr(attributes); 30 | 31 | const children = Array.from(childNodes).map(node => xmlToJson(node)); 32 | 33 | const root = [nodeName, props, ...children]; 34 | 35 | return root; 36 | }; 37 | 38 | const strToXML = str => { 39 | return new DOMParser().parseFromString( 40 | //str, 41 | str.replace(/>\n/g, '>').replace(/\n/g, ' '), 42 | 'text/xml' 43 | ); 44 | }; 45 | 46 | const parse = str => xmlToJson(strToXML(str)); 47 | 48 | export default parse; 49 | -------------------------------------------------------------------------------- /src/app/js/Model/youtube-subtitle-converter/util.js: -------------------------------------------------------------------------------- 1 | const msToObj = n => ({ 2 | d: Math.floor(n / 86400000), 3 | hr: Math.floor(n / 3600000) % 24, 4 | min: Math.floor(n / 60000) % 60, 5 | s: Math.floor(n / 1000) % 60, 6 | ms: Math.floor(n) % 1000 7 | }); 8 | 9 | const leftPad = (str, len = 2, n = 0) => String(str).padStart(len, n); 10 | 11 | function formatMs(inputMs) { 12 | const { hr, min, s, ms } = msToObj(inputMs); 13 | const timeStr = `${leftPad(hr)}:${leftPad(min)}:${leftPad(s)},${leftPad(ms, 3)}`; 14 | return timeStr; 15 | } 16 | 17 | const formatTime = (t, d, nextT) => { 18 | t = parseInt(t, 10); 19 | d = parseInt(d, 10); 20 | let end = t + d; 21 | 22 | if (nextT !== undefined) { 23 | nextT = parseInt(nextT, 10); 24 | if (end > nextT) { 25 | end = nextT; 26 | } 27 | } 28 | 29 | return { 30 | startTime: formatMs(t), 31 | endTime: formatMs(end) 32 | }; 33 | }; 34 | 35 | // setColor by text accuracy 36 | const setColor = ([tagName, { ac }, text]) => { 37 | ac = parseInt(ac, 10); 38 | 39 | if (ac === 252) { 40 | return text; 41 | } 42 | 43 | if (ac < 200) { 44 | return `${text}`; 45 | } 46 | 47 | return `${text}`; 48 | }; 49 | 50 | export { setColor, formatTime }; 51 | -------------------------------------------------------------------------------- /src/app/js/renderer.jsx: -------------------------------------------------------------------------------- 1 | import 'photonkit/dist/css/photon.css'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import log from 'electron-log'; 5 | import unhandled from 'electron-unhandled'; 6 | import App from './Containers/App.jsx'; 7 | import Settings from './Model/Settings'; 8 | import { initFFmpegPath } from './Model/FFmpegHelper'; 9 | 10 | initFFmpegPath(); 11 | 12 | window.addEventListener('error', event => { 13 | event.preventDefault(); 14 | if (event.message?.includes('ResizeObserver')) { 15 | event.stopImmediatePropagation(); 16 | return false; 17 | } 18 | }); 19 | 20 | unhandled({ 21 | logger: error => { 22 | log.error(error); 23 | } 24 | }); 25 | 26 | Settings.load(() => { 27 | ReactDOM.render(, document.querySelector('app')); 28 | }); 29 | 30 | document.ondragover = ev => { 31 | ev.preventDefault(); 32 | ev.stopPropagation(); 33 | }; 34 | 35 | document.ondrop = ev => { 36 | ev.preventDefault(); 37 | }; 38 | 39 | document.body.ondrop = ev => { 40 | ev.preventDefault(); 41 | }; 42 | -------------------------------------------------------------------------------- /src/art/closed-caption-logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/src/art/closed-caption-logo-small.png -------------------------------------------------------------------------------- /src/art/closed-caption-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/src/art/closed-caption-logo.png -------------------------------------------------------------------------------- /src/art/manage_subtitle.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/src/art/manage_subtitle.psd -------------------------------------------------------------------------------- /src/art/noun_1650297_cc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/src/art/noun_1650297_cc.png -------------------------------------------------------------------------------- /src/art/settings-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/src/art/settings-512.png -------------------------------------------------------------------------------- /src/art/settings_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/src/art/settings_1.png -------------------------------------------------------------------------------- /src/art/sub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/src/art/sub.png -------------------------------------------------------------------------------- /src/art/subitle_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/src/art/subitle_1.png -------------------------------------------------------------------------------- /src/art/subitle_1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/art/subtitle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/src/art/subtitle.png -------------------------------------------------------------------------------- /src/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current' 8 | } 9 | } 10 | ] 11 | ], 12 | env: { 13 | test: { 14 | presets: ['@babel/preset-react'] 15 | } 16 | }, 17 | plugins: [ 18 | [ 19 | 'import', 20 | { 21 | libraryName: 'antd', 22 | style: true 23 | } 24 | ], 25 | 'dynamic-import-node' 26 | ] 27 | }; 28 | -------------------------------------------------------------------------------- /src/etc/lookup/osx-lookup: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circleapps/beyondplayer/07df76d56baf72b23fd1fed7ff6deb768d656f81/src/etc/lookup/osx-lookup --------------------------------------------------------------------------------