├── .appveyor.yml ├── .babelrc ├── .editorconfig ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .jscsrc ├── .jshintignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── RELEASENOTES.json ├── RELEASENOTES.md ├── app ├── fonts │ ├── photon-entypo.eot │ ├── photon-entypo.svg │ ├── photon-entypo.ttf │ └── photon-entypo.woff ├── html │ └── main.html ├── photon │ ├── photon-theme-osx.css │ ├── photon.css │ ├── photon.js │ └── photon.min.css ├── scripts │ ├── main │ │ ├── components │ │ │ ├── application.js │ │ │ └── webview.js │ │ ├── managers │ │ │ └── configuration-manager.js │ │ ├── menus │ │ │ ├── app-menu.js │ │ │ └── tray-menu.js │ │ ├── services │ │ │ ├── debug-service.js │ │ │ ├── messenger-service.js │ │ │ ├── notification-service.js │ │ │ ├── power-service.js │ │ │ ├── requestfilter-service.js │ │ │ └── updater-service.js │ │ └── windows │ │ │ └── main-window.js │ └── renderer │ │ ├── utils │ │ ├── dom-helper.js │ │ └── language.js │ │ └── webview │ │ ├── player.js │ │ └── playlist.js └── styles │ ├── styles.css │ ├── youtube-player.css │ ├── youtube-playlist.css │ ├── youtube-umbra.css │ └── youtube-volume.css ├── bin └── cli.js ├── gulpfile.babel.js ├── icons ├── darwin │ ├── background-setup.png │ ├── icon-setup.icns │ ├── icon-tray-opaque.png │ ├── icon-tray-opaque@2x.png │ ├── icon-tray-transparent.png │ ├── icon-tray-transparent@2x.png │ └── icon.icns ├── linux │ ├── 1024x1024.png │ ├── 128x128.png │ ├── 16x16.png │ ├── 24x24.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 512x512.png │ ├── 64x64.png │ ├── 96x96.png │ ├── icon-setup.png │ ├── icon-tray-opaque.png │ ├── icon-tray-transparent.png │ └── icon.png └── win32 │ ├── background-setup.bmp │ ├── background-setup.gif │ ├── header-setup.bmp │ ├── icon-setup.ico │ ├── icon-tray-opaque.png │ ├── icon-tray-transparent.png │ └── icon.ico ├── lib ├── build.js ├── deploy.js ├── is-env.js ├── localsetup.js ├── logger.js ├── platform-helper.js ├── releasenotes.js └── required-count.js ├── package.json └── resources └── graphics └── icon.png /.appveyor.yml: -------------------------------------------------------------------------------- 1 | os: Visual Studio 2015 2 | 3 | platform: 4 | - x64 5 | 6 | branches: 7 | only: 8 | - release 9 | 10 | version: '{build}-{branch}' 11 | 12 | cache: 13 | - 'node_modules' 14 | - '%APPDATA%\npm-cache' 15 | - '%USERPROFILE%\.electron' 16 | 17 | init: 18 | - cmd: echo 🚦 Authorizing Build 19 | - ps: if ($env:OS -eq "Windows_NT" -And $env:DEPLOY_WINDOWS -eq "0") { $host.SetShouldExit(0) } 20 | - cmd: git config --global core.autocrlf input 21 | 22 | install: 23 | - cmd: echo 🔧 Setting up Environment 24 | - ps: Install-Product node 7.8.0 25 | - cmd: npm --global update npm 26 | 27 | before_build: 28 | - cmd: echo 📥 Installing Dependencies 29 | - cmd: npm install 30 | 31 | build_script: 32 | - cmd: echo 📦 Building 33 | - cmd: npm run build --metadata 34 | 35 | deploy_script: 36 | - cmd: echo 📮 Deploying 37 | - cmd: npm run deploy 38 | 39 | artifacts: 40 | - path: build\output\*.exe 41 | - path: build\output\*.yml 42 | 43 | notifications: 44 | - provider: Webhook 45 | url: https://webhooks.gitter.im/e/6cf54138e3590fed049b 46 | method: GET 47 | on_build_success: true 48 | on_build_failure: true 49 | on_build_status_changed: true 50 | 51 | test: off 52 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["electron"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.yml] 16 | indent_size = 2 17 | 18 | [package.json] 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Creating Issues 4 | 5 | To file bug reports and feature suggestions, use the [issues page](../../issues?q=is%3Aissue). 6 | 7 | 1. Make sure the issue has not been filed before. 8 | 9 | 1. [Create a new Issue](../../issues/new) by filling out the form. 10 | 11 | 1. If an issue requires more information and receives no further input, it will be closed. 12 | 13 | 14 | ## Creating Pull Requests 15 | 16 | To create pull requests, use the [Pull Requests page](../../pulls). 17 | 18 | 1. [Create a new Issue](#creating-issues) describing the Bug or Feature you are addressing, to let others know you are working on it. 19 | 20 | 1. If a related issue exists, add a comment to let others know that you'll submit a pull request. 21 | 22 | 1. [Create a new Pull Request](../../pulls/new) by filling out the form. 23 | 24 | 25 | ### Setup 26 | 27 | 1. Fork the repository. 28 | 1. Clone your fork. 29 | 1. Make a branch for your change. 30 | 1. Run `npm install`. 31 | 32 | ## Commit Message 33 | 34 | Use the AngularJS commit message format: 35 | 36 | ``` 37 | type(scope): subject 38 | ``` 39 | 40 | #### type 41 | - `feat` New feature 42 | - `fix` A bugfix 43 | - `refactor` Code changes which are neither bugfix nor feature 44 | - `docs`: Documentation changes 45 | - `test`: New tests or changes to existing tests 46 | - `chore`: Changes to tooling or library changes 47 | 48 | #### scope 49 | The context of the changes, e.g. `preferences-window` or `compiler`. Use consistent names. 50 | 51 | #### subject 52 | A **brief, yet descriptive** description of the changes, using the following format: 53 | 54 | - present tense 55 | - lowercase 56 | - no period at the end 57 | - describe what the commit does 58 | - reference to issues via their id – e.g. `(#1337)` 59 | 60 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## 🤷🏽‍♂️ Current Behaviour 4 | 8 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. 9 | 10 | ## 🎯 Expected Behaviour 11 | 15 | At vero eos et accusam et justo duo dolores et ea rebum. 16 | 17 | ## 👟 Steps to Reproduce (S2R) 18 | 21 | 1. At vero eos et accusam, 22 | 2. justo duo dolores et ea rebum, 23 | 3. stet clita kasd gubergren, 24 | 3. no sea takimata sanctus est. 25 | 26 | ## 🏡 Environmental Context 27 | 30 | **App Version** 31 | v0.0.1 32 | **Installation Type** 33 | Setup 34 | **Operating System** 35 | Windows 10 Enterprise x64 (15042.00) 36 | 37 | 38 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## 📋 Description 4 | 7 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. 8 | 9 | ## 🗂 Type 10 | 13 | - [ ] 🍾 Feature 14 | - [ ] 🚨 Bugfix 15 | - [ ] 📒 Documentation 16 | - [ ] 👷 Internals 17 | 18 | ## 🔥 Severity 19 | 22 | - [ ] 💎 Non-Breaking Changes 23 | - [ ] 💔 Breaking Changes 24 | 25 | ## 🖥 Platforms 26 | 29 | - [x] 🍏 macOS 30 | - [x] 💾 Windows 31 | - [x] 🐧 Linux 32 | 33 | ## 🛃 Tests 34 | 37 | - [ ] My changes have been tested manually. 38 | - [ ] My changes are covered by automated testing methods. 39 | 40 | ## 👨‍🎓 Miscellaneous 41 | 44 | - [ ] My changes follow the style guide. 45 | - [ ] My changes require updates to the documentation. 46 | 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # BASELINE 2 | # macOS 3 | .DS_Store 4 | 5 | # Logs 6 | logs 7 | *.log 8 | 9 | # Temporary 10 | temp 11 | tmp 12 | 13 | # Caches 14 | .cache 15 | cache 16 | .sass-cache 17 | 18 | # JetBrains 19 | .idea 20 | *.sln.iml 21 | 22 | # VSCode 23 | .vscode 24 | 25 | # Compiled 26 | build 27 | 28 | # Dependencies 29 | node_modules 30 | jspm_packages 31 | bower_components 32 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "disallowKeywords": ["with"], 3 | "disallowMixedSpacesAndTabs": true, 4 | "disallowMultipleLineStrings": true, 5 | "disallowNewlineBeforeBlockStatements": true, 6 | "disallowSpaceAfterObjectKeys": true, 7 | "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], 8 | "disallowSpaceBeforeBinaryOperators": [","], 9 | "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], 10 | "disallowSpacesInAnonymousFunctionExpression": { 11 | "beforeOpeningRoundBrace": true 12 | }, 13 | "disallowSpacesInFunctionDeclaration": { 14 | "beforeOpeningRoundBrace": true 15 | }, 16 | "disallowSpacesInNamedFunctionExpression": { 17 | "beforeOpeningRoundBrace": true 18 | }, 19 | "disallowSpacesInsideParentheses": true, 20 | "disallowTrailingComma": true, 21 | "disallowTrailingWhitespace": true, 22 | "requireCommaBeforeLineBreak": true, 23 | "requireLineFeedAtFileEnd": true, 24 | "requireSpaceAfterBinaryOperators": ["+", "-", "/", "*", "%", "==", "===", "!=", "!==", ">", ">=", "<", "<=", "&&", "||"], 25 | "requireSpaceBeforeBinaryOperators": ["+", "-", "/", "*", "%", "==", "===", "!=", "!==", ">", ">=", "<", "<=", "&&", "||"], 26 | "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"], 27 | "requireSpaceBeforeBlockStatements": true, 28 | "requireSpacesInConditionalExpression": { 29 | "afterTest": true, 30 | "beforeConsequent": true, 31 | "afterConsequent": true, 32 | "beforeAlternate": true 33 | }, 34 | "requireSpacesInFunction": { 35 | "beforeOpeningCurlyBrace": true 36 | }, 37 | "validateLineBreaks": "LF", 38 | "validateParameterSeparator": ", ", 39 | "jsDoc": { 40 | "checkParamNames": true, 41 | "requireParamTypes": true 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | resources/** -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "esnext": true, 4 | "bitwise": false, 5 | "camelcase": false, 6 | "curly": true, 7 | "eqeqeq": true, 8 | "immed": false, 9 | "indent": 4, 10 | "latedef": true, 11 | "noarg": true, 12 | "quotmark": "single", 13 | "undef": true, 14 | "unused": true, 15 | "strict": true, 16 | "trailing": true, 17 | "smarttabs": true, 18 | "sub": true, 19 | "node": true, 20 | "globals": { 21 | "Audio": false, 22 | "EventTarget": true, 23 | "global": false, 24 | "Notification": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # BASELINE 2 | # macOS 3 | .DS_Store 4 | 5 | # Logs 6 | logs 7 | *.log 8 | 9 | # Temporary 10 | temp 11 | tmp 12 | 13 | # Cache 14 | .cache 15 | cache 16 | .sass-cache/ 17 | 18 | # JetBrains 19 | .idea/ 20 | *.sln.iml 21 | 22 | # Compiled 23 | build 24 | 25 | # Node 26 | node_modules 27 | jspm_packages 28 | 29 | 30 | ## NPM 31 | # Runtime data 32 | pids 33 | *.pid 34 | *.seed 35 | 36 | # Directory for instrumented libs generated by jscoverage/JSCover 37 | lib-cov 38 | 39 | # Coverage directory used by tools like istanbul 40 | coverage 41 | 42 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 43 | .grunt 44 | 45 | 46 | # ELECTRON-CLOUD-DEPLOY CACHES 47 | electron-cloud-deploy-cache -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | include: 3 | - os: osx 4 | osx_image: xcode8.2 5 | sudo: required 6 | - os: linux 7 | dist: trusty 8 | sudo: required 9 | compiler: clang 10 | 11 | language: c 12 | 13 | branches: 14 | only: 15 | - release 16 | 17 | cache: 18 | directories: 19 | - "$HOME/.electron" 20 | - "./node_modules" 21 | 22 | addons: 23 | apt: 24 | packages: 25 | - bsdtar 26 | - g++-multilib 27 | - gcc-multilib 28 | - graphicsmagick 29 | - icnsutils 30 | - rpm 31 | - xz-utils 32 | 33 | before_install: 34 | - echo "🚦 Authorizing Build" 35 | - if [[ "${OSTYPE}" == "darwin"* ]] && [[ "${DEPLOY_MACOS}" == 0 ]]; then travis_terminate 0; fi 36 | - if [[ "${OSTYPE}" == "linux"* ]] && [[ "${DEPLOY_LINUX}" == 0 ]]; then travis_terminate 0; fi 37 | - echo "🔧 Setting up Environment" 38 | - curl -o- https://raw.githubusercontent.com/creationix/nvm/master/install.sh | NVM_DIR="${HOME}"/.nvm sh 39 | - source "${HOME}"/.nvm/nvm.sh && nvm install 7.8.0 && nvm use 7.8.0 40 | - npm --global update npm 41 | 42 | install: 43 | - echo "📥 Installing Dependencies" 44 | - npm install 45 | 46 | script: 47 | - echo "📦 Building" 48 | - npm run build --metadata 49 | 50 | after_success: 51 | - echo "📮 Deploying" 52 | - npm run deploy 53 | 54 | notifications: 55 | webhooks: 56 | urls: 57 | - https://webhooks.gitter.im/e/24dc896d6a8496d6eec2 58 | on_success: change 59 | on_failure: always 60 | on_start: never 61 | 62 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Creating Issues 4 | 5 | To file bug reports and feature suggestions, use the ["Issues"](https://github.com/sidneys/pb-for-desktop/issues?q=is%3Aissue) page. 6 | 7 | 1. Make sure the issue has not been filed before. 8 | 1. Create a new issue by filling out [the issue form](https://github.com/sidneys/pb-for-desktop/issues/new). 9 | 1. If an issue requires more information and receives no further input, it will be closed. 10 | 11 | 12 | ## Creating Pull Requests 13 | 14 | To create pull requests, use the ["Pull Requests"](https://github.com/sidneys/pb-for-desktop/pulls) page. 15 | 16 | 1. [Create a new Issue](#creating-issues) describing the Bug or Feature you are addressing, to let others know you are working on it. 17 | 1. If a related issue exists, add a comment to let others know that you'll submit a pull request. 18 | 1. Create a new pull request by filling out [the pull request form](https://github.com/sidneys/pb-for-desktop/pulls/compare). 19 | 20 | 21 | ### Setup 22 | 23 | 1. Fork the repository. 24 | 1. Clone your fork. 25 | 1. Make a branch for your change. 26 | 1. Run `npm install`. 27 | 28 | ## Commit Message 29 | 30 | Use the AngularJS commit message format: 31 | 32 | ``` 33 | type(scope): subject 34 | ``` 35 | 36 | #### type 37 | - `feat` New feature 38 | - `fix` A bugfix 39 | - `refactor` Code changes which are neither bugfix nor feature 40 | - `docs`: Documentation changes 41 | - `test`: New tests or changes to existing tests 42 | - `chore`: Changes to tooling or library changes 43 | 44 | #### scope 45 | The context of the changes, e.g. `preferences-window` or `compiler`. Use consistent names. 46 | 47 | #### subject 48 | A **brief, yet descriptive** description of the changes, using the following format: 49 | 50 | - present tense 51 | - lowercase 52 | - no period at the end 53 | - describe what the commit does 54 | - reference to issues via their id – e.g. `(#1337)` 55 | 56 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 sidneys.github.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YouTube Playlist Player [![Beta](https://img.shields.io/badge/status-beta-blue.svg?style=flat)]() [![travis](http://img.shields.io/travis/sidneys/youtube-playlist-player.svg?style=flat)](http://travis-ci.org/sidneys/youtube-playlist-player) [![appveyor](https://ci.appveyor.com/api/projects/status/d69sb6iav7tnrldq?svg=true)](https://ci.appveyor.com/project/sidneys/youtube-playlist-player) [![npm](https://img.shields.io/npm/v/youtube-playlist-player.svg?style=flat)](https://npmjs.com/package/youtube-playlist-player) [![dependencies](https://img.shields.io/david/sidneys/youtube-playlist-player.svg?style=flat-square)](https://npmjs.com/package/youtube-playlist-player) [![devDependencies](https://img.shields.io/david/dev/sidneys/youtube-playlist-player.svg?style=flat-square)](https://npmjs.com/package/youtube-playlist-player) 2 | 3 |

