├── .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 []() [](http://travis-ci.org/sidneys/youtube-playlist-player) [](https://ci.appveyor.com/project/sidneys/youtube-playlist-player) [](https://npmjs.com/package/youtube-playlist-player) [](https://npmjs.com/package/youtube-playlist-player) [](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 
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 |
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