├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.sh ├── jest.config.js ├── package.json ├── src ├── background.js ├── backgrounds │ ├── backgrounds.css │ ├── japan.svg │ └── topography.svg ├── browser-polyfill.js ├── browser-polyfill.js.map ├── ch_manifest.json ├── contentscript.js ├── defaultSettings.js ├── emoji.js ├── ff_manifest.json ├── icons │ ├── favicon2_144.png │ ├── favicon3_144.png │ ├── favicon4_128.png │ ├── favicon4_144.png │ ├── favicon4_64.png │ ├── favicon_144.png │ ├── logo.png │ ├── logo2.png │ ├── off.svg │ └── on.svg ├── manifest.json ├── options │ ├── options.css │ ├── options.html │ ├── options.js │ └── select.js ├── promo │ └── ZYT.png ├── safari │ ├── Shared (App) │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── mac-icon-128@1x.png │ │ │ │ ├── mac-icon-128@2x.png │ │ │ │ ├── mac-icon-16@1x.png │ │ │ │ ├── mac-icon-16@2x.png │ │ │ │ ├── mac-icon-256@1x.png │ │ │ │ ├── mac-icon-256@2x.png │ │ │ │ ├── mac-icon-32@1x.png │ │ │ │ ├── mac-icon-32@2x.png │ │ │ │ ├── mac-icon-512@1x.png │ │ │ │ ├── mac-icon-512@2x.png │ │ │ │ └── universal-icon-1024@1x.png │ │ │ ├── Contents.json │ │ │ └── LargeIcon.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── favicon4_144.png │ │ ├── Base.lproj │ │ │ └── Main.html │ │ ├── Resources │ │ │ ├── Icon.png │ │ │ ├── Script.js │ │ │ └── Style.css │ │ └── ViewController.swift │ ├── Shared (Extension) │ │ └── SafariWebExtensionHandler.swift │ ├── ZenTube.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ ├── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ └── xcuserdata │ │ │ │ └── admin.xcuserdatad │ │ │ │ └── UserInterfaceState.xcuserstate │ │ └── xcuserdata │ │ │ └── admin.xcuserdatad │ │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ ├── ZenTube │ │ ├── Shared (App) │ │ │ ├── Assets.xcassets │ │ │ │ ├── AccentColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── mac-icon-128@1x.png │ │ │ │ │ ├── mac-icon-128@2x.png │ │ │ │ │ ├── mac-icon-16@1x.png │ │ │ │ │ ├── mac-icon-16@2x.png │ │ │ │ │ ├── mac-icon-256@1x.png │ │ │ │ │ ├── mac-icon-256@2x.png │ │ │ │ │ ├── mac-icon-32@1x.png │ │ │ │ │ ├── mac-icon-32@2x.png │ │ │ │ │ ├── mac-icon-512@1x.png │ │ │ │ │ ├── mac-icon-512@2x.png │ │ │ │ │ └── universal-icon-1024@1x.png │ │ │ │ ├── Contents.json │ │ │ │ └── LargeIcon.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── favicon4_144.png │ │ │ ├── Base.lproj │ │ │ │ └── Main.html │ │ │ ├── Resources │ │ │ │ ├── Icon.png │ │ │ │ ├── Script.js │ │ │ │ └── Style.css │ │ │ └── ViewController.swift │ │ ├── Shared (Extension) │ │ │ └── SafariWebExtensionHandler.swift │ │ ├── ZenTube.xcodeproj │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ ├── xcshareddata │ │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ │ └── xcuserdata │ │ │ │ │ └── admin.xcuserdatad │ │ │ │ │ └── UserInterfaceState.xcuserstate │ │ │ └── xcuserdata │ │ │ │ └── admin.xcuserdatad │ │ │ │ └── xcschemes │ │ │ │ └── xcschememanagement.plist │ │ ├── iOS (App) │ │ │ ├── AppDelegate.swift │ │ │ ├── Base.lproj │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ └── Main.storyboard │ │ │ ├── Info.plist │ │ │ └── SceneDelegate.swift │ │ ├── iOS (Extension) │ │ │ └── Info.plist │ │ ├── macOS (App) │ │ │ ├── AppDelegate.swift │ │ │ ├── Base.lproj │ │ │ │ └── Main.storyboard │ │ │ ├── Info.plist │ │ │ └── ZenTube.entitlements │ │ └── macOS (Extension) │ │ │ ├── Info.plist │ │ │ └── ZenTube.entitlements │ ├── build │ │ └── sa_zentube-latest.zip │ ├── iOS (App) │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── SceneDelegate.swift │ ├── iOS (Extension) │ │ └── Info.plist │ ├── macOS (App) │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── ZenTube.entitlements │ └── macOS (Extension) │ │ ├── Info.plist │ │ └── ZenTube.entitlements └── wasm │ ├── emoji.wasm │ └── emoji.wat └── tests ├── bootstrap.js ├── jest.setup.js ├── options.spec.js └── putils.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: inversepolarity 6 | ko_fi: evenzero 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | 107 | .zip 108 | .DS_Store 109 | web-ext-artifacts/ 110 | 111 | build/.DS_Store 112 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 AlexanderClay 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZYT 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | 5 | > [!WARNING] 6 | > Maintainance temporarily halted, functionality restricted. 7 | 8 | 9 | 10 | ### Installation 11 | 12 | [![Safari](https://img.shields.io/badge/Safari-f0f0f0?style=for-the-badge&logo=Safari&logoColor=black)](https://github.com/inversepolarity/ZenTube/raw/main/src/safari/build/sa_zentube-latest.zip) 13 | [![Firefox](https://img.shields.io/badge/Mozilla_Firefox-FF7139?style=for-the-badge&logo=Firefox-Browser&logoColor=white)](https://addons.mozilla.org/en-US/firefox/addon/zentube/) [![Chrome](https://img.shields.io/badge/Google_chrome-4285F4?style=for-the-badge&logo=Google-chrome&logoColor=white)](https://bit.ly/3S3dTvT) [![Edge](https://img.shields.io/badge/Microsoft_Edge-0078D7?style=for-the-badge&logo=Microsoft-edge&logoColor=white)](https://bit.ly/3S3dTvT) [![Brave](https://img.shields.io/badge/Brave-FB542B?style=for-the-badge&logo=Brave&logoColor=white)](https://bit.ly/3S3dTvT) 14 | 15 | --- 16 | 17 | ### Rationale 18 | 19 | - Privacy is a two-way street, draw the blinds on parts of YouTube that intrude upon your senses. 20 | - Make your Youtube experience less addictive. You are NOT the product, especially if the service is free. 21 | - Toggle features on/off based on your needs and boundaries. 22 | - Participate in making YouTube a better product while engaging in a minium amount of electronic civil disobedience. 23 | 24 | 25 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | clear 2 | web-ext build --source-dir=./src --overwrite-dest -i=promo/ safari/ -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | rootDir: "tests", 4 | testTimeout: 50000, 5 | setupFilesAfterEnv: ["./jest.setup.js"], 6 | testEnvironment: "jsdom" 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zyt", 3 | "version": "1.0.0", 4 | "description": "Nirvana for YouTube", 5 | "scripts": { 6 | "build": "web-ext build --source-dir=./src --overwrite-dest -i=promo/ safari/", 7 | "build:watch": "watchman-make -p '**/*.js' '**/*.html' '**/*.json' --run ./build.sh", 8 | "build:safari": "xcrun safari-web-extension-converter --project-location ./src/safari .", 9 | "test": "jest --detectOpenHandles", 10 | "test:watch": "jest --detectOpenHandles --watch" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/inversepolarity/zyt.git" 15 | }, 16 | "keywords": ["chrome-extension","youtube","zen"], 17 | "author": "inversepolarity", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/inversepolarity/zyt/issues" 21 | }, 22 | "homepage": "https://github.com/inversepolarity/zyt#readme", 23 | "devDependencies": { 24 | "jest": "^29.4.3", 25 | "jest-chrome": "^0.8.0", 26 | "jest-environment-jsdom": "^29.4.3", 27 | "puppeteer": "^19.7.2" 28 | }, 29 | "overrides": { 30 | "jest-chrome": { 31 | "jest": "$jest" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | if (typeof browser === "undefined") { 2 | var browser = chrome; 3 | } 4 | 5 | browser.runtime.onInstalled.addListener(async ({ reason }) => { 6 | if ( 7 | reason === "install" || 8 | reason === "update" || 9 | reason === "browser_update" || 10 | reason === "chrome_update" 11 | ) { 12 | /* are any yt tabs open?*/ 13 | let tabs = await browser.tabs.query({ url: "*://*.youtube.com/*" }); 14 | await browser.storage.local.clear(); 15 | 16 | for await (const t of tabs) { 17 | try { 18 | const injection = await browser.scripting.executeScript({ 19 | target: { tabId: t.id }, 20 | files: [ 21 | "browser-polyfill.js", 22 | "defaultSettings.js", 23 | "emoji.js", 24 | "contentscript.js" 25 | ] 26 | }); 27 | } catch (err) { 28 | console.error(`failed to execute script: ${err}`); 29 | } 30 | await browser.tabs.reload(t.id, { bypassCache: true }); 31 | } 32 | } 33 | }); 34 | 35 | // browser.runtime.setUninstallURL("https://evenzero.in"); 36 | -------------------------------------------------------------------------------- /src/backgrounds/backgrounds.css: -------------------------------------------------------------------------------- 1 | a.yt-simple-endpoint.inline-block.style-scope.ytd-thumbnail { 2 | background-color: #111; 3 | opacity:0.88; 4 | box-shadow: 0px 10px 15px -3px rgba(0,0,0,0.5); 5 | background-image: url("./backgrounds/topography.svg"); 6 | } 7 | 8 | yt-formatted-string.style-scope.ytd-rich-grid-media { 9 | border:1px solid #222; 10 | background-color: #111; 11 | border-radius: 5px; 12 | padding: 5px; 13 | font-size: 1em; 14 | color: gray; 15 | } -------------------------------------------------------------------------------- /src/backgrounds/japan.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/browser-polyfill.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | if (typeof define === "function" && define.amd) { 3 | define("webextension-polyfill", ["module"], factory); 4 | } else if (typeof exports !== "undefined") { 5 | factory(module); 6 | } else { 7 | var mod = { 8 | exports: {} 9 | }; 10 | factory(mod); 11 | global.browser = mod.exports; 12 | } 13 | })(typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : this, function (module) { 14 | /* webextension-polyfill - v0.8.0 - Tue Apr 20 2021 11:27:38 */ 15 | 16 | /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ 17 | 18 | /* vim: set sts=2 sw=2 et tw=80: */ 19 | 20 | /* This Source Code Form is subject to the terms of the Mozilla Public 21 | * License, v. 2.0. If a copy of the MPL was not distributed with this 22 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 23 | "use strict"; 24 | 25 | if (typeof browser === "undefined" || Object.getPrototypeOf(browser) !== Object.prototype) { 26 | const CHROME_SEND_MESSAGE_CALLBACK_NO_RESPONSE_MESSAGE = "The message port closed before a response was received."; 27 | const SEND_RESPONSE_DEPRECATION_WARNING = "Returning a Promise is the preferred way to send a reply from an onMessage/onMessageExternal listener, as the sendResponse will be removed from the specs (See https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onMessage)"; // Wrapping the bulk of this polyfill in a one-time-use function is a minor 28 | // optimization for Firefox. Since Spidermonkey does not fully parse the 29 | // contents of a function until the first time it's called, and since it will 30 | // never actually need to be called, this allows the polyfill to be included 31 | // in Firefox nearly for free. 32 | 33 | const wrapAPIs = extensionAPIs => { 34 | // NOTE: apiMetadata is associated to the content of the api-metadata.json file 35 | // at build time by replacing the following "include" with the content of the 36 | // JSON file. 37 | const apiMetadata = { 38 | "alarms": { 39 | "clear": { 40 | "minArgs": 0, 41 | "maxArgs": 1 42 | }, 43 | "clearAll": { 44 | "minArgs": 0, 45 | "maxArgs": 0 46 | }, 47 | "get": { 48 | "minArgs": 0, 49 | "maxArgs": 1 50 | }, 51 | "getAll": { 52 | "minArgs": 0, 53 | "maxArgs": 0 54 | } 55 | }, 56 | "bookmarks": { 57 | "create": { 58 | "minArgs": 1, 59 | "maxArgs": 1 60 | }, 61 | "get": { 62 | "minArgs": 1, 63 | "maxArgs": 1 64 | }, 65 | "getChildren": { 66 | "minArgs": 1, 67 | "maxArgs": 1 68 | }, 69 | "getRecent": { 70 | "minArgs": 1, 71 | "maxArgs": 1 72 | }, 73 | "getSubTree": { 74 | "minArgs": 1, 75 | "maxArgs": 1 76 | }, 77 | "getTree": { 78 | "minArgs": 0, 79 | "maxArgs": 0 80 | }, 81 | "move": { 82 | "minArgs": 2, 83 | "maxArgs": 2 84 | }, 85 | "remove": { 86 | "minArgs": 1, 87 | "maxArgs": 1 88 | }, 89 | "removeTree": { 90 | "minArgs": 1, 91 | "maxArgs": 1 92 | }, 93 | "search": { 94 | "minArgs": 1, 95 | "maxArgs": 1 96 | }, 97 | "update": { 98 | "minArgs": 2, 99 | "maxArgs": 2 100 | } 101 | }, 102 | "browserAction": { 103 | "disable": { 104 | "minArgs": 0, 105 | "maxArgs": 1, 106 | "fallbackToNoCallback": true 107 | }, 108 | "enable": { 109 | "minArgs": 0, 110 | "maxArgs": 1, 111 | "fallbackToNoCallback": true 112 | }, 113 | "getBadgeBackgroundColor": { 114 | "minArgs": 1, 115 | "maxArgs": 1 116 | }, 117 | "getBadgeText": { 118 | "minArgs": 1, 119 | "maxArgs": 1 120 | }, 121 | "getPopup": { 122 | "minArgs": 1, 123 | "maxArgs": 1 124 | }, 125 | "getTitle": { 126 | "minArgs": 1, 127 | "maxArgs": 1 128 | }, 129 | "openPopup": { 130 | "minArgs": 0, 131 | "maxArgs": 0 132 | }, 133 | "setBadgeBackgroundColor": { 134 | "minArgs": 1, 135 | "maxArgs": 1, 136 | "fallbackToNoCallback": true 137 | }, 138 | "setBadgeText": { 139 | "minArgs": 1, 140 | "maxArgs": 1, 141 | "fallbackToNoCallback": true 142 | }, 143 | "setIcon": { 144 | "minArgs": 1, 145 | "maxArgs": 1 146 | }, 147 | "setPopup": { 148 | "minArgs": 1, 149 | "maxArgs": 1, 150 | "fallbackToNoCallback": true 151 | }, 152 | "setTitle": { 153 | "minArgs": 1, 154 | "maxArgs": 1, 155 | "fallbackToNoCallback": true 156 | } 157 | }, 158 | "browsingData": { 159 | "remove": { 160 | "minArgs": 2, 161 | "maxArgs": 2 162 | }, 163 | "removeCache": { 164 | "minArgs": 1, 165 | "maxArgs": 1 166 | }, 167 | "removeCookies": { 168 | "minArgs": 1, 169 | "maxArgs": 1 170 | }, 171 | "removeDownloads": { 172 | "minArgs": 1, 173 | "maxArgs": 1 174 | }, 175 | "removeFormData": { 176 | "minArgs": 1, 177 | "maxArgs": 1 178 | }, 179 | "removeHistory": { 180 | "minArgs": 1, 181 | "maxArgs": 1 182 | }, 183 | "removeLocalStorage": { 184 | "minArgs": 1, 185 | "maxArgs": 1 186 | }, 187 | "removePasswords": { 188 | "minArgs": 1, 189 | "maxArgs": 1 190 | }, 191 | "removePluginData": { 192 | "minArgs": 1, 193 | "maxArgs": 1 194 | }, 195 | "settings": { 196 | "minArgs": 0, 197 | "maxArgs": 0 198 | } 199 | }, 200 | "commands": { 201 | "getAll": { 202 | "minArgs": 0, 203 | "maxArgs": 0 204 | } 205 | }, 206 | "contextMenus": { 207 | "remove": { 208 | "minArgs": 1, 209 | "maxArgs": 1 210 | }, 211 | "removeAll": { 212 | "minArgs": 0, 213 | "maxArgs": 0 214 | }, 215 | "update": { 216 | "minArgs": 2, 217 | "maxArgs": 2 218 | } 219 | }, 220 | "cookies": { 221 | "get": { 222 | "minArgs": 1, 223 | "maxArgs": 1 224 | }, 225 | "getAll": { 226 | "minArgs": 1, 227 | "maxArgs": 1 228 | }, 229 | "getAllCookieStores": { 230 | "minArgs": 0, 231 | "maxArgs": 0 232 | }, 233 | "remove": { 234 | "minArgs": 1, 235 | "maxArgs": 1 236 | }, 237 | "set": { 238 | "minArgs": 1, 239 | "maxArgs": 1 240 | } 241 | }, 242 | "devtools": { 243 | "inspectedWindow": { 244 | "eval": { 245 | "minArgs": 1, 246 | "maxArgs": 2, 247 | "singleCallbackArg": false 248 | } 249 | }, 250 | "panels": { 251 | "create": { 252 | "minArgs": 3, 253 | "maxArgs": 3, 254 | "singleCallbackArg": true 255 | }, 256 | "elements": { 257 | "createSidebarPane": { 258 | "minArgs": 1, 259 | "maxArgs": 1 260 | } 261 | } 262 | } 263 | }, 264 | "downloads": { 265 | "cancel": { 266 | "minArgs": 1, 267 | "maxArgs": 1 268 | }, 269 | "download": { 270 | "minArgs": 1, 271 | "maxArgs": 1 272 | }, 273 | "erase": { 274 | "minArgs": 1, 275 | "maxArgs": 1 276 | }, 277 | "getFileIcon": { 278 | "minArgs": 1, 279 | "maxArgs": 2 280 | }, 281 | "open": { 282 | "minArgs": 1, 283 | "maxArgs": 1, 284 | "fallbackToNoCallback": true 285 | }, 286 | "pause": { 287 | "minArgs": 1, 288 | "maxArgs": 1 289 | }, 290 | "removeFile": { 291 | "minArgs": 1, 292 | "maxArgs": 1 293 | }, 294 | "resume": { 295 | "minArgs": 1, 296 | "maxArgs": 1 297 | }, 298 | "search": { 299 | "minArgs": 1, 300 | "maxArgs": 1 301 | }, 302 | "show": { 303 | "minArgs": 1, 304 | "maxArgs": 1, 305 | "fallbackToNoCallback": true 306 | } 307 | }, 308 | "extension": { 309 | "isAllowedFileSchemeAccess": { 310 | "minArgs": 0, 311 | "maxArgs": 0 312 | }, 313 | "isAllowedIncognitoAccess": { 314 | "minArgs": 0, 315 | "maxArgs": 0 316 | } 317 | }, 318 | "history": { 319 | "addUrl": { 320 | "minArgs": 1, 321 | "maxArgs": 1 322 | }, 323 | "deleteAll": { 324 | "minArgs": 0, 325 | "maxArgs": 0 326 | }, 327 | "deleteRange": { 328 | "minArgs": 1, 329 | "maxArgs": 1 330 | }, 331 | "deleteUrl": { 332 | "minArgs": 1, 333 | "maxArgs": 1 334 | }, 335 | "getVisits": { 336 | "minArgs": 1, 337 | "maxArgs": 1 338 | }, 339 | "search": { 340 | "minArgs": 1, 341 | "maxArgs": 1 342 | } 343 | }, 344 | "i18n": { 345 | "detectLanguage": { 346 | "minArgs": 1, 347 | "maxArgs": 1 348 | }, 349 | "getAcceptLanguages": { 350 | "minArgs": 0, 351 | "maxArgs": 0 352 | } 353 | }, 354 | "identity": { 355 | "launchWebAuthFlow": { 356 | "minArgs": 1, 357 | "maxArgs": 1 358 | } 359 | }, 360 | "idle": { 361 | "queryState": { 362 | "minArgs": 1, 363 | "maxArgs": 1 364 | } 365 | }, 366 | "management": { 367 | "get": { 368 | "minArgs": 1, 369 | "maxArgs": 1 370 | }, 371 | "getAll": { 372 | "minArgs": 0, 373 | "maxArgs": 0 374 | }, 375 | "getSelf": { 376 | "minArgs": 0, 377 | "maxArgs": 0 378 | }, 379 | "setEnabled": { 380 | "minArgs": 2, 381 | "maxArgs": 2 382 | }, 383 | "uninstallSelf": { 384 | "minArgs": 0, 385 | "maxArgs": 1 386 | } 387 | }, 388 | "notifications": { 389 | "clear": { 390 | "minArgs": 1, 391 | "maxArgs": 1 392 | }, 393 | "create": { 394 | "minArgs": 1, 395 | "maxArgs": 2 396 | }, 397 | "getAll": { 398 | "minArgs": 0, 399 | "maxArgs": 0 400 | }, 401 | "getPermissionLevel": { 402 | "minArgs": 0, 403 | "maxArgs": 0 404 | }, 405 | "update": { 406 | "minArgs": 2, 407 | "maxArgs": 2 408 | } 409 | }, 410 | "pageAction": { 411 | "getPopup": { 412 | "minArgs": 1, 413 | "maxArgs": 1 414 | }, 415 | "getTitle": { 416 | "minArgs": 1, 417 | "maxArgs": 1 418 | }, 419 | "hide": { 420 | "minArgs": 1, 421 | "maxArgs": 1, 422 | "fallbackToNoCallback": true 423 | }, 424 | "setIcon": { 425 | "minArgs": 1, 426 | "maxArgs": 1 427 | }, 428 | "setPopup": { 429 | "minArgs": 1, 430 | "maxArgs": 1, 431 | "fallbackToNoCallback": true 432 | }, 433 | "setTitle": { 434 | "minArgs": 1, 435 | "maxArgs": 1, 436 | "fallbackToNoCallback": true 437 | }, 438 | "show": { 439 | "minArgs": 1, 440 | "maxArgs": 1, 441 | "fallbackToNoCallback": true 442 | } 443 | }, 444 | "permissions": { 445 | "contains": { 446 | "minArgs": 1, 447 | "maxArgs": 1 448 | }, 449 | "getAll": { 450 | "minArgs": 0, 451 | "maxArgs": 0 452 | }, 453 | "remove": { 454 | "minArgs": 1, 455 | "maxArgs": 1 456 | }, 457 | "request": { 458 | "minArgs": 1, 459 | "maxArgs": 1 460 | } 461 | }, 462 | "runtime": { 463 | "getBackgroundPage": { 464 | "minArgs": 0, 465 | "maxArgs": 0 466 | }, 467 | "getPlatformInfo": { 468 | "minArgs": 0, 469 | "maxArgs": 0 470 | }, 471 | "openOptionsPage": { 472 | "minArgs": 0, 473 | "maxArgs": 0 474 | }, 475 | "requestUpdateCheck": { 476 | "minArgs": 0, 477 | "maxArgs": 0 478 | }, 479 | "sendMessage": { 480 | "minArgs": 1, 481 | "maxArgs": 3 482 | }, 483 | "sendNativeMessage": { 484 | "minArgs": 2, 485 | "maxArgs": 2 486 | }, 487 | "setUninstallURL": { 488 | "minArgs": 1, 489 | "maxArgs": 1 490 | } 491 | }, 492 | "sessions": { 493 | "getDevices": { 494 | "minArgs": 0, 495 | "maxArgs": 1 496 | }, 497 | "getRecentlyClosed": { 498 | "minArgs": 0, 499 | "maxArgs": 1 500 | }, 501 | "restore": { 502 | "minArgs": 0, 503 | "maxArgs": 1 504 | } 505 | }, 506 | "storage": { 507 | "local": { 508 | "clear": { 509 | "minArgs": 0, 510 | "maxArgs": 0 511 | }, 512 | "get": { 513 | "minArgs": 0, 514 | "maxArgs": 1 515 | }, 516 | "getBytesInUse": { 517 | "minArgs": 0, 518 | "maxArgs": 1 519 | }, 520 | "remove": { 521 | "minArgs": 1, 522 | "maxArgs": 1 523 | }, 524 | "set": { 525 | "minArgs": 1, 526 | "maxArgs": 1 527 | } 528 | }, 529 | "managed": { 530 | "get": { 531 | "minArgs": 0, 532 | "maxArgs": 1 533 | }, 534 | "getBytesInUse": { 535 | "minArgs": 0, 536 | "maxArgs": 1 537 | } 538 | }, 539 | "sync": { 540 | "clear": { 541 | "minArgs": 0, 542 | "maxArgs": 0 543 | }, 544 | "get": { 545 | "minArgs": 0, 546 | "maxArgs": 1 547 | }, 548 | "getBytesInUse": { 549 | "minArgs": 0, 550 | "maxArgs": 1 551 | }, 552 | "remove": { 553 | "minArgs": 1, 554 | "maxArgs": 1 555 | }, 556 | "set": { 557 | "minArgs": 1, 558 | "maxArgs": 1 559 | } 560 | } 561 | }, 562 | "tabs": { 563 | "captureVisibleTab": { 564 | "minArgs": 0, 565 | "maxArgs": 2 566 | }, 567 | "create": { 568 | "minArgs": 1, 569 | "maxArgs": 1 570 | }, 571 | "detectLanguage": { 572 | "minArgs": 0, 573 | "maxArgs": 1 574 | }, 575 | "discard": { 576 | "minArgs": 0, 577 | "maxArgs": 1 578 | }, 579 | "duplicate": { 580 | "minArgs": 1, 581 | "maxArgs": 1 582 | }, 583 | "executeScript": { 584 | "minArgs": 1, 585 | "maxArgs": 2 586 | }, 587 | "get": { 588 | "minArgs": 1, 589 | "maxArgs": 1 590 | }, 591 | "getCurrent": { 592 | "minArgs": 0, 593 | "maxArgs": 0 594 | }, 595 | "getZoom": { 596 | "minArgs": 0, 597 | "maxArgs": 1 598 | }, 599 | "getZoomSettings": { 600 | "minArgs": 0, 601 | "maxArgs": 1 602 | }, 603 | "goBack": { 604 | "minArgs": 0, 605 | "maxArgs": 1 606 | }, 607 | "goForward": { 608 | "minArgs": 0, 609 | "maxArgs": 1 610 | }, 611 | "highlight": { 612 | "minArgs": 1, 613 | "maxArgs": 1 614 | }, 615 | "insertCSS": { 616 | "minArgs": 1, 617 | "maxArgs": 2 618 | }, 619 | "move": { 620 | "minArgs": 2, 621 | "maxArgs": 2 622 | }, 623 | "query": { 624 | "minArgs": 1, 625 | "maxArgs": 1 626 | }, 627 | "reload": { 628 | "minArgs": 0, 629 | "maxArgs": 2 630 | }, 631 | "remove": { 632 | "minArgs": 1, 633 | "maxArgs": 1 634 | }, 635 | "removeCSS": { 636 | "minArgs": 1, 637 | "maxArgs": 2 638 | }, 639 | "sendMessage": { 640 | "minArgs": 2, 641 | "maxArgs": 3 642 | }, 643 | "setZoom": { 644 | "minArgs": 1, 645 | "maxArgs": 2 646 | }, 647 | "setZoomSettings": { 648 | "minArgs": 1, 649 | "maxArgs": 2 650 | }, 651 | "update": { 652 | "minArgs": 1, 653 | "maxArgs": 2 654 | } 655 | }, 656 | "topSites": { 657 | "get": { 658 | "minArgs": 0, 659 | "maxArgs": 0 660 | } 661 | }, 662 | "webNavigation": { 663 | "getAllFrames": { 664 | "minArgs": 1, 665 | "maxArgs": 1 666 | }, 667 | "getFrame": { 668 | "minArgs": 1, 669 | "maxArgs": 1 670 | } 671 | }, 672 | "webRequest": { 673 | "handlerBehaviorChanged": { 674 | "minArgs": 0, 675 | "maxArgs": 0 676 | } 677 | }, 678 | "windows": { 679 | "create": { 680 | "minArgs": 0, 681 | "maxArgs": 1 682 | }, 683 | "get": { 684 | "minArgs": 1, 685 | "maxArgs": 2 686 | }, 687 | "getAll": { 688 | "minArgs": 0, 689 | "maxArgs": 1 690 | }, 691 | "getCurrent": { 692 | "minArgs": 0, 693 | "maxArgs": 1 694 | }, 695 | "getLastFocused": { 696 | "minArgs": 0, 697 | "maxArgs": 1 698 | }, 699 | "remove": { 700 | "minArgs": 1, 701 | "maxArgs": 1 702 | }, 703 | "update": { 704 | "minArgs": 2, 705 | "maxArgs": 2 706 | } 707 | } 708 | }; 709 | 710 | if (Object.keys(apiMetadata).length === 0) { 711 | throw new Error("api-metadata.json has not been included in browser-polyfill"); 712 | } 713 | /** 714 | * A WeakMap subclass which creates and stores a value for any key which does 715 | * not exist when accessed, but behaves exactly as an ordinary WeakMap 716 | * otherwise. 717 | * 718 | * @param {function} createItem 719 | * A function which will be called in order to create the value for any 720 | * key which does not exist, the first time it is accessed. The 721 | * function receives, as its only argument, the key being created. 722 | */ 723 | 724 | 725 | class DefaultWeakMap extends WeakMap { 726 | constructor(createItem, items = undefined) { 727 | super(items); 728 | this.createItem = createItem; 729 | } 730 | 731 | get(key) { 732 | if (!this.has(key)) { 733 | this.set(key, this.createItem(key)); 734 | } 735 | 736 | return super.get(key); 737 | } 738 | 739 | } 740 | /** 741 | * Returns true if the given object is an object with a `then` method, and can 742 | * therefore be assumed to behave as a Promise. 743 | * 744 | * @param {*} value The value to test. 745 | * @returns {boolean} True if the value is thenable. 746 | */ 747 | 748 | 749 | const isThenable = value => { 750 | return value && typeof value === "object" && typeof value.then === "function"; 751 | }; 752 | /** 753 | * Creates and returns a function which, when called, will resolve or reject 754 | * the given promise based on how it is called: 755 | * 756 | * - If, when called, `chrome.runtime.lastError` contains a non-null object, 757 | * the promise is rejected with that value. 758 | * - If the function is called with exactly one argument, the promise is 759 | * resolved to that value. 760 | * - Otherwise, the promise is resolved to an array containing all of the 761 | * function's arguments. 762 | * 763 | * @param {object} promise 764 | * An object containing the resolution and rejection functions of a 765 | * promise. 766 | * @param {function} promise.resolve 767 | * The promise's resolution function. 768 | * @param {function} promise.reject 769 | * The promise's rejection function. 770 | * @param {object} metadata 771 | * Metadata about the wrapped method which has created the callback. 772 | * @param {boolean} metadata.singleCallbackArg 773 | * Whether or not the promise is resolved with only the first 774 | * argument of the callback, alternatively an array of all the 775 | * callback arguments is resolved. By default, if the callback 776 | * function is invoked with only a single argument, that will be 777 | * resolved to the promise, while all arguments will be resolved as 778 | * an array if multiple are given. 779 | * 780 | * @returns {function} 781 | * The generated callback function. 782 | */ 783 | 784 | 785 | const makeCallback = (promise, metadata) => { 786 | return (...callbackArgs) => { 787 | if (extensionAPIs.runtime.lastError) { 788 | promise.reject(new Error(extensionAPIs.runtime.lastError.message)); 789 | } else if (metadata.singleCallbackArg || callbackArgs.length <= 1 && metadata.singleCallbackArg !== false) { 790 | promise.resolve(callbackArgs[0]); 791 | } else { 792 | promise.resolve(callbackArgs); 793 | } 794 | }; 795 | }; 796 | 797 | const pluralizeArguments = numArgs => numArgs == 1 ? "argument" : "arguments"; 798 | /** 799 | * Creates a wrapper function for a method with the given name and metadata. 800 | * 801 | * @param {string} name 802 | * The name of the method which is being wrapped. 803 | * @param {object} metadata 804 | * Metadata about the method being wrapped. 805 | * @param {integer} metadata.minArgs 806 | * The minimum number of arguments which must be passed to the 807 | * function. If called with fewer than this number of arguments, the 808 | * wrapper will raise an exception. 809 | * @param {integer} metadata.maxArgs 810 | * The maximum number of arguments which may be passed to the 811 | * function. If called with more than this number of arguments, the 812 | * wrapper will raise an exception. 813 | * @param {boolean} metadata.singleCallbackArg 814 | * Whether or not the promise is resolved with only the first 815 | * argument of the callback, alternatively an array of all the 816 | * callback arguments is resolved. By default, if the callback 817 | * function is invoked with only a single argument, that will be 818 | * resolved to the promise, while all arguments will be resolved as 819 | * an array if multiple are given. 820 | * 821 | * @returns {function(object, ...*)} 822 | * The generated wrapper function. 823 | */ 824 | 825 | 826 | const wrapAsyncFunction = (name, metadata) => { 827 | return function asyncFunctionWrapper(target, ...args) { 828 | if (args.length < metadata.minArgs) { 829 | throw new Error(`Expected at least ${metadata.minArgs} ${pluralizeArguments(metadata.minArgs)} for ${name}(), got ${args.length}`); 830 | } 831 | 832 | if (args.length > metadata.maxArgs) { 833 | throw new Error(`Expected at most ${metadata.maxArgs} ${pluralizeArguments(metadata.maxArgs)} for ${name}(), got ${args.length}`); 834 | } 835 | 836 | return new Promise((resolve, reject) => { 837 | if (metadata.fallbackToNoCallback) { 838 | // This API method has currently no callback on Chrome, but it return a promise on Firefox, 839 | // and so the polyfill will try to call it with a callback first, and it will fallback 840 | // to not passing the callback if the first call fails. 841 | try { 842 | target[name](...args, makeCallback({ 843 | resolve, 844 | reject 845 | }, metadata)); 846 | } catch (cbError) { 847 | console.warn(`${name} API method doesn't seem to support the callback parameter, ` + "falling back to call it without a callback: ", cbError); 848 | target[name](...args); // Update the API method metadata, so that the next API calls will not try to 849 | // use the unsupported callback anymore. 850 | 851 | metadata.fallbackToNoCallback = false; 852 | metadata.noCallback = true; 853 | resolve(); 854 | } 855 | } else if (metadata.noCallback) { 856 | target[name](...args); 857 | resolve(); 858 | } else { 859 | target[name](...args, makeCallback({ 860 | resolve, 861 | reject 862 | }, metadata)); 863 | } 864 | }); 865 | }; 866 | }; 867 | /** 868 | * Wraps an existing method of the target object, so that calls to it are 869 | * intercepted by the given wrapper function. The wrapper function receives, 870 | * as its first argument, the original `target` object, followed by each of 871 | * the arguments passed to the original method. 872 | * 873 | * @param {object} target 874 | * The original target object that the wrapped method belongs to. 875 | * @param {function} method 876 | * The method being wrapped. This is used as the target of the Proxy 877 | * object which is created to wrap the method. 878 | * @param {function} wrapper 879 | * The wrapper function which is called in place of a direct invocation 880 | * of the wrapped method. 881 | * 882 | * @returns {Proxy} 883 | * A Proxy object for the given method, which invokes the given wrapper 884 | * method in its place. 885 | */ 886 | 887 | 888 | const wrapMethod = (target, method, wrapper) => { 889 | return new Proxy(method, { 890 | apply(targetMethod, thisObj, args) { 891 | return wrapper.call(thisObj, target, ...args); 892 | } 893 | 894 | }); 895 | }; 896 | 897 | let hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty); 898 | /** 899 | * Wraps an object in a Proxy which intercepts and wraps certain methods 900 | * based on the given `wrappers` and `metadata` objects. 901 | * 902 | * @param {object} target 903 | * The target object to wrap. 904 | * 905 | * @param {object} [wrappers = {}] 906 | * An object tree containing wrapper functions for special cases. Any 907 | * function present in this object tree is called in place of the 908 | * method in the same location in the `target` object tree. These 909 | * wrapper methods are invoked as described in {@see wrapMethod}. 910 | * 911 | * @param {object} [metadata = {}] 912 | * An object tree containing metadata used to automatically generate 913 | * Promise-based wrapper functions for asynchronous. Any function in 914 | * the `target` object tree which has a corresponding metadata object 915 | * in the same location in the `metadata` tree is replaced with an 916 | * automatically-generated wrapper function, as described in 917 | * {@see wrapAsyncFunction} 918 | * 919 | * @returns {Proxy} 920 | */ 921 | 922 | const wrapObject = (target, wrappers = {}, metadata = {}) => { 923 | let cache = Object.create(null); 924 | let handlers = { 925 | has(proxyTarget, prop) { 926 | return prop in target || prop in cache; 927 | }, 928 | 929 | get(proxyTarget, prop, receiver) { 930 | if (prop in cache) { 931 | return cache[prop]; 932 | } 933 | 934 | if (!(prop in target)) { 935 | return undefined; 936 | } 937 | 938 | let value = target[prop]; 939 | 940 | if (typeof value === "function") { 941 | // This is a method on the underlying object. Check if we need to do 942 | // any wrapping. 943 | if (typeof wrappers[prop] === "function") { 944 | // We have a special-case wrapper for this method. 945 | value = wrapMethod(target, target[prop], wrappers[prop]); 946 | } else if (hasOwnProperty(metadata, prop)) { 947 | // This is an async method that we have metadata for. Create a 948 | // Promise wrapper for it. 949 | let wrapper = wrapAsyncFunction(prop, metadata[prop]); 950 | value = wrapMethod(target, target[prop], wrapper); 951 | } else { 952 | // This is a method that we don't know or care about. Return the 953 | // original method, bound to the underlying object. 954 | value = value.bind(target); 955 | } 956 | } else if (typeof value === "object" && value !== null && (hasOwnProperty(wrappers, prop) || hasOwnProperty(metadata, prop))) { 957 | // This is an object that we need to do some wrapping for the children 958 | // of. Create a sub-object wrapper for it with the appropriate child 959 | // metadata. 960 | value = wrapObject(value, wrappers[prop], metadata[prop]); 961 | } else if (hasOwnProperty(metadata, "*")) { 962 | // Wrap all properties in * namespace. 963 | value = wrapObject(value, wrappers[prop], metadata["*"]); 964 | } else { 965 | // We don't need to do any wrapping for this property, 966 | // so just forward all access to the underlying object. 967 | Object.defineProperty(cache, prop, { 968 | configurable: true, 969 | enumerable: true, 970 | 971 | get() { 972 | return target[prop]; 973 | }, 974 | 975 | set(value) { 976 | target[prop] = value; 977 | } 978 | 979 | }); 980 | return value; 981 | } 982 | 983 | cache[prop] = value; 984 | return value; 985 | }, 986 | 987 | set(proxyTarget, prop, value, receiver) { 988 | if (prop in cache) { 989 | cache[prop] = value; 990 | } else { 991 | target[prop] = value; 992 | } 993 | 994 | return true; 995 | }, 996 | 997 | defineProperty(proxyTarget, prop, desc) { 998 | return Reflect.defineProperty(cache, prop, desc); 999 | }, 1000 | 1001 | deleteProperty(proxyTarget, prop) { 1002 | return Reflect.deleteProperty(cache, prop); 1003 | } 1004 | 1005 | }; // Per contract of the Proxy API, the "get" proxy handler must return the 1006 | // original value of the target if that value is declared read-only and 1007 | // non-configurable. For this reason, we create an object with the 1008 | // prototype set to `target` instead of using `target` directly. 1009 | // Otherwise we cannot return a custom object for APIs that 1010 | // are declared read-only and non-configurable, such as `chrome.devtools`. 1011 | // 1012 | // The proxy handlers themselves will still use the original `target` 1013 | // instead of the `proxyTarget`, so that the methods and properties are 1014 | // dereferenced via the original targets. 1015 | 1016 | let proxyTarget = Object.create(target); 1017 | return new Proxy(proxyTarget, handlers); 1018 | }; 1019 | /** 1020 | * Creates a set of wrapper functions for an event object, which handles 1021 | * wrapping of listener functions that those messages are passed. 1022 | * 1023 | * A single wrapper is created for each listener function, and stored in a 1024 | * map. Subsequent calls to `addListener`, `hasListener`, or `removeListener` 1025 | * retrieve the original wrapper, so that attempts to remove a 1026 | * previously-added listener work as expected. 1027 | * 1028 | * @param {DefaultWeakMap} wrapperMap 1029 | * A DefaultWeakMap object which will create the appropriate wrapper 1030 | * for a given listener function when one does not exist, and retrieve 1031 | * an existing one when it does. 1032 | * 1033 | * @returns {object} 1034 | */ 1035 | 1036 | 1037 | const wrapEvent = wrapperMap => ({ 1038 | addListener(target, listener, ...args) { 1039 | target.addListener(wrapperMap.get(listener), ...args); 1040 | }, 1041 | 1042 | hasListener(target, listener) { 1043 | return target.hasListener(wrapperMap.get(listener)); 1044 | }, 1045 | 1046 | removeListener(target, listener) { 1047 | target.removeListener(wrapperMap.get(listener)); 1048 | } 1049 | 1050 | }); 1051 | 1052 | const onRequestFinishedWrappers = new DefaultWeakMap(listener => { 1053 | if (typeof listener !== "function") { 1054 | return listener; 1055 | } 1056 | /** 1057 | * Wraps an onRequestFinished listener function so that it will return a 1058 | * `getContent()` property which returns a `Promise` rather than using a 1059 | * callback API. 1060 | * 1061 | * @param {object} req 1062 | * The HAR entry object representing the network request. 1063 | */ 1064 | 1065 | 1066 | return function onRequestFinished(req) { 1067 | const wrappedReq = wrapObject(req, {} 1068 | /* wrappers */ 1069 | , { 1070 | getContent: { 1071 | minArgs: 0, 1072 | maxArgs: 0 1073 | } 1074 | }); 1075 | listener(wrappedReq); 1076 | }; 1077 | }); // Keep track if the deprecation warning has been logged at least once. 1078 | 1079 | let loggedSendResponseDeprecationWarning = false; 1080 | const onMessageWrappers = new DefaultWeakMap(listener => { 1081 | if (typeof listener !== "function") { 1082 | return listener; 1083 | } 1084 | /** 1085 | * Wraps a message listener function so that it may send responses based on 1086 | * its return value, rather than by returning a sentinel value and calling a 1087 | * callback. If the listener function returns a Promise, the response is 1088 | * sent when the promise either resolves or rejects. 1089 | * 1090 | * @param {*} message 1091 | * The message sent by the other end of the channel. 1092 | * @param {object} sender 1093 | * Details about the sender of the message. 1094 | * @param {function(*)} sendResponse 1095 | * A callback which, when called with an arbitrary argument, sends 1096 | * that value as a response. 1097 | * @returns {boolean} 1098 | * True if the wrapped listener returned a Promise, which will later 1099 | * yield a response. False otherwise. 1100 | */ 1101 | 1102 | 1103 | return function onMessage(message, sender, sendResponse) { 1104 | let didCallSendResponse = false; 1105 | let wrappedSendResponse; 1106 | let sendResponsePromise = new Promise(resolve => { 1107 | wrappedSendResponse = function (response) { 1108 | if (!loggedSendResponseDeprecationWarning) { 1109 | console.warn(SEND_RESPONSE_DEPRECATION_WARNING, new Error().stack); 1110 | loggedSendResponseDeprecationWarning = true; 1111 | } 1112 | 1113 | didCallSendResponse = true; 1114 | resolve(response); 1115 | }; 1116 | }); 1117 | let result; 1118 | 1119 | try { 1120 | result = listener(message, sender, wrappedSendResponse); 1121 | } catch (err) { 1122 | result = Promise.reject(err); 1123 | } 1124 | 1125 | const isResultThenable = result !== true && isThenable(result); // If the listener didn't returned true or a Promise, or called 1126 | // wrappedSendResponse synchronously, we can exit earlier 1127 | // because there will be no response sent from this listener. 1128 | 1129 | if (result !== true && !isResultThenable && !didCallSendResponse) { 1130 | return false; 1131 | } // A small helper to send the message if the promise resolves 1132 | // and an error if the promise rejects (a wrapped sendMessage has 1133 | // to translate the message into a resolved promise or a rejected 1134 | // promise). 1135 | 1136 | 1137 | const sendPromisedResult = promise => { 1138 | promise.then(msg => { 1139 | // send the message value. 1140 | sendResponse(msg); 1141 | }, error => { 1142 | // Send a JSON representation of the error if the rejected value 1143 | // is an instance of error, or the object itself otherwise. 1144 | let message; 1145 | 1146 | if (error && (error instanceof Error || typeof error.message === "string")) { 1147 | message = error.message; 1148 | } else { 1149 | message = "An unexpected error occurred"; 1150 | } 1151 | 1152 | sendResponse({ 1153 | __mozWebExtensionPolyfillReject__: true, 1154 | message 1155 | }); 1156 | }).catch(err => { 1157 | // Print an error on the console if unable to send the response. 1158 | console.error("Failed to send onMessage rejected reply", err); 1159 | }); 1160 | }; // If the listener returned a Promise, send the resolved value as a 1161 | // result, otherwise wait the promise related to the wrappedSendResponse 1162 | // callback to resolve and send it as a response. 1163 | 1164 | 1165 | if (isResultThenable) { 1166 | sendPromisedResult(result); 1167 | } else { 1168 | sendPromisedResult(sendResponsePromise); 1169 | } // Let Chrome know that the listener is replying. 1170 | 1171 | 1172 | return true; 1173 | }; 1174 | }); 1175 | 1176 | const wrappedSendMessageCallback = ({ 1177 | reject, 1178 | resolve 1179 | }, reply) => { 1180 | if (extensionAPIs.runtime.lastError) { 1181 | // Detect when none of the listeners replied to the sendMessage call and resolve 1182 | // the promise to undefined as in Firefox. 1183 | // See https://github.com/mozilla/webextension-polyfill/issues/130 1184 | if (extensionAPIs.runtime.lastError.message === CHROME_SEND_MESSAGE_CALLBACK_NO_RESPONSE_MESSAGE) { 1185 | resolve(); 1186 | } else { 1187 | reject(new Error(extensionAPIs.runtime.lastError.message)); 1188 | } 1189 | } else if (reply && reply.__mozWebExtensionPolyfillReject__) { 1190 | // Convert back the JSON representation of the error into 1191 | // an Error instance. 1192 | reject(new Error(reply.message)); 1193 | } else { 1194 | resolve(reply); 1195 | } 1196 | }; 1197 | 1198 | const wrappedSendMessage = (name, metadata, apiNamespaceObj, ...args) => { 1199 | if (args.length < metadata.minArgs) { 1200 | throw new Error(`Expected at least ${metadata.minArgs} ${pluralizeArguments(metadata.minArgs)} for ${name}(), got ${args.length}`); 1201 | } 1202 | 1203 | if (args.length > metadata.maxArgs) { 1204 | throw new Error(`Expected at most ${metadata.maxArgs} ${pluralizeArguments(metadata.maxArgs)} for ${name}(), got ${args.length}`); 1205 | } 1206 | 1207 | return new Promise((resolve, reject) => { 1208 | const wrappedCb = wrappedSendMessageCallback.bind(null, { 1209 | resolve, 1210 | reject 1211 | }); 1212 | args.push(wrappedCb); 1213 | apiNamespaceObj.sendMessage(...args); 1214 | }); 1215 | }; 1216 | 1217 | const staticWrappers = { 1218 | devtools: { 1219 | network: { 1220 | onRequestFinished: wrapEvent(onRequestFinishedWrappers) 1221 | } 1222 | }, 1223 | runtime: { 1224 | onMessage: wrapEvent(onMessageWrappers), 1225 | onMessageExternal: wrapEvent(onMessageWrappers), 1226 | sendMessage: wrappedSendMessage.bind(null, "sendMessage", { 1227 | minArgs: 1, 1228 | maxArgs: 3 1229 | }) 1230 | }, 1231 | tabs: { 1232 | sendMessage: wrappedSendMessage.bind(null, "sendMessage", { 1233 | minArgs: 2, 1234 | maxArgs: 3 1235 | }) 1236 | } 1237 | }; 1238 | const settingMetadata = { 1239 | clear: { 1240 | minArgs: 1, 1241 | maxArgs: 1 1242 | }, 1243 | get: { 1244 | minArgs: 1, 1245 | maxArgs: 1 1246 | }, 1247 | set: { 1248 | minArgs: 1, 1249 | maxArgs: 1 1250 | } 1251 | }; 1252 | apiMetadata.privacy = { 1253 | network: { 1254 | "*": settingMetadata 1255 | }, 1256 | services: { 1257 | "*": settingMetadata 1258 | }, 1259 | websites: { 1260 | "*": settingMetadata 1261 | } 1262 | }; 1263 | return wrapObject(extensionAPIs, staticWrappers, apiMetadata); 1264 | }; 1265 | 1266 | if (typeof chrome != "object" || !chrome || !chrome.runtime || !chrome.runtime.id) { 1267 | throw new Error("This script should only be loaded in a browser extension."); 1268 | } // The build process adds a UMD wrapper around this file, which makes the 1269 | // `module` variable available. 1270 | 1271 | 1272 | module.exports = wrapAPIs(chrome); 1273 | } else { 1274 | module.exports = browser; 1275 | } 1276 | }); 1277 | //# sourceMappingURL=browser-polyfill.js.map 1278 | -------------------------------------------------------------------------------- /src/ch_manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Zen mode for YouTube", 3 | "manifest_version": 3, 4 | "name": "ZYT", 5 | "version": "1.1.5", 6 | "author": "surajsharma", 7 | "homepage_url": "https://evenzero.in/experiments", 8 | "icons": { 9 | "144": "icons/favicon4_144.png" 10 | }, 11 | 12 | "background": { 13 | "service_worker": "background.js" 14 | }, 15 | 16 | "content_scripts": [ 17 | { 18 | "matches": ["*://*.youtube.com/*"], 19 | "js": ["browser-polyfill.js", "defaultSettings.js", "contentscript.js", "emoji.js"], 20 | "run_at": "document_start" 21 | } 22 | ], 23 | 24 | "action": { 25 | "default_icon": "icons/favicon4_144.png", 26 | "default_title": "Nirvana for YouTube", 27 | "default_popup": "options/options.html" 28 | }, 29 | 30 | "permissions": ["storage", "tabs", "scripting", "activeTab"], 31 | "host_permissions": ["*://*.youtube.com/*"] 32 | } -------------------------------------------------------------------------------- /src/contentscript.js: -------------------------------------------------------------------------------- 1 | /* This script interacts with the YT DOM 2 | It is injected into all YT tabs at install and on popup open 3 | */ 4 | 5 | // TODO: check if offline 6 | 7 | if (typeof browser === "undefined") { 8 | var browser = chrome; 9 | } 10 | 11 | async function injectTransitionClass(options) { 12 | /* for every class css, add a transition by 13 | * looping over the state object and populating a css string 14 | * for each class this css string is then injected into the 15 | * page */ 16 | 17 | let el = document.getElementById("zentubeTransitions"); 18 | 19 | if (el) { 20 | el.parentNode.removeChild(el); 21 | } 22 | 23 | let css = ""; 24 | let customStyles = document.createElement("style"); 25 | 26 | if (options) { 27 | for (const page of Object.keys(options)) { 28 | for (const item of Object.keys(options[page])) { 29 | if (options[page][item].classes) { 30 | for (const c of options[page][item].classes) { 31 | css += `${c}{transition: all 0.2s;}`; 32 | } 33 | } 34 | } 35 | } 36 | customStyles.setAttribute("type", "text/css"); 37 | customStyles.setAttribute("id", "zentubeTransitions"); 38 | customStyles.appendChild(document.createTextNode(css)); 39 | document.documentElement.appendChild(customStyles); 40 | } 41 | } 42 | 43 | async function toggleCSS(options) { 44 | /* for every css class, add appropriate opacity by 45 | * looping over the state object and populating a css string 46 | * for each class; this css string is then injected into the 47 | * page */ 48 | if (!options) return; 49 | let css = ""; 50 | for (const page of Object.keys(options)) { 51 | for (const item of Object.keys(options[page])) { 52 | if (options[page][item]["show"]) { 53 | if (options[page][item].classes) { 54 | for (const c of options[page][item].classes) { 55 | css += `${c}{display:none; opacity:0}`; 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | let el = document.getElementById("zentube"); 63 | 64 | if (!el) { 65 | let customStyles = document.createElement("style"); 66 | customStyles.setAttribute("type", "text/css"); 67 | customStyles.setAttribute("id", "zentube"); 68 | customStyles.appendChild(document.createTextNode(css)); 69 | document.documentElement.appendChild(customStyles); 70 | } 71 | if (el) { 72 | el.textContent = css; 73 | } 74 | } 75 | 76 | async function checkStoredSettings() { 77 | /* On startup, check whether we have stored settings. 78 | If not, then store the default settings.*/ 79 | const { settings } = await browser.storage.local.get(); 80 | if (!settings || Object.keys(settings).length == 0) 81 | return await browser.storage.local.set({ settings: defaultSettings }); 82 | } 83 | 84 | async function msgListener(request, sender) { 85 | /* popup clicks */ 86 | const { settings } = JSON.parse(request); 87 | const { options } = settings; 88 | toggleCSS(options); 89 | } 90 | 91 | /** 92 | * init 93 | **/ 94 | 95 | async function initializePageAction() { 96 | /* install message listener, inject transition css, toggle css */ 97 | try { 98 | await browser.runtime.onMessage.addListener(msgListener); 99 | const { settings } = await browser.storage.local.get(); 100 | const { options } = settings; 101 | injectTransitionClass(options); 102 | toggleCSS(options); 103 | } catch (err) { 104 | onError(err); 105 | } 106 | } 107 | 108 | (async () => { 109 | try { 110 | await checkStoredSettings(); 111 | await initializePageAction(); 112 | } catch (err) { 113 | onError(err); 114 | } 115 | })(); 116 | 117 | /** 118 | * utils 119 | **/ 120 | 121 | function onError(e) { 122 | console.error(e); 123 | } 124 | -------------------------------------------------------------------------------- /src/defaultSettings.js: -------------------------------------------------------------------------------- 1 | //TODO: explore > live 2 | var defaultSettings = { 3 | currentPage: "Home", 4 | options: { 5 | Home: { 6 | preview: { 7 | label: "Preview-On-Hover", 8 | classes: [ 9 | ".ytd-video-preview", 10 | "#mouseover-overlay", 11 | ".ytd-thumbnail-overlay-loading-preview-renderer" 12 | ], 13 | show: true 14 | }, 15 | communityPosts: { 16 | label: "Latest Posts", 17 | classes: ["ytd-rich-shelf-renderer"], 18 | id: "communityPosts", 19 | show: true 20 | }, 21 | adThumbs: { 22 | label: "Ad Thumbnails", 23 | classes: [".ytd-display-ad-renderer", ".ytd-ad-slot-renderer"], 24 | show: true 25 | }, 26 | chipBar: { 27 | label: "Feed Filter Chip Bar", 28 | classes: [".ytd-feed-filter-chip-bar-renderer"], 29 | show: true 30 | }, 31 | title: { 32 | label: "Video Title", 33 | classes: ["yt-formatted-string.style-scope.ytd-rich-grid-media"], 34 | show: true 35 | } 36 | }, 37 | Video: { 38 | sidebar: { 39 | label: "Video Sidebar", 40 | classes: [".ytd-watch-next-secondary-results-renderer"], 41 | show: true 42 | }, 43 | endvideos: { 44 | label: "Recommendations", 45 | classes: [ 46 | ".ytp-endscreen-content", 47 | ".ytp-ce-video .ytp-ce-channel .ytp-ce-covering-overlay" 48 | ], 49 | show: true 50 | }, 51 | chat: { 52 | label: "Chat", 53 | classes: ["#chat"], 54 | show: true 55 | }, 56 | likes: { 57 | label: "Likes", 58 | classes: [ 59 | "ytd-menu-renderer.style-scope.ytd-watch-metadata .yt-core-attributed-string" 60 | ], 61 | show: true 62 | }, 63 | comments: { 64 | label: "Comments", 65 | classes: [".ytd-comments"], 66 | show: true 67 | }, 68 | playlist: { 69 | label: "Playlist", 70 | classes: [".ytd-playlist-panel-renderer"], 71 | show: true 72 | }, 73 | chapters: { 74 | label: "Chapters", 75 | classes: [ 76 | "ytd-engagement-panel-section-list-renderer.style-scope.ytd-watch-flexy" 77 | ], 78 | show: true 79 | }, 80 | title: { 81 | label: "Video Title", 82 | classes: ["yt-formatted-string.style-scope.ytd-watch-metadata"], 83 | show: true 84 | }, 85 | sub_count: { 86 | label: "Subscriber Count", 87 | classes: ["yt-formatted-string.style-scope.ytd-video-owner-renderer"], 88 | show: true 89 | }, 90 | description: { 91 | label: "Description Box", 92 | classes: ["ytd-text-inline-expander.style-scope.ytd-watch-metadata"], 93 | show: true 94 | }, 95 | merch: { 96 | label: "Merchandise Box", 97 | classes: ["ytd-merch-shelf-renderer.style-scope.ytd-watch-flexy"], 98 | show: true 99 | }, 100 | subscribe: { 101 | label: "Subscribe Button", 102 | classes: ["yt-button-shape.style-scope.ytd-subscribe-button-renderer"], 103 | show: true 104 | } 105 | }, 106 | Everywhere: { 107 | metadata: { 108 | label: "Video Metadata", 109 | classes: [ 110 | "span.inline-metadata-item.style-scope.ytd-video-meta-block", 111 | "yt-formatted-string.style-scope.ytd-channel-name", 112 | "yt-icon.style-scope.ytd-badge-supported-renderer", 113 | "div.badge.badge-style-type-live-now-alternate.style-scope.ytd-badge-supported-renderer", 114 | "#metadata-line", 115 | "#byline-container" 116 | ], 117 | show: true 118 | }, 119 | duration: { 120 | label: "Video Duration", 121 | classes: [ 122 | ".ytd-thumbnail-overlay-time-status-renderer", 123 | "ytd-thumbnail-overlay-time-status-renderer.style-scope.ytd-thumbnail" 124 | ], 125 | show: true 126 | }, 127 | thumbnails: { 128 | label: "Video Thumbnails", 129 | classes: [ 130 | ".ytd-macro-markers-list-item-renderer>img", 131 | ".thumbnail-container.style-scope.ytd-notification-renderer", 132 | ".yt-core-image--loaded" 133 | ], 134 | show: true 135 | }, 136 | resume: { 137 | label: "Resume Bar", 138 | classes: [ 139 | ".ytd-thumbnail-overlay-resume-playback-renderer", 140 | "ytd-thumbnail-overlay-resume-playback-renderer.style-scope.ytd-thumbnail " 141 | ], 142 | show: true 143 | }, 144 | logo: { 145 | label: "YouTube Logo", 146 | classes: ["#logo .ytd-topbar-logo-renderer"], 147 | show: true 148 | }, 149 | channelThumb: { 150 | label: "Channel Avatar", 151 | classes: [ 152 | "#avatar", 153 | "#channel-thumbnail", 154 | "tp-yt-paper-item.style-scope.ytd-guide-entry-renderer > yt-img-shadow" 155 | ], 156 | show: true 157 | } 158 | }, 159 | Special: { 160 | emoji: { 161 | label: "Emoji", 162 | classes: ["small-emoji"], 163 | show: true 164 | }, 165 | greyscale: { 166 | label: "Greyscale", 167 | classes: null, 168 | show: true 169 | } 170 | } 171 | } 172 | }; 173 | -------------------------------------------------------------------------------- /src/emoji.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | // TODO: tighten-up debounced scheduling 3 | // TODO: node.attributes.title match and strip 4 | // TODO: emoji in chat (img with small-emoji class) 5 | 6 | if (typeof browser === "undefined") { 7 | var browser = chrome; 8 | } 9 | 10 | const pattern = 11 | /\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu; 12 | 13 | const nodesToScan = ["SPAN", "YT-FORMATTED-STRING", "TITLE", "A"]; 14 | let emojishow = true, 15 | ignoreMutations = false, 16 | fullClearFirstScheduledTime = 0, 17 | fullClearTimeout = null, 18 | totalTime = 0, 19 | node, 20 | hashmap = new Map(); 21 | 22 | function scheduleDebouncedFullClear(debounceTimeMs, maxDebounceTimeMs) { 23 | const scheduled = fullClearTimeout !== null; 24 | 25 | if (scheduled) { 26 | const timeDiff = Date.now() - fullClearFirstScheduledTime; 27 | const shouldBlock = timeDiff + debounceTimeMs > maxDebounceTimeMs; 28 | if (maxDebounceTimeMs && shouldBlock) return; 29 | clearTimeout(fullClearTimeout); 30 | } else { 31 | fullClearFirstScheduledTime = Date.now(); 32 | } 33 | 34 | fullClearTimeout = emojishow 35 | ? setTimeout(fullClear, debounceTimeMs) 36 | : setTimeout(fullRestore, debounceTimeMs); 37 | } 38 | 39 | async function fullClear() { 40 | const start = Date.now(); 41 | await toggleEmoji(document.body, true); 42 | 43 | totalTime += Date.now() - start; 44 | fullClearTimeout = null; 45 | } 46 | 47 | async function fullRestore() { 48 | const start = Date.now(); 49 | await toggleEmoji(document.body, false); 50 | 51 | totalTime += Date.now() - start; 52 | fullClearTimeout = null; 53 | } 54 | 55 | async function toggleEmoji(element, remove) { 56 | if (!element) return; 57 | if (remove === undefined) return; 58 | 59 | const treeWalker = document.createTreeWalker( 60 | element, 61 | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT 62 | ); 63 | 64 | var refId = 0; 65 | // index to start allocating at 66 | 67 | var refs = {}; 68 | // store all refs here 69 | 70 | function mallocRef(obj) { 71 | // will return an int to wa, which is the index at which obj starts 72 | var id = refId; 73 | ++refId; 74 | refId[id] = obj; 75 | return id; 76 | } 77 | 78 | // This looks up the JS object based upon its id 79 | function lookupJsRef(id) { 80 | return refs[id]; 81 | } 82 | 83 | // This cleans up the memory for the JS object (by allowing it to be garbage collected) 84 | function freeJsRef(id) { 85 | delete refs[id]; 86 | } 87 | 88 | var imports = { 89 | env: { 90 | // push: function (id, value) { 91 | // lookupJsRef(id).push(value); 92 | // }, 93 | 94 | restore: function (hmapId, nodeId) { 95 | let hmap = lookupJsRef(hmapId); 96 | let n = lookupJsRef(nodeId); 97 | 98 | for (let o = 0; o < Object.keys(hmap).length; o++) { 99 | const el = hmap[Object.keys(hmap)[o]]; 100 | if (n.nodeValue == el.strip) { 101 | n.nodeValue = el.orig; 102 | } 103 | } 104 | }, 105 | 106 | createMapRef: function () { 107 | return mallocRef(hashmap); 108 | }, 109 | 110 | createNodeRef: function () { 111 | return mallocRef(node); 112 | }, 113 | 114 | length: function (id) { 115 | return lookupJsRef(id).length; 116 | }, 117 | 118 | logInt: function (value) { 119 | console.log(value); 120 | }, 121 | 122 | logRef: function (id) { 123 | console.log(lookupJsRef(id)); 124 | }, 125 | 126 | free: function (id) { 127 | freeJsRef(id); 128 | } 129 | } 130 | }; 131 | 132 | while ((node = treeWalker.nextNode())) { 133 | // TODO: move loop to wasm polynomial-time (currently exponential) 134 | 135 | if (nodesToScan.indexOf(node.parentElement.tagName) >= 0) { 136 | // TODO: use google/re2-wasm to match(how else?) 137 | 138 | const matches = node.nodeValue && node.nodeValue.match(pattern); 139 | 140 | if (matches && remove) { 141 | // TODO: replace string in WA 142 | 143 | let strip = node.nodeValue.replace(pattern, ""); 144 | 145 | if (!strip.length) { 146 | strip = " "; 147 | } 148 | 149 | if (hashmap[node.nodeValue] === undefined) { 150 | hashmap[node.nodeValue] = { 151 | orig: node.nodeValue, 152 | strip 153 | }; 154 | return; 155 | } 156 | 157 | if (hashmap[node.nodeValue] != undefined) { 158 | hashmap[node.nodeValue] = { 159 | orig: node.nodeValue, 160 | strip 161 | }; 162 | } 163 | 164 | node.nodeValue = strip; 165 | } 166 | 167 | if (!emojishow) { 168 | var wasmPath = chrome.runtime.getURL("wasm/emoji.wasm"); 169 | fetch(wasmPath) 170 | .then((response) => response.arrayBuffer()) 171 | .then((bytes) => WebAssembly.instantiate(bytes, imports)) 172 | .then((results) => { 173 | results.instance.exports.put_back(); 174 | }); 175 | } 176 | } 177 | } 178 | return; 179 | } 180 | 181 | async function onMutation(mutations) { 182 | if (ignoreMutations) return; 183 | 184 | const start = Date.now(); 185 | 186 | for (let i = 0; i < mutations.length; i++) { 187 | const mutation = mutations[i]; 188 | for (let j = 0; j < mutation.addedNodes.length; j++) { 189 | // mutated node 190 | const mnode = mutation.addedNodes[j]; 191 | const el = hashmap[mnode.nodeValue]; 192 | 193 | if (!el) { 194 | ignoreMutations = true; 195 | await toggleEmoji(mnode, emojishow); 196 | ignoreMutations = false; 197 | } else { 198 | const matches = mnode.nodeValue && mnode.nodeValue.match(pattern); 199 | if (matches && el) { 200 | mnode.nodeValue = emojishow ? el.strip : el.orig; 201 | } 202 | } 203 | } 204 | } 205 | 206 | totalTime += Date.now() - start; 207 | scheduleDebouncedFullClear(500, 1000); 208 | } 209 | 210 | MutationObserver = window.MutationObserver || window.WebKitMutationObserver; 211 | let observer = new MutationObserver(onMutation); 212 | 213 | observer.observe(document, { 214 | attributes: true, 215 | childList: true, 216 | subtree: true 217 | }); 218 | 219 | await browser.runtime.onMessage.addListener(async (request, sender) => { 220 | const { element } = await JSON.parse(request); 221 | const { settings } = await browser.storage.local.get(); 222 | const { options } = settings; 223 | emojishow = options["Special"].emoji.show; 224 | 225 | switch (element) { 226 | case "emoji": 227 | emojishow ? fullClear() : fullRestore(); 228 | break; 229 | default: 230 | break; 231 | } 232 | }); 233 | })(); 234 | -------------------------------------------------------------------------------- /src/ff_manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Nirvana for YouTube", 3 | "manifest_version": 2, 4 | "name": "ZYT", 5 | "version": "1.1.5", 6 | "author": "surajsharma", 7 | "homepage_url": "https://evenzero.in/experiments", 8 | "icons": { 9 | "144": "icons/favicon4_144.png" 10 | }, 11 | 12 | "background": { 13 | "scripts": ["background.js"] 14 | }, 15 | 16 | "content_scripts": [ 17 | { 18 | "matches": ["*://*.youtube.com/*"], 19 | "js": [ 20 | "browser-polyfill.js", 21 | "defaultSettings.js", 22 | "emoji.js", 23 | "contentscript.js" 24 | ], 25 | "run_at": "document_start" 26 | } 27 | ], 28 | 29 | "browser_action": { 30 | "default_icon": "icons/favicon4_144.png", 31 | "default_title": "Nirvana for YouTube", 32 | "default_popup": "options/options.html" 33 | }, 34 | 35 | "permissions": ["storage", "tabs", "scripting", "activeTab"] 36 | } 37 | -------------------------------------------------------------------------------- /src/icons/favicon2_144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/icons/favicon2_144.png -------------------------------------------------------------------------------- /src/icons/favicon3_144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/icons/favicon3_144.png -------------------------------------------------------------------------------- /src/icons/favicon4_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/icons/favicon4_128.png -------------------------------------------------------------------------------- /src/icons/favicon4_144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/icons/favicon4_144.png -------------------------------------------------------------------------------- /src/icons/favicon4_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/icons/favicon4_64.png -------------------------------------------------------------------------------- /src/icons/favicon_144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/icons/favicon_144.png -------------------------------------------------------------------------------- /src/icons/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/icons/logo.png -------------------------------------------------------------------------------- /src/icons/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/icons/logo2.png -------------------------------------------------------------------------------- /src/icons/off.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/on.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Nirvana for YouTube", 3 | "manifest_version": 3, 4 | "name": "ZYT", 5 | "version": "1.1.5", 6 | "author": "surajsharma", 7 | "homepage_url": "https://evenzero.in/experiments", 8 | "icons": { 9 | "144": "icons/favicon4_144.png" 10 | }, 11 | 12 | "background": { 13 | "service_worker": "background.js" 14 | }, 15 | 16 | "content_scripts": [ 17 | { 18 | "matches": ["*://*.youtube.com/*"], 19 | "js": [ 20 | "browser-polyfill.js", 21 | "defaultSettings.js", 22 | "emoji.js", 23 | "contentscript.js" 24 | ], 25 | "run_at": "document_start" 26 | } 27 | ], 28 | 29 | "web_accessible_resources": [ 30 | { 31 | "resources": [ 32 | "wasm/emoji.wasm", 33 | "defaultSettings.js", 34 | "emoji.js", 35 | "contentscript.js" 36 | ], 37 | "matches": ["*://*.youtube.com/*"] 38 | } 39 | ], 40 | 41 | "action": { 42 | "default_icon": "icons/favicon4_144.png", 43 | "default_title": "Nirvana for YouTube", 44 | "default_popup": "options/options.html" 45 | }, 46 | 47 | "permissions": ["storage", "tabs", "scripting", "activeTab"], 48 | "host_permissions": ["*://*.youtube.com/*"] 49 | } 50 | -------------------------------------------------------------------------------- /src/options/options.css: -------------------------------------------------------------------------------- 1 | body { 2 | width: 20em; 3 | font-family: -apple-system, BlinkMacSystemFont, "Roboto", sans-serif; 4 | font-size: 0.9em; 5 | font-weight: 300; 6 | background: rgb(17,17,17); 7 | background: linear-gradient(0deg, rgba(17, 17, 17, 1) 0%, rgba(5,5,5,1) 100%); 8 | color: white; 9 | -webkit-user-select: none important!; 10 | /* Safari */ 11 | -webkit-user-drag: none important!; 12 | -ms-user-select: none important!; 13 | /* IE 10 and IE 11 */ 14 | user-select: none important!; 15 | /* Standard syntax */ 16 | overflow: hidden; 17 | } 18 | section.clear-options { 19 | padding: 0.5em 0; 20 | margin: 1em 0; 21 | } 22 | #clear-button { 23 | margin: 0 1.3em 1em 0; 24 | } 25 | section.clear-options input,section.clear-options > select,#clear-button { 26 | float: right; 27 | } 28 | label { 29 | display: block; 30 | padding: 0.2em 0; 31 | } 32 | label:hover { 33 | cursor: pointer; 34 | } 35 | .logo { 36 | margin-top: -10px; 37 | display: flex; 38 | justify-content: space-between; 39 | align-items: center; 40 | justify-items: center; 41 | } 42 | .icon { 43 | margin-top: -5px; 44 | } 45 | .title { 46 | font-size: 1.5rem; 47 | margin-bottom: 0.5rem; 48 | color: lightgray; 49 | font-weight: 100; 50 | background-color: #000; 51 | font-family: -apple-system, BlinkMacSystemFont, "Roboto", sans-serif; 52 | } 53 | p { 54 | color: lightgray; 55 | } 56 | 57 | /* Switch */ 58 | .button-12 { 59 | display: flex; 60 | flex-direction: column; 61 | align-items: center; 62 | padding: 6px 14px; 63 | font-family: -apple-system, BlinkMacSystemFont, "Roboto", sans-serif; 64 | border-radius: 6px; 65 | border: 1px solid rgba(250, 50, 50, 0.1); 66 | background: rgba(94, 255, 0, 0.2); 67 | box-shadow: 0px 0.5px 1px rgba(0, 0, 0, 0.5),inset 0px 0.5px 0.5px rgba(255, 255, 255, 0.5),0px 0px 0px 0.5px rgba(0, 0, 0, 0.12); 68 | color: #dfdedf; 69 | user-select: none; 70 | -webkit-user-select: none; 71 | touch-action: manipulation; 72 | } 73 | .button-12:focus { 74 | box-shadow: inset 0px 0.8px 0px -0.25px rgba(255, 255, 255, 0.2),0px 0.5px 1px rgba(0, 0, 0, 0.1),0px 0px 0px 3.5px rgba(58, 108, 217, 0.5); 75 | outline: 0; 76 | } 77 | .button-12:hover { 78 | background: rgba(250, 50, 50, 0.5); 79 | transition: 0.2s; 80 | cursor: pointer; 81 | } 82 | .toggle { 83 | display: flex; 84 | justify-content: space-between; 85 | align-items: center; 86 | /* Vertical */ 87 | font-size: 1rem; 88 | padding: 2px; 89 | font-weight: 300; 90 | border-radius: 10px; 91 | } 92 | .switch input { 93 | display: none; 94 | } 95 | .switch { 96 | display: inline-block; 97 | width: 60px; 98 | /*=w*/ 99 | height: 15px; 100 | /*=h*/ 101 | margin: 4px; 102 | /* transform: translateY(50%); 103 | */ 104 | position: relative; 105 | } 106 | .slider { 107 | position: absolute; 108 | top: 0; 109 | bottom: 0; 110 | left: 0; 111 | right: 0; 112 | border-radius: 30px; 113 | box-shadow: 0 0 0 2px #777, 0 0 4px #777; 114 | cursor: pointer; 115 | border: 4px solid transparent; 116 | overflow: hidden; 117 | transition: 0.1s; 118 | } 119 | .slider:before { 120 | position: absolute; 121 | content: ""; 122 | width: 100%; 123 | height: 100%; 124 | background-color: #777; 125 | border-radius: 30px; 126 | transform: translateX(-30px); 127 | opacity: 0.85; 128 | /*translateX(-(w-h))*/ 129 | transition: 0.1s; 130 | } 131 | input:checked + .slider:before { 132 | transform: translateX(30px); 133 | /*translateX(w-h)*/ 134 | } 135 | .switch200 .slider:before { 136 | /* width: 200%; 137 | */ 138 | transform: translateX(-30px); 139 | /*translateX(-(w-h))*/ 140 | } 141 | .switch200 input:checked + .slider:before { 142 | background-color: red; 143 | } 144 | .switch200 input:checked + .slider { 145 | box-shadow: 0 0 0 2px red, 0 0 8px red; 146 | } 147 | #popup{ 148 | margin-bottom: 10px; 149 | } 150 | #popup > div:nth-child(odd) { 151 | background-color: rgba(30, 30, 30, 0.1); 152 | color:gray; 153 | } 154 | 155 | /* to color even children in parent-container with children of type div */ 156 | #popup > div:nth-child(even) { 157 | background-color: rgba(0, 0, 0, 0.1); 158 | margin-top: 1px; 159 | color: gray; 160 | } 161 | #popup > div > span:hover{ 162 | cursor: pointer; 163 | color: red; 164 | } 165 | 166 | /* footer */ 167 | .footer { 168 | height : 15px; 169 | } 170 | 171 | .footer_label { 172 | display: flex; 173 | flex-direction: column; 174 | justify-content: left; 175 | align-items: left; 176 | font-size: 0.8em; 177 | font-style: italic; 178 | line-height: 0 ; 179 | } 180 | 181 | #brand { 182 | color: #555; 183 | margin-top: -1px; 184 | } 185 | 186 | #version { 187 | color: #555; 188 | } 189 | 190 | #brand:hover { 191 | color: red; 192 | margin-top: -1px; 193 | cursor: pointer; 194 | } 195 | 196 | #version:hover { 197 | color: #3cff00;; 198 | cursor: pointer; 199 | } 200 | 201 | 202 | /* Custom Select */ 203 | .custom-select { 204 | position: relative; 205 | margin-top: 5px; 206 | margin-bottom: 10px; 207 | border: 1px solid rgba(124, 11, 9, 1); 208 | border-radius:5px; 209 | } 210 | 211 | .custom-select select { 212 | display: none; 213 | /*hide original SELECT element: */ 214 | } 215 | 216 | .select-selected { 217 | background-color: rgba(124, 11, 9, 0.2); 218 | border-radius:5px; 219 | } 220 | 221 | /* Style the arrow inside the select element: */ 222 | 223 | .select-selected:after { 224 | position: absolute; 225 | content: ""; 226 | top: 14px; 227 | right: 10px; 228 | width: 0; 229 | height: 0; 230 | border: 6px solid transparent; 231 | border-color: #fff transparent transparent transparent; 232 | } 233 | 234 | /* Point the arrow upwards when the select box is open (active): */ 235 | 236 | .select-selected.select-arrow-active:after { 237 | border-color: transparent transparent #fff transparent; 238 | top: 7px; 239 | } 240 | 241 | /* style the items (options), including the selected item: */ 242 | 243 | .select-items div,.select-selected { 244 | color: #ffffff; 245 | padding: 8px 16px; 246 | /* border-color: transparent transparent rgba(0, 0, 0, 0.1) transparent; 247 | */ 248 | cursor: pointer; 249 | } 250 | 251 | /* Style items (options): */ 252 | 253 | .select-items { 254 | position: absolute; 255 | background-color: rgba(124, 11, 9, 1); 256 | top: 100%; 257 | left: 0; 258 | right: 0; 259 | z-index: 99; 260 | border-bottom-right-radius: 5px; 261 | border-bottom-left-radius:5px; 262 | } 263 | 264 | /* Hide the items when the select box is closed: */ 265 | 266 | .select-hide { 267 | display: none; 268 | } 269 | 270 | .select-items div:hover, .same-as-selected { 271 | background-color: rgba(0, 0, 0, 0.2); 272 | } 273 | 274 | .unselectable{ 275 | -moz-user-select: none; 276 | -webkit-user-select: none; 277 | -ms-user-select:none; 278 | user-select:none; 279 | -o-user-select:none; 280 | } 281 | -------------------------------------------------------------------------------- /src/options/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | NIRT 9 | 10 | 11 | 12 | 13 |
18 |
19 | 23 | 24 |
25 | 32 |
33 | 34 | 37 | 44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/options/options.js: -------------------------------------------------------------------------------- 1 | /* Popup handlers */ 2 | async function updateUI(restoredSettings) { 3 | /* Update the options UI with the settings values retrieved from storage, 4 | or the default settings if the stored settings are empty. */ 5 | 6 | if (!Object.keys(restoredSettings).length) { 7 | // there's nothing in the local storage 8 | // create default popup and store default settings 9 | await browser.storage.local.set({ settings: defaultSettings }); 10 | const { options, currentPage } = defaultSettings; 11 | repopulatePopup(options, currentPage); 12 | setDropdownSelect(currentPage); 13 | } 14 | 15 | // set UI according to local storage 16 | const { options, currentPage } = restoredSettings; 17 | repopulatePopup(options, currentPage); 18 | setDropdownSelect(currentPage); 19 | } 20 | 21 | function repopulatePopup(options, cp) { 22 | if (!options) return; 23 | 24 | const popup = document.getElementById("popup"); 25 | 26 | //clear old fields 27 | while (popup.firstChild) { 28 | popup.removeChild(popup.lastChild); 29 | } 30 | 31 | //add new fields 32 | for (page of Object.keys(options)) { 33 | if (page === cp) { 34 | Object.keys(options[page]).forEach((item) => { 35 | // insert toggle field 36 | const togg = options[page][item]; 37 | 38 | const field = togg.show 39 | ? `
40 | ${togg.label} 41 | 45 |
` 46 | : `
47 | ${togg.label} 48 | 52 |
`; 53 | 54 | //TODO: web-ext warns of potential security risk, find another way 55 | popup.insertAdjacentHTML("afterbegin", field); 56 | 57 | //add event listener 58 | const el = document.getElementById(item); 59 | 60 | el && 61 | el.addEventListener("click", async (evt) => { 62 | messagePageScript({ 63 | element: item, 64 | event: evt, 65 | settings: await storeChangedSettings(item) 66 | }); 67 | }); 68 | 69 | const te = document.getElementById(page + item); 70 | 71 | te && 72 | te.addEventListener("click", async (evt) => { 73 | const e = document.getElementById(item); 74 | e && e.click(); 75 | 76 | messagePageScript({ 77 | element: item, 78 | event: evt, 79 | settings: await storeChangedSettings(item) 80 | }); 81 | }); 82 | }); 83 | } 84 | } 85 | } 86 | 87 | function setDropdownSelect(page) { 88 | if (!page) return; 89 | document.querySelector(".select-selected").innerText = page; 90 | } 91 | 92 | async function storeChangedSettings(changed) { 93 | /*fires when a toggle is clicked, syncs local storage */ 94 | 95 | let { settings } = await browser.storage.local.get(); 96 | 97 | const { currentPage } = settings; 98 | 99 | function getChangedOptions() { 100 | let changedOptions = settings.options; 101 | 102 | const checkboxes = document.querySelectorAll(".data-types [type=checkbox]"); 103 | 104 | if (!checkboxes) return; 105 | 106 | for (let item of checkboxes) { 107 | if (item.id === changed) { 108 | changedOptions[currentPage][changed]["show"] = item.checked; 109 | } 110 | } 111 | 112 | return changedOptions; 113 | } 114 | 115 | const newOptions = getChangedOptions(); 116 | 117 | settings.options = newOptions; 118 | await browser.storage.local.set({ settings }); 119 | return settings; 120 | } 121 | 122 | async function selectionChanged(value) { 123 | /* called in select.js, fired when select dropdown changes, 124 | syncs selected value to local storage 125 | */ 126 | 127 | if (value == "Select Page") return; 128 | let { settings } = await browser.storage.local.get(); 129 | 130 | if (value != settings.currentPage) { 131 | let dropdown = document.getElementById("dropdown"); 132 | 133 | if (settings) { 134 | for (let i = 0; i < dropdown.length; i++) { 135 | if (dropdown[i].innerText == value) { 136 | settings.currentPage = value; 137 | await browser.storage.local.set({ settings }); 138 | } 139 | } 140 | updateUI(settings); 141 | } 142 | } 143 | } 144 | 145 | /* Content Script handlers */ 146 | async function sendMessageToTabs(tabs, msg) { 147 | for (const tab of tabs) { 148 | try { 149 | await browser.tabs.sendMessage(tab.id, JSON.stringify(msg)); 150 | console.log(`🪛 ${msg.element} sent to ${tab.id}`); 151 | } catch (err) { 152 | onError(err); 153 | } 154 | } 155 | return true; 156 | } 157 | 158 | async function messagePageScript(msg) { 159 | /*Find all tabs, send a message to the page script.*/ 160 | try { 161 | let tabs = await browser.tabs.query({ url: "*://*.youtube.com/*" }); 162 | let res = await sendMessageToTabs(tabs, msg); 163 | } catch (err) { 164 | onError(err); 165 | } 166 | } 167 | 168 | async function injectScript() { 169 | /* inject content script into all yt tabs*/ 170 | try { 171 | let tabs = await browser.tabs.query({ url: "*://*.youtube.com/*" }); 172 | for await (const t of tabs) { 173 | const injection = await browser.scripting.executeScript({ 174 | target: { tabId: t.id }, 175 | files: ["defaultSettings.js", "emoji.js", "contentscript.js"] 176 | }); 177 | } 178 | } catch (error) { 179 | onError(error); 180 | } 181 | return true; 182 | } 183 | 184 | (async () => { 185 | /* On opening the options page, fetch stored settings and update the UI with them.*/ 186 | 187 | try { 188 | /* ip link */ 189 | let icon = document.getElementById("brand"); 190 | icon.addEventListener("click", () => { 191 | browser.tabs.create({ active: true, url: "https://ko-fi.com/evenzero" }); 192 | }); 193 | 194 | /* version display*/ 195 | let ver = document.getElementById("version"); 196 | ver.innerText = "Ver: " + browser.runtime.getManifest().version; 197 | 198 | /* version link*/ 199 | ver.addEventListener("click", () => { 200 | browser.tabs.create({ 201 | active: true, 202 | url: "https://github.com/inversepolarity/ZenYT" 203 | }); 204 | }); 205 | 206 | const { settings } = await browser.storage.local.get(); 207 | 208 | if (settings) { 209 | updateUI(settings); 210 | } 211 | 212 | /* inject contentscript */ 213 | await injectScript(); 214 | } catch (err) { 215 | onError(err); 216 | } 217 | })(); 218 | 219 | function onError(e) { 220 | console.error(e); 221 | } 222 | -------------------------------------------------------------------------------- /src/options/select.js: -------------------------------------------------------------------------------- 1 | let x, i, j, l, ll, selElmnt, a, b, c; 2 | /* Look for any elements with the class "custom-select": */ 3 | x = document.getElementsByClassName("custom-select"); 4 | l = x.length; 5 | for (i = 0; i < l; i++) { 6 | selElmnt = x[i].getElementsByTagName("select")[0]; 7 | ll = selElmnt.length; 8 | /* For each element, create a new DIV that will act as the selected item: */ 9 | a = document.createElement("DIV"); 10 | a.setAttribute("class", "select-selected"); 11 | a.innerText = selElmnt.options[selElmnt.selectedIndex].innerText; 12 | x[i].appendChild(a); 13 | /* For each element, create a new DIV that will contain the option list: */ 14 | b = document.createElement("DIV"); 15 | b.setAttribute("class", "select-items select-hide"); 16 | for (j = 1; j < ll; j++) { 17 | /* For each option in the original select element, 18 | create a new DIV that will act as an option item: */ 19 | c = document.createElement("DIV"); 20 | c.innerText = selElmnt.options[j].innerText; 21 | c.addEventListener("click", function (e) { 22 | /* When an item is clicked, update the original select box, 23 | and the selected item: */ 24 | let y, i, k, s, h, sl, yl; 25 | s = this.parentNode.parentNode.getElementsByTagName("select")[0]; 26 | sl = s.length; 27 | h = this.parentNode.previousSibling; 28 | for (i = 0; i < sl; i++) { 29 | if (s.options[i].innerText == this.innerText) { 30 | s.selectedIndex = i; 31 | h.innerText = this.innerText; 32 | y = this.parentNode.getElementsByClassName("same-as-selected"); 33 | yl = y.length; 34 | for (k = 0; k < yl; k++) { 35 | y[k].removeAttribute("class"); 36 | } 37 | this.setAttribute("class", "same-as-selected"); 38 | break; 39 | } 40 | } 41 | h.click(); 42 | }); 43 | b.appendChild(c); 44 | } 45 | x[i].appendChild(b); 46 | a.addEventListener("click", function (e) { 47 | /* When the select box is clicked, close any other select boxes, 48 | and open/close the current select box: */ 49 | e.stopPropagation(); 50 | closeAllSelect(this); 51 | this.nextSibling.classList.toggle("select-hide"); 52 | this.classList.toggle("select-arrow-active"); 53 | selectionChanged(e.target.innerText); 54 | }); 55 | } 56 | 57 | function closeAllSelect(elmnt) { 58 | /* A function that will close all select boxes in the document, 59 | except the current select box: */ 60 | let x, 61 | y, 62 | i, 63 | xl, 64 | yl, 65 | arrNo = []; 66 | x = document.getElementsByClassName("select-items"); 67 | y = document.getElementsByClassName("select-selected"); 68 | xl = x.length; 69 | yl = y.length; 70 | for (i = 0; i < yl; i++) { 71 | if (elmnt == y[i]) { 72 | arrNo.push(i); 73 | } else { 74 | y[i].classList.remove("select-arrow-active"); 75 | } 76 | } 77 | for (i = 0; i < xl; i++) { 78 | if (arrNo.indexOf(i)) { 79 | x[i].classList.add("select-hide"); 80 | } 81 | } 82 | } 83 | 84 | /* If the user clicks anywhere outside the select box, 85 | then close all select boxes: */ 86 | document.addEventListener("click", closeAllSelect); 87 | -------------------------------------------------------------------------------- /src/promo/ZYT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/promo/ZYT.png -------------------------------------------------------------------------------- /src/safari/Shared (App)/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "1024x1024", 5 | "idiom" : "universal", 6 | "filename" : "universal-icon-1024@1x.png", 7 | "platform" : "ios" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "mac-icon-16@1x.png", 13 | "scale" : "1x" 14 | }, 15 | { 16 | "size" : "16x16", 17 | "idiom" : "mac", 18 | "filename" : "mac-icon-16@2x.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "mac-icon-32@1x.png", 25 | "scale" : "1x" 26 | }, 27 | { 28 | "size" : "32x32", 29 | "idiom" : "mac", 30 | "filename" : "mac-icon-32@2x.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "mac-icon-128@1x.png", 37 | "scale" : "1x" 38 | }, 39 | { 40 | "size" : "128x128", 41 | "idiom" : "mac", 42 | "filename" : "mac-icon-128@2x.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "mac-icon-256@1x.png", 49 | "scale" : "1x" 50 | }, 51 | { 52 | "size" : "256x256", 53 | "idiom" : "mac", 54 | "filename" : "mac-icon-256@2x.png", 55 | "scale" : "2x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "mac-icon-512@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "512x512", 65 | "idiom" : "mac", 66 | "filename" : "mac-icon-512@2x.png", 67 | "scale" : "2x" 68 | } 69 | ], 70 | "info" : { 71 | "version" : 1, 72 | "author" : "xcode" 73 | } 74 | } -------------------------------------------------------------------------------- /src/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-128@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-128@1x.png -------------------------------------------------------------------------------- /src/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-128@2x.png -------------------------------------------------------------------------------- /src/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-16@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-16@1x.png -------------------------------------------------------------------------------- /src/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-16@2x.png -------------------------------------------------------------------------------- /src/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-256@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-256@1x.png -------------------------------------------------------------------------------- /src/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-256@2x.png -------------------------------------------------------------------------------- /src/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-32@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-32@1x.png -------------------------------------------------------------------------------- /src/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-32@2x.png -------------------------------------------------------------------------------- /src/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-512@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-512@1x.png -------------------------------------------------------------------------------- /src/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-512@2x.png -------------------------------------------------------------------------------- /src/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/universal-icon-1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/Shared (App)/Assets.xcassets/AppIcon.appiconset/universal-icon-1024@1x.png -------------------------------------------------------------------------------- /src/safari/Shared (App)/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/safari/Shared (App)/Assets.xcassets/LargeIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "favicon4_144.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /src/safari/Shared (App)/Assets.xcassets/LargeIcon.imageset/favicon4_144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/Shared (App)/Assets.xcassets/LargeIcon.imageset/favicon4_144.png -------------------------------------------------------------------------------- /src/safari/Shared (App)/Base.lproj/Main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ZenTube Icon 14 |

You can turn on ZenTube’s Safari extension in Settings.

15 |

You can turn on ZenTube’s extension in Safari Extensions preferences.

16 |

ZenTube’s extension is currently on. You can turn it off in Safari Extensions preferences.

17 |

ZenTube’s extension is currently off. You can turn it on in Safari Extensions preferences.

18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/safari/Shared (App)/Resources/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/Shared (App)/Resources/Icon.png -------------------------------------------------------------------------------- /src/safari/Shared (App)/Resources/Script.js: -------------------------------------------------------------------------------- 1 | function show(platform, enabled, useSettingsInsteadOfPreferences) { 2 | document.body.classList.add(`platform-${platform}`); 3 | 4 | if (useSettingsInsteadOfPreferences) { 5 | document.getElementsByClassName('platform-mac state-on')[0].innerText = "ZenTube’s extension is currently on. You can turn it off in the Extensions section of Safari Settings."; 6 | document.getElementsByClassName('platform-mac state-off')[0].innerText = "ZenTube’s extension is currently off. You can turn it on in the Extensions section of Safari Settings."; 7 | document.getElementsByClassName('platform-mac state-unknown')[0].innerText = "You can turn on ZenTube’s extension in the Extensions section of Safari Settings."; 8 | document.getElementsByClassName('platform-mac open-preferences')[0].innerText = "Quit and Open Safari Settings…"; 9 | } 10 | 11 | if (typeof enabled === "boolean") { 12 | document.body.classList.toggle(`state-on`, enabled); 13 | document.body.classList.toggle(`state-off`, !enabled); 14 | } else { 15 | document.body.classList.remove(`state-on`); 16 | document.body.classList.remove(`state-off`); 17 | } 18 | } 19 | 20 | function openPreferences() { 21 | webkit.messageHandlers.controller.postMessage("open-preferences"); 22 | } 23 | 24 | document.querySelector("button.open-preferences").addEventListener("click", openPreferences); 25 | -------------------------------------------------------------------------------- /src/safari/Shared (App)/Resources/Style.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-user-select: none; 3 | -webkit-user-drag: none; 4 | cursor: default; 5 | } 6 | 7 | :root { 8 | color-scheme: light dark; 9 | 10 | --spacing: 20px; 11 | } 12 | 13 | html { 14 | height: 100%; 15 | } 16 | 17 | body { 18 | display: flex; 19 | align-items: center; 20 | justify-content: center; 21 | flex-direction: column; 22 | 23 | gap: var(--spacing); 24 | margin: 0 calc(var(--spacing) * 2); 25 | height: 100%; 26 | 27 | font: -apple-system-short-body; 28 | text-align: center; 29 | } 30 | 31 | body:not(.platform-mac, .platform-ios) :is(.platform-mac, .platform-ios) { 32 | display: none; 33 | } 34 | 35 | body.platform-ios .platform-mac { 36 | display: none; 37 | } 38 | 39 | body.platform-mac .platform-ios { 40 | display: none; 41 | } 42 | 43 | body.platform-ios .platform-mac { 44 | display: none; 45 | } 46 | 47 | body:not(.state-on, .state-off) :is(.state-on, .state-off) { 48 | display: none; 49 | } 50 | 51 | body.state-on :is(.state-off, .state-unknown) { 52 | display: none; 53 | } 54 | 55 | body.state-off :is(.state-on, .state-unknown) { 56 | display: none; 57 | } 58 | 59 | button { 60 | font-size: 1em; 61 | } 62 | -------------------------------------------------------------------------------- /src/safari/Shared (App)/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Shared (App) 4 | // 5 | // Created by admin on 28/11/22. 6 | // 7 | 8 | import WebKit 9 | 10 | #if os(iOS) 11 | import UIKit 12 | typealias PlatformViewController = UIViewController 13 | #elseif os(macOS) 14 | import Cocoa 15 | import SafariServices 16 | typealias PlatformViewController = NSViewController 17 | #endif 18 | 19 | let extensionBundleIdentifier = "in.evenzero.ZenTube.Extension" 20 | 21 | class ViewController: PlatformViewController, WKNavigationDelegate, WKScriptMessageHandler { 22 | 23 | @IBOutlet var webView: WKWebView! 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | 28 | self.webView.navigationDelegate = self 29 | 30 | #if os(iOS) 31 | self.webView.scrollView.isScrollEnabled = false 32 | #endif 33 | 34 | self.webView.configuration.userContentController.add(self, name: "controller") 35 | 36 | self.webView.loadFileURL(Bundle.main.url(forResource: "Main", withExtension: "html")!, allowingReadAccessTo: Bundle.main.resourceURL!) 37 | } 38 | 39 | func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { 40 | #if os(iOS) 41 | webView.evaluateJavaScript("show('ios')") 42 | #elseif os(macOS) 43 | webView.evaluateJavaScript("show('mac')") 44 | 45 | SFSafariExtensionManager.getStateOfSafariExtension(withIdentifier: extensionBundleIdentifier) { (state, error) in 46 | guard let state = state, error == nil else { 47 | // Insert code to inform the user that something went wrong. 48 | return 49 | } 50 | 51 | DispatchQueue.main.async { 52 | if #available(macOS 13, *) { 53 | webView.evaluateJavaScript("show('mac', \(state.isEnabled), true)") 54 | } else { 55 | webView.evaluateJavaScript("show('mac', \(state.isEnabled), false)") 56 | } 57 | } 58 | } 59 | #endif 60 | } 61 | 62 | func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { 63 | #if os(macOS) 64 | if (message.body as! String != "open-preferences") { 65 | return; 66 | } 67 | 68 | SFSafariApplication.showPreferencesForExtension(withIdentifier: extensionBundleIdentifier) { error in 69 | guard error == nil else { 70 | // Insert code to inform the user that something went wrong. 71 | return 72 | } 73 | 74 | DispatchQueue.main.async { 75 | NSApplication.shared.terminate(nil) 76 | } 77 | } 78 | #endif 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/safari/Shared (Extension)/SafariWebExtensionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafariWebExtensionHandler.swift 3 | // Shared (Extension) 4 | // 5 | // Created by admin on 28/11/22. 6 | // 7 | 8 | import SafariServices 9 | import os.log 10 | 11 | let SFExtensionMessageKey = "message" 12 | 13 | class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { 14 | 15 | func beginRequest(with context: NSExtensionContext) { 16 | let item = context.inputItems[0] as! NSExtensionItem 17 | let message = item.userInfo?[SFExtensionMessageKey] 18 | os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@", message as! CVarArg) 19 | 20 | let response = NSExtensionItem() 21 | response.userInfo = [ SFExtensionMessageKey: [ "Response to": message ] ] 22 | 23 | context.completeRequest(returningItems: [response], completionHandler: nil) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/safari/ZenTube.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/safari/ZenTube.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/safari/ZenTube.xcodeproj/project.xcworkspace/xcuserdata/admin.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/ZenTube.xcodeproj/project.xcworkspace/xcuserdata/admin.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /src/safari/ZenTube.xcodeproj/xcuserdata/admin.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | ZenTube (iOS).xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 1 11 | 12 | ZenTube (macOS).xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/safari/ZenTube/Shared (App)/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/safari/ZenTube/Shared (App)/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "1024x1024", 5 | "idiom" : "universal", 6 | "filename" : "universal-icon-1024@1x.png", 7 | "platform" : "ios" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "mac-icon-16@1x.png", 13 | "scale" : "1x" 14 | }, 15 | { 16 | "size" : "16x16", 17 | "idiom" : "mac", 18 | "filename" : "mac-icon-16@2x.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "mac-icon-32@1x.png", 25 | "scale" : "1x" 26 | }, 27 | { 28 | "size" : "32x32", 29 | "idiom" : "mac", 30 | "filename" : "mac-icon-32@2x.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "mac-icon-128@1x.png", 37 | "scale" : "1x" 38 | }, 39 | { 40 | "size" : "128x128", 41 | "idiom" : "mac", 42 | "filename" : "mac-icon-128@2x.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "mac-icon-256@1x.png", 49 | "scale" : "1x" 50 | }, 51 | { 52 | "size" : "256x256", 53 | "idiom" : "mac", 54 | "filename" : "mac-icon-256@2x.png", 55 | "scale" : "2x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "mac-icon-512@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "512x512", 65 | "idiom" : "mac", 66 | "filename" : "mac-icon-512@2x.png", 67 | "scale" : "2x" 68 | } 69 | ], 70 | "info" : { 71 | "version" : 1, 72 | "author" : "xcode" 73 | } 74 | } -------------------------------------------------------------------------------- /src/safari/ZenTube/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-128@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/ZenTube/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-128@1x.png -------------------------------------------------------------------------------- /src/safari/ZenTube/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/ZenTube/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-128@2x.png -------------------------------------------------------------------------------- /src/safari/ZenTube/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-16@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/ZenTube/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-16@1x.png -------------------------------------------------------------------------------- /src/safari/ZenTube/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/ZenTube/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-16@2x.png -------------------------------------------------------------------------------- /src/safari/ZenTube/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-256@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/ZenTube/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-256@1x.png -------------------------------------------------------------------------------- /src/safari/ZenTube/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/ZenTube/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-256@2x.png -------------------------------------------------------------------------------- /src/safari/ZenTube/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-32@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/ZenTube/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-32@1x.png -------------------------------------------------------------------------------- /src/safari/ZenTube/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/ZenTube/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-32@2x.png -------------------------------------------------------------------------------- /src/safari/ZenTube/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-512@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/ZenTube/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-512@1x.png -------------------------------------------------------------------------------- /src/safari/ZenTube/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/ZenTube/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-512@2x.png -------------------------------------------------------------------------------- /src/safari/ZenTube/Shared (App)/Assets.xcassets/AppIcon.appiconset/universal-icon-1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/ZenTube/Shared (App)/Assets.xcassets/AppIcon.appiconset/universal-icon-1024@1x.png -------------------------------------------------------------------------------- /src/safari/ZenTube/Shared (App)/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/safari/ZenTube/Shared (App)/Assets.xcassets/LargeIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "favicon4_144.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /src/safari/ZenTube/Shared (App)/Assets.xcassets/LargeIcon.imageset/favicon4_144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/ZenTube/Shared (App)/Assets.xcassets/LargeIcon.imageset/favicon4_144.png -------------------------------------------------------------------------------- /src/safari/ZenTube/Shared (App)/Base.lproj/Main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ZenTube Icon 14 |

You can turn on ZenTube’s Safari extension in Settings.

15 |

You can turn on ZenTube’s extension in Safari Extensions preferences.

16 |

ZenTube’s extension is currently on. You can turn it off in Safari Extensions preferences.

17 |

ZenTube’s extension is currently off. You can turn it on in Safari Extensions preferences.

18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/safari/ZenTube/Shared (App)/Resources/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/ZenTube/Shared (App)/Resources/Icon.png -------------------------------------------------------------------------------- /src/safari/ZenTube/Shared (App)/Resources/Script.js: -------------------------------------------------------------------------------- 1 | function show(platform, enabled, useSettingsInsteadOfPreferences) { 2 | document.body.classList.add(`platform-${platform}`); 3 | 4 | if (useSettingsInsteadOfPreferences) { 5 | document.getElementsByClassName('platform-mac state-on')[0].innerText = "ZenTube’s extension is currently on. You can turn it off in the Extensions section of Safari Settings."; 6 | document.getElementsByClassName('platform-mac state-off')[0].innerText = "ZenTube’s extension is currently off. You can turn it on in the Extensions section of Safari Settings."; 7 | document.getElementsByClassName('platform-mac state-unknown')[0].innerText = "You can turn on ZenTube’s extension in the Extensions section of Safari Settings."; 8 | document.getElementsByClassName('platform-mac open-preferences')[0].innerText = "Quit and Open Safari Settings…"; 9 | } 10 | 11 | if (typeof enabled === "boolean") { 12 | document.body.classList.toggle(`state-on`, enabled); 13 | document.body.classList.toggle(`state-off`, !enabled); 14 | } else { 15 | document.body.classList.remove(`state-on`); 16 | document.body.classList.remove(`state-off`); 17 | } 18 | } 19 | 20 | function openPreferences() { 21 | webkit.messageHandlers.controller.postMessage("open-preferences"); 22 | } 23 | 24 | document.querySelector("button.open-preferences").addEventListener("click", openPreferences); 25 | -------------------------------------------------------------------------------- /src/safari/ZenTube/Shared (App)/Resources/Style.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-user-select: none; 3 | -webkit-user-drag: none; 4 | cursor: default; 5 | } 6 | 7 | :root { 8 | color-scheme: light dark; 9 | 10 | --spacing: 20px; 11 | } 12 | 13 | html { 14 | height: 100%; 15 | } 16 | 17 | body { 18 | display: flex; 19 | align-items: center; 20 | justify-content: center; 21 | flex-direction: column; 22 | 23 | gap: var(--spacing); 24 | margin: 0 calc(var(--spacing) * 2); 25 | height: 100%; 26 | 27 | font: -apple-system-short-body; 28 | text-align: center; 29 | } 30 | 31 | body:not(.platform-mac, .platform-ios) :is(.platform-mac, .platform-ios) { 32 | display: none; 33 | } 34 | 35 | body.platform-ios .platform-mac { 36 | display: none; 37 | } 38 | 39 | body.platform-mac .platform-ios { 40 | display: none; 41 | } 42 | 43 | body.platform-ios .platform-mac { 44 | display: none; 45 | } 46 | 47 | body:not(.state-on, .state-off) :is(.state-on, .state-off) { 48 | display: none; 49 | } 50 | 51 | body.state-on :is(.state-off, .state-unknown) { 52 | display: none; 53 | } 54 | 55 | body.state-off :is(.state-on, .state-unknown) { 56 | display: none; 57 | } 58 | 59 | button { 60 | font-size: 1em; 61 | } 62 | -------------------------------------------------------------------------------- /src/safari/ZenTube/Shared (App)/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Shared (App) 4 | // 5 | // Created by admin on 19/04/23. 6 | // 7 | 8 | import WebKit 9 | 10 | #if os(iOS) 11 | import UIKit 12 | typealias PlatformViewController = UIViewController 13 | #elseif os(macOS) 14 | import Cocoa 15 | import SafariServices 16 | typealias PlatformViewController = NSViewController 17 | #endif 18 | 19 | let extensionBundleIdentifier = "in.evenzero.ZenTube.Extension" 20 | 21 | class ViewController: PlatformViewController, WKNavigationDelegate, WKScriptMessageHandler { 22 | 23 | @IBOutlet var webView: WKWebView! 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | 28 | self.webView.navigationDelegate = self 29 | 30 | #if os(iOS) 31 | self.webView.scrollView.isScrollEnabled = false 32 | #endif 33 | 34 | self.webView.configuration.userContentController.add(self, name: "controller") 35 | 36 | self.webView.loadFileURL(Bundle.main.url(forResource: "Main", withExtension: "html")!, allowingReadAccessTo: Bundle.main.resourceURL!) 37 | } 38 | 39 | func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { 40 | #if os(iOS) 41 | webView.evaluateJavaScript("show('ios')") 42 | #elseif os(macOS) 43 | webView.evaluateJavaScript("show('mac')") 44 | 45 | SFSafariExtensionManager.getStateOfSafariExtension(withIdentifier: extensionBundleIdentifier) { (state, error) in 46 | guard let state = state, error == nil else { 47 | // Insert code to inform the user that something went wrong. 48 | return 49 | } 50 | 51 | DispatchQueue.main.async { 52 | if #available(macOS 13, *) { 53 | webView.evaluateJavaScript("show('mac', \(state.isEnabled), true)") 54 | } else { 55 | webView.evaluateJavaScript("show('mac', \(state.isEnabled), false)") 56 | } 57 | } 58 | } 59 | #endif 60 | } 61 | 62 | func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { 63 | #if os(macOS) 64 | if (message.body as! String != "open-preferences") { 65 | return; 66 | } 67 | 68 | SFSafariApplication.showPreferencesForExtension(withIdentifier: extensionBundleIdentifier) { error in 69 | guard error == nil else { 70 | // Insert code to inform the user that something went wrong. 71 | return 72 | } 73 | 74 | DispatchQueue.main.async { 75 | NSApplication.shared.terminate(nil) 76 | } 77 | } 78 | #endif 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/safari/ZenTube/Shared (Extension)/SafariWebExtensionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafariWebExtensionHandler.swift 3 | // Shared (Extension) 4 | // 5 | // Created by admin on 19/04/23. 6 | // 7 | 8 | import SafariServices 9 | import os.log 10 | 11 | let SFExtensionMessageKey = "message" 12 | 13 | class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { 14 | 15 | func beginRequest(with context: NSExtensionContext) { 16 | let item = context.inputItems[0] as! NSExtensionItem 17 | let message = item.userInfo?[SFExtensionMessageKey] 18 | os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@", message as! CVarArg) 19 | 20 | let response = NSExtensionItem() 21 | response.userInfo = [ SFExtensionMessageKey: [ "Response to": message ] ] 22 | 23 | context.completeRequest(returningItems: [response], completionHandler: nil) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/safari/ZenTube/ZenTube.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/safari/ZenTube/ZenTube.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/safari/ZenTube/ZenTube.xcodeproj/project.xcworkspace/xcuserdata/admin.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/ZenTube/ZenTube.xcodeproj/project.xcworkspace/xcuserdata/admin.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /src/safari/ZenTube/ZenTube.xcodeproj/xcuserdata/admin.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | ZenTube (iOS).xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | ZenTube (macOS).xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 1 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/safari/ZenTube/iOS (App)/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // iOS (App) 4 | // 5 | // Created by admin on 19/04/23. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 21 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/safari/ZenTube/iOS (App)/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/safari/ZenTube/iOS (App)/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/safari/ZenTube/iOS (App)/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SFSafariWebExtensionConverterVersion 6 | 14.3 7 | UIApplicationSceneManifest 8 | 9 | UIApplicationSupportsMultipleScenes 10 | 11 | UISceneConfigurations 12 | 13 | UIWindowSceneSessionRoleApplication 14 | 15 | 16 | UISceneConfigurationName 17 | Default Configuration 18 | UISceneDelegateClassName 19 | $(PRODUCT_MODULE_NAME).SceneDelegate 20 | UISceneStoryboardFile 21 | Main 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/safari/ZenTube/iOS (App)/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // iOS (App) 4 | // 5 | // Created by admin on 19/04/23. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 15 | guard let _ = (scene as? UIWindowScene) else { return } 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/safari/ZenTube/iOS (Extension)/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.Safari.web-extension 9 | NSExtensionPrincipalClass 10 | $(PRODUCT_MODULE_NAME).SafariWebExtensionHandler 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/safari/ZenTube/macOS (App)/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // macOS (App) 4 | // 5 | // Created by admin on 19/04/23. 6 | // 7 | 8 | import Cocoa 9 | 10 | @main 11 | class AppDelegate: NSObject, NSApplicationDelegate { 12 | 13 | func applicationDidFinishLaunching(_ notification: Notification) { 14 | // Override point for customization after application launch. 15 | } 16 | 17 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 18 | return true 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/safari/ZenTube/macOS (App)/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /src/safari/ZenTube/macOS (App)/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SFSafariWebExtensionConverterVersion 6 | 14.3 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/safari/ZenTube/macOS (App)/ZenTube.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/safari/ZenTube/macOS (Extension)/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.Safari.web-extension 9 | NSExtensionPrincipalClass 10 | $(PRODUCT_MODULE_NAME).SafariWebExtensionHandler 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/safari/ZenTube/macOS (Extension)/ZenTube.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/safari/build/sa_zentube-latest.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/safari/build/sa_zentube-latest.zip -------------------------------------------------------------------------------- /src/safari/iOS (App)/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // iOS (App) 4 | // 5 | // Created by admin on 28/11/22. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 21 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/safari/iOS (App)/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/safari/iOS (App)/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/safari/iOS (App)/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SFSafariWebExtensionConverterVersion 6 | 14.1 7 | UIApplicationSceneManifest 8 | 9 | UIApplicationSupportsMultipleScenes 10 | 11 | UISceneConfigurations 12 | 13 | UIWindowSceneSessionRoleApplication 14 | 15 | 16 | UISceneConfigurationName 17 | Default Configuration 18 | UISceneDelegateClassName 19 | $(PRODUCT_MODULE_NAME).SceneDelegate 20 | UISceneStoryboardFile 21 | Main 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/safari/iOS (App)/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // iOS (App) 4 | // 5 | // Created by admin on 28/11/22. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 15 | guard let _ = (scene as? UIWindowScene) else { return } 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/safari/iOS (Extension)/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.Safari.web-extension 9 | NSExtensionPrincipalClass 10 | $(PRODUCT_MODULE_NAME).SafariWebExtensionHandler 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/safari/macOS (App)/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // macOS (App) 4 | // 5 | // Created by admin on 28/11/22. 6 | // 7 | 8 | import Cocoa 9 | 10 | @main 11 | class AppDelegate: NSObject, NSApplicationDelegate { 12 | 13 | func applicationDidFinishLaunching(_ notification: Notification) { 14 | // Override point for customization after application launch. 15 | } 16 | 17 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 18 | return true 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/safari/macOS (App)/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /src/safari/macOS (App)/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SFSafariWebExtensionConverterVersion 6 | 14.1 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/safari/macOS (App)/ZenTube.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/safari/macOS (Extension)/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.Safari.web-extension 9 | NSExtensionPrincipalClass 10 | $(PRODUCT_MODULE_NAME).SafariWebExtensionHandler 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/safari/macOS (Extension)/ZenTube.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/wasm/emoji.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inversepolarity/ZYT/d384b35de81474abbd45855a830b5e03b77ee1fb/src/wasm/emoji.wasm -------------------------------------------------------------------------------- /src/wasm/emoji.wat: -------------------------------------------------------------------------------- 1 | (module 2 | ;; actions 3 | (import "env" "restore" (func $restore (result i32))) 4 | 5 | 6 | ;; ref creators 7 | (import "env" "createRef" (func $createRef (result i32))) 8 | (import "env" "createMapRef" (func $createMapRef (result i32))) 9 | (import "env" "createNodeRef" (func $createNodeRef (result i32))) 10 | 11 | ;; utils 12 | (import "env" "length" (func $length (result i32))) 13 | (import "env" "logInt" (func $logInt (result i32))) 14 | (import "env" "logRef" (func $logRef (result i32))) 15 | (import "env" "free" (func $free (result i32))) 16 | 17 | ;; from: https://github.com/WebAssembly/interface-types/issues/18#issuecomment-430605795 18 | 19 | ;; exports 20 | (func (export "put_back") 21 | (local $hashmap i32) 22 | (local $node i32) 23 | 24 | (local.set $hashmap (call $createMapRef)) 25 | (local.set $node (call $createNodeRef)) 26 | 27 | (call $restore (local.get $hashmap (local.get $node))) 28 | 29 | (call $logInt (call $length (local.get $hashmap))) 30 | (call $logRef (local.get $node)) 31 | (call $free (local.get $node)) 32 | (call $free (local.get $hashmap)) 33 | ) 34 | ) -------------------------------------------------------------------------------- /tests/bootstrap.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require("puppeteer"); 2 | 3 | async function bootstrap(options = {}) { 4 | const { devtools = false, slowMo = false, appUrl } = options; 5 | 6 | const browser = await puppeteer.launch({ 7 | headless: false, 8 | devtools, 9 | args: ["--disable-extensions-except=./src", "--load-extension=./src"], 10 | ...(slowMo && { slowMo }) 11 | }); 12 | 13 | const appPage = await browser.newPage(); 14 | await appPage.goto(appUrl, { waitUntil: "load" }); 15 | 16 | const targets = await browser.targets(); 17 | const extensionTarget = targets.find( 18 | (target) => target.type() === "service_worker" 19 | ); 20 | const partialExtensionUrl = extensionTarget.url() || ""; 21 | const [, , extensionId] = partialExtensionUrl.split("/"); 22 | 23 | const extPage = await browser.newPage(); 24 | const extensionUrl = `chrome-extension://${extensionId}/options/options.html`; 25 | await extPage.goto(extensionUrl, { waitUntil: "load" }); 26 | 27 | return { 28 | appPage, 29 | browser, 30 | extensionUrl, 31 | extPage 32 | }; 33 | } 34 | 35 | module.exports = { bootstrap }; 36 | -------------------------------------------------------------------------------- /tests/jest.setup.js: -------------------------------------------------------------------------------- 1 | Object.assign(global, require("jest-chrome")); 2 | -------------------------------------------------------------------------------- /tests/options.spec.js: -------------------------------------------------------------------------------- 1 | const { bootstrap } = require("./bootstrap"); 2 | const { chrome } = require("jest-chrome"); 3 | 4 | const { defaultSettings, getNewBrowserTab, sleep } = require("./putils"); 5 | 6 | const manifest = require("../src/manifest.json"); 7 | 8 | describe("test suite for options popup", () => { 9 | let extPage, appPage, browser; 10 | 11 | beforeAll(async () => { 12 | const context = await bootstrap({ appUrl: "https://www.youtube.com/" }); 13 | extPage = context.extPage; 14 | appPage = context.appPage; 15 | browser = context.browser; 16 | 17 | /* Mock Implementations */ 18 | 19 | const injectScript = () => jest.fn(); 20 | const sendMessageToTabs = (tabs, msg) => jest.fn(); 21 | const messagePageScript = () => jest.fn(); 22 | 23 | const selectionChanged = () => jest.fn(); 24 | const storeSettings = () => jest.fn(); 25 | const setDropdownSelect = () => jest.fn(); 26 | 27 | chrome.runtime.getManifest.mockImplementation(() => manifest); 28 | global.chrome = { 29 | storage: { 30 | local: { 31 | set: jest.fn(() => defaultSettings), 32 | get: jest.fn(() => defaultSettings) 33 | } 34 | } 35 | }; 36 | }); 37 | 38 | it("manifest version", async () => { 39 | const ver = await extPage.$("#version"); 40 | const verText = await ver.evaluate((e) => e.innerText); 41 | expect(verText).toEqual("Ver: " + chrome.runtime.getManifest().version); 42 | expect(chrome.runtime.getManifest).toBeCalled(); 43 | }); 44 | 45 | it("ip link event listener", async () => { 46 | await extPage.evaluate(() => document.querySelector("#brand").click()); 47 | const donationPage = await getNewBrowserTab(browser); 48 | expect(donationPage.url()).toBe("https://ko-fi.com/evenzero"); 49 | donationPage.close(); 50 | }); 51 | 52 | it("transitions css injected", async () => { 53 | const transitions = await appPage.$("#zentubeTransitions"); 54 | expect(transitions).toBeTruthy(); 55 | }); 56 | 57 | it("toggle css injected", async () => { 58 | const zentube = await appPage.$("#zentube"); 59 | expect(zentube).toBeTruthy(); 60 | }); 61 | 62 | it("settings stored", async () => { 63 | const { settings } = global.chrome.storage.local.get(); 64 | 65 | expect(Object.keys(defaultSettings).length).toBeTruthy(); 66 | expect(Object.keys(settings).length).toBeTruthy(); 67 | expect(settings).toBeTruthy(); 68 | expect(settings).toEqual(defaultSettings); 69 | }); 70 | 71 | it("repopulatePopup", async () => { 72 | // Popup populated 73 | let popupChildren = await extPage.evaluate(() => { 74 | return document.querySelector("#popup").children.length; 75 | }); 76 | 77 | expect(popupChildren).toEqual( 78 | Object.keys(defaultSettings.options[defaultSettings.currentPage]).length 79 | ); 80 | }); 81 | 82 | it("setDropdownSelect", async () => { 83 | let dropdownLabel = await extPage.evaluate(() => { 84 | return document.querySelector(".select-selected").innerText; 85 | }); 86 | 87 | expect(dropdownLabel).toEqual(defaultSettings.currentPage); 88 | }); 89 | 90 | // TODO selectionChanged 91 | // TODO TEST DROPDOWN ENTRIES 92 | // TODO TEST EACH BUTTON FOR EACH DROPDOWN SELECTION 93 | afterAll(async () => { 94 | await sleep(1000); 95 | extPage.close(); 96 | await sleep(1000); 97 | appPage.close(); 98 | await sleep(1000); 99 | browser.close(); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /tests/putils.js: -------------------------------------------------------------------------------- 1 | async function getNewBrowserTab(browser) { 2 | let resultPromise; 3 | 4 | async function onTargetcreatedHandler(target) { 5 | if (target.type() === "other" || "page") { 6 | let newPage = await browser.pages(); 7 | newPage = newPage.slice(-1)[0]; 8 | const newPagePromise = new Promise((y) => 9 | newPage.once("domcontentloaded", () => y(newPage)) 10 | ); 11 | 12 | const isPageLoaded = await newPage.evaluate(() => document.readyState); 13 | 14 | browser.off("targetcreated", onTargetcreatedHandler); // unsubscribing 15 | 16 | return isPageLoaded.match("complete|interactive") 17 | ? resultPromise(newPage) 18 | : resultPromise(newPagePromise); 19 | } 20 | } 21 | 22 | return new Promise((resolve) => { 23 | resultPromise = resolve; 24 | browser.on("targetcreated", onTargetcreatedHandler); 25 | }); 26 | } 27 | 28 | const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); 29 | 30 | const defaultSettings = { 31 | currentPage: "Home", 32 | options: { 33 | Home: { 34 | preview: { 35 | label: "Preview on hover", 36 | classes: [ 37 | ".ytd-video-preview", 38 | "#mouseover-overlay", 39 | ".ytd-thumbnail-overlay-loading-preview-renderer" 40 | ], 41 | show: true 42 | }, 43 | communityPosts: { 44 | label: "Latest posts", 45 | classes: ["ytd-rich-shelf-renderer"], 46 | id: "communityPosts", 47 | show: true 48 | }, 49 | adThumbs: { 50 | label: "Ad Thumbnails", 51 | classes: [".ytd-display-ad-renderer", ".ytd-ad-slot-renderer"], 52 | show: true 53 | }, 54 | chipBar: { 55 | label: "Feed Filter Chip Bar", 56 | classes: [".ytd-feed-filter-chip-bar-renderer"], 57 | show: true 58 | }, 59 | title: { 60 | label: "Video title", 61 | classes: ["yt-formatted-string.style-scope.ytd-rich-grid-media"], 62 | show: true 63 | } 64 | }, 65 | Video: { 66 | sidebar: { 67 | label: "Video Sidebar", 68 | classes: [".ytd-watch-next-secondary-results-renderer"], 69 | show: true 70 | }, 71 | nextvideos: { 72 | label: "End Recs (Default)", 73 | classes: [".ytp-ce-video .ytp-ce-channel .ytp-ce-covering-overlay"], 74 | show: true 75 | }, 76 | endvideos: { 77 | label: "End Recs (Channel)", 78 | classes: [".ytp-endscreen-content"], 79 | show: true 80 | }, 81 | chat: { 82 | label: "Chat", 83 | classes: ["#chat"], 84 | show: true 85 | }, 86 | likes: { 87 | label: "Likes", 88 | classes: [ 89 | "ytd-menu-renderer.style-scope.ytd-watch-metadata .yt-core-attributed-string" 90 | ], 91 | show: true 92 | }, 93 | comments: { 94 | label: "Comments", 95 | classes: [".ytd-comments"], 96 | show: true 97 | }, 98 | playlist: { 99 | label: "Playlist", 100 | classes: ["div.style-scope.ytd-playlist-panel-renderer"], 101 | show: true 102 | }, 103 | chapters: { 104 | label: "Chapters", 105 | classes: [ 106 | "ytd-engagement-panel-section-list-renderer.style-scope.ytd-watch-flexy" 107 | ], 108 | show: true 109 | }, 110 | subscribe: { 111 | label: "Subscribe Button", 112 | classes: ["yt-button-shape.style-scope.ytd-subscribe-button-renderer"], 113 | show: true 114 | }, 115 | title: { 116 | label: "Video Title", 117 | classes: ["yt-formatted-string.style-scope.ytd-watch-metadata"], 118 | show: true 119 | }, 120 | sub_count: { 121 | label: "Subscriber count", 122 | classes: ["yt-formatted-string.style-scope.ytd-video-owner-renderer"], 123 | show: true 124 | }, 125 | description: { 126 | label: "Description Box", 127 | classes: ["ytd-text-inline-expander.style-scope.ytd-watch-metadata"], 128 | show: true 129 | }, 130 | merch: { 131 | label: "Merchandise Box", 132 | classes: ["ytd-merch-shelf-renderer.style-scope.ytd-watch-flexy"], 133 | show: true 134 | } 135 | }, 136 | Everywhere: { 137 | metadata: { 138 | label: "Video Metadata", 139 | classes: [ 140 | "span.inline-metadata-item.style-scope.ytd-video-meta-block", 141 | "yt-formatted-string.style-scope.ytd-channel-name", 142 | "yt-icon.style-scope.ytd-badge-supported-renderer", 143 | "div.badge.badge-style-type-live-now-alternate.style-scope.ytd-badge-supported-renderer", 144 | "#metadata-line", 145 | "#byline-container" 146 | ], 147 | show: true 148 | }, 149 | duration: { 150 | label: "Video Duration", 151 | classes: [ 152 | "span.style-scope.ytd-thumbnail-overlay-time-status-renderer" 153 | ], 154 | show: true 155 | }, 156 | thumbnails: { 157 | label: "Video Thumbnails", 158 | classes: [ 159 | ".ytd-macro-markers-list-item-renderer>img", 160 | ".thumbnail-container.style-scope.ytd-notification-renderer", 161 | ".yt-core-image--loaded" 162 | ], 163 | show: true 164 | }, 165 | resume: { 166 | label: "Resume bar", 167 | classes: [ 168 | "div.style-scope.ytd-thumbnail-overlay-resume-playback-renderer" 169 | ], 170 | show: true 171 | }, 172 | logo: { 173 | label: "YouTube Logo", 174 | classes: ["#logo .ytd-topbar-logo-renderer"], 175 | show: true 176 | }, 177 | channelThumb: { 178 | label: "Channel Avatar", 179 | classes: [ 180 | "#avatar", 181 | "#channel-thumbnail", 182 | "tp-yt-paper-item.style-scope.ytd-guide-entry-renderer > yt-img-shadow" 183 | ], 184 | show: true 185 | } 186 | } 187 | } 188 | }; 189 | 190 | module.exports = { defaultSettings, getNewBrowserTab, sleep }; 191 | --------------------------------------------------------------------------------