├── .github └── FUNDING.yml ├── .gitignore ├── README.md ├── arts ├── logo-45.png └── media.jpg ├── config.js ├── docs └── index.html ├── icons ├── logo-icon-black-and-white-inverse │ └── icon.iconset │ │ ├── Icon │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x.png ├── logo-icon-black-and-white │ └── icon.iconset │ │ ├── Icon │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x.png ├── logo-icon.icns └── logo-icon.png ├── index.html ├── main.js ├── package-lock.json ├── package.json ├── privacy.md ├── src ├── app.js ├── globalShortcuts.js ├── preferences.js ├── ui-controls.js ├── urlHandler.js └── wvHelper.js └── views └── preferences.html /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: https://www.paypal.me/EDanchenkov 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | MenuTube-darwin-x64 4 | vendor 5 | *.lock 6 | .bundle 7 | docs/_site 8 | 9 | .idea 10 | builds/ 11 | dist 12 | entitlements 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![image](./arts/logo-45.png) MenuTube for macOS 2 | 3 | [Download](https://github.com/edanchenkov/MenuTube/releases) | [Website](http://menutube.rednuclearmonkey.com/) | [Red Nuclear Monkey](https://rednuclearmonkey.com/) 4 | 5 | ![image](./arts/media.jpg) 6 | 7 | Do you enjoy listening to YouTube's podcasts, audiobooks, interviews or anything else that doesn't require to focus on video? If yes, then MenuTube is for you! Put entire full functional YouTube website into your macOs's menu bar. 8 | 9 | Features: 10 | 11 | - Browse mobile YouTube version 12 | - Watch or listen to bazillion hours of content 13 | - Control media playback using keys on your keyboard! 14 | - Intuitive and simple UI 15 | - And more 16 | 17 | # Installation 18 | 19 | [Download](https://github.com/edanchenkov/MenuTube/releases) 20 | 21 | ### Build macOS application (darwin) 22 | 23 | ```bash 24 | npm i 25 | npm run dist 26 | ``` 27 | 28 | #### Run dev 29 | 30 | ```bash 31 | npm start 32 | ``` 33 | -------------------------------------------------------------------------------- /arts/logo-45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/arts/logo-45.png -------------------------------------------------------------------------------- /arts/media.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/arts/media.jpg -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | var Config = require("electron-config"); 2 | 3 | var instance, 4 | path = ""; 5 | 6 | if (typeof __dirname !== "undefined") { 7 | path = __dirname; 8 | } 9 | 10 | // PATH TO CONFIG: 11 | // /Users//Library/Application\ Support/MenuTube/ 12 | 13 | /* 14 | * Configs that should be saved locally must be listed here, 15 | * also settings that user can implicitly change 16 | * NOT VALID ANYMORE - NO LOCAL SETTINGS NO PREFERENCES PAGE! 17 | * */ 18 | var userPreferences = { 19 | adBlock: false, 20 | 21 | alwaysOnTop: true, 22 | windowResize: true, 23 | windowDraggable: true, 24 | windowPosition: "trayCenter", 25 | globalShortcuts: true, 26 | PIPModeByDefault: false, 27 | highlightTray: true, 28 | rememberBounds: true, 29 | theme: "red-theme", 30 | desktopMode: false, 31 | }; 32 | 33 | var defaults = { 34 | showOnRightClick: false, 35 | userAgent: 36 | "MMozilla/5.0 (Linux; Android 9; SM-G960F Build/PPR1.180610.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/74.0.3729.157 Mobile Safari/537.36", 37 | desktopUserAgent: 38 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36", 39 | externalLinks: false, 40 | icon: path + "/icons/logo-icon-black-and-white/icon.iconset/icon_16x16.png", 41 | iconPressed: 42 | path + 43 | "/icons/logo-icon-black-and-white-inverse/icon.iconset/icon_16x16.png", 44 | preloadWindow: true, 45 | showDockIcon: false, 46 | showOnAllWorkspaces: false, 47 | browserWindow: { 48 | width: 500, 49 | height: 500, 50 | webPreferences: { 51 | nodeIntegration: true, 52 | enableRemoteModule: true, 53 | webviewTag: true, 54 | }, 55 | }, 56 | }; 57 | 58 | function AppConfig() { 59 | var config = new Config(); 60 | 61 | if (typeof config.store === "object") { 62 | /* 63 | * Ignore all local configs from now on 64 | * */ 65 | // This is another hack 66 | userPreferences.adBlock = config.store.adBlock; 67 | 68 | config.set(Object.assign(userPreferences, {})); 69 | } 70 | 71 | this.config = config; 72 | 73 | Object.defineProperty(this, "store", { 74 | get: function () { 75 | // Small hack to avoid issues with versioning and changes to preferences page 76 | defaults.browserWindow.alwaysOnTop = userPreferences.alwaysOnTop; 77 | return { 78 | all: Object.assign(defaults, userPreferences), 79 | userPreferences: userPreferences, 80 | defaults: defaults, 81 | }; 82 | }, 83 | }); 84 | } 85 | 86 | AppConfig.prototype = { 87 | update: function (config) { 88 | // This is another hack 89 | this.config.set(config); 90 | }, 91 | }; 92 | 93 | var getInstance = function () { 94 | instance = instance || new AppConfig(); 95 | return instance; 96 | }; 97 | 98 | module.exports = getInstance(); 99 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Redirecting to https://menutube.rednuclearmonkey.com/ 4 | 8 | 9 | -------------------------------------------------------------------------------- /icons/logo-icon-black-and-white-inverse/icon.iconset/Icon : -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/icons/logo-icon-black-and-white-inverse/icon.iconset/Icon -------------------------------------------------------------------------------- /icons/logo-icon-black-and-white-inverse/icon.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/icons/logo-icon-black-and-white-inverse/icon.iconset/icon_128x128.png -------------------------------------------------------------------------------- /icons/logo-icon-black-and-white-inverse/icon.iconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/icons/logo-icon-black-and-white-inverse/icon.iconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /icons/logo-icon-black-and-white-inverse/icon.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/icons/logo-icon-black-and-white-inverse/icon.iconset/icon_16x16.png -------------------------------------------------------------------------------- /icons/logo-icon-black-and-white-inverse/icon.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/icons/logo-icon-black-and-white-inverse/icon.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /icons/logo-icon-black-and-white-inverse/icon.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/icons/logo-icon-black-and-white-inverse/icon.iconset/icon_256x256.png -------------------------------------------------------------------------------- /icons/logo-icon-black-and-white-inverse/icon.iconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/icons/logo-icon-black-and-white-inverse/icon.iconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /icons/logo-icon-black-and-white-inverse/icon.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/icons/logo-icon-black-and-white-inverse/icon.iconset/icon_32x32.png -------------------------------------------------------------------------------- /icons/logo-icon-black-and-white-inverse/icon.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/icons/logo-icon-black-and-white-inverse/icon.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /icons/logo-icon-black-and-white-inverse/icon.iconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/icons/logo-icon-black-and-white-inverse/icon.iconset/icon_512x512.png -------------------------------------------------------------------------------- /icons/logo-icon-black-and-white-inverse/icon.iconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/icons/logo-icon-black-and-white-inverse/icon.iconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /icons/logo-icon-black-and-white/icon.iconset/Icon : -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/icons/logo-icon-black-and-white/icon.iconset/Icon -------------------------------------------------------------------------------- /icons/logo-icon-black-and-white/icon.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/icons/logo-icon-black-and-white/icon.iconset/icon_128x128.png -------------------------------------------------------------------------------- /icons/logo-icon-black-and-white/icon.iconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/icons/logo-icon-black-and-white/icon.iconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /icons/logo-icon-black-and-white/icon.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/icons/logo-icon-black-and-white/icon.iconset/icon_16x16.png -------------------------------------------------------------------------------- /icons/logo-icon-black-and-white/icon.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/icons/logo-icon-black-and-white/icon.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /icons/logo-icon-black-and-white/icon.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/icons/logo-icon-black-and-white/icon.iconset/icon_256x256.png -------------------------------------------------------------------------------- /icons/logo-icon-black-and-white/icon.iconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/icons/logo-icon-black-and-white/icon.iconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /icons/logo-icon-black-and-white/icon.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/icons/logo-icon-black-and-white/icon.iconset/icon_32x32.png -------------------------------------------------------------------------------- /icons/logo-icon-black-and-white/icon.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/icons/logo-icon-black-and-white/icon.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /icons/logo-icon-black-and-white/icon.iconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/icons/logo-icon-black-and-white/icon.iconset/icon_512x512.png -------------------------------------------------------------------------------- /icons/logo-icon-black-and-white/icon.iconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/icons/logo-icon-black-and-white/icon.iconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /icons/logo-icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/icons/logo-icon.icns -------------------------------------------------------------------------------- /icons/logo-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edanchenkov/MenuTube/e1fea3785cb0a9c552dde54aa837a486d704f28b/icons/logo-icon.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MenuTube 6 | 11 | 16 | 203 | 204 | 205 | 206 |
207 | Drag to move or double click to exit PIP mode 208 |
209 |
210 | 236 | 237 |
238 |