4 |

5 | Watch & edit your YouTube playlist on the desktop.
6 | Available for macOS, Windows and Linux. 7 |

8 | 9 | 10 | ## Features 11 | 12 | > **Multiple Viewing Modes** 13 | 14 | Supports regular Playback as well as YouTube TV (Leanback) viewing modes. 15 | 16 | > **Unobstrusive** 17 | 18 | Watch videos without browser chrome. 19 | 20 | > **Efficient** 21 | 22 | Enables hardware-accelerated h264 YouTube playback across platforms. 23 | 24 | > **Simple** 25 | 26 | Copy & paste a YouTube playlist URL to get started. Login to edit playlists. 27 | 28 | > **Adblocker** 29 | 30 | Filters on-page and in-stream ads. 31 | 32 | 33 | ## Contents 34 | 35 | 1. [Installation](#installation) 36 | 2. [Developers](#development) 37 | 3. [Continuous Integration](#continuous-integration) 38 | 5. [Contact](#contact) 39 | 6. [Author](#author) 40 | 41 | 42 | ## Installation 43 | 44 | ### Standard Installation 45 | 46 | Download the latest version of YouTube Playlist Player on the [Releases](https://github.com/sidneys/youtube-playlist-player/releases) page. 47 | 48 | ### Installation as Commandline Tool 49 | 50 | ```bash 51 | npm install --global youtube-playlist-player # Installs the node CLI module 52 | youtube-playlist-player # Runs it 53 | ``` 54 | 55 | 56 | ## Developers 57 | 58 | ### Sources 59 | 60 | Clone the repo and install dependencies. 61 | 62 | ```shell 63 | git clone https://github.com/sidneys/youtube-playlist-player.git youtube-playlist-player 64 | cd youtube-playlist-player 65 | npm install 66 | ``` 67 | 68 | ### Scripts 69 | 70 | #### npm run **start** 71 | 72 | Run the app with integrated Electron. 73 | 74 | ```bash 75 | npm run start 76 | npm run start:dev # with Debugging Tools 77 | npm run start:livereload # with Debugging Tools and Livereload 78 | ``` 79 | 80 | #### npm run **localsetup** 81 | 82 | Install the app in the System app folder and start it. 83 | 84 | ```bash 85 | npm run localsetup 86 | npm run localsetup:rebuild # Build before installation 87 | npm run localsetup:rebuild:dev # Build before installation, use Developer Tools 88 | ``` 89 | 90 | #### npm run **build** 91 | 92 | Build the app and create installers (see [requirements](#build-requirements)). 93 | 94 | ```bash 95 | npm run build # build all available platforms 96 | npm run build macos windows # build specific platforms (macos/linux/windows) 97 | ``` 98 | 99 | ### Build Requirements 100 | 101 | * Building for Windows requires [`wine`](https://winehq.org) and [`mono`](https://nsis.sourceforge.net/Docs/Chapter3.htm) (on macOS, Linux) 102 | * Building for Linux requires [`fakeroot`](https://wiki.debian.org/FakeRoot) and [`dpkg `](https://wiki.ubuntuusers.de/dpkg/) (on macOS, Windows) 103 | * Only macOS can build for other platforms. 104 | 105 | #### macOS Build Setup 106 | 107 | Install [Homebrew](https://brew.sh), then run: 108 | 109 | ```bash 110 | brew install wine mono fakeroot dpkg 111 | ``` 112 | 113 | #### Linux Build Setup 114 | 115 | ```bash 116 | sudo apt-get install wine mono fakeroot dpkg 117 | ``` 118 | 119 | 120 | ## Continuous Integration 121 | 122 | > Turnkey **build-in-the-cloud** for Windows 10, macOS and Linux. 123 | 124 | The process is managed by a custom layer of node scripts and Electron-optimized configuration templates. 125 | Completed Installation packages are deployed to [GitHub Releases](https://github.com/sidneys/youtube-playlist-player/releases). Builds for all platforms and architectures take about 5 minutes. 126 | Backed by the open-source-friendly guys at [Travis](http://travis-ci.org/) and AppVeyor](https://ci.appveyor.com/) and running [electron-packager](https://github.com/electron-userland/electron-packager) under the hood. 127 | 128 | ### Setup 129 | 130 | 1. [Fork](https://github.com/sidneys/youtube-playlist-player/fork) the repo 131 | 2. Generate your GitHub [Personal Access Token](https://github.com/settings/tokens) using "repo" as scope. Copy it to the clipboard. 132 | 3. **macOS + Linux** 133 | 1. Sign in to [Travis](http://travis-ci.org/) using GitHub. 134 | 2. Open your [Travis Profile](https://travis-ci.org/profile), click "Sync Account" and wait for the process to complete. 135 | 3. Find this repository in the list, enable it and click "⚙" to open its settings. 136 | 4. Create a new Environment Variable named **GITHUB_TOKEN**. Paste your Token from step 2 as *value*. 137 | 4. **Windows** 138 | 1. Sign in to [AppVeyor](https://ci.appveyor.com/) using GitHub. 139 | 2. Click on ["New Project"](https://ci.appveyor.com/projects/new), select "GitHub", look up this repo in the list and click "Add". 140 | 3. After import navigate to the *Settings* > *Environment* subsection 141 | 4. Select "Add Variable", insert **GITHUB_TOKEN** for *name*, paste your Token as *value*. Save. 142 | 143 | ### Triggering Builds 144 | 145 | 1. Add a new Tag to start the build process: 146 | 147 | ```shell 148 | git tag -a v1.0.1 149 | git push --tags 150 | ``` 151 | The builds are started in parallel and added to the "Releases" page of the GitHub repo (in draft mode). 152 | 153 | 2. Use the editing feature to publish the new app version. 154 | 155 | 3. There is no step 3 156 | 157 | 158 | ## Contact ![Contributions Wanted](https://img.shields.io/badge/contributions-wanted-red.svg?style=flat) 159 | 160 | * [Gitter](http://gitter.im/sidneys/youtube-playlist-player) Developer Chat 161 | * [Issues](http;//github.com/sidneys/youtube-playlist-player/issues) File, track and discuss features and issues 162 | * [Wiki](http;//github.com/sidneys/youtube-playlist-player/wiki) Read or contribute to the project Wiki 163 | 164 | 165 | ## Author 166 | 167 | [sidneys](http://sidneys.github.io) 2016 168 | -------------------------------------------------------------------------------- /RELEASENOTES.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.2.5": { 3 | "🍾 features": [ 4 | "External docked playlist window", 5 | "User interface overhaul", 6 | "Remote control pane", 7 | "YouTube TV volume control", 8 | "Ad blocker" 9 | ], 10 | "📒 documentation": [ 11 | "Augments installation image asset" 12 | ], 13 | "👷 internals": [ 14 | "Request filter module", 15 | "Complete refactor", 16 | "Upgrades Electron to v1.6.7", 17 | "Upgrades dependencies" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /RELEASENOTES.md: -------------------------------------------------------------------------------- 1 | ## 1.2.5 2 | 3 | #### 🍾 Features 4 | 5 | - External docked playlist window 6 | - User interface overhaul 7 | - Remote control pane 8 | - YouTube TV volume control 9 | - Ad blocker 10 | 11 | #### 📒 Documentation 12 | 13 | - Augments installation image asset 14 | 15 | #### 👷 Internals 16 | 17 | - Request filter module 18 | - Complete refactor 19 | - Upgrades Electron to v1.6.7 20 | - Upgrades dependencies 21 | -------------------------------------------------------------------------------- /app/fonts/photon-entypo.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sidneys/youtube-playlist-player/234a01e8a6a9addade21ae2aff656807c824582e/app/fonts/photon-entypo.eot -------------------------------------------------------------------------------- /app/fonts/photon-entypo.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sidneys/youtube-playlist-player/234a01e8a6a9addade21ae2aff656807c824582e/app/fonts/photon-entypo.ttf -------------------------------------------------------------------------------- /app/fonts/photon-entypo.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sidneys/youtube-playlist-player/234a01e8a6a9addade21ae2aff656807c824582e/app/fonts/photon-entypo.woff -------------------------------------------------------------------------------- /app/html/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | YouTube on Top 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 66 | 67 | 68 |
69 |
Connecting...
70 |
71 | 72 | 73 |
74 | 77 | 78 |
79 | 80 | 81 | 82 | 89 | 90 | 91 | 92 |
93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /app/photon/photon-theme-osx.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * ===================================================== 3 | * Photon v0.1.2 4 | * Copyright 2016 Connor Sears 5 | * Licensed under MIT (https://github.com/connors/proton/blob/master/LICENSE) 6 | * 7 | * v0.1.2 designed by @connors. 8 | * ===================================================== 9 | */ 10 | 11 | .toolbar { 12 | box-shadow: inset 0 1px 0 #f5f4f5; 13 | background-color: #e8e6e8; 14 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #e8e6e8), color-stop(100%, #d1cfd1)); 15 | background-image: -webkit-linear-gradient(top, #e8e6e8 0%, #d1cfd1 100%); 16 | background-image: linear-gradient(to bottom, #e8e6e8 0%, #d1cfd1 100%); 17 | } 18 | 19 | .toolbar-header { 20 | border-bottom: 1px solid #c2c0c2; 21 | } 22 | 23 | .toolbar-footer { 24 | border-top: 1px solid #c2c0c2; 25 | } 26 | 27 | .toolbar-actions { 28 | margin-top: 4px; 29 | margin-bottom: 3px; 30 | padding-right: 3px; 31 | padding-left: 3px; 32 | padding-bottom: 3px; 33 | } 34 | 35 | .title { 36 | font-weight: 400; 37 | color: #555; 38 | } 39 | 40 | .btn { 41 | border-radius: 4px; 42 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.06); 43 | } 44 | 45 | .btn-group .btn + .btn { 46 | border-left: 1px solid #c2c0c2; 47 | } 48 | .btn-group .active { 49 | color: #fff; 50 | background-color: #6d6c6d; 51 | } 52 | .btn-group .active .icon { 53 | color: #fff; 54 | } 55 | 56 | .btn-default { 57 | color: #333; 58 | border-top-color: #c2c0c2; 59 | border-right-color: #c2c0c2; 60 | border-bottom-color: #a19fa1; 61 | border-left-color: #c2c0c2; 62 | background-color: #fcfcfc; 63 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fcfcfc), color-stop(100%, #f1f1f1)); 64 | background-image: -webkit-linear-gradient(top, #fcfcfc 0%, #f1f1f1 100%); 65 | background-image: linear-gradient(to bottom, #fcfcfc 0%, #f1f1f1 100%); 66 | } 67 | .btn-default:active { 68 | background-color: #ddd; 69 | background-image: none; 70 | } 71 | 72 | .btn-primary, 73 | .btn-positive, 74 | .btn-negative, 75 | .btn-warning { 76 | text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); 77 | } 78 | 79 | .btn-primary { 80 | border-color: #388df8; 81 | border-bottom-color: #0866dc; 82 | background-color: #6eb4f7; 83 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #6eb4f7), color-stop(100%, #1a82fb)); 84 | background-image: -webkit-linear-gradient(top, #6eb4f7 0%, #1a82fb 100%); 85 | background-image: linear-gradient(to bottom, #6eb4f7 0%, #1a82fb 100%); 86 | } 87 | .btn-primary:active { 88 | background-color: #3e9bf4; 89 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #3e9bf4), color-stop(100%, #0469de)); 90 | background-image: -webkit-linear-gradient(top, #3e9bf4 0%, #0469de 100%); 91 | background-image: linear-gradient(to bottom, #3e9bf4 0%, #0469de 100%); 92 | } 93 | 94 | .btn-positive { 95 | border-color: #29a03b; 96 | border-bottom-color: #248b34; 97 | background-color: #5bd46d; 98 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #5bd46d), color-stop(100%, #29a03b)); 99 | background-image: -webkit-linear-gradient(top, #5bd46d 0%, #29a03b 100%); 100 | background-image: linear-gradient(to bottom, #5bd46d 0%, #29a03b 100%); 101 | } 102 | .btn-positive:active { 103 | background-color: #34c84a; 104 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #34c84a), color-stop(100%, #248b34)); 105 | background-image: -webkit-linear-gradient(top, #34c84a 0%, #248b34 100%); 106 | background-image: linear-gradient(to bottom, #34c84a 0%, #248b34 100%); 107 | } 108 | 109 | .btn-negative { 110 | border-color: #fb2f29; 111 | border-bottom-color: #fb1710; 112 | background-color: #fd918d; 113 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fd918d), color-stop(100%, #fb2f29)); 114 | background-image: -webkit-linear-gradient(top, #fd918d 0%, #fb2f29 100%); 115 | background-image: linear-gradient(to bottom, #fd918d 0%, #fb2f29 100%); 116 | } 117 | .btn-negative:active { 118 | background-color: #fc605b; 119 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fc605b), color-stop(100%, #fb1710)); 120 | background-image: -webkit-linear-gradient(top, #fc605b 0%, #fb1710 100%); 121 | background-image: linear-gradient(to bottom, #fc605b 0%, #fb1710 100%); 122 | } 123 | 124 | .btn-warning { 125 | border-color: #fcaa0e; 126 | border-bottom-color: #ee9d02; 127 | background-color: #fece72; 128 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fece72), color-stop(100%, #fcaa0e)); 129 | background-image: -webkit-linear-gradient(top, #fece72 0%, #fcaa0e 100%); 130 | background-image: linear-gradient(to bottom, #fece72 0%, #fcaa0e 100%); 131 | } 132 | .btn-warning:active { 133 | background-color: #fdbc40; 134 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fdbc40), color-stop(100%, #ee9d02)); 135 | background-image: -webkit-linear-gradient(top, #fdbc40 0%, #ee9d02 100%); 136 | background-image: linear-gradient(to bottom, #fdbc40 0%, #ee9d02 100%); 137 | } 138 | 139 | .btn-link { 140 | color: #3b99fc; 141 | box-shadow: none; 142 | } 143 | .btn-link:active { 144 | color: #097ffb; 145 | } 146 | 147 | .btn .icon { 148 | color: #737475; 149 | } 150 | 151 | .form-control { 152 | background-color: #fff; 153 | border: 1px solid #ddd; 154 | border-radius: 4px; 155 | } 156 | .form-control:focus { 157 | border-color: #6db3fd; 158 | box-shadow: 0 0 0 3px #6db3fd; 159 | } 160 | 161 | .pane { 162 | border-left: 1px solid #ddd; 163 | } 164 | 165 | .img-rounded { 166 | border-radius: 4px; 167 | } 168 | 169 | .list-group-item { 170 | color: #414142; 171 | border-top: 1px solid #ddd; 172 | } 173 | .list-group-item.active, .list-group-item.selected { 174 | background-color: #116cd6; 175 | } 176 | 177 | .nav-group-item { 178 | padding: 2px 10px 2px 25px; 179 | color: #333; 180 | } 181 | .nav-group-item:active, .nav-group-item.active { 182 | background-color: #dcdfe1; 183 | } 184 | .nav-group-item .icon { 185 | color: #737475; 186 | } 187 | 188 | .nav-group-title { 189 | font-size: 12px; 190 | letter-spacing: normal; 191 | text-transform: none; 192 | color: #666666; 193 | } 194 | 195 | thead { 196 | background-color: #f5f5f4; 197 | } 198 | 199 | .table-striped tr:nth-child(even) { 200 | background-color: #f5f5f4; 201 | } 202 | 203 | tr:active, 204 | .table-striped tr:active:nth-child(even) { 205 | color: #fff; 206 | background-color: #116cd6; 207 | } 208 | 209 | thead tr:active { 210 | color: #333; 211 | background-color: #f5f5f4; 212 | } 213 | 214 | th { 215 | border-right: 1px solid #ddd; 216 | border-bottom: 1px solid #ddd; 217 | } 218 | 219 | .tab-group { 220 | border-top: 1px solid #989698; 221 | border-bottom: 1px solid #989698; 222 | } 223 | 224 | .tab-item { 225 | border-left: 1px solid #989698; 226 | background-color: #b8b6b8; 227 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #b8b6b8), color-stop(100%, #b0aeb0)); 228 | background-image: -webkit-linear-gradient(top, #b8b6b8 0%, #b0aeb0 100%); 229 | background-image: linear-gradient(to bottom, #b8b6b8 0%, #b0aeb0 100%); 230 | } 231 | .tab-item.active { 232 | background-color: #d4d2d4; 233 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #d4d2d4), color-stop(100%, #cccacc)); 234 | background-image: -webkit-linear-gradient(top, #d4d2d4 0%, #cccacc 100%); 235 | background-image: linear-gradient(to bottom, #d4d2d4 0%, #cccacc 100%); 236 | } 237 | .tab-item .icon-close-tab { 238 | color: #666; 239 | } 240 | .tab-item:after { 241 | position: absolute; 242 | top: 0; 243 | right: 0; 244 | bottom: 0; 245 | left: 0; 246 | content: ""; 247 | background-color: rgba(0, 0, 0, 0.08); 248 | opacity: 0; 249 | transition: opacity .1s linear; 250 | z-index: 1; 251 | } 252 | .tab-item:hover:not(.active):after { 253 | opacity: 1; 254 | } 255 | .tab-item .icon-close-tab:hover { 256 | background-color: rgba(0, 0, 0, 0.08); 257 | } 258 | 259 | .padded { 260 | padding: 10px; 261 | } 262 | 263 | .padded-less { 264 | padding: 5px; 265 | } 266 | 267 | .padded-more { 268 | padding: 20px; 269 | } 270 | 271 | .padded-vertically { 272 | padding-top: 10px; 273 | padding-bottom: 10px; 274 | } 275 | 276 | .padded-vertically-less { 277 | padding-top: 5px; 278 | padding-bottom: 5px; 279 | } 280 | 281 | .padded-vertically-more { 282 | padding-top: 20px; 283 | padding-bottom: 20px; 284 | } 285 | 286 | .padded-horizontally { 287 | padding-right: 10px; 288 | padding-left: 10px; 289 | } 290 | 291 | .padded-horizontally-less { 292 | padding-right: 5px; 293 | padding-left: 5px; 294 | } 295 | 296 | .padded-horizontally-more { 297 | padding-right: 20px; 298 | padding-left: 20px; 299 | } 300 | 301 | .padded-top { 302 | padding-top: 10px; 303 | } 304 | 305 | .padded-top-less { 306 | padding-top: 5px; 307 | } 308 | 309 | .padded-top-more { 310 | padding-top: 20px; 311 | } 312 | 313 | .padded-bottom { 314 | padding-bottom: 10px; 315 | } 316 | 317 | .padded-bottom-less { 318 | padding-bottom: 5px; 319 | } 320 | 321 | .padded-bottom-more { 322 | padding-bottom: 20px; 323 | } 324 | 325 | .sidebar { 326 | background-color: #f5f5f4; 327 | } 328 | -------------------------------------------------------------------------------- /app/photon/photon.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("load", function() { 2 | var slidersRound = document.querySelectorAll(".slider.slider-round"); 3 | for (var i = 0; i < slidersRound.length; i++) { 4 | sliderHighlightArea(slidersRound[i]); 5 | } 6 | var slidersVertical = document.querySelectorAll(".slider.slider-vertical"); 7 | for (var i = 0; i < slidersVertical.length; i++) { 8 | slidersVertical[i].style.marginBottom = slidersVertical[i].offsetWidth + "px"; 9 | } 10 | }, false); 11 | function sliderHighlightArea(e) { 12 | if (e.min == "") { 13 | e.min = 0; 14 | } 15 | if (e.max == "") { 16 | e.max = 100; 17 | } 18 | e.addEventListener("input", function() { 19 | this.sliderVisualCalc(); 20 | }, false); 21 | e.sliderVisualCalc = function() { 22 | this.style.backgroundSize = (100 * ((this.value - this.min) / (this.max - this.min))) + "% 100%"; 23 | } 24 | e.sliderVisualCalc(); 25 | } 26 | -------------------------------------------------------------------------------- /app/scripts/main/components/application.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /** 5 | * Modules 6 | * Node 7 | * @constant 8 | */ 9 | const path = require('path'); 10 | 11 | /** 12 | * Modules 13 | * Electron 14 | * @constant 15 | */ 16 | const electron = require('electron'); 17 | const { app } = electron; 18 | 19 | /** 20 | * Modules 21 | * External 22 | * @constant 23 | */ 24 | const appRootPath = require('app-root-path'); 25 | 26 | /** 27 | * Modules 28 | * Configuration 29 | */ 30 | require('events').EventEmitter.defaultMaxListeners = 0; 31 | appRootPath.setPath(path.join(__dirname, '..', '..', '..', '..')); 32 | 33 | /** 34 | * Modules 35 | * Internal 36 | * @constant 37 | */ 38 | const logger = require(path.join(appRootPath.path, 'lib', 'logger'))({ write: true }); 39 | const appMenu = require(path.join(appRootPath.path, 'app', 'scripts', 'main', 'menus', 'app-menu')); // jshint ignore:line 40 | const mainWindow = require(path.join(appRootPath.path, 'app', 'scripts', 'main', 'windows', 'main-window')); // jshint ignore:line 41 | const configurationManager = require(path.join(appRootPath.path, 'app', 'scripts', 'main', 'managers', 'configuration-manager')); // jshint ignore:line 42 | const trayMenu = require(path.join(appRootPath.path, 'app', 'scripts', 'main', 'menus', 'tray-menu')); // jshint ignore:line 43 | const updaterService = require(path.join(appRootPath.path, 'app', 'scripts', 'main', 'services', 'updater-service')); // jshint ignore:line 44 | const powerService = require(path.join(appRootPath.path, 'app', 'scripts', 'main', 'services', 'power-service')); // jshint ignore:line 45 | const debugService = require(path.join(appRootPath.path, 'app', 'scripts', 'main', 'services', 'debug-service')); // jshint ignore:line 46 | 47 | 48 | /** 49 | * Disable GPU 50 | */ 51 | app.disableHardwareAcceleration(); 52 | 53 | 54 | /** 55 | * @listens Electron.App#before-quit 56 | */ 57 | app.on('before-quit', () => { 58 | logger.debug('app#before-quit'); 59 | 60 | app.isQuitting = true; 61 | }); 62 | 63 | /** 64 | * @listens Electron.App#ready 65 | */ 66 | app.once('ready', () => { 67 | logger.debug('app#ready'); 68 | }); 69 | -------------------------------------------------------------------------------- /app/scripts/main/managers/configuration-manager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /** 5 | * Modules 6 | * Node 7 | * @constant 8 | */ 9 | const path = require('path'); 10 | const util = require('util'); 11 | 12 | /** 13 | * Modules 14 | * Electron 15 | * @constant 16 | */ 17 | const electron = require('electron'); 18 | const { app, BrowserWindow, session } = electron.remote || electron; 19 | 20 | /** 21 | * Modules 22 | * External 23 | * @constant 24 | */ 25 | const _ = require('lodash'); 26 | const appRootPath = require('app-root-path')['path']; 27 | const Appdirectory = require('appdirectory'); 28 | const electronSettings = require('electron-settings'); 29 | 30 | /** 31 | * Modules 32 | * Internal 33 | * @constant 34 | */ 35 | const logger = require(path.join(appRootPath, 'lib', 'logger'))({ write: true }); 36 | const packageJson = require(path.join(appRootPath, 'package.json')); 37 | const platformHelper = require(path.join(appRootPath, 'lib', 'platform-helper')); 38 | const requestfilterService = require(path.join(appRootPath, 'app', 'scripts', 'main', 'services', 'requestfilter-service')); 39 | 40 | /** 41 | * Application 42 | * @constant 43 | * @default 44 | */ 45 | const appName = packageJson.name; 46 | const appVersion = packageJson.version; 47 | 48 | /** 49 | * Filesystem 50 | * @constant 51 | * @default 52 | */ 53 | const appLogDirectory = (new Appdirectory(appName)).userLogs(); 54 | 55 | /** 56 | * @constant 57 | * @default 58 | */ 59 | const defaultInterval = 1000; 60 | const defaultDebounce = 300; 61 | 62 | 63 | /** 64 | * Get Main Window 65 | * @returns {Electron.BrowserWindow} 66 | */ 67 | let getPrimaryWindow = () => { 68 | logger.debug('getPrimaryWindow'); 69 | 70 | return BrowserWindow.getAllWindows()[0]; 71 | }; 72 | 73 | /** 74 | * Show app in menubar or task bar only 75 | * @param {Boolean} enable - True: show dock icon, false: hide icon 76 | */ 77 | let setWindowInTrayOnly = (enable) => { 78 | logger.debug('setWindowInTrayOnly'); 79 | 80 | let interval = setInterval(() => { 81 | const win = getPrimaryWindow(); 82 | if (!win) { return; } 83 | 84 | switch (platformHelper.type) { 85 | case 'darwin': 86 | if (enable) { 87 | app.dock.hide(); 88 | } else { app.dock.show(); } 89 | break; 90 | case 'win32': 91 | win.setSkipTaskbar(enable); 92 | break; 93 | case 'linux': 94 | win.setSkipTaskbar(enable); 95 | break; 96 | } 97 | 98 | clearInterval(interval); 99 | }, defaultInterval); 100 | }; 101 | 102 | 103 | /** 104 | * Show float primary window 105 | * @param {Boolean} enable - Enables always-on-top & translucency, deactivates inputs, shadow 106 | */ 107 | let setWindowFloat = (enable) => { 108 | logger.debug('setWindowFloat'); 109 | 110 | let interval = setInterval(() => { 111 | const win = getPrimaryWindow(); 112 | if (!win) { return; } 113 | 114 | if (enable) { 115 | win.webContents.executeJavaScript('document.querySelector("html").classList.add("window-float")'); 116 | win.setIgnoreMouseEvents(true); 117 | win.setHasShadow(false); 118 | win.setAlwaysOnTop(true); 119 | } else { 120 | win.webContents.executeJavaScript('document.querySelector("html").classList.remove("window-float")'); 121 | win.setIgnoreMouseEvents(false); 122 | win.setHasShadow(true); 123 | } 124 | 125 | clearInterval(interval); 126 | }, defaultInterval); 127 | }; 128 | 129 | 130 | /** 131 | * Configuration Items 132 | * @namespace 133 | */ 134 | let configurationItems = { 135 | /** 136 | * Application version 137 | * @readonly 138 | */ 139 | internalVersion: { 140 | keypath: 'internalVersion', 141 | default: appVersion, 142 | init() { 143 | logger.debug(this.keypath, 'init'); 144 | }, 145 | get() { 146 | logger.debug(this.keypath, 'get'); 147 | 148 | return electronSettings.get(this.keypath); 149 | }, 150 | set(value) { 151 | logger.debug(this.keypath, 'set'); 152 | 153 | electronSettings.set(this.keypath, value); 154 | } 155 | }, 156 | /** 157 | * Filter Ads 158 | */ 159 | filterAds: { 160 | keypath: 'filterAds', 161 | default: true, 162 | init() { 163 | logger.debug(this.keypath, 'init'); 164 | 165 | this.implement(this.get()); 166 | }, 167 | get() { 168 | logger.debug(this.keypath, 'get'); 169 | 170 | return electronSettings.get(this.keypath); 171 | }, 172 | set(value) { 173 | logger.debug(this.keypath, 'set'); 174 | 175 | this.implement(value); 176 | electronSettings.set(this.keypath, value); 177 | }, 178 | implement(value) { 179 | logger.debug(this.keypath, 'implement', value); 180 | 181 | if (value) { 182 | requestfilterService.register(session.fromPartition('persist:player')); 183 | } else { 184 | requestfilterService.unregister(session.fromPartition('persist:player')); 185 | } 186 | } 187 | }, 188 | /** 189 | * Application log file 190 | * @readonly 191 | */ 192 | logFile: { 193 | keypath: 'logFile', 194 | default: path.join(appLogDirectory, appName + '.log'), 195 | init() { 196 | logger.debug(this.keypath, 'init'); 197 | }, 198 | get() { 199 | logger.debug(this.keypath, 'get'); 200 | 201 | return electronSettings.get(this.keypath); 202 | }, 203 | set(value) { 204 | logger.debug(this.keypath, 'set'); 205 | 206 | electronSettings.set(this.keypath, value); 207 | } 208 | }, 209 | /** 210 | * activeView 211 | */ 212 | activeView: { 213 | keypath: 'activeView', 214 | default: '', 215 | init() { 216 | logger.debug(this.keypath, 'init'); 217 | }, 218 | get() { 219 | logger.debug(this.keypath, 'get'); 220 | 221 | return electronSettings.get(this.keypath); 222 | }, 223 | set(value) { 224 | logger.debug(this.keypath, 'set'); 225 | 226 | electronSettings.set(this.keypath, value); 227 | } 228 | }, 229 | /** 230 | * playlistId 231 | */ 232 | playlistId: { 233 | keypath: 'playlistId', 234 | default: '', 235 | init() { 236 | logger.debug(this.keypath, 'init'); 237 | }, 238 | get() { 239 | logger.debug(this.keypath, 'get'); 240 | 241 | return electronSettings.get(this.keypath); 242 | }, 243 | set(value) { 244 | logger.debug(this.keypath, 'set'); 245 | 246 | electronSettings.set(this.keypath, value); 247 | }, 248 | watch(callback) { 249 | logger.debug(this.keypath, 'watch'); 250 | 251 | electronSettings.watch(this.keypath, (newValue, oldValue) => callback(newValue, oldValue)); 252 | } 253 | }, 254 | /** 255 | * playerType 256 | */ 257 | playerType: { 258 | keypath: 'playerType', 259 | default: 'embed', 260 | init() { 261 | logger.debug(this.keypath, 'init'); 262 | }, 263 | get() { 264 | logger.debug(this.keypath, 'get'); 265 | 266 | return electronSettings.get(this.keypath); 267 | }, 268 | set(value) { 269 | logger.debug(this.keypath, 'set'); 270 | 271 | electronSettings.set(this.keypath, value); 272 | }, 273 | watch(callback) { 274 | logger.debug(this.keypath, 'watch'); 275 | 276 | electronSettings.watch(this.keypath, (newValue, oldValue) => callback(newValue, oldValue)); 277 | } 278 | }, 279 | /** 280 | * Application update release notes 281 | * @readonly 282 | */ 283 | releaseNotes: { 284 | keypath: 'releaseNotes', 285 | default: '', 286 | init() { 287 | logger.debug(this.keypath, 'init'); 288 | }, 289 | get() { 290 | logger.debug(this.keypath, 'get'); 291 | 292 | return electronSettings.get(this.keypath); 293 | }, 294 | set(value) { 295 | logger.debug(this.keypath, 'set'); 296 | 297 | electronSettings.set(this.keypath, value); 298 | } 299 | }, 300 | /** 301 | * Show application always on top 302 | */ 303 | windowAlwaysOnTop: { 304 | keypath: 'windowAlwaysOnTop', 305 | default: true, 306 | init() { 307 | logger.debug(this.keypath, 'init'); 308 | 309 | // Wait for main window 310 | let interval = setInterval(() => { 311 | const winList = BrowserWindow.getAllWindows(); 312 | if (!winList) { return; } 313 | 314 | this.implement(this.get()); 315 | 316 | clearInterval(interval); 317 | }, defaultInterval); 318 | }, 319 | get() { 320 | logger.debug(this.keypath, 'get'); 321 | 322 | return electronSettings.get(this.keypath); 323 | }, 324 | set(value) { 325 | logger.debug(this.keypath, 'set', value); 326 | 327 | this.implement(value); 328 | electronSettings.set(this.keypath, value); 329 | }, 330 | implement(value) { 331 | logger.debug(this.keypath, 'implement', value); 332 | 333 | const winList = BrowserWindow.getAllWindows(); 334 | if (!winList) { return; } 335 | 336 | winList.forEach((win) => { 337 | win.setAlwaysOnTop(value); 338 | }); 339 | } 340 | }, 341 | /** 342 | * Show application in menubar / taskbar only 343 | */ 344 | windowInTrayOnly: { 345 | keypath: 'windowInTrayOnly', 346 | default: false, 347 | init() { 348 | logger.debug(this.keypath, 'init'); 349 | 350 | this.implement(this.get()); 351 | }, 352 | get() { 353 | logger.debug(this.keypath, 'get'); 354 | 355 | return electronSettings.get(this.keypath); 356 | }, 357 | set(value) { 358 | logger.debug(this.keypath, 'set'); 359 | 360 | this.implement(value); 361 | electronSettings.set(this.keypath, value); 362 | }, 363 | implement(value) { 364 | logger.debug(this.keypath, 'implement', value); 365 | 366 | setWindowInTrayOnly(value); 367 | } 368 | }, 369 | /** 370 | * windowFloat 371 | */ 372 | windowFloat: { 373 | keypath: 'windowFloat', 374 | default: false, 375 | init() { 376 | logger.debug(this.keypath, 'init'); 377 | 378 | this.implement(this.get()); 379 | }, 380 | get() { 381 | logger.debug(this.keypath, 'get'); 382 | 383 | return electronSettings.get(this.keypath); 384 | }, 385 | set(value) { 386 | logger.debug(this.keypath, 'set'); 387 | 388 | this.implement(value); 389 | electronSettings.set(this.keypath, value); 390 | }, 391 | implement(value) { 392 | logger.debug(this.keypath, 'implement', value); 393 | 394 | setWindowFloat(value); 395 | } 396 | }, 397 | /** 398 | * Main Window position / size 399 | * @readonly 400 | */ 401 | windowBounds: { 402 | keypath: 'windowBounds', 403 | default: { x: 100, y: 200, width: 1280, height: 720 }, 404 | init() { 405 | logger.debug(this.keypath, 'init'); 406 | 407 | this.implement(this.get()); 408 | 409 | /** 410 | * @listens Electron.App#before-quit 411 | */ 412 | app.on('before-quit', () => { 413 | logger.debug('app#before-quit'); 414 | 415 | const win = getPrimaryWindow(); 416 | if (!win) { return; } 417 | const bounds = win.getBounds(); 418 | if (!bounds) { return; } 419 | 420 | this.set(win.getBounds()); 421 | }); 422 | }, 423 | get() { 424 | logger.debug(this.keypath, 'get'); 425 | 426 | return electronSettings.get(this.keypath); 427 | }, 428 | set(value) { 429 | logger.debug(this.keypath, 'set', util.inspect(value)); 430 | 431 | let debounced = _.debounce(() => { 432 | electronSettings.set(this.keypath, value); 433 | }, defaultDebounce); 434 | 435 | debounced(); 436 | }, 437 | implement(value) { 438 | logger.debug(this.keypath, 'implement', util.inspect(value)); 439 | 440 | let interval = setInterval(() => { 441 | const win = getPrimaryWindow(); 442 | if (!win) { return; } 443 | 444 | win.setBounds(value); 445 | 446 | clearInterval(interval); 447 | }, defaultInterval); 448 | } 449 | }, 450 | /** 451 | * Main Window visibility 452 | * @readonly 453 | */ 454 | windowVisible: { 455 | keypath: 'windowVisible', 456 | default: true, 457 | init() { 458 | logger.debug(this.keypath, 'init'); 459 | 460 | // Wait for main window 461 | let interval = setInterval(() => { 462 | const win = getPrimaryWindow(); 463 | if (!win) { return; } 464 | 465 | this.implement(this.get()); 466 | 467 | /** 468 | * @listens Electron.BrowserWindow#show 469 | */ 470 | win.on('show', () => { 471 | this.set(true); 472 | }); 473 | 474 | /** 475 | * @listens Electron.BrowserWindow#hide 476 | */ 477 | win.on('hide', () => { 478 | this.set(false); 479 | }); 480 | 481 | clearInterval(interval); 482 | }, defaultInterval); 483 | }, 484 | get() { 485 | logger.debug(this.keypath, 'get'); 486 | 487 | return electronSettings.get(this.keypath); 488 | }, 489 | set(value) { 490 | logger.debug(this.keypath, 'set', value); 491 | 492 | let debounced = _.debounce(() => { 493 | electronSettings.set(this.keypath, value); 494 | }, defaultDebounce); 495 | 496 | debounced(); 497 | }, 498 | implement(value) { 499 | logger.debug(this.keypath, 'implement', value); 500 | 501 | const win = getPrimaryWindow(); 502 | if (!win) { return; } 503 | 504 | if (value) { win.show(); } 505 | else { win.hide(); } 506 | } 507 | } 508 | }; 509 | 510 | /** 511 | * Access single item 512 | * @returns {Object|void} 513 | * @function 514 | * 515 | * @public 516 | */ 517 | let getItem = (itemId) => { 518 | logger.debug('getConfigurationItem', itemId); 519 | 520 | if (configurationItems.hasOwnProperty(itemId)) { 521 | return configurationItems[itemId]; 522 | } 523 | }; 524 | 525 | /** 526 | * Get defaults of all items 527 | * @returns {Object} 528 | * @function 529 | */ 530 | let getConfigurationDefaults = () => { 531 | logger.debug('getConfigurationDefaults'); 532 | 533 | let defaults = {}; 534 | for (let item of Object.keys(configurationItems)) { 535 | defaults[item] = getItem(item).default; 536 | } 537 | 538 | return defaults; 539 | }; 540 | 541 | /** 542 | * Set defaults of all items 543 | * @returns {Object} 544 | * @function 545 | */ 546 | let setConfigurationDefaults = (callback = () => {}) => { 547 | logger.debug('setConfigurationDefaults'); 548 | 549 | let configuration = electronSettings.getAll(); 550 | let configurationDefaults = getConfigurationDefaults(); 551 | 552 | electronSettings.setAll(_.defaultsDeep(configuration, configurationDefaults)); 553 | 554 | callback(null); 555 | }; 556 | 557 | /** 558 | * Initialize all items – calling their init() method 559 | * @param {Function=} callback - Callback 560 | * @function 561 | */ 562 | let initializeItems = (callback = () => {}) => { 563 | logger.debug('initConfigurationItems'); 564 | 565 | let configurationItemList = Object.keys(configurationItems); 566 | 567 | configurationItemList.forEach((item, itemIndex) => { 568 | getItem(item).init(); 569 | 570 | // Last item 571 | if (configurationItemList.length === (itemIndex + 1)) { 572 | logger.debug('initConfigurationItems', 'complete'); 573 | callback(null); 574 | } 575 | }); 576 | }; 577 | 578 | /** 579 | * Remove unknown items 580 | * @param {Function=} callback - Callback 581 | * @function 582 | */ 583 | let removeLegacyItems = (callback = () => {}) => { 584 | logger.debug('cleanConfiguration'); 585 | 586 | let savedSettings = electronSettings.getAll(); 587 | let savedSettingsList = Object.keys(savedSettings); 588 | 589 | savedSettingsList.forEach((item, itemIndex) => { 590 | if (!configurationItems.hasOwnProperty(item)) { 591 | electronSettings.delete(item); 592 | logger.debug('cleanConfiguration', 'deleted', item); 593 | } 594 | 595 | // Last item 596 | if (savedSettingsList.length === (itemIndex + 1)) { 597 | logger.debug('cleanConfiguration', 'complete'); 598 | callback(null); 599 | } 600 | }); 601 | }; 602 | 603 | 604 | /** 605 | * @listens Electron.App#ready 606 | */ 607 | app.once('ready', () => { 608 | logger.debug('app#ready'); 609 | 610 | // Remove item unknown 611 | setConfigurationDefaults(() => { 612 | // Initialize items 613 | initializeItems(() => { 614 | // Set Defaults 615 | removeLegacyItems(() => { 616 | logger.debug('app#ready', 'complete'); 617 | }); 618 | }); 619 | }); 620 | }); 621 | 622 | /** 623 | * @listens Electron.App#before-quit 624 | */ 625 | app.on('before-quit', () => { 626 | logger.debug('app#before-quit'); 627 | 628 | logger.info('settings', electronSettings.getAll()); 629 | logger.info('file', electronSettings.file()); 630 | }); 631 | 632 | /** 633 | * @exports 634 | */ 635 | module.exports = getItem; 636 | -------------------------------------------------------------------------------- /app/scripts/main/menus/app-menu.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /** 5 | * Modules 6 | * Node 7 | * @constant 8 | */ 9 | const path = require('path'); 10 | const url = require('url'); 11 | 12 | /** 13 | * Modules 14 | * Electron 15 | * @constant 16 | */ 17 | const { app, BrowserWindow, Menu, shell, webContents } = require('electron'); 18 | 19 | /** 20 | * Modules 21 | * External 22 | * @constant 23 | */ 24 | const appRootPath = require('app-root-path')['path']; 25 | 26 | /** 27 | * Modules 28 | * Internal 29 | * @constant 30 | */ 31 | //const isDebug = require(path.join(appRootPath, 'lib', 'is-env'))('debug'); 32 | const logger = require(path.join(appRootPath, 'lib', 'logger'))({ write: false }); 33 | const packageJson = require(path.join(appRootPath, 'package.json')); 34 | 35 | 36 | /** 37 | * Application 38 | * @constant 39 | * @default 40 | */ 41 | const appProductName = packageJson.productName || packageJson.name; 42 | const appHomepage = packageJson.homepage; 43 | 44 | 45 | /** 46 | * @instance 47 | */ 48 | let appMenu = {}; 49 | 50 | 51 | /** 52 | * App Menu Template 53 | * @function 54 | */ 55 | let getAppMenuTemplate = () => { 56 | let template = [ 57 | { 58 | label: 'Edit', 59 | submenu: [ 60 | { 61 | label: 'Undo', 62 | accelerator: 'CommandOrControl+Z', 63 | role: 'undo' 64 | }, 65 | { 66 | label: 'Redo', 67 | accelerator: 'Shift+CommandOrControl+Z', 68 | role: 'redo' 69 | }, 70 | { 71 | type: 'separator' 72 | }, 73 | { 74 | label: 'Cut', 75 | accelerator: 'CommandOrControl+X', 76 | role: 'cut' 77 | }, 78 | { 79 | label: 'Copy', 80 | accelerator: 'CommandOrControl+C', 81 | role: 'copy' 82 | }, 83 | { 84 | label: 'Paste', 85 | accelerator: 'CommandOrControl+V', 86 | role: 'paste' 87 | }, 88 | { 89 | label: 'Select All', 90 | accelerator: 'CommandOrControl+A', 91 | role: 'selectall' 92 | } 93 | ] 94 | }, 95 | { 96 | label: 'View', 97 | submenu: [ 98 | { 99 | label: 'Toggle Full Screen', 100 | accelerator: (() => { 101 | if (process.platform === 'darwin') { 102 | return 'Ctrl+Command+F'; 103 | } 104 | else { 105 | return 'F11'; 106 | } 107 | })(), 108 | click(item, focusedWindow) { 109 | if (focusedWindow) { 110 | focusedWindow.setFullScreen(!focusedWindow.isFullScreen()); 111 | } 112 | } 113 | }, 114 | { 115 | type: 'separator' 116 | }, 117 | { 118 | label: 'Reset Zoom', 119 | accelerator: 'CommandOrControl+0', 120 | click() { 121 | webContents.getAllWebContents().forEach((contents) => { 122 | contents.send('zoom', 'reset'); 123 | }); 124 | } 125 | }, 126 | { 127 | label: 'Zoom In', 128 | accelerator: 'CommandOrControl+Plus', 129 | click() { 130 | webContents.getAllWebContents().forEach((contents) => { 131 | contents.send('zoom', 'in'); 132 | }); 133 | } 134 | }, 135 | { 136 | label: 'Zoom Out', 137 | accelerator: 'CommandOrControl+-', 138 | click() { 139 | webContents.getAllWebContents().forEach((contents) => { 140 | contents.send('zoom', 'out'); 141 | }); 142 | } 143 | }, 144 | { 145 | //visible: isDebug, 146 | type: 'separator' 147 | }, 148 | { 149 | //visible: isDebug, 150 | label: 'Reload', 151 | accelerator: 'CommandOrControl+R', 152 | click(item, focusedWindow) { 153 | if (focusedWindow) { 154 | focusedWindow.reload(); 155 | } 156 | } 157 | }, 158 | { 159 | //visible: isDebug, 160 | label: 'Toggle Developer Tools', 161 | accelerator: (() => { 162 | if (process.platform === 'darwin') { 163 | return 'Alt+Command+I'; 164 | } 165 | else { 166 | return 'Ctrl+Shift+I'; 167 | } 168 | })(), 169 | click(item, focusedWindow) { 170 | if (focusedWindow) { 171 | focusedWindow.toggleDevTools(); 172 | } 173 | } 174 | } 175 | ] 176 | }, 177 | { 178 | label: 'Window', 179 | role: 'window', 180 | submenu: [ 181 | { 182 | label: 'Minimize', 183 | accelerator: 'CommandOrControl+M', 184 | role: 'minimize' 185 | }, 186 | { 187 | label: 'Close', 188 | accelerator: 'CommandOrControl+W', 189 | role: 'close' 190 | } 191 | ] 192 | }, 193 | { 194 | label: 'Help', 195 | role: 'help', 196 | submenu: [ 197 | { 198 | label: 'Learn More', 199 | click() { 200 | shell.openExternal(appHomepage); 201 | } 202 | } 203 | ] 204 | } 205 | ]; 206 | 207 | if (process.platform === 'darwin') { 208 | template.unshift({ 209 | label: appProductName, 210 | submenu: [ 211 | { 212 | label: `About ${appProductName}`, 213 | role: 'about' 214 | }, 215 | { 216 | type: 'separator' 217 | }, 218 | { 219 | label: 'Services', 220 | role: 'services', 221 | submenu: [] 222 | }, 223 | { 224 | type: 'separator' 225 | }, 226 | { 227 | label: `Hide ${appProductName}`, 228 | accelerator: 'Command+H', 229 | role: 'hide' 230 | }, 231 | { 232 | label: 'Hide Others', 233 | accelerator: 'Command+Shift+H', 234 | role: 'hideothers' 235 | }, 236 | { 237 | label: 'Show All', 238 | role: 'unhide' 239 | }, 240 | { 241 | type: 'separator' 242 | }, 243 | { 244 | label: 'Quit', 245 | accelerator: 'Command+Q', 246 | click() { 247 | app.quit(); 248 | } 249 | } 250 | ] 251 | }); 252 | } 253 | 254 | return template; 255 | }; 256 | 257 | 258 | /** 259 | * @listens Electron.App#ready 260 | */ 261 | app.on('ready', () => { 262 | logger.debug('app#ready'); 263 | 264 | appMenu = Menu.buildFromTemplate(getAppMenuTemplate()); 265 | Menu.setApplicationMenu(appMenu); 266 | }); 267 | 268 | 269 | /** 270 | * @exports 271 | */ 272 | module.exports = appMenu; 273 | -------------------------------------------------------------------------------- /app/scripts/main/menus/tray-menu.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /** 5 | * Modules 6 | * Node 7 | * @constant 8 | */ 9 | const os = require('os'); 10 | const path = require('path'); 11 | 12 | /** 13 | * Modules 14 | * Electron 15 | * @constant 16 | */ 17 | const { app, BrowserWindow, Menu, session, Tray } = require('electron'); 18 | 19 | /** 20 | * Modules 21 | * External 22 | * @constant 23 | */ 24 | const appRootPath = require('app-root-path')['path']; 25 | 26 | /** 27 | * Modules 28 | * Internal 29 | * @constant 30 | */ 31 | const configurationManager = require(path.join(appRootPath, 'app', 'scripts', 'main', 'managers', 'configuration-manager')); 32 | const logger = require(path.join(appRootPath, 'lib', 'logger'))({ write: true }); 33 | const messengerService = require(path.join(appRootPath, 'app', 'scripts', 'main', 'services', 'messenger-service')); 34 | const packageJson = require(path.join(appRootPath, 'package.json')); 35 | const platformHelper = require(path.join(appRootPath, 'lib', 'platform-helper')); 36 | 37 | 38 | /** 39 | * Application 40 | * @constant 41 | * @default 42 | */ 43 | const appProductName = packageJson.productName || packageJson.name; 44 | const appVersion = packageJson.version; 45 | 46 | /** 47 | * Filesystem 48 | * @constant 49 | * @default 50 | */ 51 | const appTrayIconOpaque = path.join(appRootPath, 'icons', platformHelper.type, `icon-tray-opaque${platformHelper.trayImageExtension}`); 52 | 53 | 54 | /** 55 | * @instance 56 | */ 57 | let trayMenu = {}; 58 | 59 | /** 60 | * Tray Menu Template 61 | * @function 62 | */ 63 | let getTrayMenuTemplate = () => { 64 | return [ 65 | { 66 | id: 'productName', 67 | label: `Show ${appProductName}`, 68 | click() { 69 | BrowserWindow.getAllWindows()[0].show(); 70 | } 71 | }, 72 | { 73 | id: 'appVersion', 74 | label: `Version ${appVersion}`, 75 | type: 'normal', 76 | enabled: false 77 | }, 78 | { 79 | type: 'separator' 80 | }, 81 | { 82 | id: 'filterAds', 83 | label: '👊 YouTube AdBlock', 84 | type: 'checkbox', 85 | checked: configurationManager('filterAds').get(), 86 | click(menuItem) { 87 | configurationManager('filterAds').set(menuItem.checked); 88 | } 89 | }, 90 | { 91 | id: 'logout', 92 | label: '🔥 Change YouTube Playlist...', 93 | type: 'normal', 94 | click() { 95 | messengerService.showQuestion('Are you sure you want to log out from YouTube?', 96 | `${appProductName} will log out from YouTube.${os.EOL}` + 97 | `All unsaved changes will be lost.`, 98 | (result) => { 99 | if (result === 0) { 100 | require('electron-settings').deleteAll(); 101 | logger.debug('logout', 'settings reset'); 102 | 103 | const ses = session.fromPartition('persist:app'); 104 | 105 | ses.clearCache(() => { 106 | logger.debug('logout', 'cache cleared'); 107 | 108 | ses.clearStorageData({ 109 | storages: [ 110 | 'appcache', 'cookies', 'filesystem', 'indexdb', 'localstorage', 'shadercache', 111 | 'websql', 'serviceworkers' 112 | ], 113 | quotas: ['temporary', 'persistent', 'syncable'] 114 | }, () => { 115 | logger.debug('logout', 'storage cleared'); 116 | logger.log('logout', 'relaunching'); 117 | 118 | app.relaunch(); 119 | app.exit(); 120 | }); 121 | }); 122 | } 123 | }); 124 | } 125 | }, 126 | { 127 | type: 'separator' 128 | }, 129 | { 130 | id: 'windowAlwaysOnTop', 131 | label: '📤 Always on Top', 132 | type: 'checkbox', 133 | checked: configurationManager('windowAlwaysOnTop').get(), 134 | click(menuItem) { 135 | configurationManager('windowAlwaysOnTop').set(menuItem.checked); 136 | } 137 | }, 138 | { 139 | id: 'windowInTrayOnly', 140 | label: platformHelper.isMacOS ? '📌 Hide Dock Icon' : '📌 Minimize to Tray', 141 | type: 'checkbox', 142 | checked: configurationManager('windowInTrayOnly').get(), 143 | click(menuItem) { 144 | configurationManager('windowInTrayOnly').set(menuItem.checked); 145 | } 146 | }, 147 | { 148 | id: 'windowFloat', 149 | label: '😎 FloatMode™', 150 | type: 'checkbox', 151 | checked: configurationManager('windowFloat').get(), 152 | click(menuItem) { 153 | configurationManager('windowFloat').set(menuItem.checked); 154 | 155 | // Check related item 156 | let relatedItem = menuItem.menu.items.find(item => { 157 | return item.id === 'windowAlwaysOnTop'; 158 | }); 159 | relatedItem.checked = true; 160 | } 161 | }, 162 | { 163 | type: 'separator' 164 | }, 165 | { 166 | label: `Quit ${appProductName}`, 167 | click() { 168 | app.quit(); 169 | } 170 | } 171 | ]; 172 | }; 173 | 174 | /** 175 | * @class 176 | * @extends Electron.Tray 177 | */ 178 | class TrayMenu extends Tray { 179 | constructor(template) { 180 | super(appTrayIconOpaque); 181 | 182 | this.setToolTip(appProductName); 183 | this.setContextMenu(Menu.buildFromTemplate(template)); 184 | 185 | /** 186 | * @listens Electron.Tray#click 187 | */ 188 | this.on('click', () => { 189 | logger.debug('TrayMenu#click'); 190 | 191 | if (platformHelper.isWindows) { 192 | let mainWindow = BrowserWindow.getAllWindows()[0]; 193 | 194 | if (!mainWindow) { return; } 195 | 196 | if (mainWindow.isVisible()) { 197 | mainWindow.hide(); 198 | } else { 199 | mainWindow.show(); 200 | } 201 | } 202 | }); 203 | } 204 | } 205 | 206 | 207 | /** 208 | * Create instance 209 | */ 210 | let create = () => { 211 | logger.debug('create'); 212 | 213 | if (!(trayMenu instanceof TrayMenu)) { 214 | trayMenu = new TrayMenu(getTrayMenuTemplate()); 215 | } 216 | }; 217 | 218 | 219 | /** 220 | * @listens Electron.App#ready 221 | */ 222 | app.on('ready', () => { 223 | logger.debug('app#ready'); 224 | 225 | create(); 226 | }); 227 | 228 | 229 | /** 230 | * @exports 231 | */ 232 | module.exports = trayMenu; 233 | -------------------------------------------------------------------------------- /app/scripts/main/services/debug-service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /** 5 | * Modules 6 | * Node 7 | * @constant 8 | */ 9 | const path = require('path'); 10 | 11 | /** 12 | * Modules 13 | * Electron 14 | * @constant 15 | */ 16 | const electron = require('electron'); 17 | const { app, webContents } = electron || electron.remote; 18 | 19 | /** 20 | * Modules 21 | * External 22 | * @constant 23 | */ 24 | const appRootPath = require('app-root-path')['path']; 25 | const tryRequire = require('try-require'); 26 | 27 | /** 28 | * Modules 29 | * Internal 30 | * @constant 31 | */ 32 | const isDebug = require(path.join(appRootPath, 'lib', 'is-env'))('debug'); 33 | const isLivereload = require(path.join(appRootPath, 'lib', 'is-env'))('livereload'); 34 | const logger = require(path.join(appRootPath, 'lib', 'logger'))({ write: true }); 35 | 36 | 37 | /** 38 | * @constant 39 | * @default 40 | */ 41 | const defaultTimeout = 5000; 42 | 43 | 44 | /** 45 | * Init 46 | */ 47 | let init = () => { 48 | logger.debug('init'); 49 | 50 | let timeout = setTimeout(() => { 51 | webContents.getAllWebContents().forEach((contents) => { 52 | 53 | /** 54 | * Developer Tools 55 | */ 56 | if (isDebug) { 57 | logger.info('opening developer tools:', `"${contents.getURL()}"`); 58 | 59 | contents.openDevTools({ mode: 'undocked' }); 60 | } 61 | 62 | /** 63 | * Live Reload 64 | */ 65 | if (isLivereload) { 66 | logger.info('starting live reload:', `"${contents.getURL()}"`); 67 | 68 | tryRequire('electron-connect').client.create(); 69 | } 70 | }); 71 | clearTimeout(timeout); 72 | }, defaultTimeout); 73 | }; 74 | 75 | 76 | /** 77 | * @listens Electron.App#ready 78 | */ 79 | app.once('ready', () => { 80 | logger.debug('app#ready'); 81 | 82 | init(); 83 | }); 84 | -------------------------------------------------------------------------------- /app/scripts/main/services/messenger-service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /** 5 | * Modules 6 | * Node 7 | * @constant 8 | */ 9 | const fs = require('fs-extra'); 10 | const os = require('os'); 11 | const path = require('path'); 12 | 13 | /** 14 | * Modules 15 | * Electron 16 | * @constant 17 | */ 18 | const electron = require('electron'); 19 | const { app, dialog } = electron || electron.remote; 20 | 21 | /** 22 | * Modules 23 | * External 24 | * @constant 25 | */ 26 | const _ = require('lodash'); 27 | const appRootPath = require('app-root-path')['path']; 28 | const fileType = require('file-type'); 29 | const readChunk = require('read-chunk'); 30 | 31 | /** 32 | * Modules 33 | * Internal 34 | * @constant 35 | */ 36 | const logger = require(path.join(appRootPath, 'lib', 'logger'))({ write: true }); 37 | const packageJson = require(path.join(appRootPath, 'package.json')); 38 | const platformHelper = require(path.join(appRootPath, 'lib', 'platform-helper')); 39 | 40 | 41 | /** 42 | * Application 43 | * @constant 44 | * @default 45 | */ 46 | const appProductName = packageJson.productName || packageJson.name; 47 | 48 | /** 49 | * @constant 50 | * @default 51 | */ 52 | const defaultTimeout = 250; 53 | 54 | 55 | /** 56 | * Display Message Box 57 | * @param {String} title - Title 58 | * @param {String} message - Message 59 | * @param {Array} buttonList - Buttons 60 | * @param {Boolean} isError - Buttons 61 | * @param {Function=} callback - Callback 62 | * @function 63 | */ 64 | let displayDialog = function(title, message, buttonList, isError, callback = () => {}) { 65 | let dialogTitle = title || appProductName; 66 | let dialogMessage = message || title; 67 | 68 | let timeout = setTimeout(() => { 69 | dialog.showMessageBox({ 70 | type: isError ? 'error' : 'warning', 71 | buttons: buttonList || ['OK'], 72 | defaultId: 0, 73 | title: dialogTitle, 74 | message: dialogTitle, 75 | detail: os.EOL + dialogMessage + os.EOL 76 | }, (response) => { 77 | logger.debug('displayDialog', `title: '${title}' message: '${message}' response: '${response} (${buttonList[response]})'`); 78 | callback(response); 79 | }); 80 | 81 | clearTimeout(timeout); 82 | }, defaultTimeout); 83 | }; 84 | 85 | /** 86 | * Validate Files by Mimetype 87 | * @function 88 | */ 89 | let validateFileType = function(file, acceptedFiletype, callback) { 90 | logger.debug('validateFileType', file, acceptedFiletype); 91 | 92 | let filePath = path.normalize(file.toString()); 93 | 94 | fs.stat(filePath, function(err) { 95 | if (err) { return callback(err); } 96 | 97 | let detectedType = fileType(readChunk.sync(filePath, 0, 262)).mime; 98 | let isValidFile = _.startsWith(detectedType, acceptedFiletype); 99 | 100 | if (!isValidFile) { 101 | logger.error('validFileType', detectedType); 102 | 103 | return callback(new Error(`Filetype incorrect: ${detectedType}`)); 104 | } 105 | 106 | callback(null, filePath); 107 | }); 108 | }; 109 | 110 | 111 | /** 112 | * Info 113 | * @param {String=} title - Title 114 | * @param {String=} message - Message 115 | * @param {Function=} callback - Callback 116 | * @function 117 | * 118 | * @public 119 | */ 120 | let showInfo = function(title, message, callback = () => {}) { 121 | return displayDialog(title, message, ['Dismiss'], false, callback); 122 | }; 123 | 124 | 125 | /** 126 | * Info 127 | * @param {String=} title - Title 128 | * @param {String} fileType - audio,video 129 | * @param {String=} folder - Initial lookup folder 130 | * @param {Function=} callback - Callback 131 | * @function 132 | * 133 | * @public 134 | */ 135 | let openFile = function(title, fileType, folder, callback = () => {}) { 136 | let dialogTitle = title || appProductName; 137 | let initialFolder = folder || app.getPath(name); 138 | 139 | let fileTypes = { 140 | image: ['jpg', 'jpeg', 'bmg', 'png', 'tif'], 141 | audio: ['aiff', 'm4a', 'mp3', 'mp4', 'wav'] 142 | }; 143 | 144 | 145 | if (!fileTypes[fileType]) { 146 | return; 147 | } 148 | 149 | logger.debug('initialFolder', initialFolder); 150 | logger.debug('dialogTitle', dialogTitle); 151 | logger.debug('title', title); 152 | logger.debug('fileType', fileType); 153 | 154 | 155 | dialog.showOpenDialog({ 156 | title: dialogTitle, 157 | properties: ['openFile', 'showHiddenFiles'], 158 | defaultPath: initialFolder, 159 | filters: [{ name: 'Sound', extensions: fileTypes[fileType] }] 160 | }, (filePath) => { 161 | 162 | if (!filePath) { 163 | logger.error('showOpenDialog', 'filepath required'); 164 | return callback(new Error(`Filepath missing`)); 165 | } 166 | 167 | validateFileType(filePath, fileType, function(err, filePath) { 168 | if (err) { 169 | return displayDialog(`Incompatible file.${os.EOL}`, `Compatible formats are: ${fileTypes[fileType]}`, ['Dismiss'], false, () => { 170 | logger.error('validateFileType', err); 171 | callback(new Error(`File content error: ${filePath}`)); 172 | }); 173 | } 174 | 175 | callback(null, filePath); 176 | }); 177 | }); 178 | }; 179 | 180 | /** 181 | * Yes/No 182 | * @param {String=} title - Title 183 | * @param {String=} message - Error Message 184 | * @param {Function=} callback - Callback 185 | * @function 186 | * 187 | * @public 188 | */ 189 | let showQuestion = function(title, message, callback = () => {}) { 190 | app.focus(); 191 | 192 | return displayDialog(title, message, ['Yes', 'No'], false, callback); 193 | }; 194 | 195 | /** 196 | * Error 197 | * @param {String} message - Error Message 198 | * @param {Function=} callback - Callback 199 | * @function 200 | * 201 | * @public 202 | */ 203 | let showError = function(message, callback = () => {}) { 204 | // Add Quit button 205 | callback = (result) => { 206 | if (result === 2) { return app.quit(); } 207 | return callback; 208 | }; 209 | 210 | if (platformHelper.isMacOS) { 211 | app.dock.bounce('critical'); 212 | } 213 | 214 | app.focus(); 215 | 216 | return displayDialog('Error', message, ['Cancel', 'OK', `Quit ${appProductName}`], true, callback); 217 | }; 218 | 219 | 220 | /** 221 | * @exports 222 | */ 223 | module.exports = { 224 | openFile: openFile, 225 | showError: showError, 226 | showInfo: showInfo, 227 | showQuestion: showQuestion 228 | }; 229 | -------------------------------------------------------------------------------- /app/scripts/main/services/notification-service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /** 5 | * Modules 6 | * Node 7 | * @constant 8 | */ 9 | const path = require('path'); 10 | 11 | /** 12 | * Modules 13 | * Electron 14 | * @constant 15 | */ 16 | const electron = require('electron'); 17 | const { webContents } = electron || electron.remote; 18 | 19 | /** 20 | * Modules 21 | * External 22 | * @constant 23 | */ 24 | const _ = require('lodash'); 25 | const appRootPath = require('app-root-path')['path']; 26 | 27 | /** 28 | * Modules 29 | * Internal 30 | * @constant 31 | */ 32 | const logger = require(path.join(appRootPath, 'lib', 'logger'))({ write: true }); 33 | const packageJson = require(path.join(appRootPath, 'package.json')); 34 | const platformHelper = require(path.join(appRootPath, 'lib', 'platform-helper')); 35 | 36 | 37 | /** 38 | * Application 39 | * @constant 40 | * @default 41 | */ 42 | const appIcon = path.join(appRootPath, 'icons', platformHelper.type, `icon${platformHelper.iconImageExtension(platformHelper.type)}`); 43 | const appProductName = packageJson.productName || packageJson.name; 44 | 45 | 46 | /** 47 | * Default HTML5 notification options 48 | * @constant 49 | * @default 50 | */ 51 | const defaultOptions = { 52 | silent: true 53 | }; 54 | 55 | /** 56 | * Show Notification 57 | * @param {String=} title - Title 58 | * @param {Object=} options - Title 59 | * @function 60 | * 61 | * @public 62 | */ 63 | let showNotification = (title, options) => { 64 | logger.debug('showNotification'); 65 | 66 | if (!_.isString(title)) { return; } 67 | 68 | const notificationTitle = _.trim(title); 69 | const notificationOptions = JSON.stringify(_.defaultsDeep(options, defaultOptions)); 70 | 71 | const code = `new Notification('${notificationTitle}', ${notificationOptions});`; 72 | 73 | if (webContents.getAllWebContents().length === 0) { 74 | logger.warn('could not show notification', 'no webcontents available'); 75 | return; 76 | } 77 | 78 | webContents.getAllWebContents()[0].executeJavaScript(code, true); 79 | }; 80 | 81 | 82 | /** 83 | * @exports 84 | */ 85 | module.exports = { 86 | show: showNotification 87 | }; 88 | -------------------------------------------------------------------------------- /app/scripts/main/services/power-service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /** 5 | * Modules 6 | * Node 7 | * @constant 8 | */ 9 | const path = require('path'); 10 | 11 | /** 12 | * Modules 13 | * Electron 14 | * @constant 15 | */ 16 | const electron = require('electron'); 17 | const { app } = electron || electron.remote; 18 | 19 | /** 20 | * Modules 21 | * External 22 | * @constant 23 | */ 24 | const appRootPath = require('app-root-path')['path']; 25 | 26 | /** 27 | * Modules 28 | * Internal 29 | * @constant 30 | */ 31 | const logger = require(path.join(appRootPath, 'lib', 'logger'))({ write: true }); 32 | 33 | 34 | /** 35 | * @constant 36 | * @default 37 | */ 38 | const defaultTimeout = 5000; 39 | 40 | 41 | /** 42 | * Init 43 | */ 44 | let init = () => { 45 | logger.debug('init'); 46 | 47 | /** 48 | * @listens Electron.powerMonitor#suspend 49 | */ 50 | electron.powerMonitor.on('suspend', () => { 51 | logger.log('webview#suspend'); 52 | }); 53 | 54 | /** 55 | * @listens Electron.powerMonitor#resume 56 | */ 57 | electron.powerMonitor.on('resume', () => { 58 | logger.log('webview#resume'); 59 | 60 | let timeout = setTimeout(() => { 61 | logger.log('webview#resume', 'relaunching app'); 62 | 63 | app.relaunch(); 64 | app.exit(); 65 | 66 | clearTimeout(timeout); 67 | }, defaultTimeout); 68 | }); 69 | }; 70 | 71 | 72 | /** 73 | * @listens Electron.App#ready 74 | */ 75 | app.once('ready', () => { 76 | logger.debug('app#ready'); 77 | 78 | init(); 79 | }); 80 | -------------------------------------------------------------------------------- /app/scripts/main/services/requestfilter-service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /** 5 | * Modules 6 | * Node 7 | * @constant 8 | */ 9 | const path = require('path'); 10 | 11 | /** 12 | * Modules 13 | * Electron 14 | * @constant 15 | */ 16 | const electron = require('electron'); 17 | const { webContents } = electron || electron.remote; 18 | 19 | /** 20 | * Modules 21 | * External 22 | * @constant 23 | */ 24 | const appRootPath = require('app-root-path').path; 25 | 26 | /** 27 | * Modules 28 | * Internal 29 | * @constant 30 | */ 31 | const logger = require(path.join(appRootPath, 'lib', 'logger'))({ write: true }); 32 | 33 | 34 | /** 35 | * @constant 36 | * @default 37 | */ 38 | const defaultInterval = 1000; 39 | 40 | /** 41 | * @default 42 | */ 43 | let isEnabled = false; 44 | 45 | /** 46 | * Array of URL patterns 47 | * @default 48 | */ 49 | let filterList = [ 50 | '*://*.doubleclick.net/*', 51 | '*://*.google.com/pagead*', 52 | '*://*.google.com/uds/api/ads/*', 53 | '*://*.googleadservices.com/pagead*', 54 | '*://*.googleapis.com/*log_interaction*?*', 55 | '*://*.googleapis.com/adsmeasurement*', 56 | '*://*.googleapis.com/plus*', 57 | '*://*.googleapis.com/youtubei/v1/player/ad_break?*', 58 | '*://*.googleusercontent.com/generate_204*', 59 | '*://*.gstatic.com/csi*?*ad_at*', 60 | '*://*.gstatic.com/csi*?*ad_to_video*', 61 | '*://*.gstatic.com/csi*?*mod_ad*', 62 | '*://*.gstatic.com/csi*?*yt_ad*', 63 | '*://*.youtube-nocookie.com/api/ads/trueview_redirect?*', 64 | '*://*.youtube-nocookie.com/gen_204*', 65 | '*://*.youtube.com/ad_data_204*', 66 | '*://*.youtube.com/api/stats/ads*?*', 67 | '*://*.youtube.com/api/stats/atr*?*', 68 | '*://*.youtube.com/api/stats/qoe*?*', 69 | '*://*.youtube.com/api/stats/watchtime*?*', 70 | '*://*.youtube.com/generate_204*', 71 | '*://*.youtube.com/gen_204*', 72 | '*://*.youtube.com/get_ad_tags?*', 73 | '*://*.youtube.com/player_204*', 74 | '*://*.youtube.com/ptracking?*', 75 | '*://*.youtube.com/set_awesome*', 76 | '*://*.youtube.com/stream_204*', 77 | '*://*.youtube.com/yva_video?*adformat*', 78 | '*://*.youtube.com/yva_video?*preroll*', 79 | '*://csi.gstatic.com/csi?*video_to_ad*', 80 | '*://manifest.googlevideo.com/generate_204*' 81 | ]; 82 | 83 | /** 84 | * Enable URL filter for a session 85 | * @param {Electron.Session} session - Session 86 | * @param {Function=} callback - Callback 87 | */ 88 | let register = (session, callback = () => {}) => { 89 | logger.debug('register'); 90 | 91 | session.webRequest.onBeforeRequest({ urls: filterList }, (details, callback) => { 92 | logger.debug(`blocked url: ${details.url}`); 93 | callback({ cancel: true }); 94 | }); 95 | 96 | callback(); 97 | }; 98 | 99 | /** 100 | * Remove URL filter for a session 101 | * @param {Electron.Session} session - Session 102 | * @param {Function=} callback - Callback 103 | */ 104 | let unregister = (session, callback = () => {}) => { 105 | logger.debug('unregister'); 106 | 107 | session.webRequest.onBeforeRequest({ urls: filterList }, null); 108 | 109 | callback(); 110 | }; 111 | 112 | /** 113 | * Enable URL filter for all sessions 114 | * @param {Function=} callback - Callback 115 | */ 116 | let registerAll = (callback = () => {}) => { 117 | logger.debug('addFilters'); 118 | 119 | let interval = setInterval(() => { 120 | const contentsList = webContents.getAllWebContents(); 121 | if (contentsList.length === 0) { return; } 122 | 123 | contentsList.forEach((contents, index, array) => { 124 | if (contents.session) { register(contents.session); } 125 | 126 | if (array.length === (index + 1)) { 127 | isEnabled = true; 128 | callback(null); 129 | } 130 | }); 131 | clearInterval(interval); 132 | }, defaultInterval); 133 | 134 | }; 135 | 136 | /** 137 | * Disable URL filter for all sessions 138 | * @param {Function=} callback - Callback 139 | */ 140 | let unregisterAll = (callback = () => {}) => { 141 | logger.debug('removeFilters'); 142 | 143 | let interval = setInterval(() => { 144 | const contentsList = webContents.getAllWebContents(); 145 | if (contentsList.length === 0) { return; } 146 | contentsList.forEach((contents, index, array) => { 147 | if (contents.session) { unregister(contents.session); } 148 | 149 | if (array.length === (index + 1)) { 150 | isEnabled = false; 151 | callback(null); 152 | } 153 | }); 154 | clearInterval(interval); 155 | }, defaultInterval); 156 | }; 157 | 158 | /** 159 | * Update URL filter list 160 | * @param {Array} list - Array of URL patterns 161 | */ 162 | let setFilter = (list) => { 163 | logger.debug('setFilter'); 164 | 165 | // Skip if nothing changed 166 | if (list.toString() === filterList.toString()) { return; } 167 | 168 | // Update filter 169 | filterList = list; 170 | 171 | // If filters are already active, remove current filters first 172 | if (isEnabled) { 173 | unregisterAll(() => registerAll(() => logger.info(`enabled ${filterList.length} url filters`))); 174 | } 175 | }; 176 | 177 | 178 | /** 179 | * @exports 180 | */ 181 | module.exports = { 182 | register: register, 183 | unregister: unregister, 184 | registerAll: registerAll, 185 | unregisterAll: unregisterAll, 186 | setFilter: setFilter 187 | }; 188 | -------------------------------------------------------------------------------- /app/scripts/main/services/updater-service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /** 5 | * Modules 6 | * Node 7 | * @constant 8 | */ 9 | const os = require('os'); 10 | const path = require('path'); 11 | 12 | /** 13 | * Modules 14 | * Electron 15 | * @constant 16 | */ 17 | const electron = require('electron'); 18 | const { app, BrowserWindow } = electron || electron.remote; 19 | 20 | /** 21 | * Modules 22 | * External 23 | * @constant 24 | */ 25 | const appRootPath = require('app-root-path')['path']; 26 | const semverCompare = require('semver-compare'); 27 | const { autoUpdater } = require('electron-updater'); 28 | 29 | /** 30 | * Modules 31 | * Internal 32 | * @constant 33 | */ 34 | const isDebug = require(path.join(appRootPath, 'lib', 'is-env'))('debug'); 35 | const logger = require(path.join(appRootPath, 'lib', 'logger'))({ write: true }); 36 | const messengerService = require(path.join(appRootPath, 'app', 'scripts', 'main', 'services', 'messenger-service')); 37 | const packageJson = require(path.join(appRootPath, 'package.json')); 38 | const platformHelper = require(path.join(appRootPath, 'lib', 'platform-helper')); 39 | const configurationManager = require(path.join(appRootPath, 'app', 'scripts', 'main', 'managers', 'configuration-manager')); 40 | const notificationService = require(path.join(appRootPath, 'app', 'scripts', 'main', 'services', 'notification-service')); 41 | 42 | /** 43 | * Application 44 | * @constant 45 | * @default 46 | */ 47 | const appProductName = packageJson.productName || packageJson.name; 48 | const appVersion = packageJson.version; 49 | 50 | 51 | /** 52 | * @instance 53 | */ 54 | let updaterService; 55 | 56 | /** 57 | * Updater 58 | * @returns autoUpdater 59 | * @class 60 | */ 61 | class Updater { 62 | constructor() { 63 | if (platformHelper.isLinux) { return; } 64 | 65 | this.init(); 66 | } 67 | 68 | init() { 69 | logger.debug('init'); 70 | 71 | // Extend stateful 72 | autoUpdater.isUpdating = false; 73 | 74 | // Set Logger 75 | autoUpdater.logger = logger; 76 | 77 | /** 78 | * @listens AutoUpdater#error 79 | */ 80 | autoUpdater.on('error', (error) => { 81 | logger.error('autoUpdater#error', error.message); 82 | 83 | autoUpdater.isUpdating = false; 84 | }); 85 | 86 | /** 87 | * @listens AutoUpdater#checking-for-update 88 | */ 89 | autoUpdater.on('checking-for-update', () => { 90 | logger.info('autoUpdater#checking-for-update'); 91 | 92 | autoUpdater.isUpdating = true; 93 | }); 94 | 95 | /** 96 | * @listens AutoUpdater#update-available 97 | */ 98 | autoUpdater.on('update-available', (info) => { 99 | logger.info('autoUpdater#update-available', info); 100 | 101 | autoUpdater.isUpdating = true; 102 | 103 | notificationService.show(`Update available for ${appProductName}`, { body: `Version: ${info.version}` }); 104 | }); 105 | 106 | /** 107 | * @listens AutoUpdater#update-not-available 108 | */ 109 | autoUpdater.on('update-not-available', (info) => { 110 | logger.info('autoUpdater#update-not-available', info); 111 | 112 | autoUpdater.isUpdating = false; 113 | }); 114 | 115 | /** 116 | * @listens AutoUpdater#download-progress 117 | */ 118 | autoUpdater.on('download-progress', (progress) => { 119 | logger.info('autoUpdater#download-progress', progress.percent); 120 | 121 | // Show update progress bar (Windows only) 122 | if (platformHelper.isWindows) { 123 | const win = BrowserWindow.getAllWindows()[0]; 124 | if (!win) { return; } 125 | 126 | win.setProgressBar(progress.percent / 100); 127 | } 128 | }); 129 | 130 | /** 131 | * @listens AutoUpdater#update-downloaded 132 | */ 133 | autoUpdater.on('update-downloaded', (info) => { 134 | logger.info('autoUpdater#update-downloaded', info); 135 | 136 | autoUpdater.isUpdating = true; 137 | 138 | notificationService.show(`Update ready to install for ${appProductName}`, { body: `Version: ${info.version}` }); 139 | 140 | if (Boolean(info.releaseNotes)) { 141 | configurationManager('releaseNotes').set(info.releaseNotes); 142 | logger.info('autoUpdater#update-downloaded', 'releaseNotes', info.releaseNotes); 143 | } 144 | 145 | messengerService.showQuestion( 146 | `Update successfully installed`, 147 | `${appProductName} has been updated successfully.${os.EOL}${os.EOL}` + 148 | `To apply the changes and complete the updating process, the app needs to be restarted.${os.EOL}${os.EOL}` + 149 | `Restart now?`, (response) => { 150 | if (response === 0) { 151 | BrowserWindow.getAllWindows().forEach((window) => { window.destroy(); }); 152 | autoUpdater.quitAndInstall(); 153 | } 154 | if (response === 1) { return true; } 155 | 156 | return true; 157 | }); 158 | }); 159 | 160 | autoUpdater.checkForUpdates(); 161 | 162 | return autoUpdater; 163 | } 164 | } 165 | 166 | 167 | /** 168 | * Updates internal version to current version 169 | * @function 170 | */ 171 | let bumpInternalVersion = () => { 172 | logger.debug('bumpInternalVersion'); 173 | 174 | let internalVersion = configurationManager('internalVersion').get(); 175 | 176 | // DEBUG 177 | logger.debug('bumpInternalVersion', 'packageJson.version', packageJson.version); 178 | logger.debug('bumpInternalVersion', 'internalVersion', internalVersion); 179 | logger.debug('bumpInternalVersion', 'semverCompare(packageJson.version, internalVersion)', semverCompare(packageJson.version, internalVersion)); 180 | 181 | // Initialize version 182 | if (!internalVersion) { 183 | configurationManager('internalVersion').set(packageJson.version); 184 | 185 | return; 186 | } 187 | 188 | // Compare internal/current version 189 | let wasUpdated = Boolean(semverCompare(packageJson.version, internalVersion) === 1); 190 | 191 | // DEBUG 192 | logger.debug('bumpInternalVersion', 'wasUpdated', wasUpdated); 193 | 194 | // Update internal version 195 | if (wasUpdated) { 196 | configurationManager('internalVersion').set(packageJson.version); 197 | 198 | const releaseNotes = configurationManager('releaseNotes').get(); 199 | 200 | if (Boolean(releaseNotes)) { 201 | messengerService.showInfo(`${appProductName} has been updated to ${appVersion}.`, `Release Notes:${os.EOL}${os.EOL}${releaseNotes}`); 202 | logger.info(`${appProductName} has been updated to ${appVersion}.`, `Release Notes:${os.EOL}${os.EOL}${releaseNotes}`); 203 | } else { 204 | messengerService.showInfo(`Update complete`, `${appProductName} has been updated to ${appVersion}.`); 205 | logger.info(`Update complete`, `${appProductName} has been updated to ${appVersion}.`); 206 | } 207 | 208 | notificationService.show(`Update complete for ${appProductName}`, { body: `Version: ${appVersion}` }); 209 | } 210 | }; 211 | 212 | 213 | /** 214 | * Init 215 | */ 216 | let init = () => { 217 | logger.debug('init'); 218 | 219 | // Only update if run from within purpose-built (signed) Electron binary 220 | if (process.defaultApp) { return; } 221 | 222 | updaterService = new Updater(); 223 | 224 | bumpInternalVersion(); 225 | }; 226 | 227 | 228 | /** 229 | * @listens Electron.App#browser-window-focus 230 | */ 231 | app.on('browser-window-focus', () => { 232 | logger.debug('app#browser-window-focus'); 233 | 234 | if (!updaterService) { return; } 235 | 236 | if (Boolean(updaterService.isUpdating) === false) { 237 | if (updaterService.checkForUpdates) { 238 | updaterService.checkForUpdates(); 239 | } 240 | } 241 | }); 242 | 243 | /** 244 | * @listens Electron.App#ready 245 | */ 246 | app.once('ready', () => { 247 | logger.debug('app#ready'); 248 | 249 | init(); 250 | }); -------------------------------------------------------------------------------- /app/scripts/main/windows/main-window.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /** 5 | * Modules 6 | * Node 7 | * @constant 8 | */ 9 | const path = require('path'); 10 | const url = require('url'); 11 | 12 | /** 13 | * Modules 14 | * Electron 15 | * @constant 16 | */ 17 | const electron = require('electron'); 18 | const { app, BrowserWindow, shell } = electron; 19 | 20 | /** 21 | * Modules 22 | * External 23 | * @constant 24 | */ 25 | const appRootPath = require('app-root-path')['path']; 26 | 27 | /** 28 | * Modules 29 | * Internal 30 | * @constant 31 | */ 32 | const logger = require(path.join(appRootPath, 'lib', 'logger'))({ write: true }); 33 | const packageJson = require(path.join(appRootPath, 'package.json')); 34 | const platformHelper = require(path.join(appRootPath, 'lib', 'platform-helper')); 35 | 36 | 37 | /** 38 | * Application 39 | * @constant 40 | * @default 41 | */ 42 | const windowTitle = packageJson.productName || packageJson.name; 43 | const windowIcon = path.join(appRootPath, 'icons', platformHelper.type, `icon${platformHelper.iconImageExtension(platformHelper.type)}`); 44 | const windowUrl = url.format({ protocol: 'file:', pathname: path.join(appRootPath, 'app', 'html', 'main.html') }); 45 | 46 | 47 | /** 48 | * @instance 49 | */ 50 | let appWindow = {}; 51 | 52 | 53 | /** 54 | * AppWindow 55 | * @class 56 | * @extends Electron.BrowserWindow 57 | */ 58 | class AppWindow extends BrowserWindow { 59 | constructor() { 60 | super({ 61 | acceptFirstMouse: true, 62 | autoHideMenuBar: true, 63 | backgroundColor: platformHelper.isMacOS ? '#0095A5A6' : '#95A5A6', 64 | frame: !platformHelper.isMacOS, 65 | fullscreenable: true, 66 | icon: windowIcon, 67 | minWidth: 256, 68 | minHeight: 256, 69 | partition: 'partition:app', 70 | show: false, 71 | thickFrame: true, 72 | overlayScrollbars: true, 73 | sharedWorker: true, 74 | title: windowTitle, 75 | titleBarStyle: platformHelper.isMacOS ? 'hidden-inset' : 'default', 76 | transparent: false, 77 | vibrancy: 'dark', 78 | webPreferences: { 79 | allowDisplayingInsecureContent: true, 80 | allowRunningInsecureContent: true, 81 | experimentalFeatures: true, 82 | nodeIntegration: true, 83 | webaudio: true, 84 | webgl: true, 85 | webSecurity: false 86 | } 87 | }); 88 | 89 | this.init(); 90 | } 91 | 92 | init() { 93 | logger.debug('init'); 94 | 95 | /** 96 | * @listens Electron.BrowserWindow#close 97 | */ 98 | this.on('close', ev => { 99 | logger.debug('AppWindow#close'); 100 | 101 | if (!app.isQuitting) { 102 | ev.preventDefault(); 103 | this.hide(); 104 | } 105 | }); 106 | 107 | /** 108 | * @listens Electron.BrowserWindow#show 109 | */ 110 | this.on('show', () => { 111 | logger.debug('AppWindow#show'); 112 | }); 113 | 114 | /** 115 | * @listens Electron.BrowserWindow#hide 116 | */ 117 | this.on('hide', () => { 118 | logger.debug('AppWindow#hide'); 119 | }); 120 | 121 | /** 122 | * @listens Electron.BrowserWindow#move 123 | */ 124 | this.on('move', () => { 125 | logger.debug('AppWindow#move'); 126 | }); 127 | 128 | /** 129 | * @listens Electron.BrowserWindow#resize 130 | */ 131 | this.on('resize', () => { 132 | logger.debug('AppWindow#resize'); 133 | }); 134 | 135 | /** 136 | * @listens Electron~WebContents#will-navigate 137 | */ 138 | this.webContents.on('will-navigate', (event, url) => { 139 | logger.debug('AppWindow.webContents#will-navigate'); 140 | 141 | event.preventDefault(); 142 | if (url) { 143 | shell.openExternal(url); 144 | } 145 | }); 146 | 147 | /** 148 | * @listens Electron~WebContents#dom-ready 149 | */ 150 | this.webContents.on('dom-ready', () => { 151 | logger.debug('AppWindow.webContents#dom-ready'); 152 | }); 153 | 154 | this.loadURL(windowUrl); 155 | 156 | return this; 157 | } 158 | } 159 | 160 | 161 | /** 162 | * Create instance 163 | */ 164 | let create = () => { 165 | logger.debug('create'); 166 | 167 | if (!(appWindow instanceof AppWindow)) { 168 | appWindow = new AppWindow(); 169 | } 170 | }; 171 | 172 | 173 | /** 174 | * @listens Electron.App#on 175 | */ 176 | app.on('activate', () => { 177 | logger.debug('app#activate'); 178 | 179 | appWindow.show(); 180 | }); 181 | 182 | /** 183 | * @listens Electron.App#on 184 | */ 185 | app.once('ready', () => { 186 | logger.debug('app#ready'); 187 | 188 | create(); 189 | }); 190 | 191 | 192 | /** 193 | * @exports 194 | */ 195 | module.exports = appWindow; 196 | -------------------------------------------------------------------------------- /app/scripts/renderer/utils/dom-helper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /** 5 | * Modules 6 | * Node 7 | * @constant 8 | */ 9 | const fs = require('fs'); 10 | const path = require('path'); 11 | 12 | /** 13 | * Modules 14 | * External 15 | * @constant 16 | */ 17 | const appRootPath = require('app-root-path')['path']; 18 | const fileUrl = require('file-url'); 19 | 20 | /** 21 | * Modules 22 | * Internal 23 | * @constant 24 | */ 25 | const language = require(path.join(appRootPath, 'app', 'scripts', 'renderer', 'utils', 'language')); 26 | const logger = require(path.join(appRootPath, 'lib', 'logger'))({ write: true }); 27 | 28 | 29 | /** 30 | * Add platform name as class to elements 31 | * @param {String=} element - Element (Default: ) 32 | * @function 33 | * 34 | * @public 35 | */ 36 | let addPlatformClass = (element) => { 37 | let elementName = element || 'html'; 38 | let elementTarget = document.querySelector(elementName); 39 | 40 | // Add nodejs platform name 41 | elementTarget.classList.add(process.platform); 42 | 43 | // Add readable platform name 44 | switch (process.platform) { 45 | case 'darwin': 46 | elementTarget.classList.add('macos'); 47 | elementTarget.classList.add('osx'); 48 | break; 49 | case 'win32': 50 | elementTarget.classList.add('windows'); 51 | elementTarget.classList.add('win'); 52 | break; 53 | case 'linux': 54 | elementTarget.classList.add('unix'); 55 | break; 56 | } 57 | }; 58 | 59 | /** 60 | * Check if Object is an HTML Element 61 | * @param {*} object - Object 62 | * @returns {Boolean} - Type 63 | * @function 64 | * 65 | * @public 66 | */ 67 | let isHtmlElement = (object) => { 68 | return language.getPrototypes(object).indexOf('HTMLElement') === 1; 69 | }; 70 | 71 | /** 72 | * Load external scripts 73 | * @param {String} filePath - Path to JavaScript 74 | * @function 75 | * 76 | * @public 77 | */ 78 | let loadScript = (filePath) => { 79 | let url = fileUrl(filePath); 80 | 81 | let script = document.createElement('script'); 82 | script.src = url; 83 | script.type = 'text/javascript'; 84 | 85 | script.onload = () => { 86 | console.debug('dom-helper', 'loadScript', 'complete', url); 87 | }; 88 | 89 | document.getElementsByTagName('head')[0].appendChild(script); 90 | }; 91 | 92 | /** 93 | * Load external stylesheets 94 | * @param {String} filePath - Path to CSS 95 | * @function 96 | * 97 | * @public 98 | */ 99 | let loadStylesheet = (filePath) => { 100 | let url = fileUrl(filePath); 101 | 102 | let link = document.createElement('link'); 103 | link.href = url; 104 | link.type = 'text/css'; 105 | link.rel = 'stylesheet'; 106 | 107 | link.onload = () => { 108 | console.debug('dom-helper', 'loadStylesheet', 'complete', url); 109 | }; 110 | 111 | document.getElementsByTagName('head')[0].appendChild(link); 112 | }; 113 | 114 | /** 115 | * Set element text content 116 | * @param {HTMLElement} element - Element 117 | * @param {String} text - Text 118 | * @param {Number=} delay - Delay 119 | * @function 120 | * 121 | * @public 122 | */ 123 | let setText = (element, text = '', delay = 0) => { 124 | let timeout = setTimeout(() => { 125 | element.innerText = text; 126 | clearTimeout(timeout); 127 | }, delay); 128 | }; 129 | 130 | /** 131 | * Set element visibility 132 | * @param {HTMLElement} element - Element 133 | * @param {Boolean} visible - Show or hide 134 | * @param {Number=} delay - Delay 135 | * @function 136 | * 137 | * @public 138 | */ 139 | let setVisibility = (element, visible, delay = 0) => { 140 | let timeout = setTimeout(() => { 141 | if (visible) { 142 | element.classList.add('show'); 143 | element.classList.remove('hide'); 144 | } else { 145 | element.classList.add('hide'); 146 | element.classList.remove('show'); 147 | } 148 | clearTimeout(timeout); 149 | }, delay); 150 | }; 151 | 152 | /** 153 | * Inject CSS 154 | * @param {Electron.WebViewElement|HTMLElement|Electron.WebContents} webview - Electron Webview 155 | * @param {String} filepath - Stylesheet filepath 156 | * @param {Function=} callback - Callback Function 157 | */ 158 | let injectCSS = (webview, filepath, callback = () => {}) => { 159 | //logger.debug('injectStylesheet'); 160 | 161 | fs.readFile(filepath, (err, data) => { 162 | if (err) { 163 | logger.error('injectStylesheet', err); 164 | return callback(err); 165 | } 166 | 167 | webview.insertCSS(data.toString()); 168 | 169 | callback(null, filepath); 170 | }); 171 | }; 172 | 173 | /** 174 | * Adds #removeEventListener to Events 175 | */ 176 | EventTarget.prototype.addEventListenerBase = EventTarget.prototype.addEventListener; 177 | EventTarget.prototype.addEventListener = function(type, listener) { 178 | if (!this.EventList) { this.EventList = []; } 179 | this.addEventListenerBase.apply(this, arguments); 180 | if (!this.EventList[type]) { this.EventList[type] = []; } 181 | const list = this.EventList[type]; 182 | for (let index = 0; index !== list.length; index++) { 183 | if (list[index] === listener) { return; } 184 | } 185 | list.push(listener); 186 | }; 187 | EventTarget.prototype.removeEventListenerBase = EventTarget.prototype.removeEventListener; 188 | EventTarget.prototype.removeEventListener = function(type, listener) { 189 | if (!this.EventList) { this.EventList = []; } 190 | if (listener instanceof Function) { this.removeEventListenerBase.apply(this, arguments); } 191 | if (!this.EventList[type]) { return; } 192 | let list = this.EventList[type]; 193 | for (let index = 0; index !== list.length;) { 194 | const item = list[index]; 195 | if (!listener) { 196 | this.removeEventListenerBase(type, item); 197 | list.splice(index, 1); 198 | continue; 199 | } else if (item === listener) { 200 | list.splice(index, 1); 201 | break; 202 | } 203 | index++; 204 | } 205 | if (list.length === 0) { delete this.EventList[type]; } 206 | }; 207 | 208 | 209 | /** 210 | * @exports 211 | */ 212 | module.exports = { 213 | addPlatformClass: addPlatformClass, 214 | injectCSS: injectCSS, 215 | isHtmlElement: isHtmlElement, 216 | loadScript: loadScript, 217 | loadStylesheet: loadStylesheet, 218 | setText: setText, 219 | setVisibility: setVisibility 220 | }; 221 | -------------------------------------------------------------------------------- /app/scripts/renderer/utils/language.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /** 5 | * Modules 6 | * Node 7 | * @constant 8 | */ 9 | const path = require('path'); 10 | 11 | /** 12 | * Modules 13 | * External 14 | * @constant 15 | */ 16 | const appRootPath = require('app-root-path')['path']; 17 | 18 | /** 19 | * Modules 20 | * Internal 21 | * @constant 22 | */ 23 | const isDebug = require(path.join(appRootPath, 'lib', 'is-env'))('debug'); 24 | 25 | 26 | /** 27 | * List Event Handlers 28 | * @param {HTMLElement} target - Target Element 29 | * @return {Array|undefined} - List Event Handlers 30 | * @function 31 | * 32 | * @public 33 | */ 34 | let getEventHandlersList = (target) => { 35 | if (!isDebug || !window.chrome) { return; } 36 | 37 | //noinspection JSUnresolvedFunction,JSHint 38 | return getEventListeners(target); 39 | }; 40 | 41 | /** 42 | * Get Prototype chain 43 | * @param {*} object - Variable 44 | * @returns {Array} - List of prototypes names 45 | * @function 46 | * 47 | * @public 48 | */ 49 | let getPrototypeList = (object) => { 50 | let prototypeList = []; 51 | let parent = object; 52 | 53 | while (true) { 54 | parent = Object.getPrototypeOf(parent); 55 | if (parent === null) { 56 | break; 57 | } 58 | prototypeList.push(Object.prototype.toString.call(parent).match(/^\[object\s(.*)]$/)[1]); 59 | } 60 | return prototypeList; 61 | }; 62 | 63 | /** 64 | * Get root Prototype 65 | * @param {*} object - Variable 66 | * @returns {String} - Type 67 | * @function 68 | * 69 | * @public 70 | */ 71 | let getPrototype = (object) => { 72 | return getPrototypeList(object)[0]; 73 | }; 74 | 75 | 76 | /** 77 | * @exports 78 | */ 79 | module.exports = { 80 | getEventHandlers: getEventHandlersList, 81 | getPrototype: getPrototype, 82 | getPrototypes: getPrototypeList 83 | }; 84 | 85 | -------------------------------------------------------------------------------- /app/scripts/renderer/webview/player.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /** 5 | * Modules 6 | * Node 7 | * @constant 8 | */ 9 | const path = require('path'); 10 | 11 | /** 12 | * Modules 13 | * Electron 14 | * @constant 15 | */ 16 | const electron = require('electron'); 17 | const { ipcRenderer } = electron; 18 | 19 | /** 20 | * Modules 21 | * External 22 | * @constant 23 | */ 24 | const appRootPath = require('app-root-path')['path']; 25 | 26 | /** 27 | * Modules 28 | * Internal 29 | * @constant 30 | */ 31 | const logger = require(path.join(appRootPath, 'lib', 'logger'))({ write: true }); 32 | 33 | /** 34 | * jQuery 35 | */ 36 | let jQuery; 37 | 38 | /** 39 | * @constant 40 | * @default 41 | */ 42 | const defaultInterval = 2000; 43 | 44 | /** 45 | * Leanback Volume Control 46 | * @constant 47 | * @default 48 | */ 49 | const volumeControlWidth = 100; 50 | const steps = 10; 51 | let videoElement; 52 | let currentVolume; 53 | let muteButton; 54 | let volumes = []; 55 | 56 | /** 57 | * @default 58 | */ 59 | let playerType; 60 | 61 | /** 62 | * @returns {String} - tv, embed 63 | */ 64 | let getPlayerType = () => { 65 | logger.debug('getPlayerType'); 66 | 67 | return location.pathname.split('/')[1]; 68 | }; 69 | 70 | /** 71 | * Creates modified