├── .gitignore ├── LICENSE ├── Motherless Plus.user.js ├── PornHub Plus.user.js ├── README.md ├── Sxyporn Plus.user.css ├── XNXX Plus.user.js ├── XVIDEOS Plus.user.js ├── YouPorn Plus.user.js └── xHamster Plus.user.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /Motherless Plus.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @author Mr. Nope 3 | // @version 2023-05-25 4 | // @name Motherless Plus 5 | // @match *://*.motherless.com/* 6 | // @description A kinder Motherless. Because you're worth it. 7 | // @run-at document_end 8 | // @date 2023-05-25 9 | // @license MIT 10 | // ==/UserScript== 11 | 12 | 'use strict'; 13 | 14 | setTimeout(() => { 15 | const OPTIONS = { 16 | cinemaMode: JSON.parse(localStorage.getItem('plus_cinemaMode')) || false 17 | }; 18 | 19 | // const playerSettings = JSON.parse(localStorage.getItem('mgp_player')); 20 | 21 | // Change default quality from 720p to 1080p 22 | // playerSettings.quality = 1080; 23 | 24 | // Prevent problem with videos not loading unless clearing cache and reloading. 25 | // localStorage.setItem('mgp_player', JSON.stringify(playerSettings)); 26 | 27 | /** 28 | * Shared styles 29 | */ 30 | const sharedStyles = ` 31 | /* Our own elements */ 32 | 33 | .plus-buttons { 34 | background: rgba(27, 27, 27, 0.9); 35 | box-shadow: 0px 0px 12px rgba(20, 111, 223, 0.9); 36 | font-size: 12px; 37 | position: fixed; 38 | bottom: 10px; 39 | padding: 10px 22px 8px 24px; 40 | right: 0; 41 | z-index: 100; 42 | transition: all 0.2s ease; 43 | 44 | /* Negative margin-right calculated later based on width of buttons */ 45 | } 46 | 47 | .plus-buttons:hover { 48 | box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.3); 49 | } 50 | 51 | .plus-buttons .plus-button { 52 | margin: 10px 0; 53 | padding: 6px 15px; 54 | border-radius: 4px; 55 | font-weight: 700; 56 | display: block; 57 | position: relative; 58 | text-align: center; 59 | vertical-align: top; 60 | cursor: pointer; 61 | border: none; 62 | text-decoration: none; 63 | } 64 | 65 | .plus-buttons a.plus-button { 66 | background: rgb(221, 221, 221); 67 | color: rgb(51, 51, 51); 68 | } 69 | 70 | .plus-buttons a.plus-button:hover { 71 | background: rgb(187, 187, 187); 72 | color: rgb(51, 51, 51); 73 | } 74 | 75 | .plus-buttons a.plus-button.plus-button-isOn { 76 | background: rgb(20, 111, 223); 77 | color: rgb(255, 255, 255); 78 | } 79 | 80 | .plus-buttons a.plus-button.plus-button-isOn:hover { 81 | background: rgb(0, 91, 203); 82 | color: rgb(255, 255, 255); 83 | } 84 | 85 | .plus-hidden { 86 | display: none !important; 87 | } 88 | 89 | .plus-letters { 90 | align-items: center; 91 | color: #ccc; 92 | display: flex; 93 | justify-content: space-between; 94 | margin: 0 22px 18px; 95 | text-transform: uppercase; 96 | } 97 | 98 | .plus-letters span { 99 | cursor: pointer; 100 | } 101 | `; 102 | 103 | /** 104 | * Color Theme 105 | */ 106 | const themeStyles = ` 107 | .plus-buttons { 108 | box-shadow: 0px 0px 12px rgba(236, 86, 124, 0.85); 109 | } 110 | 111 | .plus-buttons:hover { 112 | box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.3); 113 | } 114 | 115 | .plus-buttons a.plus-button { 116 | background: rgb(47, 47, 47); 117 | color: rgb(172, 172, 172); 118 | } 119 | 120 | .plus-buttons a.plus-button:hover { 121 | background: rgb(79, 79, 79); 122 | color: rgb(204, 204, 204); 123 | } 124 | 125 | .plus-buttons a.plus-button.plus-button-isOn { 126 | background: rgb(236, 86, 124); 127 | color: rgb(235, 235, 235); 128 | } 129 | 130 | .plus-buttons a.plus-button.plus-button-isOn:hover { 131 | background: rgb(236, 86, 124); 132 | color: rgb(255, 255, 255); 133 | } 134 | `; 135 | 136 | /** 137 | * Site-Specific Styles 138 | */ 139 | 140 | const generalStyles = ` 141 | /* Hide elements */ 142 | 143 | .realsex, 144 | .mhp1138_cinemaState, 145 | .networkBar, 146 | .sniperModeEngaged, 147 | .footer, 148 | .footer-title, 149 | .ad-link, 150 | .removeAdLink, 151 | .removeAdLink + iframe, 152 | .abovePlayer, 153 | .streamatesModelsContainer, 154 | #welcome, 155 | #welcomePremium, 156 | #headerUpgradePremiumBtn, 157 | #PornhubNetworkBar, 158 | #js-abContainterMain, 159 | #hd-rightColVideoPage > :not(#relatedVideosVPage), 160 | .bottomNotification, 161 | .trailerUnderplayerPreview, 162 | .sectionCarousel { 163 | display: none !important; 164 | visibility: hidden !important; 165 | opacity: 0 !important; 166 | height: 0 !important; 167 | width: 0 !important; 168 | } 169 | 170 | /* Allow narrower page width */ 171 | 172 | html.supportsGridLayout.fluidContainer .container, 173 | html.supportsGridLayout.fluidContainer .section_wrapper { 174 | min-width: 700px !important; 175 | } 176 | 177 | /* Hide tricky ads with obfuscated tag names */ 178 | .adLinks + *, 179 | .adLinks + * + *, 180 | .wrapper.hd + * { 181 | display: none !important; 182 | } 183 | 184 | /* Full-width video */ 185 | #vpContentContainer { 186 | display: block !important; 187 | } 188 | 189 | /* Recommended videos */ 190 | #recommendedVideosVPage { 191 | text-align: center !important; 192 | } 193 | 194 | /* "Recommended Porn" heading */ 195 | #recommendedVideosVPage h3 { 196 | text-align: center !important; 197 | display: block !important; 198 | float: unset !important; 199 | } 200 | 201 | /* Recommended videos layout */ 202 | #recommendedVideos { 203 | list-style: none !important; 204 | display: flex !important; 205 | flex-direction: row !important; 206 | align-items: flex-start !important; 207 | justify-content: space-between !important; 208 | text-align: left !important; 209 | } 210 | 211 | /* Thumbnail wrapper */ 212 | #recommendedVideosPage .videoBox { 213 | font-size: 0.813rem !important; /* 13px/16px = 0.813rem */ 214 | color: #a6adc8 !important; /* Mocha -> Subtext0 */ 215 | margin: 0 5px !important; 216 | } 217 | 218 | /* Thumbnail wrapper */ 219 | #recommendedVideos .videoBox .phimage { 220 | width: auto !important; 221 | border-radius: 4px !important; 222 | } 223 | 224 | /* Thumbnail info wrapper */ 225 | .thumbnail-info-wrapper { 226 | color: #a6adc8 !important; /* Mocha -> Subtext0 */ 227 | font-size: 0.813rem !important; /* 13px/16px = 0.813rem */ 228 | float: none !important; 229 | width: auto !important; 230 | } 231 | 232 | /* Title of video or playlist */ 233 | .thumbnail-info-wrapper .title { 234 | font-size: 0.813rem !important; 235 | font-weight: 700 !important; 236 | margin: 12px 0 3px !important; 237 | } 238 | 239 | /* The user/channel name wrapper */ 240 | .thumbnail-info-wrapper .usernameWrap { 241 | margin-top: -1px; /* Fixes slight off-center text */ 242 | } 243 | 244 | /* The user/channel name link */ 245 | .thumbnail-info-wrapper .usernameWrap a { 246 | font-size: 0.813rem !important; /* 13px/16px = 0.813rem */ 247 | color: #a6adc8 !important; 248 | } 249 | 250 | /* The verified badge and user/channel name wrapper */ 251 | .thumbnail-info-wrapper .videoUploaderBlock { 252 | margin-bottom: 5px !important; 253 | } 254 | 255 | /* The verified badge */ 256 | .thumbnail-info-wrapper .own-video-thumbnail { 257 | margin-right: 2px !important; 258 | } 259 | 260 | /* The views and likes wrapper */ 261 | .thumbnail-info-wrapper .videoDetailsBlock { 262 | margin-bottom: 5px !important; 263 | } 264 | 265 | /* The views */ 266 | .thumbnail-info-wrapper .videoDetailsBlock .views { 267 | font-size: 0.813rem !important; /* 13px/16px = 0.813rem */ 268 | } 269 | 270 | /* The rating */ 271 | .thumbnail-info-wrapper .videoDetailsBlock .rating-container i, 272 | .thumbnail-info-wrapper .videoDetailsBlock .rating-container .value { 273 | font-size: 0.813rem !important; /* 13px/16px = 0.813rem */ 274 | } 275 | 276 | /* The "load more" button */ 277 | .more_recommended_btn { 278 | margin: 2rem 0 !important; 279 | } 280 | 281 | /* Make "HD" icon more visible on thumbnails */ 282 | 283 | .hd-thumbnail { 284 | color: #f90 !important; 285 | } 286 | 287 | /* Show all playlists without scrolling in "add to" */ 288 | 289 | .slimScrollDiv { 290 | height: auto !important; 291 | } 292 | 293 | #scrollbar_watch { 294 | max-height: unset !important; 295 | } 296 | 297 | /* Hide premium video from related videos sidebar */ 298 | 299 | #relateRecommendedItems li:nth-of-type(5) { 300 | display: none !important; 301 | } 302 | 303 | /* Prevent animating player size change on each page load */ 304 | 305 | #main-container .video-wrapper #player.wide { 306 | transition: none !important; 307 | } 308 | 309 | /* Allow narrower player */ 310 | 311 | #player { 312 | min-width: 0 !important; 313 | } 314 | 315 | /* Fit more playlists into "add to" popup */ 316 | 317 | .playlist-menu-addTo { 318 | display: none; 319 | } 320 | 321 | .add-to-playlist-menu #scrollThumbs, 322 | .playlist-option-menu #scrollThumbs { 323 | height: 320px !important; 324 | max-height: 35vh !important; 325 | } 326 | 327 | .add-to-playlist-menu ul.custom-playlist li { 328 | font-size: 12px; 329 | height: 24px; 330 | } 331 | 332 | .add-to-playlist-menu .playlist-menu-createNew { 333 | font-size: 12px !important; 334 | height: 38px !important; 335 | } 336 | 337 | .add-to-playlist-menu .playlist-menu-createNew a { 338 | padding-top: 8px !important; 339 | font-weight: 400 !important; 340 | } 341 | 342 | /* Hide playlist bar if disabled in options */ 343 | 344 | .playlist-bar { 345 | display: ${OPTIONS.hidePlaylistBar ? 'none' : 'block'}; 346 | } 347 | 348 | /** 349 | * Improve loading indicator lines on thumbnails 350 | * 351 | * Using colors from the Catppuccin palette available at https://github.com/catppuccin. 352 | */ 353 | 354 | .preloadLine { 355 | background: #81c8be; /* Mocha -> Teal */ 356 | box-shadow: 0 0 3px #1e1e2e; /* Mocha -> Base */ 357 | } 358 | 359 | /* Fade in and out semitransparent elements */ 360 | 361 | .tab-menu-item { 362 | opacity: 0.4 !important; 363 | padding: 0 16px !important; 364 | transition: all 0.2s ease !important; 365 | } 366 | 367 | .tab-menu-item.active { 368 | opacity: 0.5 !important; 369 | } 370 | 371 | .tab-menu-wrapper-cell:hover .tab-menu-item { 372 | opacity: 1 !important; 373 | } 374 | 375 | .votes-fav-wrap .icon-wrapper:hover, 376 | .votes-fav-wrap .icon-wrapper.active:hover { 377 | opacity: 1 !important; 378 | } 379 | 380 | .votes-fav-wrap .icon-wrapper { 381 | opacity: 0.4 !important; 382 | transition: all 0.2s ease !important; 383 | } 384 | 385 | .votes-fav-wrap .icon-wrapper.active { 386 | opacity: 0.7 !important; 387 | } 388 | `; 389 | 390 | /** 391 | * References to video element and container if they exist on the page 392 | */ 393 | const videoContainer = document.querySelector('#ml-main-video'); 394 | const video = document.querySelector('#ml-main-video_html5_api'); 395 | const distractions = [ 396 | 'header' 397 | ]; 398 | const isOnVideoPage = 399 | /^http[s]*:\/\/(www\.)*motherless\.com\/watch\//.test(window.location.href) && !!videoContainer; 400 | 401 | const handleDistractions = () => { 402 | distractions.forEach((distraction) => { 403 | console.log(distraction); 404 | const element = document.querySelector(distraction); 405 | 406 | if (element) { 407 | const handleMouseOver = () => { 408 | element.style.opacity = 1; 409 | }; 410 | 411 | const handleMouseOut = () => { 412 | element.style.opacity = 0.2; 413 | }; 414 | 415 | if (OPTIONS.cinemaMode) { 416 | element.style.transition = 'all 0.2s ease'; 417 | element.style.opacity = 0.2; 418 | 419 | element.addEventListener('mouseover', handleMouseOver, false); 420 | element.addEventListener('mouseout', handleMouseOut, false); 421 | } else { 422 | element.style.opacity = 1; 423 | element.removeEventListener('mouseover', handleMouseOver, false); 424 | element.removeEventListener('mouseout', handleMouseOut, false); 425 | } 426 | } 427 | }); 428 | }; 429 | 430 | /** 431 | * Returns an `on` or `off` CSS class name based on the boolean evaluation 432 | * of the `state` parameter, as convenience method when setting UI state. 433 | */ 434 | const getButtonState = state => { 435 | return state ? 'plus-button-isOn' : 'plus-button-isOff'; 436 | }; 437 | 438 | /** 439 | * Option buttons 440 | */ 441 | 442 | const cinemaButton = document.createElement('a'); 443 | const cinemaButtonText = document.createElement('span'); 444 | const cinemaButtonState = getButtonState(OPTIONS.cinemaMode); 445 | 446 | const scrollButton = document.createElement('a'); 447 | const scrollButtonText = document.createElement('span'); 448 | 449 | cinemaButtonText.textContent = 'Cinema mode'; 450 | cinemaButtonText.classList.add('text'); 451 | cinemaButton.appendChild(cinemaButtonText); 452 | cinemaButton.classList.add(cinemaButtonState, 'plus-button'); 453 | cinemaButton.addEventListener('click', () => { 454 | OPTIONS.cinemaMode = !OPTIONS.cinemaMode; 455 | localStorage.setItem('plus_cinemaMode', OPTIONS.cinemaMode); 456 | 457 | if (OPTIONS.cinemaMode) { 458 | cinemaButton.classList.replace('plus-button-isOff', 'plus-button-isOn'); 459 | } else { 460 | cinemaButton.classList.replace('plus-button-isOn', 'plus-button-isOff'); 461 | } 462 | 463 | if (isOnVideoPage) { 464 | video.addEventListener('click', () => { 465 | const playButton = document.querySelector('.vjs-big-play-button'); 466 | 467 | if (playButton.offsetParent !== null) { 468 | const event = new MouseEvent('click', { 469 | view: window, 470 | bubbles: true, 471 | cancelable: true, 472 | }); 473 | 474 | playButton.dispatchEvent(event); 475 | } 476 | }); 477 | 478 | handleDistractions(); 479 | } 480 | }); 481 | 482 | scrollButtonText.textContent = "Scroll to video"; 483 | scrollButtonText.classList.add('text'); 484 | scrollButton.appendChild(scrollButtonText); 485 | scrollButton.classList.add('plus-button'); 486 | scrollButton.addEventListener('click', () => { 487 | const container = document.querySelector('.main_content'); 488 | const header = document.querySelector('header'); 489 | 490 | if (container && header) { 491 | const destination = { 492 | left: container.scrollX, 493 | top: container.offsetTop - header.scrollHeight, 494 | behavior: 'smooth' 495 | }; 496 | window.scroll(destination); 497 | } 498 | }); 499 | 500 | /** 501 | * Order option buttons in a container 502 | */ 503 | 504 | const buttons = document.createElement('div'); 505 | 506 | buttons.classList.add('plus-buttons'); 507 | 508 | buttons.appendChild(cinemaButton); 509 | buttons.appendChild(scrollButton); 510 | 511 | document.body.appendChild(buttons); // Button container ready and added to page 512 | 513 | if (isOnVideoPage) { 514 | } 515 | 516 | 517 | /* 518 | * Add styles 519 | */ 520 | 521 | GM_addStyle(sharedStyles); 522 | GM_addStyle(themeStyles); 523 | GM_addStyle(generalStyles); 524 | 525 | /* 526 | * Add dynamic styles 527 | */ 528 | 529 | const dynamicStyles = ` 530 | .plus-buttons { 531 | margin-right: -${buttons.getBoundingClientRect().width - 18}px; 532 | margin-top: -${buttons.getBoundingClientRect().height - 18}px; 533 | } 534 | 535 | .plus-buttons:hover { 536 | margin-right: 0; 537 | margin-left: 0; 538 | } 539 | `; 540 | 541 | GM_addStyle(dynamicStyles); 542 | }, 1000); 543 | -------------------------------------------------------------------------------- /PornHub Plus.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @author Mr. Nope 3 | // @version 2022-07-23 4 | // @name PornHub Plus 5 | // @match *://*.pornhub.com/* 6 | // @match *://*.pornhubpremium.com/* 7 | // @description A kinder PornHub. Because you're worth it. 8 | // @date 2022-07-23 9 | // @license CC0 10 | // ==/UserScript== 11 | 12 | /** 13 | * # CHANGELOG 14 | * 15 | * ## 2022-07-22 16 | * 17 | * Adapting the script for modern user script managers that maximise their use of the WebExtension 18 | * API for higher quality and better performing solutions more integrated with the browser without 19 | * questionable implementation choices. 20 | * 21 | * Changes mean less meta-data and no more wrapper functions. It's currently only being tested in 22 | * FireMonkey but incompatibilities should be minor and simple to fix. 23 | * 24 | * ### Features 25 | * 26 | * - **Redirect to free if a model has no premium videos** 27 | * Videos pages of models with no premium content redirects to free, as many popular models 28 | * don't make premium videos and there's no way to toggle free content. Removing "premium" 29 | * from the URL can reveal tons of videos. 30 | * 31 | * ### Chores 32 | * 33 | * - Adding the changelog which for now resides in the script itself. 34 | * - Formatting properly and removing unnecessary wrapper functions. 35 | * - Adding Prettier and EditorConfig formatter rules (not yet automated). 36 | * 37 | * ### Notes 38 | * 39 | * - Switching from SemVer to DateVer for simplicity (nobody gives a shit except the updater). 40 | * - Needs a major overhaul and should be rewritten for more structure. 41 | * - Tooling would be useful: NPM, linting, formatting, versioning, changelog generation, etc. 42 | */ 43 | 44 | 'use strict'; 45 | 46 | setTimeout(() => { 47 | const OPTIONS = { 48 | showTitles: JSON.parse(localStorage.getItem('plus_showTitles')) || false, 49 | loadMore: JSON.parse(localStorage.getItem('plus_loadMore')) || true, 50 | cinemaMode: JSON.parse(localStorage.getItem('plus_cinemaMode')) || false, 51 | openWithoutPlaylist: JSON.parse(localStorage.getItem('plus_openWithoutPlaylist')) || true, 52 | showOnlyHd: JSON.parse(localStorage.getItem('plus_showOnlyHd')) || false, 53 | redirectToVideos: JSON.parse(localStorage.getItem('plus_redirectToVideos')) || false, 54 | redirectPremiumVideos: JSON.parse(localStorage.getItem('plus_redirectPremiumVideos')) || false, 55 | hideWatchedVideos: JSON.parse(localStorage.getItem('plus_hideWatchedVideos')) || false, 56 | hidePlaylistBar: JSON.parse(localStorage.getItem('plus_hidePlaylistBar')) || false, 57 | durationFilter: JSON.parse(localStorage.getItem('plus_durationFilter')) || { 58 | max: 0, 59 | min: 0 60 | }, 61 | durationPresets: [{ 62 | label: 'Micro', 63 | min: 0, 64 | max: 2 65 | }, 66 | { 67 | label: 'Short', 68 | min: 3, 69 | max: 8 70 | }, 71 | { 72 | label: 'Average', 73 | min: 8, 74 | max: 18 75 | }, 76 | { 77 | label: 'Long', 78 | min: 18, 79 | max: 40 80 | }, 81 | { 82 | label: 'Magnum', 83 | min: 40, 84 | max: 0 85 | } 86 | ], 87 | relatedColumns: JSON.parse(localStorage.getItem('plus_relatedColumns')) || 3, 88 | }; 89 | 90 | /** 91 | * Player settings 92 | * 93 | * Reference as of 8/16/2022: 94 | * { 95 | * "buildNumber": "220808.544", 96 | * "version": "6.2.2", 97 | * "adrolls": { 98 | * "0": { 99 | * "commonTime": 0, 100 | * "views": 0, 101 | * "timeouts": { 102 | * "attempt": 0, 103 | * "time": 0, 104 | * "report": true 105 | * } 106 | * } 107 | * }, 108 | * "quality": 1080, 109 | * "playbackRate": 1, 110 | * "adaptive": { 111 | * "hlsLevel": 2 112 | * }, 113 | * "volume": { 114 | * "volume": 52, 115 | * "muted": false 116 | * }, 117 | * "focusedPlayer": "playerDiv_336166192" 118 | * } 119 | */ 120 | 121 | // const playerSettings = JSON.parse(localStorage.getItem('mgp_player')); 122 | 123 | // Change default quality from 720p to 1080p 124 | // playerSettings.quality = 1080; 125 | 126 | // Prevent problem with videos not loading unless clearing cache and reloading. 127 | // localStorage.setItem('mgp_player', JSON.stringify(playerSettings)); 128 | 129 | /** 130 | * Shared styles 131 | */ 132 | const sharedStyles = ` 133 | /* Our own elements */ 134 | 135 | .plus-buttons { 136 | background: rgba(27, 27, 27, 0.9); 137 | box-shadow: 0px 0px 12px rgba(20, 111, 223, 0.9); 138 | font-size: 12px; 139 | position: fixed; 140 | bottom: 10px; 141 | padding: 10px 22px 8px 24px; 142 | right: 0; 143 | z-index: 100; 144 | transition: all 0.2s ease; 145 | 146 | /* Negative margin-right calculated later based on width of buttons */ 147 | } 148 | 149 | .plus-buttons:hover { 150 | box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.3); 151 | } 152 | 153 | .plus-buttons .plus-button { 154 | margin: 10px 0; 155 | padding: 6px 15px; 156 | border-radius: 4px; 157 | font-weight: 700; 158 | display: block; 159 | position: relative; 160 | text-align: center; 161 | vertical-align: top; 162 | cursor: pointer; 163 | border: none; 164 | text-decoration: none; 165 | } 166 | 167 | .plus-buttons a.plus-button { 168 | background: rgb(221, 221, 221); 169 | color: rgb(51, 51, 51); 170 | } 171 | 172 | .plus-buttons a.plus-button:hover { 173 | background: rgb(187, 187, 187); 174 | color: rgb(51, 51, 51); 175 | } 176 | 177 | .plus-buttons a.plus-button.plus-button-isOn { 178 | background: rgb(20, 111, 223); 179 | color: rgb(255, 255, 255); 180 | } 181 | 182 | .plus-buttons a.plus-button.plus-button-isOn:hover { 183 | background: rgb(0, 91, 203); 184 | color: rgb(255, 255, 255); 185 | } 186 | 187 | .plus-hidden { 188 | display: none !important; 189 | } 190 | 191 | .plus-letters { 192 | align-items: center; 193 | color: #ccc; 194 | display: flex; 195 | justify-content: space-between; 196 | margin: 0 22px 18px; 197 | text-transform: uppercase; 198 | } 199 | 200 | .plus-letters span { 201 | cursor: pointer; 202 | } 203 | `; 204 | 205 | /** 206 | * Color Theme 207 | */ 208 | const themeStyles = ` 209 | .plus-buttons { 210 | box-shadow: 0px 0px 12px rgba(255, 153, 0, 0.85); 211 | } 212 | 213 | .plus-buttons:hover { 214 | box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.3); 215 | } 216 | 217 | .plus-buttons a.plus-button { 218 | background: rgb(47, 47, 47); 219 | color: rgb(172, 172, 172); 220 | } 221 | 222 | .plus-buttons a.plus-button:hover { 223 | background: rgb(79, 79, 79); 224 | color: rgb(204, 204, 204); 225 | } 226 | 227 | .plus-buttons a.plus-button.plus-button-isOn { 228 | background: rgb(255, 153, 0); 229 | color: rgb(0, 0, 0); 230 | } 231 | 232 | .plus-buttons a.plus-button.plus-button-isOn:hover { 233 | background: rgb(255, 153, 0); 234 | color: rgb(255, 255, 255); 235 | } 236 | `; 237 | 238 | /** 239 | * Site-Specific Styles 240 | */ 241 | const generalStyles = ` 242 | /* Hide elements */ 243 | 244 | .realsex, 245 | .mhp1138_cinemaState, 246 | .networkBar, 247 | .sniperModeEngaged, 248 | .footer, 249 | .footer-title, 250 | .ad-link, 251 | .removeAdLink, 252 | .removeAdLink + iframe, 253 | .abovePlayer, 254 | .streamatesModelsContainer, 255 | #welcome, 256 | #welcomePremium, 257 | #headerUpgradePremiumBtn, 258 | #PornhubNetworkBar, 259 | #js-abContainterMain, 260 | #hd-rightColVideoPage > :not(#relatedVideosVPage), 261 | .bottomNotification, 262 | .trailerUnderplayerPreview, 263 | .sectionCarousel { 264 | display: none !important; 265 | visibility: hidden !important; 266 | opacity: 0 !important; 267 | height: 0 !important; 268 | width: 0 !important; 269 | } 270 | 271 | /* Allow narrower page width */ 272 | 273 | html.supportsGridLayout.fluidContainer .container, 274 | html.supportsGridLayout.fluidContainer .section_wrapper { 275 | min-width: 700px !important; 276 | } 277 | 278 | /* Hide tricky ads with obfuscated tag names */ 279 | .adLinks + *, 280 | .adLinks + * + *, 281 | .wrapper.hd + * { 282 | display: none !important; 283 | } 284 | 285 | /* Full-width video */ 286 | #vpContentContainer { 287 | display: block !important; 288 | } 289 | 290 | /* Recommended videos */ 291 | #recommendedVideosVPage { 292 | text-align: center !important; 293 | } 294 | 295 | /* "Recommended Porn" heading */ 296 | #recommendedVideosVPage h3 { 297 | text-align: center !important; 298 | display: block !important; 299 | float: unset !important; 300 | } 301 | 302 | /* Recommended videos layout */ 303 | #recommendedVideos { 304 | list-style: none !important; 305 | display: flex !important; 306 | flex-direction: row !important; 307 | align-items: flex-start !important; 308 | justify-content: space-between !important; 309 | text-align: left !important; 310 | } 311 | 312 | /* Thumbnail wrapper */ 313 | #recommendedVideosPage .videoBox { 314 | font-size: 0.813rem !important; /* 13px/16px = 0.813rem */ 315 | color: #a6adc8 !important; /* Mocha -> Subtext0 */ 316 | margin: 0 5px !important; 317 | } 318 | 319 | /* Thumbnail wrapper */ 320 | #recommendedVideos .videoBox .phimage { 321 | width: auto !important; 322 | border-radius: 4px !important; 323 | } 324 | 325 | /* Thumbnail info wrapper */ 326 | .thumbnail-info-wrapper { 327 | color: #a6adc8 !important; /* Mocha -> Subtext0 */ 328 | font-size: 0.813rem !important; /* 13px/16px = 0.813rem */ 329 | float: none !important; 330 | width: auto !important; 331 | } 332 | 333 | /* Title of video or playlist */ 334 | .thumbnail-info-wrapper .title { 335 | font-size: 0.813rem !important; 336 | font-weight: 700 !important; 337 | margin: 12px 0 3px !important; 338 | } 339 | 340 | /* The user/channel name wrapper */ 341 | .thumbnail-info-wrapper .usernameWrap { 342 | margin-top: -1px; /* Fixes slight off-center text */ 343 | } 344 | 345 | /* The user/channel name link */ 346 | .thumbnail-info-wrapper .usernameWrap a { 347 | font-size: 0.813rem !important; /* 13px/16px = 0.813rem */ 348 | color: #a6adc8 !important; 349 | } 350 | 351 | /* The verified badge and user/channel name wrapper */ 352 | .thumbnail-info-wrapper .videoUploaderBlock { 353 | margin-bottom: 5px !important; 354 | } 355 | 356 | /* The verified badge */ 357 | .thumbnail-info-wrapper .own-video-thumbnail { 358 | margin-right: 2px !important; 359 | } 360 | 361 | /* The views and likes wrapper */ 362 | .thumbnail-info-wrapper .videoDetailsBlock { 363 | margin-bottom: 5px !important; 364 | } 365 | 366 | /* The views */ 367 | .thumbnail-info-wrapper .videoDetailsBlock .views { 368 | font-size: 0.813rem !important; /* 13px/16px = 0.813rem */ 369 | } 370 | 371 | /* The rating */ 372 | .thumbnail-info-wrapper .videoDetailsBlock .rating-container i, 373 | .thumbnail-info-wrapper .videoDetailsBlock .rating-container .value { 374 | font-size: 0.813rem !important; /* 13px/16px = 0.813rem */ 375 | } 376 | 377 | /* The "load more" button */ 378 | .more_recommended_btn { 379 | margin: 2rem 0 !important; 380 | } 381 | 382 | /* Make "HD" icon more visible on thumbnails */ 383 | 384 | .hd-thumbnail { 385 | color: #f90 !important; 386 | } 387 | 388 | /* Show all playlists without scrolling in "add to" */ 389 | 390 | .slimScrollDiv { 391 | height: auto !important; 392 | } 393 | 394 | #scrollbar_watch { 395 | max-height: unset !important; 396 | } 397 | 398 | /* Hide premium video from related videos sidebar */ 399 | 400 | #relateRecommendedItems li:nth-of-type(5) { 401 | display: none !important; 402 | } 403 | 404 | /* Prevent animating player size change on each page load */ 405 | 406 | #main-container .video-wrapper #player.wide { 407 | transition: none !important; 408 | } 409 | 410 | /* Allow narrower player */ 411 | 412 | #player { 413 | min-width: 0 !important; 414 | } 415 | 416 | /* Fit more playlists into "add to" popup */ 417 | 418 | .playlist-menu-addTo { 419 | display: none; 420 | } 421 | 422 | .add-to-playlist-menu #scrollThumbs, 423 | .playlist-option-menu #scrollThumbs { 424 | height: 320px !important; 425 | max-height: 35vh !important; 426 | } 427 | 428 | .add-to-playlist-menu ul.custom-playlist li { 429 | font-size: 12px; 430 | height: 24px; 431 | } 432 | 433 | .add-to-playlist-menu .playlist-menu-createNew { 434 | font-size: 12px !important; 435 | height: 38px !important; 436 | } 437 | 438 | .add-to-playlist-menu .playlist-menu-createNew a { 439 | padding-top: 8px !important; 440 | font-weight: 400 !important; 441 | } 442 | 443 | /* Hide playlist bar if disabled in options */ 444 | 445 | .playlist-bar { 446 | display: ${OPTIONS.hidePlaylistBar ? 'none' : 'block'}; 447 | } 448 | 449 | /** 450 | * Improve loading indicator lines on thumbnails 451 | * 452 | * Using colors from the Catppuccin palette available at https://github.com/catppuccin. 453 | */ 454 | 455 | .preloadLine { 456 | background: #81c8be; /* Mocha -> Teal */ 457 | box-shadow: 0 0 3px #1e1e2e; /* Mocha -> Base */ 458 | } 459 | 460 | /* Fade in and out semitransparent elements */ 461 | 462 | .tab-menu-item { 463 | opacity: 0.4 !important; 464 | padding: 0 16px !important; 465 | transition: all 0.2s ease !important; 466 | } 467 | 468 | .tab-menu-item.active { 469 | opacity: 0.5 !important; 470 | } 471 | 472 | .tab-menu-wrapper-cell:hover .tab-menu-item { 473 | opacity: 1 !important; 474 | } 475 | 476 | .votes-fav-wrap .icon-wrapper:hover, 477 | .votes-fav-wrap .icon-wrapper.active:hover { 478 | opacity: 1 !important; 479 | } 480 | 481 | .votes-fav-wrap .icon-wrapper { 482 | opacity: 0.4 !important; 483 | transition: all 0.2s ease !important; 484 | } 485 | 486 | .votes-fav-wrap .icon-wrapper.active { 487 | opacity: 0.7 !important; 488 | } 489 | `; 490 | 491 | /** 492 | * References to video element and container if they exist on the page 493 | */ 494 | const player = document.querySelector('#player'); 495 | const video = document.querySelector('video'); 496 | const distractions = [ 497 | '.video-info-row:not(.userRow)', 498 | '#header' 499 | ]; 500 | const isOnVideoPage = 501 | /^http[s]*:\/\/[www.]*pornhub\.com\/view_video.php/.test(window.location.href) && player; 502 | 503 | /** 504 | * Creation of option buttons 505 | */ 506 | 507 | const showTitlesButton = document.createElement('a'); 508 | const showTitlesButtonText = document.createElement('span'); 509 | const showTitlesButtonState = getButtonState(OPTIONS.showTitles); 510 | 511 | const loadMoreButton = document.createElement('a'); 512 | const loadMoreButtonText = document.createElement('span'); 513 | const loadMoreButtonState = getButtonState(OPTIONS.loadMore); 514 | 515 | const cinemaButton = document.createElement('a'); 516 | const cinemaButtonText = document.createElement('span'); 517 | const cinemaButtonState = getButtonState(OPTIONS.cinemaMode); 518 | 519 | const scrollButton = document.createElement('a'); 520 | const scrollButtonText = document.createElement('span'); 521 | 522 | const scrollPlaylistsButton = document.createElement('a'); 523 | const scrollPlaylistsButtonText = document.createElement('span'); 524 | 525 | const playlistBarButton = document.createElement('a'); 526 | const playlistBarButtonText = document.createElement('span'); 527 | const playlistBarButtonState = getButtonState(OPTIONS.hidePlaylistBar); 528 | 529 | const verifiedButton = document.createElement('a'); 530 | const verifiedButtonText = document.createElement('span'); 531 | const verifiedButtonState = getButtonState(OPTIONS.showOnlyVerified); 532 | 533 | const hideWatchedButton = document.createElement('a'); 534 | const hideWatchedButtonText = document.createElement('span'); 535 | const hideWatchedButtonState = getButtonState(OPTIONS.hideWatchedVideos); 536 | 537 | const hdButton = document.createElement('a'); 538 | const hdButtonText = document.createElement('span'); 539 | const hdButtonState = getButtonState(OPTIONS.showOnlyHd); 540 | 541 | const redirectToVideosButton = document.createElement('a'); 542 | const redirectToVideosButtonText = document.createElement('span'); 543 | const redirectToVideosButtonState = getButtonState(OPTIONS.redirectToVideos); 544 | 545 | const redirectPremiumVideosButton = document.createElement('a'); 546 | const redirectPremiumVideosButtonText = document.createElement('span'); 547 | const redirectPremiumVideosButtonState = getButtonState(OPTIONS.redirectPremiumVideos); 548 | 549 | const durationShortButton = document.createElement('a'); 550 | const durationShortButtonText = document.createElement('span'); 551 | const durationShortButtonState = getButtonState(!OPTIONS.durationFilter.min); 552 | 553 | const durationMediumButton = document.createElement('a'); 554 | const durationMediumButtonText = document.createElement('span'); 555 | const durationMediumButtonState = getButtonState(OPTIONS.durationFilter.min <= 8 && OPTIONS.durationFilter.max >= 20); 556 | 557 | const openWithoutPlaylistButton = document.createElement('a'); 558 | const openWithoutPlaylistButtonText = document.createElement('span'); 559 | const openWithoutPlaylistButtonState = getButtonState(OPTIONS.openWithoutPlaylist); 560 | 561 | const largerButton = document.createElement('a'); 562 | const largerButtonText = document.createElement('span'); 563 | 564 | const smallerButton = document.createElement('a'); 565 | const smallerButtonText = document.createElement('span'); 566 | 567 | /** 568 | * Returns an `on` or `off` CSS class name based on the boolean evaluation 569 | * of the `state` parameter, as convenience method when setting UI state. 570 | */ 571 | function getButtonState(state) { 572 | return state ? 'plus-button-isOn' : 'plus-button-isOff'; 573 | } 574 | 575 | cinemaButtonText.textContent = 'Cinema Mode'; 576 | cinemaButtonText.classList.add('text'); 577 | cinemaButton.appendChild(cinemaButtonText); 578 | cinemaButton.classList.add(cinemaButtonState, 'plus-button'); 579 | cinemaButton.addEventListener('click', () => { 580 | OPTIONS.cinemaMode = !OPTIONS.cinemaMode; 581 | localStorage.setItem('plus_cinemaMode', OPTIONS.cinemaMode); 582 | 583 | if (OPTIONS.cinemaMode) { 584 | cinemaButton.classList.replace('plus-button-isOff', 'plus-button-isOn'); 585 | } else { 586 | cinemaButton.classList.replace('plus-button-isOn', 'plus-button-isOff'); 587 | } 588 | 589 | if (isOnVideoPage) { 590 | handleDistractions(); 591 | } 592 | }); 593 | 594 | showTitlesButtonText.textContent = "Show video titles"; 595 | showTitlesButtonText.classList.add('text'); 596 | showTitlesButton.appendChild(showTitlesButtonText); 597 | showTitlesButton.classList.add('plus-button'); 598 | showTitlesButton.addEventListener('click', () => { 599 | const container = document.querySelector('#main-container'); 600 | 601 | if (isOnVideoPage) { 602 | container.scrollIntoView(); 603 | } 604 | }); 605 | 606 | loadMoreButtonText.textContent = "Autoload more"; 607 | loadMoreButtonText.classList.add('text'); 608 | loadMoreButton.appendChild(loadMoreButtonText); 609 | loadMoreButton.classList.add('plus-button'); 610 | loadMoreButton.addEventListener('click', () => { 611 | if (OPTIONS.loadMore) { 612 | loadMoreButton.classList.replace('plus-button-isOff', 'plus-button-isOn'); 613 | } else { 614 | loadMoreButton.classList.replace('plus-button-isOn', 'plus-button-isOff'); 615 | } 616 | 617 | if (document.querySelector('.videoBox') && OPTIONS.loadMore) { 618 | loadMore(); 619 | } 620 | }); 621 | 622 | scrollButtonText.textContent = "Scroll to Video"; 623 | scrollButtonText.classList.add('text'); 624 | scrollButton.appendChild(scrollButtonText); 625 | scrollButton.classList.add('plus-button'); 626 | scrollButton.addEventListener('click', () => { 627 | const container = document.querySelector('#main-container'); 628 | 629 | if (container) { 630 | container.scrollIntoView(); 631 | } 632 | }); 633 | 634 | scrollPlaylistsButtonText.textContent = "Scroll to Playlists"; 635 | scrollPlaylistsButtonText.classList.add('text'); 636 | scrollPlaylistsButton.appendChild(scrollPlaylistsButtonText); 637 | scrollPlaylistsButton.classList.add('plus-button'); 638 | scrollPlaylistsButton.addEventListener('click', () => { 639 | const container = document.querySelector('#under-player-playlists'); 640 | 641 | if (container) { 642 | container.scrollIntoView(); 643 | } 644 | }); 645 | 646 | verifiedButtonText.textContent = 'Verified Only'; 647 | verifiedButtonText.classList.add('text'); 648 | verifiedButton.appendChild(verifiedButtonText); 649 | verifiedButton.classList.add(verifiedButtonState, 'plus-button'); 650 | verifiedButton.addEventListener('click', () => { 651 | OPTIONS.showOnlyVerified = !OPTIONS.showOnlyVerified; 652 | localStorage.setItem('plus_showOnlyVerified', OPTIONS.showOnlyVerified); 653 | 654 | if (OPTIONS.showOnlyVerified) { 655 | verifiedButton.classList.replace('plus-button-isOff', 'plus-button-isOn'); 656 | } else { 657 | verifiedButton.classList.replace('plus-button-isOn', 'plus-button-isOff'); 658 | } 659 | 660 | filterVideos(); 661 | }); 662 | 663 | hdButtonText.textContent = 'HD Only'; 664 | hdButtonText.classList.add('text'); 665 | hdButton.appendChild(hdButtonText); 666 | hdButton.classList.add(hdButtonState, 'plus-button'); 667 | hdButton.addEventListener('click', () => { 668 | OPTIONS.showOnlyHd = !OPTIONS.showOnlyHd; 669 | localStorage.setItem('plus_showOnlyHd', OPTIONS.showOnlyHd); 670 | 671 | if (OPTIONS.showOnlyHd) { 672 | hdButton.classList.replace('plus-button-isOff', 'plus-button-isOn'); 673 | } else { 674 | hdButton.classList.replace('plus-button-isOn', 'plus-button-isOff'); 675 | } 676 | 677 | filterVideos(); 678 | }); 679 | 680 | 681 | playlistBarButtonText.textContent = 'Hide Playlist Bar'; 682 | playlistBarButtonText.classList.add('text'); 683 | playlistBarButton.appendChild(playlistBarButtonText); 684 | playlistBarButton.classList.add(playlistBarButtonState, 'plus-button'); 685 | playlistBarButton.addEventListener('click', () => { 686 | OPTIONS.hidePlaylistBar = !OPTIONS.hidePlaylistBar; 687 | localStorage.setItem('plus_hidePlaylistBar', OPTIONS.hidePlaylistBar); 688 | 689 | if (OPTIONS.hidePlaylistBar) { 690 | playlistBarButton.classList.replace('plus-button-isOff', 'plus-button-isOn'); 691 | } else { 692 | playlistBarButton.classList.replace('plus-button-isOn', 'plus-button-isOff'); 693 | } 694 | 695 | const playlistBar = document.querySelector('.playlist-bar'); 696 | 697 | if (playlistBar) { 698 | playlistBar.style.display = OPTIONS.hidePlaylistBar ? 'none' : 'block'; 699 | } 700 | }); 701 | 702 | hideWatchedButtonText.textContent = 'Unwatched Only'; 703 | hideWatchedButtonText.classList.add('text'); 704 | hideWatchedButton.appendChild(hideWatchedButtonText); 705 | hideWatchedButton.classList.add(hideWatchedButtonState, 'plus-button'); 706 | hideWatchedButton.addEventListener('click', () => { 707 | OPTIONS.hideWatchedVideos = !OPTIONS.hideWatchedVideos; 708 | localStorage.setItem('plus_hideWatchedVideos', OPTIONS.hideWatchedVideos); 709 | 710 | if (OPTIONS.hideWatchedVideos) { 711 | hideWatchedButton.classList.replace('plus-button-isOff', 'plus-button-isOn'); 712 | } else { 713 | hideWatchedButton.classList.replace('plus-button-isOn', 'plus-button-isOff'); 714 | } 715 | 716 | filterVideos(); 717 | }); 718 | 719 | redirectToVideosButtonText.textContent = 'Redirect Profiles to Uploads'; 720 | redirectToVideosButtonText.classList.add('text'); 721 | redirectToVideosButton.appendChild(redirectToVideosButtonText); 722 | redirectToVideosButton.classList.add(redirectToVideosButtonState, 'plus-button'); 723 | redirectToVideosButton.addEventListener('click', () => { 724 | OPTIONS.redirectToVideos = !OPTIONS.redirectToVideos; 725 | localStorage.setItem('plus_redirectToVideos', OPTIONS.redirectToVideos); 726 | 727 | if (OPTIONS.redirectToVideos) { 728 | redirectToVideosButton.classList.replace('plus-button-isOff', 'plus-button-isOn'); 729 | } else { 730 | redirectToVideosButton.classList.replace('plus-button-isOn', 'plus-button-isOff'); 731 | } 732 | }); 733 | 734 | redirectPremiumVideosButtonText.textContent = 'Redirect Empty Premium Members To Regular'; 735 | redirectPremiumVideosButtonText.classList.add('text'); 736 | redirectPremiumVideosButton.appendChild(redirectPremiumVideosButtonText); 737 | redirectPremiumVideosButton.classList.add(redirectPremiumVideosButtonState, 'plus-button'); 738 | redirectPremiumVideosButton.addEventListener('click', () => { 739 | OPTIONS.redirectPremiumVideos = !OPTIONS.redirectPremiumVideos; 740 | localStorage.setItem('plus_redirectPremiumVideos', OPTIONS.redirectPremiumVideos); 741 | 742 | if (OPTIONS.redirectPremiumVideos) { 743 | redirectPremiumVideosButton.classList.replace('plus-button-isOff', 'plus-button-isOn'); 744 | } else { 745 | redirectPremiumVideosButton.classList.replace('plus-button-isOn', 'plus-button-isOff'); 746 | } 747 | }); 748 | 749 | durationShortButtonText.textContent = 'Short Videos (< 8 min)'; 750 | durationShortButtonText.classList.add('text'); 751 | durationShortButton.appendChild(durationShortButtonText); 752 | durationShortButton.classList.add(durationShortButtonState, 'plus-button'); 753 | durationShortButton.addEventListener('click', () => { 754 | OPTIONS.durationFilter.min = OPTIONS.durationFilter.min ? 0 : 8; 755 | localStorage.setItem('plus_durationFilter', JSON.stringify(OPTIONS.durationFilter)); 756 | 757 | if (!OPTIONS.durationFilter.min) { 758 | durationShortButton.classList.replace('plus-button-isOff', 'plus-button-isOn'); 759 | filterVideos(); 760 | } else { 761 | durationShortButton.classList.replace('plus-button-isOn', 'plus-button-isOff'); 762 | filterVideos(); 763 | } 764 | }); 765 | 766 | durationMediumButtonText.textContent = 'Medium Videos (8-20 min)'; 767 | durationMediumButtonText.classList.add('text'); 768 | durationMediumButton.appendChild(durationMediumButtonText); 769 | durationMediumButton.classList.add(durationMediumButtonState, 'plus-button'); 770 | durationMediumButton.addEventListener('click', () => { 771 | OPTIONS.durationFilter.min = OPTIONS.durationFilter.min !== 8 ? 8 : 0; 772 | OPTIONS.durationFilter.max = OPTIONS.durationFilter.max !== 20 ? 20 : 0; 773 | 774 | localStorage.setItem('plus_durationFilter', JSON.stringify(OPTIONS.durationFilter)); 775 | 776 | if (OPTIONS.durationFilter.min === 8 && OPTIONS.durationFilter.max === 20) { 777 | durationMediumButton.classList.replace('plus-button-isOff', 'plus-button-isOn'); 778 | filterVideos(); 779 | } else { 780 | durationMediumButton.classList.replace('plus-button-isOn', 'plus-button-isOff'); 781 | filterVideos(); 782 | } 783 | }); 784 | 785 | 786 | largerButton.textContent = "Larger previews"; 787 | largerButtonText.classList.add('text'); 788 | largerButton.appendChild(largerButtonText); 789 | largerButton.classList.add('plus-button'); 790 | largerButton.addEventListener('click', () => { 791 | setRelatedColumns(OPTIONS.relatedColumns - 1) 792 | }); 793 | 794 | smallerButton.textContent = "Smaller previews"; 795 | smallerButtonText.classList.add('text'); 796 | smallerButton.appendChild(smallerButtonText); 797 | smallerButton.classList.add('plus-button'); 798 | smallerButton.addEventListener('click', () => { 799 | setRelatedColumns(OPTIONS.relatedColumns + 1) 800 | }); 801 | 802 | /** 803 | * Order option buttons in a container 804 | */ 805 | 806 | const buttons = document.createElement('div'); 807 | const durationFilters = []; 808 | 809 | buttons.classList.add('plus-buttons'); 810 | 811 | buttons.appendChild(cinemaButton); 812 | buttons.appendChild(scrollButton); 813 | buttons.appendChild(scrollPlaylistsButton); 814 | buttons.appendChild(smallerButton); 815 | buttons.appendChild(largerButton); 816 | buttons.appendChild(verifiedButton); 817 | buttons.appendChild(hdButton); 818 | buttons.appendChild(hideWatchedButton); 819 | buttons.appendChild(redirectToVideosButton); 820 | buttons.appendChild(redirectPremiumVideosButton); 821 | buttons.appendChild(playlistBarButton); 822 | 823 | /** 824 | * Generate buttons for filtering by duration. Buttons are created based on 825 | * `OPTIONS.durationPresets`, an array of objects containing max and min duration in minutes, 826 | * and a label (like "Short Videos"). 827 | */ 828 | OPTIONS.durationPresets.forEach(preset => { 829 | const button = document.createElement('a'); 830 | const buttonText = document.createElement('span'); 831 | const buttonState = getButtonState(OPTIONS.durationFilter.min === preset.min && 832 | OPTIONS.durationFilter.max === preset.max); 833 | 834 | buttonText.textContent = `${preset.label} (${preset.min}-${preset.max} mins)`; 835 | buttonText.classList.add('text'); 836 | button.appendChild(buttonText); 837 | button.classList.add(buttonState, 'plus-button'); 838 | 839 | durationFilters.push({ 840 | button, 841 | preset 842 | }); 843 | }); 844 | 845 | /** 846 | * We needed access to all buttons, their state, and the duration values, to be able to switch 847 | * all the buttons to off state before we apply the newly selected filters. For simplicity and 848 | * sanity, only one duration range can be selected at a time. 849 | */ 850 | durationFilters.forEach(({ 851 | button, 852 | preset 853 | }) => { 854 | buttons.appendChild(button); 855 | 856 | button.addEventListener('click', () => { 857 | durationFilters.forEach(filter => filter.button.classList.replace('plus-button-isOn', 'plus-button-isOff')); 858 | 859 | OPTIONS.durationFilter.min = OPTIONS.durationFilter.min === preset.min ? 0 : preset.min; 860 | OPTIONS.durationFilter.max = OPTIONS.durationFilter.max === preset.max ? 0 : preset.max; 861 | 862 | localStorage.setItem('plus_durationFilter', JSON.stringify(OPTIONS.durationFilter)); 863 | 864 | if (OPTIONS.durationFilter.min === preset.min && 865 | OPTIONS.durationFilter.max === preset.max) { 866 | button.classList.replace('plus-button-isOff', 'plus-button-isOn'); 867 | filterVideos(); 868 | } else { 869 | button.classList.replace('plus-button-isOn', 'plus-button-isOff'); 870 | filterVideos(); 871 | } 872 | }); 873 | }); 874 | 875 | document.body.appendChild(buttons); // Button container ready and added to page 876 | 877 | /** 878 | * Observe for DOM mutations, such as loading more videos. 879 | */ 880 | const observer = new MutationObserver(function(mutations) { 881 | mutations.forEach(function(mutation) { 882 | if (!!mutation.target.className) { 883 | // Filter videos when loading more 884 | if (mutation.addedNodes.length) { 885 | filterVideos(); 886 | showTitles(); 887 | } 888 | 889 | // Always wide player 890 | if (!!mutation.target.id.match(/\bplayer\b/)) { 891 | mutation.target.className = "wide"; 892 | return; 893 | } 894 | 895 | if (!!mutation.target.id.match(/main-container/) && !!mutation.previousSibling && !!mutation.previousSibling.id && !!mutation.previousSibling.id.match(/rightColVideoPage/)) { 896 | mutation.previousSibling.className = "wide"; 897 | return; 898 | } 899 | 900 | // Update wide player button / HTML5 only 901 | if (!!mutation.target.className.match(/mhp1138_front/)) { 902 | mutation.target.childNodes.forEach(function(node) { 903 | if (!!node.className.match(/mhp1138_cinema/)) { 904 | node.className = "mhp1138_cinema mhp1138_cinemaState"; 905 | return; 906 | } 907 | }); 908 | return; 909 | } 910 | 911 | // Center the video 912 | if (!!mutation.target.className.match(/playerFlvContainer/)) { 913 | mutation.addedNodes.forEach(function(element) { 914 | if (!!element && element.id === "pb_template") { 915 | var node = document.createElement("div"); 916 | node.className = "mhp1138_playerStateIcon"; 917 | node.setAttribute("style", "opacity: 1"); 918 | node.innerHTML = 919 | "
" + 920 | "
" + 921 | "
" + 922 | "
"; 923 | element.appendChild(node); 924 | return; 925 | } 926 | }); 927 | } 928 | return; 929 | } 930 | }); 931 | }); 932 | 933 | observer.observe(document, { 934 | childList: true, 935 | subtree: true 936 | }); 937 | 938 | /** 939 | * General UI related functions 940 | */ 941 | 942 | /** 943 | * Clicking a video on a playlist page opens it without the playlist at the 944 | * top if the option is enabled. 945 | */ 946 | function updatePlaylistLinks() { 947 | if (OPTIONS.openWithoutPlaylist) { 948 | document.querySelectorAll('#videoPlaylist li a').forEach(link => { 949 | link.href = link.href.replace('pkey', 'nopkey'); 950 | }); 951 | } else { 952 | document.querySelectorAll('#videoPlaylist li a').forEach(link => { 953 | link.href = link.href.replace('nopkey', 'pkey'); 954 | }); 955 | } 956 | } 957 | 958 | /** 959 | * Allow scrolling the page when mouse hovers playlists in "add to", by 960 | * cloning the playlist scroll container to remove the listeners that 961 | * `preventDefault()`. 962 | */ 963 | function fixScrollContainer(container) { 964 | if (container) { 965 | container.parentNode.replaceChild(container.cloneNode(true), container); 966 | } 967 | } 968 | 969 | /** 970 | * Video thumbnail box related functions 971 | */ 972 | 973 | /** 974 | * Checks if video box links to a video made by a verified member 975 | */ 976 | function videoIsVerified(box) { 977 | return box.querySelector('.own-video-thumbnail'); 978 | } 979 | 980 | /** 981 | * Checks if video box links to a HD video 982 | */ 983 | function videoIsHd(box) { 984 | return box.querySelector('.hd-thumbnail'); 985 | } 986 | 987 | /** 988 | * Checks if the video box has a "watched" label on it (the video has 989 | * already been viewed) 990 | */ 991 | function videoIsWatched(box) { 992 | return box.querySelector('.watchedVideoText'); 993 | } 994 | 995 | /** 996 | * Checks if video box links to a video that is within the selected duration 997 | * range, if one has been selected in options. 998 | */ 999 | function videoIsWithinDuration(box) { 1000 | // Parse integer minutes from video duration text 1001 | const mins = parseInt(box.querySelector('.duration').textContent.split(":")[0]); 1002 | const minMins = OPTIONS.durationFilter.min; 1003 | const maxMins = OPTIONS.durationFilter.max; 1004 | 1005 | // If either max or min duration has been selected 1006 | if (minMins || maxMins) { 1007 | // If any max duration is set (otherwise defaults to 0 for no max) 1008 | const hasMaxDuration = !!maxMins; 1009 | // True if the video is shorther than we want (min defaults to 0) 1010 | const isBelowMin = mins < minMins; 1011 | // True if a max duration is set and the video exceeds it 1012 | const isAboveMax = hasMaxDuration && (mins > maxMins - 1); 1013 | // One minute negative offset since we ignore any extra seconds 1014 | 1015 | return !isBelowMin && !isAboveMax; 1016 | } else { 1017 | return true; 1018 | } 1019 | } 1020 | 1021 | /** 1022 | * Sorts elements in the "add to playlist" list. 1023 | */ 1024 | function sortPlaylistList(list) { 1025 | const playlistItems = {}; 1026 | 1027 | // Get playlist titles 1028 | list.querySelectorAll('li').forEach(item => { 1029 | const name = item.querySelector('.playlist-name').innerText; 1030 | playlistItems[name] = item; 1031 | }); 1032 | 1033 | // Sort by titles and re-insert into list 1034 | Object.keys(playlistItems).sort().forEach(item => { 1035 | list.appendChild(playlistItems[item]); 1036 | }); 1037 | } 1038 | 1039 | /** 1040 | * Automatically load more videos. 1041 | */ 1042 | function loadMore() { 1043 | document.querySelector('#loadMoreRelatedVideosCenter').click(); 1044 | } 1045 | 1046 | /** 1047 | * Show video titles. 1048 | */ 1049 | function showTitles() { 1050 | if (OPTIONS.showTitles) { 1051 | document.querySelector('.videoUploaderBlock').classList.remove('plus-hidden'); 1052 | document.querySelector('.thumbnail-info-wrapper').classList.remove('plus-hidden'); 1053 | } else { 1054 | document.querySelector('.videoUploaderBlock').classList.add('plus-hidden'); 1055 | document.querySelector('.thumbnail-info-wrapper').classList.add('plus-hidden'); 1056 | } 1057 | } 1058 | 1059 | /** 1060 | * Resets video thumbnail box to its original visible state. 1061 | */ 1062 | function resetVideo(box) { 1063 | showVideo(box); 1064 | } 1065 | 1066 | /** 1067 | * Shows the video thumbnail box. 1068 | */ 1069 | function showVideo(box) { 1070 | box.classList.remove('plus-hidden'); 1071 | } 1072 | 1073 | /** 1074 | * Hides the video thumbnail box. 1075 | */ 1076 | function hideVideo(box) { 1077 | box.classList.add('plus-hidden'); 1078 | } 1079 | 1080 | /** 1081 | * Does the required checks to filter out unwanted video boxes according to 1082 | * options. Each box is reset to it's original visible state, then it's 1083 | * checked against relevant options to determine if it should be hidden or 1084 | * stay visible. 1085 | */ 1086 | function filterVideos() { 1087 | document.querySelectorAll('li.videoblock.videoBox').forEach(box => { 1088 | const state = { 1089 | verified: videoIsVerified(box), 1090 | watched: videoIsWatched(box), 1091 | hd: videoIsHd(box), 1092 | inDurationRange: videoIsWithinDuration(box) 1093 | }; 1094 | 1095 | const shouldHide = 1096 | (OPTIONS.showOnlyHd && !state.hd) || 1097 | (OPTIONS.showOnlyVerified && !state.verified) || 1098 | (OPTIONS.hideWatchedVideos && state.watched) || 1099 | !state.inDurationRange; 1100 | 1101 | // Reset the box to its original visible state so we can focus only on 1102 | // what to hide instead of also on what to unhide 1103 | resetVideo(box); 1104 | 1105 | if (shouldHide) { 1106 | hideVideo(box); 1107 | } 1108 | }); 1109 | } 1110 | 1111 | /** 1112 | * Filters "add to playlist" list by letter. 1113 | */ 1114 | function filterPlaylistListByCharacter(list, letter) { 1115 | if (list && letter === '#') { 1116 | // Special characters specified 1117 | list.querySelectorAll('li').forEach(item => { 1118 | const name = item.querySelector('.playlist-name').innerText; 1119 | 1120 | // Hide items with non-alphabetic first characters 1121 | if (!name[0].match(/[a-z]/i)) { 1122 | item.classList.remove('plus-hidden'); 1123 | } else { 1124 | item.classList.add('plus-hidden'); 1125 | } 1126 | }); 1127 | } else if (list && letter && letter.length === 1) { 1128 | // Letter specified 1129 | list.querySelectorAll('li').forEach(item => { 1130 | const name = item.querySelector('.playlist-name').innerText; 1131 | 1132 | if (name[0].toLowerCase() !== letter.toLowerCase()) { 1133 | item.classList.add('plus-hidden'); 1134 | } else { 1135 | item.classList.remove('plus-hidden'); 1136 | } 1137 | }); 1138 | } else { 1139 | // Reset specified 1140 | list.querySelectorAll('li').forEach(item => item.classList.remove('plus-hidden')); 1141 | } 1142 | } 1143 | 1144 | const handleDistractions = () => { 1145 | distractions.forEach((distraction) => { 1146 | const element = document.querySelector(distraction); 1147 | 1148 | if (element) { 1149 | const handleMouseOver = () => { 1150 | element.style.opacity = 1; 1151 | } 1152 | 1153 | const handleMouseOut = () => { 1154 | element.style.opacity = 0.2; 1155 | } 1156 | 1157 | if (OPTIONS.cinemaMode) { 1158 | element.style.transition = 'all 0.2s ease'; 1159 | element.style.opacity = 0.2; 1160 | 1161 | element.addEventListener('mouseover', handleMouseOver, false); 1162 | element.addEventListener('mouseout', handleMouseOut, false); 1163 | } else { 1164 | element.style.opacity = 1; 1165 | element.removeEventListener('mouseover', handleMouseOver, false); 1166 | element.removeEventListener('mouseout', handleMouseOut, false); 1167 | } 1168 | } 1169 | }); 1170 | }; 1171 | 1172 | const setRelatedColumns = columns => { 1173 | // Removes a column to the video preview grids, making them bigger 1174 | const container = document.getElementById('relatedVideosCenter'); 1175 | 1176 | if (container) { 1177 | const min = 1; 1178 | const max = 8; 1179 | 1180 | if (columns <= min) { 1181 | columns = min; 1182 | } else if (columns >= max) { 1183 | columns = max; 1184 | } 1185 | 1186 | container.style.gridTemplateColumns = `repeat(${columns}, 1fr)`; 1187 | localStorage.setItem('plus_relatedColumns', columns); 1188 | OPTIONS.relatedColumns = columns; 1189 | } 1190 | }; 1191 | 1192 | /** 1193 | * Initialize video pages (that contain a valid video element) 1194 | */ 1195 | 1196 | if (isOnVideoPage) { 1197 | // Let us scroll the page despite the mouse pointer hovering over the "Add to" playlist area 1198 | // const scrollContainer = document.querySelector('#scrollbar_watch'); 1199 | 1200 | // if (scrollContainer) { 1201 | // fixScrollContainer(scrollContainer); 1202 | // } 1203 | 1204 | handleDistractions(); 1205 | setRelatedColumns(OPTIONS.relatedColumns); 1206 | 1207 | // Listen to "add to" tab clicks 1208 | const addToTab = document.querySelector('[data-tab="add-to-tab"]'); 1209 | const addToTabContainer = document.querySelector('.add-to-tab'); 1210 | 1211 | const initSortingFeature = () => { 1212 | // Only run once 1213 | addToTab.removeEventListener('click', initSortingFeature, false); 1214 | 1215 | // Add sort playlist list button 1216 | const subActions = document.querySelector('.add-to-tab .video-actions-sub-menu'); 1217 | const sortItem = document.createElement('div'); 1218 | const hideItem = document.createElement('div'); 1219 | 1220 | sortItem.innerHTML = 'Sort alphabetically'; 1221 | sortItem.classList.add('js-sortItem', 'tab-sub-menu-item'); 1222 | sortItem.addEventListener('click', () => { 1223 | const playlistList = document.querySelector('#custom-playlist-detailed'); 1224 | 1225 | if (playlistList) { 1226 | sortPlaylistList(playlistList); 1227 | } 1228 | }); 1229 | 1230 | // Add hide button 1231 | hideItem.innerHTML = 'Hide'; 1232 | hideItem.classList.add('js-sortItem', 'tab-sub-menu-item'); 1233 | hideItem.addEventListener('click', () => { 1234 | if (addToTabContainer.classList.contains('active')) { 1235 | addToTabContainer.classList.remove('active'); 1236 | } 1237 | }); 1238 | 1239 | subActions.insertBefore(sortItem, subActions.firstElementChild); 1240 | subActions.insertBefore(hideItem, subActions.firstElementChild); 1241 | 1242 | 1243 | const letters = document.createElement('div'); 1244 | const resetButton = document.createElement('span'); 1245 | const specialButton = document.createElement('span'); 1246 | 1247 | for (let i = 0; i < 26; i++) { 1248 | const letter = document.createElement('span'); 1249 | const char = (i + 10).toString(36); 1250 | 1251 | letter.innerHTML = char; 1252 | letter.setAttribute('data-letter', char); 1253 | letters.appendChild(letter); 1254 | } 1255 | 1256 | resetButton.innerHTML = 'All'; 1257 | specialButton.innerHTML = '#'; 1258 | specialButton.setAttribute('data-letter', '#') 1259 | 1260 | letters.insertBefore(specialButton, letters.firstElementChild); 1261 | letters.insertBefore(resetButton, letters.firstElementChild); 1262 | letters.classList.add('plus-letters'); 1263 | letters.addEventListener('click', event => { 1264 | const list = document.querySelector('#custom-playlist-detailed'); 1265 | const letter = event.target.getAttribute('data-letter'); 1266 | 1267 | filterPlaylistListByCharacter(list, letter); 1268 | }); 1269 | 1270 | subActions.parentNode.insertBefore(letters, subActions); 1271 | }; 1272 | 1273 | // Initialize sorting on "add to" tab click 1274 | // addToTab.addEventListener('click', initSortingFeature); 1275 | } 1276 | 1277 | /** 1278 | * Initialize any page that contains a video box 1279 | */ 1280 | 1281 | if (document.querySelector('.videoBox')) { 1282 | setTimeout(() => { 1283 | filterVideos(); 1284 | updatePlaylistLinks(); 1285 | showTitles(); 1286 | }, 1000); 1287 | } 1288 | 1289 | /** 1290 | * Initialize profile pages, channel pages, user pages, star pages 1291 | */ 1292 | 1293 | /** 1294 | * Model, pornstar. user, and channel pages 1295 | */ 1296 | 1297 | if ( 1298 | /^https?:\/\/(www\.)?pornhub(premium)?\.com\/pornstar\/([^\/]+)$/.test(location.href) || 1299 | /^https?:\/\/(www\.)?pornhub(premium)?\.com\/model\/([^\/]+)$/.test(location.href) || 1300 | /^https?:\/\/(www\.)?pornhub(premium)?\.com\/users\/([^\/]+)$/.test(location.href) || 1301 | /^https?:\/\/(www\.)?pornhub(premium)?\.com\/channels\/([^\/]+)$/.test(location.href) 1302 | ) { 1303 | /** 1304 | * Redirect profile pages straight to their video uploads page if the setting is 1305 | * enabled, except in case we just came from the video page (don't loop back). 1306 | * Regex checks if /pornstar/ is followed by a word but no more slashes. 1307 | */ 1308 | if (OPTIONS.redirectToVideos) { 1309 | // Don't redirect if coming stright from videos (e.g. when navigating back) 1310 | if (!/.+\/videos.*/.test(document.referrer)) { 1311 | location.href += '/videos/upload'; 1312 | } 1313 | } 1314 | } 1315 | 1316 | /** 1317 | * Premium model video pages 1318 | */ 1319 | 1320 | if ( 1321 | /^https?:\/\/(www\.)?pornhubpremium\.com\/model\/.+\/videos.*$/.test(location.href) && 1322 | !/^https?:\/\/(www\.)?pornhubpremium\.com\/model\/.+\/videos.*$/.test(document.referrer) && 1323 | !/^https?:\/\/(www\.)?pornhubpremium\.com\/pornstars\?performerType=.+/.test(document.referrer) 1324 | ) { 1325 | // If a model has no premium videos then redirect to the free site 1326 | if (OPTIONS.redirectPremiumVideos) { 1327 | // Check for the empty icon 1328 | const isEmpty = document.querySelector('.video.emptyIcon'); 1329 | 1330 | if (isEmpty) { 1331 | location.hostname = 'pornhub.com'; 1332 | } 1333 | } 1334 | } 1335 | 1336 | 1337 | /* 1338 | * Add styles 1339 | */ 1340 | 1341 | GM_addStyle(sharedStyles); 1342 | GM_addStyle(themeStyles); 1343 | GM_addStyle(generalStyles); 1344 | 1345 | /* 1346 | * Add dynamic styles 1347 | */ 1348 | 1349 | const dynamicStyles = ` 1350 | .plus-buttons { 1351 | margin-right: -${buttons.getBoundingClientRect().width - 18}px; 1352 | margin-top: -${buttons.getBoundingClientRect().height - 18}px; 1353 | } 1354 | 1355 | .plus-buttons:hover { 1356 | margin-right: 0; 1357 | margin-left: 0; 1358 | } 1359 | `; 1360 | 1361 | GM_addStyle(dynamicStyles); 1362 | }, 1000); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # User scripts 2 | User scripts for adult sites, for personal use. Use whatever you want. 3 | 4 | **Note:** 5 | 6 | Scripts are only written and tested in FireMonkey, because I prefer it. Some others have more features and more polished UI, but I find that FireMonkey is leaner and does the job in a less hacky manner. It also handles styles. 7 | 8 | Fixing compatibility issues to make the scripts manager agnostic is a work in progress. 9 | 10 | 11 | ### Using FireMonkey because it: 12 | 13 | * ...handles both scripts and styles. 14 | * ...supports style pre-processing (LESS, SCSS, etc.) similarly to Stylish. 15 | * ...hands scripts to Firefox's UserScripts API, an actual standard, which does the rest. Lean. 16 | * ...lets you extensively customize it, with quick access to great documentation. 17 | * ...is actively developed by the AMO GOAT, Erosman. Enough said. 18 | 19 | ### Notes on compatibility 20 | FireMonkey, as mentioned, injects scripts differently. This means scripts don't necessarily work in 21 | other managers without compatibility adjustments. 22 | 23 | For example, wrapping code in an immediately invoked function expression is not needed, but you'll 24 | want to do that anyway for your scripts to play nice with other managers. It also accepts fewer 25 | meta-data tags and some take slightly different values. It will, however, ignore any unsupported 26 | ones included, preventing compatibility issues. 27 | -------------------------------------------------------------------------------- /Sxyporn Plus.user.css: -------------------------------------------------------------------------------- 1 | /* 2 | ==UserCSS== 3 | @name Sxyporn Plus 4 | @match *://sxyprn.net/* 5 | @version 0.0.1 6 | ==/UserCSS== 7 | */ 8 | 9 | .sharing_toolbox, 10 | .splitter { 11 | display: none; 12 | } 13 | 14 | .next_page, 15 | .back_to, 16 | .show_more, 17 | .mysums_blog { 18 | background: none; 19 | border: none; 20 | padding: 12px 10px; 21 | margin: 12px 0; 22 | } 23 | 24 | #top_panel { 25 | padding: 20px 0; 26 | } 27 | 28 | #top_panel_menu span { 29 | padding-left: 32px; 30 | margin-left: 2px; 31 | } 32 | 33 | #player_el { 34 | height: auto; 35 | } 36 | 37 | .vid_container { 38 | border: none; 39 | } 40 | 41 | #wrapper_div { 42 | width: auto; 43 | margin: 0 100px; 44 | } 45 | 46 | #vid_container_id, 47 | .post_el_post.post_el_small { 48 | width: 100%; 49 | max-width: 100%; 50 | margin: 0; 51 | } 52 | 53 | .post_el_small { 54 | background: none; 55 | width: 556px; 56 | max-width: 100%; 57 | margin: 0; 58 | } 59 | 60 | .post_vid_thumb { 61 | display: inline-block; 62 | text-align: center; 63 | } 64 | 65 | .mini_post_vid_thumb { 66 | transform: translateY(-50%) translateX(-50%); 67 | left: 50%; 68 | } 69 | 70 | .block_header { 71 | border: none; 72 | } 73 | 74 | .splitter_block_header { 75 | background: none; 76 | top: -32px; 77 | } 78 | 79 | .main_footer { 80 | font-size: 0.8125rem; 81 | text-align: center; 82 | border: none; 83 | margin: 36px 0 26px; 84 | } 85 | 86 | .main_footer span { 87 | font-size: 0.95rem; 88 | margin: 0 0.5rem; 89 | } 90 | 91 | #scroll_top_wrap { 92 | cursor: default; 93 | } 94 | 95 | #scroll_top_div { 96 | background: none; 97 | color: #ffffff; 98 | opacity: 0.2; 99 | } 100 | 101 | #scroll_top_wrap:hover #scroll_top_div { 102 | background: none; 103 | color: #ffffff; 104 | opacity: 0.5; 105 | } -------------------------------------------------------------------------------- /XNXX Plus.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @author Mr. Nope 3 | // @version 1.0 4 | // @name XNXX Plus 5 | // @description A kinder XNXX. Because you're worth it. 6 | // @namespace Nope 7 | // @date 2019-02-23 8 | // @include *xnxx.com* 9 | // @run-at document-start 10 | // @grant none 11 | // @license Public Domain 12 | // @icon http://www.viraltrendzz.com/facts/disappointing-truths-porn-industry/attachment/xnxx-logo/ 13 | // @grant GM_addStyle 14 | // ==/UserScript== 15 | 16 | 'use strict'; 17 | 18 | (() => { 19 | const OPTIONS = { 20 | autoplay: JSON.parse(localStorage.getItem('plus_autoplay')) || false, 21 | cinemaMode: JSON.parse(localStorage.getItem('plus_cinemaMode')) || false 22 | }; 23 | 24 | /** 25 | * Shared Styles 26 | */ 27 | const sharedStyles = ` 28 | /* Our own elements */ 29 | 30 | .plus-buttons { 31 | background: rgba(27, 27, 27, 0.9); 32 | box-shadow: 0px 0px 12px rgba(20, 111, 223, 0.9); 33 | font-size: 12px; 34 | position: fixed; 35 | bottom: 10px; 36 | padding: 10px 22px 8px 24px; 37 | right: 0; 38 | z-index: 100; 39 | transition: all 0.3s ease; 40 | 41 | /* Negative margin-right calculated later based on width of buttons */ 42 | } 43 | 44 | .plus-buttons:hover { 45 | box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.3); 46 | } 47 | 48 | .plus-buttons .plus-button { 49 | margin: 10px 0; 50 | padding: 6px 15px; 51 | border-radius: 4px; 52 | font-weight: 700; 53 | display: block; 54 | position: relative; 55 | text-align: center; 56 | vertical-align: top; 57 | cursor: pointer; 58 | border: none; 59 | text-decoration: none; 60 | } 61 | 62 | .plus-buttons a.plus-button { 63 | background: rgb(221, 221, 221); 64 | color: rgb(51, 51, 51); 65 | } 66 | 67 | .plus-buttons a.plus-button:hover { 68 | background: rgb(187, 187, 187); 69 | color: rgb(51, 51, 51); 70 | } 71 | 72 | .plus-buttons a.plus-button.plus-button-isOn { 73 | background: rgb(20, 111, 223); 74 | color: rgb(255, 255, 255); 75 | } 76 | 77 | .plus-buttons a.plus-button.plus-button-isOn:hover { 78 | background: rgb(0, 91, 203); 79 | color: rgb(255, 255, 255); 80 | } 81 | 82 | .plus-hidden { 83 | display: none !important; 84 | } 85 | `; 86 | 87 | /** 88 | * Color Theme 89 | */ 90 | const themeStyles = ` 91 | .plus-buttons { 92 | box-shadow: 0px 0px 12px rgba(102, 147, 241, 0.85); 93 | } 94 | 95 | .plus-buttons:hover { 96 | box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.3); 97 | } 98 | 99 | .plus-buttons a.plus-button { 100 | background: rgb(47, 47, 47); 101 | color: rgb(172, 172, 172); 102 | } 103 | 104 | .plus-buttons a.plus-button:hover { 105 | background: rgb(79, 79, 79); 106 | color: rgb(204, 204, 204); 107 | } 108 | 109 | .plus-buttons a.plus-button.plus-button-isOn { 110 | background: rgb(102, 147, 241); 111 | color: rgb(255, 255, 255); 112 | } 113 | 114 | .plus-buttons a.plus-button.plus-button-isOn:hover { 115 | background: rgb(102, 147, 241); 116 | color: rgb(0, 0, 0); 117 | } 118 | `; 119 | 120 | /** 121 | * Site-Specific Styles 122 | */ 123 | const generalStyles = ` 124 | /* Hide elements */ 125 | 126 | .abovePlayer, 127 | .streamatesModelsContainer, 128 | #headerUpgradePremiumBtn, 129 | #headerUploadBtn, 130 | #PornhubNetworkBar, 131 | #js-abContainterMain, 132 | #hd-rightColVideoPage > :not(#relatedVideosVPage) { 133 | display: none !important; 134 | } 135 | 136 | #related-videos .thumb-block { 137 | opacity: 1; 138 | } 139 | 140 | #related-videos .thumb-block:hover { 141 | opacity: 1; 142 | } 143 | `; 144 | 145 | /** 146 | * Run on page load 147 | */ 148 | window.addEventListener('DOMContentLoaded', () => { 149 | const video = document.querySelector('#html5video video'); // References the HTML5 Video element 150 | 151 | /** 152 | * Create option buttons 153 | */ 154 | 155 | const buttons = document.createElement('div'); 156 | 157 | const scrollButton = document.createElement('a'); 158 | const scrollButtonText = document.createElement('span'); 159 | 160 | const autoplayButton = document.createElement('a'); 161 | const autoplayButtonText = document.createElement('span'); 162 | const autoplayButtonState = OPTIONS.autoplay ? 'plus-button-isOn' : 'plus-button-isOff'; 163 | 164 | const cinemaModeButton = document.createElement('a'); 165 | const cinemaModeButtonText = document.createElement('span'); 166 | const cinemaModeButtonState = OPTIONS.cinemaMode ? 'plus-button-isOn' : 'plus-button-isOff'; 167 | 168 | scrollButtonText.textContent = "Scroll to Top"; 169 | scrollButtonText.classList.add('text'); 170 | scrollButton.appendChild(scrollButtonText); 171 | scrollButton.classList.add('plus-button'); 172 | scrollButton.addEventListener('click', () => { 173 | window.scrollTo({ top: 0 }); 174 | }); 175 | 176 | cinemaModeButtonText.textContent = 'Cinema Mode'; 177 | cinemaModeButtonText.classList.add('text'); 178 | cinemaModeButton.appendChild(cinemaModeButtonText); 179 | cinemaModeButton.classList.add(cinemaModeButtonState, 'plus-button'); 180 | cinemaModeButton.addEventListener('click', () => { 181 | OPTIONS.cinemaMode = !OPTIONS.cinemaMode; 182 | localStorage.setItem('plus_cinemaMode', OPTIONS.cinemaMode); 183 | 184 | if (OPTIONS.cinemaMode) { 185 | cinemaModeButton.classList.replace('plus-button-isOff', 'plus-button-isOn'); 186 | } else { 187 | cinemaModeButton.classList.replace('plus-button-isOn', 'plus-button-isOff'); 188 | } 189 | }); 190 | 191 | autoplayButtonText.textContent = 'Autoplay'; 192 | autoplayButtonText.classList.add('text'); 193 | autoplayButton.appendChild(autoplayButtonText); 194 | autoplayButton.classList.add(autoplayButtonState, 'plus-button'); 195 | autoplayButton.addEventListener('click', () => { 196 | OPTIONS.autoplay = !OPTIONS.autoplay; 197 | localStorage.setItem('plus_autoplay', OPTIONS.autoplay); 198 | 199 | if (OPTIONS.autoplay) { 200 | autoplayButton.classList.replace('plus-button-isOff', 'plus-button-isOn'); 201 | } else { 202 | autoplayButton.classList.replace('plus-button-isOn', 'plus-button-isOff'); 203 | } 204 | }); 205 | 206 | buttons.classList.add('plus-buttons'); 207 | 208 | buttons.appendChild(scrollButton); 209 | buttons.appendChild(autoplayButton); 210 | buttons.appendChild(cinemaModeButton); 211 | 212 | document.body.appendChild(buttons); 213 | 214 | /** 215 | * Initialize video pages containing valid video element 216 | */ 217 | 218 | if (/^http[s]*:\/\/[www.]*xnxx\.com\/video/.test(window.location.href) && video) { 219 | 220 | /** 221 | * Toggle cinema mode if enabled 222 | */ 223 | if (video && OPTIONS.cinemaMode) { 224 | document.querySelector('#content').classList.add('player-enlarged'); 225 | document.querySelector('.mobile-hide').style.display = 'none'; 226 | 227 | console.log('test'); 228 | // Button is not always available right away, so we wait for `canplay` 229 | video.addEventListener('canplay', function onCanPlay() { 230 | document.querySelector('.buttons-bar.right :nth-child(3)').dispatchEvent(new MouseEvent('click')); 231 | 232 | // Only run once 233 | video.removeEventListener('canplay', onCanPlay, false); 234 | }); 235 | } 236 | 237 | /** 238 | * Autoplay video if enabled 239 | */ 240 | if (video && OPTIONS.autoplay) { 241 | video.addEventListener('canplay', function onCanPlay() { 242 | document.querySelector('.big-buttons .play').dispatchEvent(new MouseEvent('click')); 243 | 244 | // Only run once 245 | video.removeEventListener('canplay', onCanPlay, false); 246 | }); 247 | 248 | } 249 | } 250 | 251 | /** 252 | * Add styles 253 | */ 254 | 255 | GM_addStyle(sharedStyles); 256 | GM_addStyle(themeStyles); 257 | GM_addStyle(generalStyles); 258 | 259 | /** 260 | * Add dynamic styles 261 | */ 262 | 263 | const dynamicStyles = ` 264 | .plus-buttons { 265 | margin-right: -${buttons.getBoundingClientRect().width - 23}px; 266 | } 267 | 268 | .plus-buttons:hover { 269 | margin-right: 0; 270 | } 271 | `; 272 | 273 | GM_addStyle(dynamicStyles); 274 | }); 275 | })(); -------------------------------------------------------------------------------- /XVIDEOS Plus.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @author Mr. Nope 3 | // @version 2022-12-17 4 | // @name XVIDEOS Plus 5 | // @description A kinder XVIDEOS. Because you're worth it. 6 | // @match *://*.xvideos.com/* 7 | // @match *://*.xvideos.red/* 8 | // @run-at document_idle 9 | // @grant GM_addStyle 10 | // @icon https://seeklogo.com/images/X/xvideos-logo-77E7B4F168-seeklogo.com.png 11 | // ==/UserScript== 12 | 13 | 'use strict'; 14 | 15 | 16 | const OPTIONS = { 17 | scrollToVideo: Boolean(JSON.parse(localStorage.getItem('plus_scrollToVideo'))), 18 | autoplay: Boolean(JSON.parse(localStorage.getItem('plus_autoplay'))), 19 | cinemaMode: Boolean(JSON.parse(localStorage.getItem('plus_cinemaMode'))), 20 | autoGoToRed: Boolean(JSON.parse(localStorage.getItem('plus_autoGoToRed'))), 21 | autoGoToRegular: Boolean(JSON.parse(localStorage.getItem('plus_autoGoToRegular'))) 22 | }; 23 | 24 | /** 25 | * Site-specific styles 26 | */ 27 | const rootStyles = ` 28 | /* Variables */ 29 | 30 | :root { 31 | --color-brand-primary: rgb(178, 14, 0); 32 | --color-brand-primary-hover: rgb(198, 34, 0); 33 | } 34 | `; 35 | 36 | /** 37 | * Shared Styles 38 | */ 39 | const sharedStyles = ` 40 | /* Our own elements */ 41 | 42 | .plus-buttons { 43 | background: rgba(27, 27, 27, 0.9); 44 | box-shadow: 0px 0px 12px rgba(20, 111, 223, 0.9); 45 | font-size: 12px; 46 | position: fixed; 47 | bottom: 10px; 48 | padding: 10px 22px 8px 24px; 49 | right: 0; 50 | z-index: 100; 51 | transition: all 0.3s ease; 52 | 53 | /* Negative margin-right calculated later based on width of buttons */ 54 | } 55 | 56 | .plus-buttons:hover { 57 | box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.3); 58 | } 59 | 60 | .plus-buttons .plus-button { 61 | margin: 10px 0; 62 | padding: 6px 15px; 63 | border-radius: 4px; 64 | font-weight: 700; 65 | display: block; 66 | position: relative; 67 | text-align: center; 68 | vertical-align: top; 69 | cursor: pointer; 70 | border: none; 71 | text-decoration: none; 72 | } 73 | 74 | .plus-buttons a.plus-button { 75 | background: rgb(221, 221, 221); 76 | color: rgb(51, 51, 51); 77 | } 78 | 79 | .plus-buttons a.plus-button:hover { 80 | background: rgb(187, 187, 187); 81 | color: rgb(51, 51, 51); 82 | } 83 | 84 | .plus-buttons a.plus-button.plus-button-isOn { 85 | background: rgb(20, 111, 223); 86 | color: rgb(255, 255, 255); 87 | } 88 | 89 | .plus-buttons a.plus-button.plus-button-isOn:hover { 90 | background: rgb(0, 91, 203); 91 | color: rgb(255, 255, 255); 92 | } 93 | 94 | .plus-hidden { 95 | display: none !important; 96 | } 97 | `; 98 | 99 | /** 100 | * Color Theme 101 | */ 102 | const themeStyles = ` 103 | .plus-buttons { 104 | box-shadow: 0px 0px 12px rgb(135, 0, 0); 105 | } 106 | 107 | .plus-buttons:hover { 108 | box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.3); 109 | } 110 | 111 | .plus-buttons a.plus-button { 112 | background: rgb(47, 47, 47); 113 | color: rgb(172, 172, 172); 114 | } 115 | 116 | .plus-buttons a.plus-button:hover { 117 | background: rgb(79, 79, 79); 118 | color: rgb(204, 204, 204); 119 | } 120 | 121 | .plus-buttons a.plus-button.plus-button-isOn { 122 | background: var(--color-brand-primary); 123 | color: rgb(204, 204, 204); 124 | } 125 | 126 | .plus-buttons a.plus-button.plus-button-isOn:hover { 127 | background: var(--color-brand-primary-hover); 128 | color: rgb(232, 232, 232); 129 | } 130 | `; 131 | 132 | /** 133 | * Site-Specific Styles 134 | */ 135 | const generalStyles = ` 136 | /* Hidden */ 137 | 138 | #video-sponsor-links, 139 | .related-content .related-content__btns::before { 140 | display: none !important; 141 | } 142 | 143 | /* Related videos and playlists tabs */ 144 | 145 | .related-content .related-content__btns { 146 | margin: 9px 0 6px 0; 147 | } 148 | 149 | .related-content .related-content__btns a.link { 150 | border-bottom: none; 151 | } 152 | 153 | .related-content .related-content__btns a.link.active { 154 | color: var(--color-brand-primary); 155 | font-weight: 700; 156 | } 157 | 158 | /* Thumbnails */ 159 | 160 | #related-videos .thumb-block { 161 | opacity: 0.75; 162 | transition: opacity ease-in 1000ms 1000ms; 163 | } 164 | 165 | #related-videos .thumb-block:hover { 166 | opacity: 1; 167 | transition: opacity ease-out 500ms; 168 | } 169 | 170 | /* Add to favorites */ 171 | 172 | .favlist-elem-line, 173 | .favlist-elem-small-line { 174 | margin: 0; 175 | padding: 1px; 176 | } 177 | 178 | .favlist-elem-line .favlist-e-title, 179 | .favlist-elem-small-line .favlist-e-title { 180 | font-size: 14px; 181 | } 182 | 183 | .favlist-elem-line .favlist-e-left > *, 184 | .favlist-elem-small-line .favlist-e-left > * { 185 | vertical-align: middle; 186 | } 187 | 188 | .x-overlay.favlist-opverlay .x-body { 189 | transform: scale(0.6); 190 | } 191 | 192 | .x-overlay.favlist-overlay .x-body { 193 | padding: 25px; 194 | position: relative; 195 | max-width: 120ch; 196 | } 197 | 198 | .x-overlay.x-overlay-box .x-body { 199 | height: auto; 200 | margin: 80px auto 5px; 201 | padding: 25px; 202 | font-size: 14px; 203 | line-height: 1; 204 | } 205 | `; 206 | 207 | /** 208 | * Video player 209 | */ 210 | 211 | const video = document.getElementsByTagName('video')[0]; // References the HTML5 Video element 212 | 213 | /** 214 | * Create option buttons 215 | */ 216 | 217 | const buttons = document.createElement('div'); 218 | 219 | const scrollToTopButton = document.createElement('a'); 220 | const scrollToTopButtonText = document.createElement('span'); 221 | 222 | const scrollToVideoButton = document.createElement('a'); 223 | const scrollToVideoButtonText = document.createElement('span'); 224 | const scrollToVideoButtonState = OPTIONS.scrollToVideo ? 'plus-button-isOn' : 'plus-button-isOff'; 225 | 226 | const autoplayButton = document.createElement('a'); 227 | const autoplayButtonText = document.createElement('span'); 228 | const autoplayButtonState = OPTIONS.autoplay ? 'plus-button-isOn' : 'plus-button-isOff'; 229 | 230 | const cinemaModeButton = document.createElement('a'); 231 | const cinemaModeButtonText = document.createElement('span'); 232 | const cinemaModeButtonState = OPTIONS.cinemaMode ? 'plus-button-isOn' : 'plus-button-isOff'; 233 | 234 | 235 | const goToRedButton = document.createElement('a'); 236 | const goToRedButtonText = document.createElement('span'); 237 | 238 | const autoGoToRedButton = document.createElement('a'); 239 | const autoGoToRedButtonText = document.createElement('span'); 240 | const autoGoToRedButtonState = OPTIONS.autoGoToRed ? 'plus-button-isOn' : 'plus-button-isOff'; 241 | 242 | const autoGoToRegularButton = document.createElement('a'); 243 | const autoGoToRegularButtonText = document.createElement('span'); 244 | const autoGoToRegularButtonState = OPTIONS.autoGoToRegular ? 'plus-button-isOn' : 'plus-button-isOff'; 245 | 246 | scrollToTopButtonText.textContent = "Scroll to top"; 247 | scrollToTopButtonText.classList.add('text'); 248 | scrollToTopButton.appendChild(scrollToTopButtonText); 249 | scrollToTopButton.classList.add('plus-button'); 250 | scrollToTopButton.addEventListener('click', () => { 251 | window.scrollTo({ top: 0 }); 252 | }); 253 | 254 | scrollToVideoButtonText.textContent = "Scroll to video"; 255 | scrollToVideoButtonText.classList.add('text'); 256 | scrollToVideoButton.appendChild(scrollToVideoButtonText); 257 | scrollToVideoButton.classList.add(scrollToVideoButtonState, 'plus-button'); 258 | scrollToVideoButton.addEventListener('click', () => { 259 | OPTIONS.scrollToVideo = !OPTIONS.scrollToVideo; 260 | localStorage.setItem('plus_scrollToVideo', OPTIONS.scrollToVideo); 261 | 262 | if (OPTIONS.scrollToVideo) { 263 | scrollToVideoButton.classList.replace('plus-button-isOff', 'plus-button-isOn'); 264 | const videoContainer = document.querySelector('#content'); 265 | const top = videoContainer.offsetTop; 266 | 267 | window.scrollTo({ top }); 268 | } else { 269 | scrollToVideoButton.classList.replace('plus-button-isOn', 'plus-button-isOff'); 270 | } 271 | }); 272 | 273 | 274 | 275 | autoplayButtonText.textContent = 'Autoplay'; 276 | autoplayButtonText.classList.add('text'); 277 | autoplayButton.appendChild(autoplayButtonText); 278 | autoplayButton.classList.add(autoplayButtonState, 'plus-button'); 279 | autoplayButton.addEventListener('click', () => { 280 | OPTIONS.autoplay = !OPTIONS.autoplay; 281 | localStorage.setItem('plus_autoplay', OPTIONS.autoplay); 282 | 283 | if (OPTIONS.autoplay) { 284 | autoplayButton.classList.replace('plus-button-isOff', 'plus-button-isOn'); 285 | } else { 286 | autoplayButton.classList.replace('plus-button-isOn', 'plus-button-isOff'); 287 | } 288 | }); 289 | 290 | cinemaModeButtonText.textContent = 'Cinema mode'; 291 | cinemaModeButtonText.classList.add('text'); 292 | cinemaModeButton.appendChild(cinemaModeButtonText); 293 | cinemaModeButton.classList.add(cinemaModeButtonState, 'plus-button'); 294 | cinemaModeButton.addEventListener('click', () => { 295 | OPTIONS.cinemaMode = !OPTIONS.cinemaMode; 296 | localStorage.setItem('plus_cinemaMode', OPTIONS.cinemaMode); 297 | 298 | if (OPTIONS.cinemaMode) { 299 | cinemaModeButton.classList.replace('plus-button-isOff', 'plus-button-isOn'); 300 | } else { 301 | cinemaModeButton.classList.replace('plus-button-isOn', 'plus-button-isOff'); 302 | } 303 | }); 304 | 305 | goToRedButtonText.textContent = `Switch to ${location.hostname.endsWith('xvideos.red') ? 'Regular' : 'RED'}`; 306 | goToRedButtonText.classList.add('text'); 307 | goToRedButton.appendChild(goToRedButtonText); 308 | goToRedButton.classList.add('plus-button'); 309 | goToRedButton.addEventListener('click', () => { 310 | if (location.hostname.endsWith('xvideos.com')) { 311 | location.hostname = location.hostname.replace('xvideos.com', 'xvideos.red'); 312 | } else if (location.hostname.endsWith('xvideos.red')) { 313 | location.hostname = location.hostname.replace('xvideos.red', 'xvideos.com'); 314 | } 315 | }); 316 | 317 | autoGoToRedButtonText.textContent = "Auto-switch to RED"; 318 | autoGoToRedButtonText.classList.add('text'); 319 | autoGoToRedButton.appendChild(autoGoToRedButtonText); 320 | autoGoToRedButton.classList.add(autoGoToRedButtonState, 'plus-button'); 321 | autoGoToRedButton.addEventListener('click', () => { 322 | OPTIONS.autoGoToRed = !OPTIONS.autoGoToRed; 323 | localStorage.setItem('plus_autoGoToRed', OPTIONS.autoGoToRed); 324 | 325 | if (OPTIONS.autoGoToRed) { 326 | autoGoToRedButton.classList.replace('plus-button-isOff', 'plus-button-isOn'); 327 | } else { 328 | autoGoToRedButton.classList.replace('plus-button-isOn', 'plus-button-isOff'); 329 | } 330 | }); 331 | 332 | autoGoToRegularButtonText.textContent = 'Auto-switch to Regular'; 333 | autoGoToRegularButtonText.classList.add('text'); 334 | autoGoToRegularButton.appendChild(autoGoToRegularButtonText); 335 | autoGoToRegularButton.classList.add(autoGoToRegularButtonState, 'plus-button'); 336 | autoGoToRegularButton.addEventListener('click', () => { 337 | OPTIONS.autoGoToRegular = !OPTIONS.autoGoToRegular; 338 | localStorage.setItem('plus_autoGoToRegular', OPTIONS.autoGoToRegular); 339 | 340 | if (OPTIONS.autoGoToRegular) { 341 | autoGoToRegularButton.classList.replace('plus-button-isOff', 'plus-button-isOn'); 342 | } else { 343 | autoGoToRegularButton.classList.replace('plus-button-isOn', 'plus-button-isOff'); 344 | } 345 | }); 346 | 347 | if (OPTIONS.autoGoToRed && location.hostname.endsWith('xvideos.com')) { 348 | location.hostname = location.hostname.replace('xvideos.com', 'xvideos.red'); 349 | } 350 | 351 | if (OPTIONS.autoGoToRegular && location.hostname.endsWith('xvideos.red')) { 352 | location.hostname = location.hostname.replace('xvideos.red', 'xvideos.com'); 353 | } 354 | 355 | autoplayButtonText.textContent = 'Autoplay'; 356 | autoplayButtonText.classList.add('text'); 357 | autoplayButton.appendChild(autoplayButtonText); 358 | autoplayButton.classList.add(autoplayButtonState, 'plus-button'); 359 | autoplayButton.addEventListener('click', () => { 360 | OPTIONS.autoplay = !OPTIONS.autoplay; 361 | localStorage.setItem('plus_autoplay', OPTIONS.autoplay); 362 | 363 | if (OPTIONS.autoplay) { 364 | autoplayButton.classList.replace('plus-button-isOff', 'plus-button-isOn'); 365 | } else { 366 | autoplayButton.classList.replace('plus-button-isOn', 'plus-button-isOff'); 367 | } 368 | }); 369 | 370 | buttons.classList.add('plus-buttons'); 371 | 372 | buttons.appendChild(scrollToTopButton); 373 | buttons.appendChild(scrollToVideoButton); 374 | buttons.appendChild(autoplayButton); 375 | buttons.appendChild(cinemaModeButton); 376 | buttons.appendChild(autoGoToRedButton); 377 | buttons.appendChild(goToRedButton); 378 | 379 | document.body.appendChild(buttons); 380 | 381 | /** 382 | * Initialize video pages containing valid video element 383 | */ 384 | if (video) { 385 | /** 386 | * Autoscroll to video 387 | */ 388 | if (OPTIONS.scrollToVideo) { 389 | const videoContainer = document.querySelector('#content'); 390 | const top = videoContainer.offsetTop; 391 | 392 | scrollToVideoButton.classList.replace('plus-button-isOff', 'plus-button-isOn'); 393 | window.scrollTo({ top }); 394 | } 395 | 396 | /** 397 | * "Always go RED" buttons 398 | */ 399 | if (OPTIONS.scrollToVideo) { 400 | const videoContainer = document.querySelector('#content'); 401 | const top = videoContainer.offsetTop; 402 | 403 | scrollToVideoButton.classList.replace('plus-button-isOff', 'plus-button-isOn'); 404 | window.scrollTo({ top }); 405 | } 406 | 407 | /** 408 | * Auto-enable cinema mode if enabled 409 | */ 410 | if (OPTIONS.cinemaMode && !document.querySelector('.player-enlarged')) { 411 | const button = document.querySelector('#content .buttons-bar img[src*="icon-screen-expand"]'); 412 | 413 | if (button) { 414 | button.dispatchEvent(new MouseEvent('click')); 415 | } 416 | } 417 | 418 | /** 419 | * Autoplay video if enabled 420 | */ 421 | if (OPTIONS.autoplay) { 422 | document.querySelector('.buttons-bar img[src*="icon-play"]').dispatchEvent(new MouseEvent('click')); 423 | } 424 | } 425 | 426 | /** 427 | * Print some stuff to the console. 428 | */ 429 | 430 | const selectors = new Map(); 431 | selectors.set('playlist', new Map()); 432 | selectors.get('playlist').set('items', '.favlist-elem'); 433 | selectors.get('playlist').set('count', '.favlist-e-nb-videos'); 434 | selectors.get('playlist').set('tags', '.tag'); 435 | 436 | const counts = new Map(); 437 | counts.set('playlist', new Map()); 438 | counts.get('playlist').set('total', 0); 439 | counts.set('videos', new Map()); 440 | counts.get('videos').set('total', 0); 441 | counts.get('videos').set('public', 0); 442 | counts.get('videos').set('private', 0); 443 | counts.set('tags', new Map()); 444 | counts.get('tags')[Symbol.iterator] = function* () { 445 | // Sort by number of times the tag is used for a playlist. 446 | yield* [...this.entries()].sort((a, b) => b[1] - a[1]); 447 | }; 448 | 449 | const playlists = document.querySelectorAll(selectors.get('playlist').get('items')); 450 | 451 | counts.get('playlist').set('total', playlists.length); 452 | 453 | for (const playlist of playlists) { 454 | const tagSelector = selectors.get('playlist').get('tags'); 455 | const tagNodes = playlist.querySelectorAll(tagSelector); 456 | const countSelector = selectors.get('playlist').get('count'); 457 | const countNode = playlist.querySelector(countSelector); 458 | const countText = countNode.textContent; 459 | const countAdded = Number.parseInt(countText) || Number(0); 460 | const countOld = counts.get('videos').get('total'); 461 | const countNew = countOld + countAdded; 462 | 463 | counts.get('videos').set('total', countNew); 464 | 465 | for (const tag of tagNodes) { 466 | const tagText = tag.textContent; 467 | const tagExists = counts.get('tags').has(tagText); 468 | const tagOld = tagExists ? counts.get('tags').get(tagText) : 0; 469 | const tagNew = tagOld + 1; 470 | 471 | counts.get('tags').set(tagText, tagNew); 472 | } 473 | } 474 | 475 | console.group('XVIDEOS Plus · Statistics'); 476 | console.info('Total videos: %s', counts.get('videos').get('total')), 477 | console.info('Total playlists: %s', counts.get('playlist').get('total')), 478 | console.table([...(counts.get('tags'))]); 479 | console.groupEnd(); 480 | 481 | /** 482 | * Add styles 483 | */ 484 | 485 | GM_addStyle(rootStyles); 486 | GM_addStyle(sharedStyles); 487 | GM_addStyle(themeStyles); 488 | GM_addStyle(generalStyles); 489 | 490 | /** 491 | * Add dynamic styles 492 | */ 493 | 494 | const dynamicStyles = ` 495 | .plus-buttons { 496 | margin-right: -${buttons.getBoundingClientRect().width - 23}px; 497 | } 498 | 499 | .plus-buttons:hover { 500 | margin-right: 0; 501 | } 502 | `; 503 | 504 | GM_addStyle(dynamicStyles); -------------------------------------------------------------------------------- /YouPorn Plus.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @author Mr. Nope 3 | // @version 2023-05-25 4 | // @name YouPorn Plus 5 | // @match *://*.youporn.com/* 6 | // @match *://*.youpornpremium.com/* 7 | // @description A kinder YouPorn. Because you're worth it. 8 | // @run-at document_end 9 | // @date 2023-05-25 10 | // @license MIT 11 | // ==/UserScript== 12 | 13 | 'use strict'; 14 | 15 | setTimeout(() => { 16 | const OPTIONS = { 17 | cinemaMode: JSON.parse(localStorage.getItem('plus_cinemaMode')) || false 18 | }; 19 | 20 | console.log(document); 21 | console.log(window); 22 | 23 | console.log(unsafeWindow); 24 | 25 | // const playerSettings = JSON.parse(localStorage.getItem('mgp_player')); 26 | 27 | // Change default quality from 720p to 1080p 28 | // playerSettings.quality = 1080; 29 | 30 | // Prevent problem with videos not loading unless clearing cache and reloading. 31 | // localStorage.setItem('mgp_player', JSON.stringify(playerSettings)); 32 | 33 | /** 34 | * Shared styles 35 | */ 36 | const sharedStyles = ` 37 | /* Our own elements */ 38 | 39 | .plus-buttons { 40 | background: rgba(27, 27, 27, 0.9); 41 | box-shadow: 0px 0px 12px rgba(20, 111, 223, 0.9); 42 | font-size: 12px; 43 | position: fixed; 44 | bottom: 10px; 45 | padding: 10px 22px 8px 24px; 46 | right: 0; 47 | z-index: 100; 48 | transition: all 0.2s ease; 49 | 50 | /* Negative margin-right calculated later based on width of buttons */ 51 | } 52 | 53 | .plus-buttons:hover { 54 | box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.3); 55 | } 56 | 57 | .plus-buttons .plus-button { 58 | margin: 10px 0; 59 | padding: 6px 15px; 60 | border-radius: 4px; 61 | font-weight: 700; 62 | display: block; 63 | position: relative; 64 | text-align: center; 65 | vertical-align: top; 66 | cursor: pointer; 67 | border: none; 68 | text-decoration: none; 69 | } 70 | 71 | .plus-buttons a.plus-button { 72 | background: rgb(221, 221, 221); 73 | color: rgb(51, 51, 51); 74 | } 75 | 76 | .plus-buttons a.plus-button:hover { 77 | background: rgb(187, 187, 187); 78 | color: rgb(51, 51, 51); 79 | } 80 | t by defining these APIs in the content script (ISOLATED) world, supported by the extension APIs available to the ISOLATED world. By forcing user scripts to move from ISOLATED to USERSCRIPT, these extension-defined APIs would at first lose access to the privileged APIs. 81 | 82 | This access can be restored by establishing a (synchronous) communication channel between the ISOLATED and USERSCRIPT worlds. This can achieved with existing DOM APIs, e.g. with a pre-shared secret (event name) + custom events on shared document/window. This technique may be familiar to some, as it is a way to communicate between MAIN and ISOLATED. Although used in practice, I discourage the use of window.postMessage for communication because that can be intercepted and/or break web pages (for previous discussion, see Proposal: deprecate window.postMessage(message, '*') for use with extensions #78). 83 | In the future, a dedicated API to communicate between worlds could be considered. 84 | 85 | When multiple scripts match and have the same 86 | .plus-buttons a.plus-button.plus-button-isOn { 87 | background: rgb(20, 111, 223); 88 | color: rgb(255, 255, 255); 89 | } 90 | 91 | .plus-buttons a.plus-button.plus-button-isOn:hover { 92 | background: rgb(0, 91, 203); 93 | color: rgb(255, 255, 255); 94 | } 95 | 96 | .plus-hidden { 97 | display: none !important; 98 | } 99 | 100 | .plus-letters { 101 | align-items: center; 102 | color: #ccc; 103 | display: flex; 104 | justify-content: space-between; 105 | margin: 0 22px 18px; 106 | text-transform: uppercase; 107 | } 108 | 109 | .plus-letters span { 110 | cursor: pointer; 111 | } 112 | `; 113 | 114 | /** 115 | * Color Theme 116 | */ 117 | const themeStyles = ` 118 | .plus-buttons { 119 | box-shadow: 0px 0px 12px rgba(236, 86, 124, 0.85); 120 | } 121 | 122 | .plus-buttons:hover { 123 | box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.3); 124 | } 125 | 126 | .plus-buttons a.plus-button { 127 | background: rgb(47, 47, 47); 128 | color: rgb(172, 172, 172); 129 | } 130 | 131 | .plus-buttons a.plus-button:hover { 132 | background: rgb(79, 79, 79); 133 | color: rgb(204, 204, 204); 134 | } 135 | 136 | .plus-buttons a.plus-button.plus-button-isOn { 137 | background: rgb(236, 86, 124); 138 | color: rgb(235, 235, 235); 139 | } 140 | 141 | .plus-buttons a.plus-button.plus-button-isOn:hover { 142 | background: rgb(236, 86, 124); 143 | color: rgb(255, 255, 255); 144 | } 145 | `; 146 | 147 | /** 148 | * Site-Specific Styles 149 | */ 150 | const generalStyles = ` 151 | /* Hide elements */ 152 | 153 | .realsex, 154 | .mhp1138_cinemaState, 155 | .networkBar, 156 | .sniperModeEngaged, 157 | .footer, 158 | .footer-title, 159 | .ad-link, 160 | .removeAdLink, 161 | .removeAdLink + iframe, 162 | .abovePlayer, 163 | .streamatesModelsContainer, 164 | #welcome, 165 | #welcomePremium, 166 | #headerUpgradePremiumBtn, 167 | #PornhubNetworkBar, 168 | #js-abContainterMain, 169 | #hd-rightColVideoPage > :not(#relatedVideosVPage), 170 | .bottomNotification, 171 | .trailerUnderplayerPreview, 172 | .sectionCarousel { 173 | display: none !important; 174 | visibility: hidden !important; 175 | opacity: 0 !important; 176 | height: 0 !important; 177 | width: 0 !important; 178 | } 179 | 180 | /* Allow narrower page width */ 181 | 182 | html.supportsGridLayout.fluidContainer .container, 183 | html.supportsGridLayout.fluidContainer .section_wrapper { 184 | min-width: 700px !important; 185 | } 186 | 187 | /* Hide tricky ads with obfuscated tag names */ 188 | .adLinks + *, 189 | .adLinks + * + *, 190 | .wrapper.hd + * { 191 | display: none !important; 192 | } 193 | 194 | /* Full-width video */ 195 | #vpContentContainer { 196 | display: block !important; 197 | } 198 | 199 | /* Recommended videos */ 200 | #recommendedVideosVPage { 201 | text-align: center !important; 202 | } 203 | 204 | /* "Recommended Porn" heading */ 205 | #recommendedVideosVPage h3 { 206 | text-align: center !important; 207 | display: block !important; 208 | float: unset !important; 209 | } 210 | 211 | /* Recommended videos layout */ 212 | #recommendedVideos { 213 | list-style: none !important; 214 | display: flex !important; 215 | flex-direction: row !important; 216 | align-items: flex-start !important; 217 | justify-content: space-between !important; 218 | text-align: left !important; 219 | } 220 | 221 | /* Thumbnail wrapper */ 222 | #recommendedVideosPage .videoBox { 223 | font-size: 0.813rem !important; /* 13px/16px = 0.813rem */ 224 | color: #a6adc8 !important; /* Mocha -> Subtext0 */ 225 | margin: 0 5px !important; 226 | } 227 | 228 | /* Thumbnail wrapper */ 229 | #recommendedVideos .videoBox .phimage { 230 | width: auto !important; 231 | border-radius: 4px !important; 232 | } 233 | 234 | /* Thumbnail info wrapper */ 235 | .thumbnail-info-wrapper { 236 | color: #a6adc8 !important; /* Mocha -> Subtext0 */ 237 | font-size: 0.813rem !important; /* 13px/16px = 0.813rem */ 238 | float: none !important; 239 | width: auto !important; 240 | } 241 | 242 | /* Title of video or playlist */ 243 | .thumbnail-info-wrapper .title { 244 | font-size: 0.813rem !important; 245 | font-weight: 700 !important; 246 | margin: 12px 0 3px !important; 247 | } 248 | 249 | /* The user/channel name wrapper */ 250 | .thumbnail-info-wrapper .usernameWrap { 251 | margin-top: -1px; /* Fixes slight off-center text */ 252 | } 253 | 254 | /* The user/channel name link */ 255 | .thumbnail-info-wrapper .usernameWrap a { 256 | font-size: 0.813rem !important; /* 13px/16px = 0.813rem */ 257 | color: #a6adc8 !important; 258 | } 259 | 260 | /* The verified badge and user/channel name wrapper */ 261 | .thumbnail-info-wrapper .videoUploaderBlock { 262 | margin-bottom: 5px !important; 263 | } 264 | 265 | /* The verified badge */ 266 | .thumbnail-info-wrapper .own-video-thumbnail { 267 | margin-right: 2px !important; 268 | } 269 | 270 | /* The views and likes wrapper */ 271 | .thumbnail-info-wrapper .videoDetailsBlock { 272 | margin-bottom: 5px !important; 273 | } 274 | 275 | /* The views */ 276 | .thumbnail-info-wrapper .videoDetailsBlock .views { 277 | font-size: 0.813rem !important; /* 13px/16px = 0.813rem */ 278 | } 279 | 280 | /* The rating */ 281 | .thumbnail-info-wrapper .videoDetailsBlock .rating-container i, 282 | .thumbnail-info-wrapper .videoDetailsBlock .rating-container .value { 283 | font-size: 0.813rem !important; /* 13px/16px = 0.813rem */ 284 | } 285 | 286 | /* The "load more" button */ 287 | .more_recommended_btn { 288 | margin: 2rem 0 !important; 289 | } 290 | 291 | /* Make "HD" icon more visible on thumbnails */ 292 | 293 | .hd-thumbnail { 294 | color: #f90 !important; 295 | } 296 | 297 | /* Show all playlists without scrolling in "add to" */ 298 | 299 | .slimScrollDiv { 300 | height: auto !important; 301 | } 302 | 303 | #scrollbar_watch { 304 | max-height: unset !important; 305 | } 306 | 307 | /* Hide premium video from related videos sidebar */ 308 | 309 | #relateRecommendedItems li:nth-of-type(5) { 310 | display: none !important; 311 | } 312 | 313 | /* Prevent animating player size change on each page load */ 314 | 315 | #main-container .video-wrapper #player.wide { 316 | transition: none !important; 317 | } 318 | 319 | /* Allow narrower player */ 320 | 321 | #player { 322 | min-width: 0 !important; 323 | } 324 | 325 | /* Fit more playlists into "add to" popup */ 326 | 327 | .playlist-menu-addTo { 328 | display: none; 329 | } 330 | 331 | .add-to-playlist-menu #scrollThumbs, 332 | .playlist-option-menu #scrollThumbs { 333 | height: 320px !important; 334 | max-height: 35vh !important; 335 | } 336 | 337 | .add-to-playlist-menu ul.custom-playlist li { 338 | font-size: 12px; 339 | height: 24px; 340 | } 341 | 342 | .add-to-playlist-menu .playlist-menu-createNew { 343 | font-size: 12px !important; 344 | height: 38px !important; 345 | } 346 | 347 | .add-to-playlist-menu .playlist-menu-createNew a { 348 | padding-top: 8px !important; 349 | font-weight: 400 !important; 350 | } 351 | 352 | /* Hide playlist bar if disabled in options */ 353 | 354 | .playlist-bar { 355 | display: ${OPTIONS.hidePlaylistBar ? 'none' : 'block'}; 356 | } 357 | 358 | /** 359 | * Improve loading indicator lines on thumbnails 360 | * 361 | * Using colors from the Catppuccin palette available at https://github.com/catppuccin. 362 | */ 363 | 364 | .preloadLine { 365 | background: #81c8be; /* Mocha -> Teal */ 366 | box-shadow: 0 0 3px #1e1e2e; /* Mocha -> Base */ 367 | } 368 | 369 | /* Fade in and out semitransparent elements */ 370 | 371 | .tab-menu-item { 372 | opacity: 0.4 !important; 373 | padding: 0 16px !important; 374 | transition: all 0.2s ease !important; 375 | } 376 | 377 | .tab-menu-item.active { 378 | opacity: 0.5 !important; 379 | } 380 | 381 | .tab-menu-wrapper-cell:hover .tab-menu-item { 382 | opacity: 1 !important; 383 | } 384 | 385 | .votes-fav-wrap .icon-wrapper:hover, 386 | .votes-fav-wrap .icon-wrapper.active:hover { 387 | opacity: 1 !important; 388 | } 389 | 390 | .votes-fav-wrap .icon-wrapper { 391 | opacity: 0.4 !important; 392 | transition: all 0.2s ease !important; 393 | } 394 | 395 | .votes-fav-wrap .icon-wrapper.active { 396 | opacity: 0.7 !important; 397 | } 398 | `; 399 | 400 | /** 401 | * References to video element and container if they exist on the page 402 | */ 403 | const videoContainer = document.querySelector('#videoContainer'); 404 | const distractions = [ 405 | 'header' 406 | ]; 407 | const isOnVideoPage = 408 | /^http[s]*:\/\/(www\.)*youporn(premium)?\.com\/watch\//.test(window.location.href) && !!videoContainer; 409 | 410 | const handleDistractions = () => { 411 | distractions.forEach((distraction) => { 412 | console.log(distraction); 413 | const element = document.querySelector(distraction); 414 | 415 | if (element) { 416 | const handleMouseOver = () => { 417 | element.style.opacity = 1; 418 | }; 419 | 420 | const handleMouseOut = () => { 421 | element.style.opacity = 0.2; 422 | }; 423 | 424 | if (OPTIONS.cinemaMode) { 425 | element.style.transition = 'all 0.2s ease'; 426 | element.style.opacity = 0.2; 427 | 428 | element.addEventListener('mouseover', handleMouseOver, false); 429 | element.addEventListener('mouseout', handleMouseOut, false); 430 | } else { 431 | element.style.opacity = 1; 432 | element.removeEventListener('mouseover', handleMouseOver, false); 433 | element.removeEventListener('mouseout', handleMouseOut, false); 434 | } 435 | } 436 | }); 437 | }; 438 | 439 | /** 440 | * Returns an `on` or `off` CSS class name based on the boolean evaluation 441 | * of the `state` parameter, as convenience method when setting UI state. 442 | */ 443 | const getButtonState = state => { 444 | return state ? 'plus-button-isOn' : 'plus-button-isOff'; 445 | }; 446 | 447 | /** 448 | * Option buttons 449 | */ 450 | 451 | const cinemaButton = document.createElement('a'); 452 | const cinemaButtonText = document.createElement('span'); 453 | const cinemaButtonState = getButtonState(OPTIONS.cinemaMode); 454 | 455 | const scrollButton = document.createElement('a'); 456 | const scrollButtonText = document.createElement('span'); 457 | 458 | cinemaButtonText.textContent = 'Cinema mode'; 459 | cinemaButtonText.classList.add('text'); 460 | cinemaButton.appendChild(cinemaButtonText); 461 | cinemaButton.classList.add(cinemaButtonState, 'plus-button'); 462 | cinemaButton.addEventListener('click', () => { 463 | OPTIONS.cinemaMode = !OPTIONS.cinemaMode; 464 | localStorage.setItem('plus_cinemaMode', OPTIONS.cinemaMode); 465 | 466 | if (OPTIONS.cinemaMode) { 467 | cinemaButton.classList.replace('plus-button-isOff', 'plus-button-isOn'); 468 | } else { 469 | cinemaButton.classList.replace('plus-button-isOn', 'plus-button-isOff'); 470 | } 471 | 472 | if (isOnVideoPage) { 473 | handleDistractions(); 474 | } 475 | }); 476 | 477 | scrollButtonText.textContent = "Scroll to video"; 478 | scrollButtonText.classList.add('text'); 479 | scrollButton.appendChild(scrollButtonText); 480 | scrollButton.classList.add('plus-button'); 481 | scrollButton.addEventListener('click', () => { 482 | const container = document.querySelector('.main_content'); 483 | const header = document.querySelector('header'); 484 | 485 | if (container && header) { 486 | const destination = { 487 | left: container.scrollX, 488 | top: container.offsetTop - header.scrollHeight, 489 | behavior: 'smooth' 490 | }; 491 | window.scroll(destination); 492 | } 493 | }); 494 | 495 | /** 496 | * Order option buttons in a container 497 | */ 498 | 499 | const buttons = document.createElement('div'); 500 | 501 | buttons.classList.add('plus-buttons'); 502 | 503 | buttons.appendChild(cinemaButton); 504 | buttons.appendChild(scrollButton); 505 | 506 | document.body.appendChild(buttons); // Button container ready and added to page 507 | 508 | if (isOnVideoPage) { 509 | let timer = setInterval(() => { 510 | if (typeof window.expandHD === 'function') { 511 | console.log(unsafeWindow); 512 | window.expandHD(); 513 | clearInterval(timer); 514 | } 515 | }, 500); 516 | } 517 | 518 | 519 | /* 520 | * Add styles 521 | */ 522 | 523 | GM_addStyle(sharedStyles); 524 | GM_addStyle(themeStyles); 525 | GM_addStyle(generalStyles); 526 | 527 | /* 528 | * Add dynamic styles 529 | */ 530 | 531 | const dynamicStyles = ` 532 | .plus-buttons { 533 | margin-right: -${buttons.getBoundingClientRect().width - 18}px; 534 | margin-top: -${buttons.getBoundingClientRect().height - 18}px; 535 | } 536 | 537 | .plus-buttons:hover { 538 | margin-right: 0; 539 | margin-left: 0; 540 | } 541 | `; 542 | 543 | GM_addStyle(dynamicStyles); 544 | }, 1000); -------------------------------------------------------------------------------- /xHamster Plus.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @author Mr. Nope 3 | // @version 20230612 4 | // @name xHamster Plus 5 | // @description A kinder xHamster. Because you're worth it. 6 | // @include *xhamster.com* 7 | // @grant GM_addStyle 8 | // @license CC0 9 | // @icon https://static-cl.xhcdn.com/xh-tpl3/images/favicon/apple-touch-icon.png 10 | // ==/UserScript== 11 | 12 | 'use strict'; 13 | 14 | (() => { 15 | const OPTIONS = { 16 | cinemaMode: JSON.parse(localStorage.getItem('plus_cinemaMode')) || true, 17 | autoLanguage: JSON.parse(localStorage.getItem('plus_autoLanguage')) || false 18 | }; 19 | 20 | /** 21 | * Shared Styles 22 | */ 23 | 24 | const sharedStyles = ` 25 | /* Our own elements */ 26 | 27 | .plus-buttons { 28 | background: rgba(67, 67, 67, 0.85); 29 | box-shadow: 0px 0px 12px rgba(20, 111, 223, 0.85); 30 | font-size: 12px; 31 | position: fixed; 32 | bottom: 10px; 33 | padding: 10px 22px 8px 24px; 34 | right: 0; 35 | z-index: 100; 36 | transition: all 0.3s ease; 37 | 38 | /* Negative margin-right calculated later based on width of buttons */ 39 | } 40 | 41 | .plus-buttons:hover { 42 | box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.3); 43 | } 44 | 45 | .plus-buttons .plus-button { 46 | margin: 10px 0; 47 | padding: 6px 15px; 48 | border-radius: 4px; 49 | font-weight: 700; 50 | display: block; 51 | position: relative; 52 | text-align: center; 53 | vertical-align: top; 54 | cursor: pointer; 55 | border: none; 56 | text-decoration: none; 57 | } 58 | 59 | .plus-buttons a.plus-button { 60 | background: rgb(221, 221, 221); 61 | color: rgb(51, 51, 51); 62 | } 63 | 64 | .plus-buttons a.plus-button:hover { 65 | background: rgb(187, 187, 187); 66 | color: rgb(51, 51, 51); 67 | } 68 | 69 | .plus-buttons a.plus-button.plus-button-isOn { 70 | background: rgb(20, 111, 223); 71 | color: rgb(255, 255, 255); 72 | } 73 | 74 | .plus-buttons a.plus-button.plus-button-isOn:hover { 75 | background: rgb(0, 91, 203); 76 | color: rgb(255, 255, 255); 77 | } 78 | 79 | .plus-hidden { 80 | display: none !important; 81 | } 82 | `; 83 | 84 | /** 85 | * Color Theme 86 | */ 87 | 88 | const themeStyles = ` 89 | .plus-buttons { 90 | box-shadow: 0px 0px 18px rgba(227, 68, 73, 1); 91 | } 92 | 93 | .plus-buttons:hover { 94 | box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.3); 95 | } 96 | 97 | .plus-buttons a.plus-button { 98 | background: rgb(218, 218, 218); 99 | color: rgb(48, 48, 48); 100 | } 101 | 102 | .plus-buttons a.plus-button:hover { 103 | background: rgb(204, 204, 204); 104 | color: rgb(48, 48, 48); 105 | } 106 | 107 | .plus-buttons a.plus-button.plus-button-isOn { 108 | background: rgb(227, 68, 73); 109 | color: rgb(255, 255, 255); 110 | } 111 | 112 | .plus-buttons a.plus-button.plus-button-isOn:hover { 113 | background: rgb(212, 32, 37); 114 | color: rgb(255, 255, 255); 115 | } 116 | `; 117 | 118 | /** 119 | * Site-Specific Styles 120 | */ 121 | 122 | const generalStyles = ` 123 | /* Hide elements */ 124 | 125 | .yld-pdright-rectangle, 126 | .main-wrap > aside, 127 | .up-arrow, 128 | .premium-overlay, 129 | .bottom-widget-section, 130 | .clipstore-bottom, 131 | .wid-spot-container, 132 | .wid-banner-container, 133 | .wixx-eplayer, 134 | .wixx-ecam-thumb, 135 | .wixx-epremium-overlay, 136 | .wixx-eright-rectangle, 137 | aside[data-role="promo"], 138 | .ytd-j, 139 | .ytd-jcam-thumb, 140 | div[data-role="ytd-jbanner-underplayer"], 141 | div[data-role="wixx-ebanner-underplayer"] { 142 | display: none !important; 143 | } 144 | 145 | /* Remove right-side banner/sponsors from category pages/searches */ 146 | 147 | .thumb-list--banner { 148 | height: auto !important; 149 | width: auto !important; 150 | } 151 | 152 | /* Remove bottom banner, under video player */ 153 | 154 | .video-page .controls { 155 | margin-top: 15px; 156 | } 157 | 158 | /* Increase large player size */ 159 | 160 | .video-page.video-page--large-mode .player-container__player { 161 | height: 720px; 162 | } 163 | 164 | /* Show all playlists without scrolling when adding to favorites */ 165 | 166 | .favorites-dropdown__list { 167 | max-height: unset !important; 168 | } 169 | 170 | /* Fix z-index of comments so playlists are positioned above */ 171 | 172 | .video-page .comments-container { 173 | position: relative !important; 174 | z-index: 0 !important; 175 | } 176 | 177 | .video-page:not(.video-page--large-mode) .player-container { 178 | margin: 10px auto 0; 179 | } 180 | 181 | .video-page:not(.video-page--large-mode) .entity-container, 182 | .video-page:not(.video-page--large-mode) .comments-wrap { 183 | margin: 0 auto; 184 | } 185 | 186 | /* Minor stylistic improvements */ 187 | 188 | .entity-container { 189 | margin: 22px 0; 190 | margin-bottom: 22px; 191 | border-top: 1px solid #ccc; 192 | } 193 | `; 194 | 195 | /** 196 | * Checks for a subdomain, and if found it hacks it to pieces and puts them in a blender. 197 | * Returns an object containing a boolean `isModified`, `true` if the returned hostname 198 | * differs from the initial, and hopefully the desired hostname in `to` and the initial 199 | * hostname in `from`. 200 | * 201 | * @example 202 | * 203 | * // ru.example.com => example.com in this snippet: 204 | * 205 | * const { isModified, to, from } = changeTopLevelHost({ to: 'example.com' }); 206 | * 207 | * @param {object} opts - Arguments object, cleaner than separate parameters. 208 | * @param {string} opts.to - Valid hostname to strip down to, e.g. `example.com`. 209 | * @returns {object} - Object with `newHostname`, `oldHostname`, and `hasChanged`. 210 | * @throws {TypeError} - Throws on invalid hostname parameter. 211 | */ 212 | const changeTopLevelHost = ({ to }) => { 213 | // Constructor gives an empty string if `hostname` is undefined, prevents errors. 214 | const validParts = String(to).split('.'); 215 | 216 | // Need to provide string with two or more parts separated by periods, e.g. `xhamster.com`. If 217 | // only `xhamster` is specified, `.com` would be stripped and the redirect would break. 218 | if (validParts.length < 2) { 219 | throw new TypeError( 220 | `Function "${changeTopLevelHost.name}" expects a valid hostname (domain and TLD).` 221 | ); 222 | } 223 | 224 | // Filter out unwanted parts of hostname and check if the result differs. 225 | const fromHostname = location.hostname; 226 | const toHostname = to.split('.').filter(part => validParts.includes(part)).join('.'); 227 | const isModified = fromHostname !== toHostname; 228 | 229 | // Throw if new hostname doesn't have 2+ parts, i.e. valid parameters were not provided. 230 | if (toHostname.split('.').length < 2) { 231 | throw new TypeError( 232 | `Function "${changeTopLevelHost.name}" resulted in an invalid URL: ${toHostname}` 233 | ); 234 | } 235 | 236 | // Skip `Object` prototype as we only need these properties. 237 | return Object.create(null, { 238 | isModified: { value: isModified }, 239 | from: { value: fromHostname }, 240 | to: { value: toHostname }, 241 | }); 242 | }; 243 | 244 | /** 245 | * Store shit in variables for faster access 246 | */ 247 | 248 | const player = document.querySelector('#player-container'); 249 | const video = document.querySelector('#player-container video'); 250 | const html = document.querySelector('html'); 251 | 252 | /** 253 | * Switch to English 254 | */ 255 | 256 | if (OPTIONS.autoLanguage && html.lang !== 'us') { 257 | console.info('NX: Changing language to English.'); 258 | 259 | try { 260 | const { pathname } = location; 261 | const { to: newHostname } = changeTopLevelHost({ to: 'xhamster.com' }); // Make desired hostname. 262 | const { href: newUrl } = new URL(pathname, `https://${newHostname}`); // HTTPS always. 263 | 264 | // We need to set the `lang` cookie... 265 | document.cookie = 'lang=us; domain=xhamster.com; path=/'; 266 | 267 | // ...and then redirect to the English site. 268 | window.location = newUrl; 269 | 270 | console.info('NX: Language change successful.'); // Persistent console needed to see this. 271 | } catch (error) { 272 | console.error(`Unable to change language to English. Error: ${error}`); 273 | } 274 | } 275 | 276 | /** 277 | * Toggle cinema mode 278 | */ 279 | 280 | if (video && OPTIONS.cinemaMode) { 281 | // Button is not always available right away, so we wait for `canplay` 282 | video.addEventListener('canplay', function onCanPlay() { 283 | const largePlayerButton = document.querySelector('.large-mode'); 284 | 285 | // Click large player button 286 | largePlayerButton.dispatchEvent(new MouseEvent('click')); 287 | 288 | // Only run once 289 | video.removeEventListener('canplay', onCanPlay, false); 290 | }); 291 | } 292 | 293 | /** 294 | * Show video "about" section by default. 295 | */ 296 | 297 | const aboutButton = document.querySelector('.xh-button.about-control'); 298 | const aboutContainer = document.querySelector('.ab-info.controls-info__item.xh-helper-hidden'); 299 | 300 | if (aboutContainer && aboutButton) { 301 | aboutButton.classList.add('selected'); 302 | aboutContainer.classList.remove('xh-helper-hidden'); 303 | } 304 | 305 | /** 306 | * Auto-pause background tabs 307 | */ 308 | 309 | const channel = new BroadcastChannel('autopause'); 310 | 311 | const triggerPauseVideo = () => { 312 | channel.postMessage(null); 313 | }; 314 | 315 | const doPauseVideo = () => { 316 | video.pause(); 317 | }; 318 | 319 | const setAutoPause = (shouldPause) => { 320 | if (OPTIONS.autoPause && video) { 321 | if (shouldPause) { 322 | video.addEventListener('play', triggerPauseVideo); 323 | channel.addEventListener('message', doPauseVideo); 324 | } else { 325 | video.removeEventListener('play', triggerPauseVideo); 326 | channel.removeEventListener('message', doPauseVideo); 327 | } 328 | } 329 | }; 330 | 331 | if (OPTIONS.autoPause && video) { 332 | setAutoPause(true); 333 | } 334 | 335 | /** 336 | * Create buttons for options 337 | */ 338 | 339 | const buttons = document.createElement('div'); 340 | const scrollButton = document.createElement('a'); 341 | const scrollButtonText = document.createElement('span'); 342 | const cinemaModeButton = document.createElement('a'); 343 | const cinemaModeButtonText = document.createElement('span'); 344 | const cinemaModeButtonState = OPTIONS.cinemaMode ? 'plus-button-isOn' : 'plus-button-isOff'; 345 | const languageButton = document.createElement('a'); 346 | const languageButtonText = document.createElement('span'); 347 | const languageButtonState = OPTIONS.autoLanguage ? 'plus-button-isOn' : 'plus-button-isOff'; 348 | 349 | scrollButtonText.textContent = "Scroll to Top"; 350 | scrollButtonText.classList.add('text'); 351 | scrollButton.appendChild(scrollButtonText); 352 | scrollButton.classList.add('plus-button'); 353 | scrollButton.addEventListener('click', () => { 354 | window.scrollTo({ 355 | top: 0 356 | }); 357 | }); 358 | 359 | cinemaModeButtonText.textContent = 'Cinema mode'; 360 | cinemaModeButtonText.classList.add('text'); 361 | cinemaModeButton.appendChild(cinemaModeButtonText); 362 | cinemaModeButton.classList.add(cinemaModeButtonState, 'plus-button'); 363 | cinemaModeButton.addEventListener('click', () => { 364 | OPTIONS.cinemaMode = !OPTIONS.cinemaMode; 365 | localStorage.setItem('plus_cinemaMode', OPTIONS.cinemaMode); 366 | 367 | if (OPTIONS.cinemaMode) { 368 | cinemaModeButton.classList.replace('plus-button-isOff', 'plus-button-isOn'); 369 | } else { 370 | cinemaModeButton.classList.replace('plus-button-isOn', 'plus-button-isOff'); 371 | } 372 | }); 373 | 374 | languageButtonText.textContent = 'Auto-redirect to English'; 375 | languageButtonText.classList.add('text'); 376 | languageButton.appendChild(languageButtonText); 377 | languageButton.classList.add(languageButtonState, 'plus-button'); 378 | languageButton.addEventListener('click', () => { 379 | OPTIONS.autoLanguage = !OPTIONS.autoLanguage; 380 | localStorage.setItem('plus_autoLanguage', OPTIONS.autoLanguage); 381 | 382 | if (OPTIONS.autoLanguage) { 383 | languageButton.classList.replace('plus-button-isOff', 'plus-button-isOn'); 384 | } else { 385 | languageButton.classList.replace('plus-button-isOn', 'plus-button-isOff'); 386 | } 387 | }); 388 | 389 | buttons.classList.add('plus-buttons'); 390 | 391 | buttons.appendChild(scrollButton); 392 | buttons.appendChild(cinemaModeButton); 393 | buttons.appendChild(languageButton); 394 | 395 | document.body.appendChild(buttons); 396 | 397 | /** 398 | * Add styles 399 | */ 400 | 401 | GM_addStyle(sharedStyles); 402 | GM_addStyle(themeStyles); 403 | GM_addStyle(generalStyles); 404 | 405 | /** 406 | * Add dynamic styles 407 | */ 408 | 409 | const dynamicStyles = ` 410 | .plus-buttons { 411 | margin-right: -${buttons.getBoundingClientRect().width - 23}px; 412 | } 413 | 414 | .plus-buttons:hover { 415 | margin-right: 0; 416 | } 417 | 418 | .video-page.video-page--large-mode .player-container__player { 419 | max-height: ${window.innerHeight - 60}px; 420 | } 421 | `; 422 | 423 | GM_addStyle(dynamicStyles); 424 | 425 | /** 426 | * Updating dynamic styles on window resize 427 | */ 428 | 429 | if (player) { 430 | window.addEventListener('resize', () => { 431 | if (player.classList.contains('xplayer-large-mode')) { 432 | player.style.maxHeight = `${window.innerHeight - 60}px`; 433 | } 434 | }); 435 | } 436 | })(); --------------------------------------------------------------------------------