├── .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 | "
" +
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 | })();
--------------------------------------------------------------------------------