239 | 240 | 241 | 242 | 243 | 244 |

245 |
246 |
247 |
248 | 249 |
250 | 251 | 278 | 279 | 280 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | var electron = require("electron"); 2 | var Analytics = require("electron-google-analytics"); 3 | 4 | const { menubar } = require("menubar"); 5 | var ipcMain = require("electron").ipcMain; 6 | 7 | var AppConfig = require("./config.js"); 8 | var config = AppConfig.store.all; 9 | 10 | var mb = menubar(Object.assign(AppConfig.store.defaults)); 11 | 12 | var accelerators = [ 13 | "Cmd+Ctrl+Left", 14 | "Cmd+Ctrl+Right", 15 | "Cmd+Ctrl+j", 16 | "Cmd+Ctrl+l", 17 | "Cmd+Ctrl+y", 18 | ]; 19 | 20 | var defaultMenu = [ 21 | { 22 | label: "Edit", 23 | submenu: [ 24 | { 25 | label: "Undo", 26 | accelerator: "CmdOrCtrl+Z", 27 | role: "undo", 28 | }, 29 | { 30 | label: "Redo", 31 | accelerator: "Shift+CmdOrCtrl+Z", 32 | role: "redo", 33 | }, 34 | { 35 | type: "separator", 36 | }, 37 | { 38 | label: "Cut", 39 | accelerator: "CmdOrCtrl+X", 40 | role: "cut", 41 | }, 42 | { 43 | label: "Copy", 44 | accelerator: "CmdOrCtrl+C", 45 | role: "copy", 46 | }, 47 | { 48 | label: "Paste", 49 | accelerator: "CmdOrCtrl+V", 50 | role: "paste", 51 | }, 52 | { 53 | label: "Select All", 54 | accelerator: "CmdOrCtrl+A", 55 | role: "selectall", 56 | }, 57 | ], 58 | }, 59 | { 60 | label: "View", 61 | submenu: [ 62 | { 63 | label: "Reload", 64 | accelerator: "CmdOrCtrl+R", 65 | click: function (item, focusedWindow) { 66 | if (focusedWindow) focusedWindow.reload(); 67 | }, 68 | }, 69 | { 70 | label: "Toggle Developer Tools", 71 | accelerator: (function () { 72 | if (process.platform === "darwin") return "Alt+Command+I"; 73 | else return "Ctrl+Shift+I"; 74 | })(), 75 | click: function (item, focusedWindow) { 76 | if (focusedWindow) focusedWindow.toggleDevTools(); 77 | }, 78 | }, 79 | { 80 | label: "Quit", 81 | accelerator: "Command+Q", 82 | click: function () { 83 | mb && mb.app && mb.app.quit(); 84 | }, 85 | }, 86 | ], 87 | }, 88 | ]; 89 | 90 | mb.on("ready", function ready() { 91 | console.info("App version", mb.app.getVersion()); 92 | console.info("Main process is ready, continue..."); 93 | console.info("Debug:", !!process.env.npm_config_debug); 94 | 95 | const analytics = new Analytics.default("UA-92232645-1"); 96 | 97 | analytics.set("appName", "MenuTube"); 98 | analytics.set("appVersion", mb.app.getVersion()); 99 | 100 | analytics 101 | .event("AppCycle", "OnReady", { 102 | evLabel: "appVersion", 103 | evValue: mb.app.getVersion(), 104 | }) 105 | .then((data) => { 106 | return ipcMain.on("navigatedPage", function (event, e) { 107 | analytics.pageview(e.host, e.url, e.title, data.clientID).then(() => { 108 | console.log("Page view event sent: " + e.url); 109 | }); 110 | }); 111 | }) 112 | .catch(console.info); 113 | 114 | /* 115 | * Set app menu to be able to use copy and paste shortcuts 116 | * */ 117 | var Menu = electron.Menu; 118 | Menu.setApplicationMenu(Menu.buildFromTemplate(defaultMenu)); 119 | 120 | //* 121 | // Hide from dock and finder 122 | // */ 123 | if (!process.env.npm_config_debug) { 124 | mb.app.dock.hide(); 125 | } 126 | 127 | var globalShortcut = electron.globalShortcut; 128 | 129 | var registerGlobalShortcuts = function () { 130 | var shortcutsHandler = function (accelerator) { 131 | mb.window.webContents.send("global-shortcut", { 132 | accelerator: accelerator, 133 | }); 134 | }; 135 | 136 | for (var i = 0; i < accelerators.length; i++) { 137 | var a = accelerators[i]; 138 | if (!globalShortcut.isRegistered(a)) { 139 | globalShortcut.register(a, shortcutsHandler.bind(globalShortcut, a)); 140 | } 141 | } 142 | }; 143 | 144 | var toggleWindow = function () { 145 | if (mb.window.isVisible()) { 146 | mb.hideWindow(); 147 | } else { 148 | mb.showWindow(); 149 | } 150 | }; 151 | 152 | ipcMain.on("updatePreferences", function (e, config) { 153 | for (var key in config) { 154 | if (config.hasOwnProperty(key)) { 155 | mb.setOption(key, config[key]); 156 | } 157 | } 158 | 159 | mb.window.webContents.send("on-preference-change", { theme: config.theme }); 160 | 161 | AppConfig.update(config); 162 | }); 163 | 164 | ipcMain.on("toggleWindow", function () { 165 | toggleWindow(); 166 | }); 167 | 168 | mb.tray.on("right-click", toggleWindow); 169 | 170 | registerGlobalShortcuts(); 171 | }); 172 | 173 | mb.on("after-create-window", function () { 174 | mb.window.setResizable(config.windowResize); 175 | mb.window.setMinimumSize( 176 | config.browserWindow.width, 177 | config.browserWindow.height 178 | ); 179 | }); 180 | 181 | var bounds; 182 | mb.on("after-show", function () { 183 | /* Skip first show */ 184 | if (typeof bounds !== "undefined") { 185 | mb.window.setBounds(bounds); 186 | } else { 187 | if (config.rememberBounds && typeof config.bounds !== "undefined") { 188 | mb.window.setBounds(config.bounds); 189 | } 190 | } 191 | 192 | if (config.highlightTray) { 193 | mb.tray.setImage(AppConfig.store.defaults.iconPressed); 194 | } else { 195 | mb.tray.setHighlightMode("never"); 196 | } 197 | }); 198 | 199 | mb.on("after-hide", function () { 200 | bounds = mb.window.getBounds(); 201 | mb.tray.setImage(AppConfig.store.defaults.icon); 202 | }); 203 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MenuTube", 3 | "version": "1.7.4", 4 | "description": "Catch YouTube into your macOS menu bar!", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "DEBUG=true electron .", 9 | "dist": "electron-builder", 10 | "dist:debug": "DEBUG=electron-builder electron-builder --dir", 11 | "prettier": "prettier --write ." 12 | }, 13 | "build": { 14 | "productName": "MenuTube", 15 | "appId": "com.rednuclearmonkey.menutube", 16 | "icon": "./icons/logo-icon.icns", 17 | "mac": { 18 | "target": [ 19 | "dmg" 20 | ], 21 | "gatekeeperAssess": false, 22 | "asarUnpack": [], 23 | "type": "distribution", 24 | "hardenedRuntime": false 25 | } 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/edanchenkov/MenuTube.git" 30 | }, 31 | "author": "", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/edanchenkov/MenuTube/issues" 35 | }, 36 | "homepage": "https://github.com/edanchenkov/MenuTube#readme", 37 | "dependencies": { 38 | "@exponent/electron-cookies": "^2.0.0", 39 | "bulma": "^0.3.2", 40 | "electron-config": "^0.2.1", 41 | "font-awesome": "^4.7.0", 42 | "electron-google-analytics": "^1.0.2", 43 | "menubar": "^9.0.1" 44 | }, 45 | "devDependencies": { 46 | "electron": "^11.5.0", 47 | "electron-builder": "^22.4.1", 48 | "electron-packager": "^14.2.1", 49 | "jshint": "^2.9.5", 50 | "prettier": "^2.5.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /privacy.md: -------------------------------------------------------------------------------- 1 | # Privacy policy 2 | 3 | This privacy policy ("policy") will help you understand how MenuTube uses and protects the data while you are using MenuTube. 4 | 5 | We reserve the right to change this policy at any given time. If you want to make sure that you are up to date with the latest changes, we advise you to frequently visit this page. 6 | 7 | ## What User Data We Collect 8 | 9 | When you use MenuTube, we don't specifically collect your data. However we use Google Analytics. This way we might collect following information: 10 | 11 | - Data profile regarding your online behavior using MenuTube. Not directly assiciated with a user. 12 | 13 | We are collecting your data for several reasons: 14 | 15 | - To better understand your needs. 16 | - To improve our services and products. 17 | - To customize MenuTube according to your online behavior and personal preferences. 18 | 19 | We do not collect any personal information or track specific activity. 20 | We do not sell or distribute any of your data. 21 | 22 | We do care about privacy! If you have any concerns about it, please contant use hello@rednuclearmonkey.com. 23 | 24 | ## Safeguarding and Securing the Data 25 | 26 | MenuTube is committed to securing your data and keeping it confidential. MenuTube has done all in its power to prevent data theft, unauthorized access, and disclosure by implementing the latest technologies and software, which help us safeguard all the information we collect online. 27 | 28 | ## Links to Other Websites 29 | 30 | MenuTube contains links that lead to other websites. MenuTube is not held responsible for your data and privacy protection. Visiting those websites is not governed by this privacy policy agreement. Make sure to read the privacy policy documentation of the website you go to from our website. 31 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | exports.continueInit = function (wv, controls) { 2 | console.info("Main process is initialised and seems to work"); 3 | 4 | var AppConfig = require("./../config.js"); 5 | var ipcRenderer = require("electron").ipcRenderer; 6 | 7 | var config = AppConfig.store; 8 | 9 | document.body.classList.add(config.userPreferences.theme); 10 | ipcRenderer.on("on-preference-change", function (e, data) { 11 | var classList = document.body.className.split(" "); 12 | for (var i = 0; i < classList.length; i++) { 13 | var className = classList[i]; 14 | if (className && className.indexOf("-theme") >= 0) { 15 | document.body.classList.remove(className); 16 | } 17 | } 18 | document.body.classList.add(data.theme); 19 | }); 20 | 21 | if ( 22 | config.userPreferences.windowDraggable && 23 | typeof document.body !== "undefined" 24 | ) { 25 | document.body.classList.add("draggable"); 26 | setTimeout(function () { 27 | document.body.style.width = "100%"; 28 | document.body.style.height = "100%"; 29 | }, 100); 30 | } 31 | 32 | if (config.userPreferences.PIPModeByDefault) { 33 | document.body.classList.add("PIP-mode"); 34 | } 35 | 36 | if (typeof wv !== "undefined" && typeof wv.loadURL === "function") { 37 | var globalShortcuts = require("./globalShortcuts.js"); 38 | var uiControls = require("./ui-controls.js"); 39 | var urlHandler = require("./urlHandler.js"); 40 | 41 | var options = { userAgent: config.defaults.userAgent }; 42 | 43 | if (config.userPreferences.desktopMode) { 44 | options.userAgent = config.defaults.desktopUserAgent; 45 | } 46 | 47 | /* This all is not the best approach for handling loading state, 48 | * but it is enough for now 49 | * */ 50 | var ts = Date.now(); 51 | var splashScreen = document.querySelector(".splash-screen"); 52 | var hideSplashScreen = function () { 53 | splashScreen.classList.add("hide"); 54 | setTimeout(function () { 55 | splashScreen.style.display = "none"; 56 | // Must be longer that hide transition, see index.html 57 | }, 600); 58 | }; 59 | 60 | wv.addEventListener("dom-ready", function () { 61 | var diff = Date.now() - ts; 62 | if (diff > 1250) { 63 | hideSplashScreen(); 64 | } else { 65 | hideSplashScreen(); 66 | } 67 | 68 | /* DEBUG wvHelper */ 69 | // wv.openDevTools(); 70 | }); 71 | 72 | wv.loadURL("https://www.youtube.com/", options).then(() => { 73 | urlHandler.init(wv); 74 | globalShortcuts.init(wv); 75 | uiControls.init(wv, controls); 76 | }); 77 | } else { 78 | alert("Something went wrong, cannot create webview..."); 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /src/globalShortcuts.js: -------------------------------------------------------------------------------- 1 | exports.init = function (wv) { 2 | var ipcRenderer = require("electron").ipcRenderer; 3 | ipcRenderer.on("global-shortcut", function (e, data) { 4 | var accelerator = data.accelerator; 5 | 6 | switch (accelerator) { 7 | case "Cmd+Ctrl+Left": 8 | wv.send("changeTime", -5); 9 | break; 10 | case "Cmd+Ctrl+Right": 11 | wv.send("changeTime", 5); 12 | break; 13 | case "Cmd+Ctrl+l": 14 | wv.send("changeTime", 15); 15 | break; 16 | case "Cmd+Ctrl+j": 17 | wv.send("changeTime", -15); 18 | break; 19 | case "Cmd+Ctrl+y": 20 | ipcRenderer.send("toggleWindow"); 21 | break; 22 | } 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /src/preferences.js: -------------------------------------------------------------------------------- 1 | var AppConfig = require("./../config.js"); 2 | var config = AppConfig.store.userPreferences; 3 | var ipcRenderer = require("electron").ipcRenderer; 4 | 5 | /* Must reflect in preference.html and configs */ 6 | var groups = [ 7 | "adBlock", 8 | // 'alwaysOnTop', 9 | // 'windowResize', 10 | // 'windowDraggable', 11 | // 'windowPosition', 12 | // 'externalLinks', 13 | // 'globalShortcuts', 14 | // 'PIPModeByDefault', 15 | // 'highlightTray', 16 | // 'rememberBounds', 17 | // 'theme' 18 | ]; 19 | 20 | var translateValue = function (value) { 21 | if (value === "yes") { 22 | value = true; 23 | } else if (value === "no") { 24 | value = false; 25 | } 26 | return value; 27 | }; 28 | 29 | for (var key in config) { 30 | var value = config[key]; 31 | if (groups.indexOf(key) >= 0) { 32 | var elements = document.querySelectorAll('input[name="' + key + '"]'); 33 | 34 | for (var i = 0; i < elements.length; i++) { 35 | var input = elements[i]; 36 | var inputValue = input.value; 37 | 38 | inputValue = translateValue(inputValue); 39 | input.checked = inputValue === value; 40 | 41 | input.onclick = function (e) { 42 | var target = e.target; 43 | config[target.name] = translateValue(target.value); 44 | }; 45 | } 46 | } 47 | } 48 | 49 | var saveButton = document.getElementById("save-btn"); 50 | saveButton.addEventListener( 51 | "click", 52 | function () { 53 | saveButton.classList.add("is-loading"); 54 | setTimeout(function () { 55 | saveButton.classList.remove("is-loading"); 56 | }, 500); 57 | // config.userAgent = userAgents[selectEl.options[selectEl.options.selectedIndex].value]; 58 | ipcRenderer.send("updatePreferences", config); 59 | }, 60 | false 61 | ); 62 | -------------------------------------------------------------------------------- /src/ui-controls.js: -------------------------------------------------------------------------------- 1 | var remote = require("electron").remote; 2 | var Menu = remote.Menu; 3 | var MenuItem = remote.MenuItem; 4 | var BrowserWindow = remote.BrowserWindow; 5 | var app = remote.app; 6 | var shell = remote.shell; 7 | var ipcRenderer = require("electron").ipcRenderer; 8 | 9 | var AppConfig = require("./../config.js"); 10 | var config = AppConfig.store; 11 | 12 | var urlHandler = require("./urlHandler.js"); 13 | var wv = window.wv; 14 | 15 | var clickHandler = function (name, menu) { 16 | switch (name) { 17 | case "backButton": 18 | if (wv.canGoBack()) { 19 | wv.goBack(); 20 | } 21 | break; 22 | case "refreshButton": 23 | wv.reload(); 24 | break; 25 | case "forwardButton": 26 | if (wv.canGoForward) { 27 | wv.goForward(); 28 | } 29 | break; 30 | case "preferenceButton": 31 | menu.popup(remote.getCurrentWindow()); 32 | break; 33 | case "PIPMode": 34 | document.body.classList.add("PIP-mode"); 35 | wv.send("enterPIPMode"); 36 | break; 37 | case "PIPDragArea": 38 | document.body.classList.remove("PIP-mode"); 39 | wv.reload(); 40 | break; 41 | } 42 | }; 43 | 44 | var dynamicLabel = "Check releases and notes"; 45 | 46 | var defaultMenuItems = [ 47 | { 48 | label: "Open in browser", 49 | click: function () { 50 | hideAndPause(); 51 | shell.openExternal(urlHandler.getCurrentURL()); 52 | }, 53 | role: "help", 54 | }, 55 | { 56 | type: "separator", 57 | }, 58 | { 59 | label: "Preferences", 60 | click: function () { 61 | var win = new BrowserWindow({ 62 | frame: true, 63 | webPreferences: { 64 | nodeIntegration: true, 65 | }, 66 | }); 67 | 68 | hideAndPause(); 69 | var path = app.getAppPath(); 70 | win.loadURL("file://" + path + "/views/preferences.html"); 71 | win.show(); 72 | }, 73 | role: "help", 74 | }, 75 | { 76 | label: dynamicLabel, 77 | click: function () { 78 | hideAndPause(); 79 | shell.openExternal("https://github.com/edanchenkov/MenuTube/releases"); 80 | }, 81 | role: "help", 82 | }, 83 | { 84 | type: "separator", 85 | }, 86 | { 87 | label: "Reload", 88 | click: function () { 89 | if ( 90 | typeof window !== "undefined" && 91 | typeof window.location !== "undefined" && 92 | typeof window.location.reload == "function" 93 | ) { 94 | window.location.reload(); 95 | } 96 | }, 97 | role: "help", 98 | accelerator: "Cmd+R", 99 | }, 100 | { 101 | label: "Quit", 102 | click: function () { 103 | app.quit(); 104 | }, 105 | role: "help", 106 | accelerator: "Cmd+Q", 107 | }, 108 | ]; 109 | 110 | var hideAndPause = function () { 111 | ipcRenderer.send("toggleWindow"); 112 | wv.send("pause"); 113 | }; 114 | 115 | var buildMenu = function (menu, menuItems) { 116 | menu.clear(); 117 | 118 | for (var i = 0; i < menuItems.length; i++) { 119 | var mi = menuItems[i]; 120 | menu.append( 121 | new MenuItem({ 122 | label: mi.label, 123 | click: mi.click, 124 | type: mi.type, 125 | role: mi.role, 126 | accelerator: mi.accelerator, 127 | submenu: mi.submenu, 128 | }) 129 | ); 130 | } 131 | }; 132 | 133 | /* 134 | * This should not be here, but lets make it simple for now 135 | * TODO: Time outs and retries should be handled where this function is called, not within the function 136 | * */ 137 | var attempts = 5; 138 | var checkForUpdate = function (menu, controls) { 139 | var fetch = window.fetch; 140 | 141 | if (typeof fetch !== "function") { 142 | return; 143 | } 144 | 145 | if (attempts > 0) { 146 | attempts--; 147 | setTimeout(function () { 148 | fetch( 149 | "https://api.github.com/repos/edanchenkov/MenuTube/releases/latest" 150 | ).then(function (res) { 151 | if (typeof res !== "undefined" && typeof res.json === "function") { 152 | res.json().then(function (data) { 153 | if ( 154 | typeof data !== "undefined" && 155 | data.hasOwnProperty("tag_name") 156 | ) { 157 | if (data.tag_name !== remote.app.getVersion()) { 158 | var menuItems = defaultMenuItems.map(function (mi) { 159 | /* 160 | * Should check against something else probably, not label 161 | * */ 162 | if (mi.label === dynamicLabel) { 163 | mi.label = "(!) NEW VERSION IS AVAILABLE"; 164 | } 165 | return mi; 166 | }); 167 | 168 | var prefIcon = controls.preferenceButton.querySelector("i.fa"); 169 | 170 | prefIcon.classList.remove("fa-bars"); 171 | prefIcon.classList.add( 172 | "fa-exclamation-circle", 173 | "update-available" 174 | ); 175 | 176 | buildMenu(menu, menuItems); 177 | } 178 | } 179 | }); 180 | } 181 | }, checkForUpdate.bind(this, menu, controls)); 182 | }, 2000); 183 | } 184 | }; 185 | 186 | exports.init = function (wv, controls) { 187 | var menu = new Menu(); 188 | buildMenu(menu, defaultMenuItems); 189 | checkForUpdate(menu, controls); 190 | 191 | for (var c in controls) { 192 | if (controls.hasOwnProperty(c)) { 193 | var el = controls[c]; 194 | if (el && typeof el.addEventListener === "function") { 195 | var event = "click"; 196 | 197 | if (el.classList.contains("PIP-drag-area")) { 198 | event = "dblclick"; 199 | } 200 | 201 | if (c === "desktopModeButton") { 202 | if (config.userPreferences.desktopMode) { 203 | el.classList.add("active"); 204 | } else { 205 | el.classList.remove("active"); 206 | } 207 | } 208 | 209 | el.addEventListener(event, clickHandler.bind(el, c, menu), true); 210 | } 211 | } 212 | } 213 | }; 214 | -------------------------------------------------------------------------------- /src/urlHandler.js: -------------------------------------------------------------------------------- 1 | var urlHandler = {}; 2 | var AppConfig = require("./../config.js"); 3 | var config = AppConfig.store; 4 | 5 | module.exports = { 6 | init: function (wv) { 7 | var remote = require("electron").remote; 8 | var shell = remote.shell; 9 | 10 | var that = this; 11 | 12 | /* 13 | * Everything related to events below is a huge mess. 14 | * Fires multiple times, nothing we can do about it, 15 | * unless Electron js fixed it. 16 | * 17 | * USE CAREFULLY! 18 | * 19 | * */ 20 | 21 | wv.addEventListener("did-navigate-in-page", function (e) { 22 | urlHandler.currentURL = e.url; 23 | 24 | if (that.isVideoURL(e.url)) { 25 | urlHandler.videoHistory = urlHandler.videoHistory || []; 26 | urlHandler.videoHistory.push(e.url); 27 | if (config.userPreferences.PIPModeByDefault) { 28 | wv.send("enterPIPMode"); 29 | } 30 | 31 | wv.send("onDidNavigateVideoPage", e.url); 32 | } 33 | 34 | if (that.isAllowedURL(e.url)) { 35 | urlHandler.fullHistory = urlHandler.fullHistory || []; 36 | urlHandler.fullHistory.push(e.url); 37 | } 38 | }); 39 | 40 | /** 41 | * START 42 | * Hack to prevent user to go to unwanted links. 43 | * Must be (!) replaced with proper solution when 44 | * https://github.com/electron/electron/issues/1378 45 | * gets proper fix 46 | **/ 47 | 48 | wv.addEventListener("did-navigate", function (e) { 49 | if (!that.isAllowedURL(e.url)) { 50 | if (e.url === "about:blank") { 51 | wv.goForward(); 52 | } else { 53 | wv.goBack(); 54 | } 55 | } 56 | }); 57 | 58 | wv.addEventListener("will-navigate", function (e) { 59 | if (!that.isAllowedURL(e.url)) { 60 | shell.openExternal(e.url); 61 | } 62 | }); 63 | 64 | /** 65 | * END 66 | * */ 67 | }, 68 | getCurrentURL: function () { 69 | return urlHandler.currentURL; 70 | }, 71 | isVideoURL: function (url) { 72 | return url.indexOf(".youtube.com/") > -1 && url.indexOf("watch?v=") > -1; 73 | }, 74 | isAllowedURL: function (url) { 75 | return ( 76 | url.indexOf(".youtube.com/") > -1 || 77 | url.indexOf("accounts.google.com") > -1 78 | ); 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /src/wvHelper.js: -------------------------------------------------------------------------------- 1 | const _newListener = document.addEventListener; 2 | 3 | // Hijack event listener to prevent YouTube from stopping on blue window (lose focus) 4 | document.addEventListener = () => { 5 | _newListener.apply(document, arguments); 6 | }; 7 | 8 | (function () { 9 | var AppConfig = require("./../config.js"); 10 | var config = AppConfig.store.userPreferences; 11 | 12 | if (!!config.adBlock) { 13 | var observeDOM = (function () { 14 | var MutationObserver = 15 | window.MutationObserver || window.WebKitMutationObserver; 16 | 17 | return function (obj, callback) { 18 | if (!obj || !obj.nodeType === 1) { 19 | return; 20 | } // validation 21 | 22 | if (MutationObserver) { 23 | // define a new observer 24 | var obs = new MutationObserver(function (mutations, observer) { 25 | callback(mutations); 26 | }); 27 | // have the observer observe foo for changes in children 28 | obs.observe(obj, { childList: true, subtree: true }); 29 | } else if (window.addEventListener) { 30 | obj.addEventListener("DOMNodeInserted", callback, false); 31 | obj.addEventListener("DOMNodeRemoved", callback, false); 32 | } 33 | }; 34 | })(); 35 | 36 | var currentTitle = ""; 37 | 38 | _newListener("DOMContentLoaded", () => { 39 | observeDOM(document.body, function (m) { 40 | // Sending only video urls 41 | if ( 42 | window.location.pathname.includes("watch") && 43 | window.document.title !== currentTitle 44 | ) { 45 | ipcRenderer.send("navigatedPage", { 46 | host: window.location.host, 47 | url: window.location.pathname + window.location.search, 48 | title: window.document.title, 49 | }); 50 | currentTitle = window.document.title; 51 | } 52 | 53 | const _check = (v) => v !== null && v !== undefined; 54 | const ad = [...document.querySelectorAll(".ad-showing")][0]; 55 | if (_check(ad)) { 56 | const video = document.querySelector("video"); 57 | if (_check(video) && !isNaN(video.duration)) { 58 | video.currentTime = video.duration; 59 | } 60 | } 61 | }); 62 | }); 63 | } 64 | 65 | var ipcRenderer = require("electron").ipcRenderer; 66 | var attempts = 1000; 67 | 68 | var setStream = function (video) { 69 | var id = video.id.replace("player_", ""); 70 | var stream = 71 | ''; 74 | document.body.innerHTML = stream; 75 | }; 76 | 77 | ipcRenderer.on("playPause", function () { 78 | var video = document.querySelector("video"); 79 | 80 | if (typeof video === "undefined" || video === null) { 81 | return; 82 | } 83 | 84 | if (video.paused) { 85 | video.play(); 86 | } else { 87 | video.pause(); 88 | } 89 | }); 90 | 91 | ipcRenderer.on("pause", function () { 92 | var video = document.querySelector("video"); 93 | 94 | if (typeof video === "undefined" || video === null) { 95 | return; 96 | } 97 | 98 | if (!video.paused) { 99 | video.pause(); 100 | } 101 | }); 102 | 103 | ipcRenderer.on("changeTime", function (event, time) { 104 | var video = document.querySelector("video"); 105 | 106 | if (typeof video === "undefined" || video === null) { 107 | return; 108 | } 109 | 110 | video.currentTime += time; 111 | }); 112 | 113 | ipcRenderer.on("enterPIPMode", function _retry() { 114 | var video = document.querySelector("video"); 115 | 116 | if (!document.URL.includes("watch?v=")) { 117 | return; 118 | } 119 | 120 | if (typeof video === "undefined" || video === null) { 121 | if (attempts > 0) { 122 | setTimeout(_retry, 100); 123 | attempts--; 124 | } else { 125 | attempts = 1000; 126 | } 127 | return; 128 | } 129 | 130 | if (typeof video !== "undefined") { 131 | document.body.innerHTML = ""; 132 | document.body.style.backgroundColor = "black"; 133 | document.body.className = ""; 134 | video.style.width = "100%"; 135 | video.style.height = "100%"; 136 | video.style.position = "absolute"; 137 | video.style.top = 0; 138 | video.style.left = 0; 139 | video.style.zIndex = 9999; 140 | 141 | document.body.appendChild(video); 142 | 143 | setTimeout(function () { 144 | video.play(); 145 | }, 100); 146 | } 147 | }); 148 | 149 | ipcRenderer.on("onDidNavigateVideoPage", function _retry(event, url) { 150 | var video = document.querySelector("video"); 151 | 152 | if (typeof video === "undefined" || video === null) { 153 | if (attempts > 0) { 154 | setTimeout(_retry, 100); 155 | attempts--; 156 | } else { 157 | attempts = 1000; 158 | } 159 | return; 160 | } 161 | 162 | if (typeof video === "undefined") { 163 | return; 164 | } 165 | 166 | if (video.src.indexOf("m3u8") > -1) { 167 | setStream(video); 168 | } 169 | }); 170 | })(); 171 | -------------------------------------------------------------------------------- /views/preferences.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Controls 6 | 11 | 16 | 26 | 27 | 28 |
29 |
30 |
31 |

Controls

32 |
33 |
34 |
35 | 36 |
37 |
38 |

39 | Disable ads 40 | Require a restart or reload (CMD+R) 43 |

44 |
45 |

46 | 50 | 54 |

55 |
56 |
57 |
58 |

59 | Use media keys and shortcuts on keyboard to manage video/audio 60 |

61 | 94 |
95 |
96 |
97 | Save 98 |
99 | 100 | 114 | 115 | 116 | 117 | --------------------------------------------------------------------------------