Sleazy Fork is available in English.

ThisVid.com Improved

Infinite scroll (optional). Preview for private videos. Filter: duration, public/private, include/exclude terms. Check access to private vids. Mass friend request button. Sorts messages. Download button 📼

  1. // ==UserScript==
  2. // @name ThisVid.com Improved
  3. // @namespace http://tampermonkey.net/
  4. // @version 5.0.8
  5. // @license MIT
  6. // @description Infinite scroll (optional). Preview for private videos. Filter: duration, public/private, include/exclude terms. Check access to private vids. Mass friend request button. Sorts messages. Download button 📼
  7. // @author smartacephale
  8. // @supportURL https://github.com/smartacephale/sleazy-fork
  9. // @match https://*.thisvid.com/*
  10. // @icon https://www.google.com/s2/favicons?sz=64&domain=thisvid.com
  11. // @grant GM_addStyle
  12. // @require https://cdn.jsdelivr.net/npm/billy-herrington-utils@1.1.8/dist/billy-herrington-utils.umd.js
  13. // @require https://cdn.jsdelivr.net/npm/jabroni-outfit@1.4.9/dist/jabroni-outfit.umd.js
  14. // @require https://cdn.jsdelivr.net/npm/lskdb@1.0.2/dist/lskdb.umd.js
  15. // @require https://update.greasyfork.org/scripts/494204/data-manager.user.js?version=1458190
  16. // @require https://update.greasyfork.org/scripts/494205/pagination-manager.user.js?version=1459738
  17. // @run-at document-idle
  18. // ==/UserScript==
  19. /* globals $ DataManager PaginationManager */
  20.  
  21. const { Tick, parseDom, fetchWith, fetchHtml, timeToSeconds, parseCSSUrl, circularShift, range, listenEvents, replaceElementTag,
  22. sanitizeStr, chunks, downloader, AsyncPool } = window.bhutils;
  23. Object.assign(unsafeWindow, { bhutils: window.bhutils });
  24. const { JabroniOutfitStore, defaultStateWithDurationAndPrivacy, JabroniOutfitUI, defaultSchemeWithPrivateFilter } = window.jabronioutfit;
  25. const { LSKDB } = window.lskdb;
  26.  
  27. const SponsaaLogo = `
  28. Kono bangumi ha(wa) goran no suponsaa no teikyou de okurishimasu⣿⣿⣿⣿
  29. ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
  30. ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⡟⣟⢻⢛⢟⠿⢿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
  31. ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣿⣾⣾⣵⣧⣷⢽⢮⢧⢿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
  32. ⣿⣿⣿⣿⣿⣿⣯⣭⣧⣯⣮⣧⣯⣧⣯⡮⣵⣱⢕⣕⢕⣕⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
  33. ⣿⣿⣿⣿⣿⡫⡻⣝⢯⡻⣝⡟⣟⢽⡫⡟⣏⢏⡏⡝⡭⡹⡩⣻⣿⣿⣿⣿⠟⠟⢟⡟⠟⠻⠛⠟⠻⠻⣿⣿⣿⡟⠟⠻⠛⠟⠻⠻⣿⣿
  34. ⣿⣿⣿⣿⡿⣻⣿⣿⣿⡿⣿⣿⡿⣿⣿⢿⢿⡻⢾⠽⡺⡞⣗⠷⣿⣿⣿⡏⠀⠀⠀⣣⣤⡄⠀⠠⣄⡆⠫⠋⠻⢕⣤⡄⠀ ⢀⣤⣔⣿⣿
  35. ⣿⣿⣿⣿⣷⣷⣷⣾⣶⣯⣶⣶⣷⣷⣾⣷⣳⣵⣧⣳⡵⣕⣮⣞⣾⣿⡟⠄⢀⣦⠀⢘⣽⡇⠀⠨⣿⡌⠀⠣⣠⠹⠿⡭⠀ ⠐⣿⣿⣿⣿
  36. ⣿⣿⣿⣿⣕⣵⣱⣫⣳⡯⣯⣫⣯⣞⣮⣎⣮⣪⣢⣣⣝⣜⡜⣜⣾⣿⠃⠀⠀⠑⠀⠀⢺⡇⠀ ⢘⣾⠀⢄⢄⠘⠀⢘⢎⠀⢈⣿⣿⣿⣿
  37. ⣿⣿⣿⣿⣿⣙⣛⣛⢻⢛⢟⢟⣛⢻⢹⣙⢳⢹⢚⢕⣓⡓⡏⣗⣿⣓⣀⣀⣿⣿⣮⢀⣀⣇⣀⣐⣿⣔⣀⢁⢀⣀⣀⣅⣀⡠⣿⣿⣿⣿
  38. ⣿⣿⣿⣿⣿⣾⡞⣞⢷⡻⡯⡷⣗⢯⢷⢞⢷⢻⢞⢷⡳⣻⣺⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
  39. ⣿⣿⣿⣿⣿⣿⣷⣵⡵⣼⢼⢼⡴⣵⢵⡵⣵⢵⡵⣵⣪⣾⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
  40. ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣮⣧⣫⣪⡪⡣⣫⣪⣣⣯⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
  41. ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿`;
  42.  
  43. const haveAccessColor = 'linear-gradient(90deg, #31623b, #212144)';
  44. const haveNoAccessColor = 'linear-gradient(90deg, #462525, #46464a)';
  45. const succColor = 'linear-gradient(#2f6eb34f, #66666647)';
  46. const failColor = 'linear-gradient(rgba(179, 47, 47, 0.31), rgba(102, 102, 102, 0.28))';
  47. const friendProfileColor = 'radial-gradient(circle, rgb(28, 42, 50) 48%, rgb(0, 0, 0) 100%)';
  48.  
  49. class THISVID_RULES {
  50. constructor() {
  51. const { href, pathname } = window.location;
  52.  
  53. this.PAGINATION_ALLOWED = [
  54. /\.com\/$/,
  55. /\/(categories|tags?)\//,
  56. /\/?q=.*/,
  57. /\/(\w+-)?(rated|popular|private|newest|winners|updates)\/(\d+\/)?$/,
  58. /\/members\/\d+\/\w+_videos\//,
  59. /\/playlist\/\d+\//,
  60. /\/my_(\w+)_videos\//
  61. ].some(r => r.test(href));
  62.  
  63. this.IS_MEMBER_PAGE = /^\/members\/\d+\/$/.test(pathname);
  64. this.IS_WATCHLATER_KIND = /^\/my_(\w+)_videos\//.test(pathname);
  65. this.IS_MESSAGES_PAGE = /^\/my_messages\//.test(pathname);
  66. this.IS_PLAYLIST = /^\/playlist\/\d+\//.test(pathname);
  67. this.IS_VIDEO_PAGE = /^\/videos\//.test(pathname);
  68.  
  69. this.PAGE_HAS_VIDEO = this.GET_THUMBS(document).length > 0;
  70.  
  71. this.PAGINATION = document.querySelector('.pagination');
  72. this.PAGINATION_LAST = this.GET_PAGINATION_LAST();
  73.  
  74. this.CONTAINER = Array.from(document.querySelectorAll('.thumbs-items')).pop();
  75.  
  76. this.MY_ID = document.querySelector('[target="_self"]')?.href.match(/\/(\d+)\//)[1] || null;
  77. this.LOGGED_IN = !!this.MY_ID;
  78. this.IS_MY_MEMBER_PAGE = this.LOGGED_IN && !!document.querySelector('.my-avatar');
  79. this.IS_OTHER_MEMBER_PAGE = !this.IS_MY_MEMBER_PAGE && this.IS_MEMBER_PAGE;
  80. this.IS_MEMBER_FRIEND = this.IS_OTHER_MEMBER_PAGE && document.querySelector('.case-left')?.innerText.includes('is in your friends');
  81.  
  82. // highlight friend page profile
  83. if (this.IS_MEMBER_FRIEND) {
  84. document.querySelector('.profile').style.background = friendProfileColor;
  85. }
  86.  
  87. // playlist page add link to video
  88. if (this.IS_PLAYLIST) {
  89. const videoUrl = this.PLAYLIST_THUMB_URL(pathname);
  90. const desc = document.querySelector('.tools-left > li:nth-child(4) > .title-description');
  91. const link = replaceElementTag(desc, 'a');
  92. link.href = videoUrl;
  93. }
  94. }
  95.  
  96. GET_PAGINATION_LAST(doc) {
  97. return parseInt((doc || document).querySelector('.pagination-next')?.previousElementSibling?.innerText) || 1;
  98. }
  99.  
  100. GET_THUMBS(html) {
  101. if (this.IS_WATCHLATER_KIND) {
  102. return Array.from(html.querySelectorAll('.thumb-holder'));
  103. }
  104. let thumbs = Array.from(html.querySelectorAll('.tumbpu[title]'));
  105. if (thumbs.length === 0 && html?.classList?.contains('tumbpu')) thumbs = [html];
  106. return thumbs.filter(thumb => !thumb?.parentElement.classList.contains('thumbs-photo'));
  107. }
  108.  
  109. PLAYLIST_THUMB_URL(src) {
  110. return src.replace(/playlist\/\d+\/video/, () => 'videos');
  111. }
  112.  
  113. THUMB_URL(thumb) {
  114. if (this.IS_WATCHLATER_KIND) {
  115. return thumb.firstElementChild.href;
  116. }
  117. let url = thumb.getAttribute('href');
  118. if (this.IS_PLAYLIST) url = this.PLAYLIST_THUMB_URL(url);
  119. return url;
  120. }
  121.  
  122. URL_DATA(proxyLocation) {
  123. const url = new URL(proxyLocation || window.location);
  124. const offset = this.IS_PLAYLIST ? 1 : (parseInt(url.pathname.match(/\/(\d+)\/?$/)?.[1]) || 1);
  125.  
  126. if (url.pathname === '/') url.pathname = '/latest-updates/';
  127. if (!/\/(\d+)\/?$/.test(url.pathname)) url.pathname = `${url.pathname}${offset}/`;
  128.  
  129. const iteratable_url = (n) => {
  130. if (this.IS_PLAYLIST) {
  131. url.search = `mode=async&function=get_block&block_id=playlist_view_playlist_view&sort_by=added2fav_date&from=${n}&_=${Date.now()}`;
  132. } else {
  133. url.pathname = url.pathname.replace(/\/\d+\/$/, `/${n}/`);
  134. }
  135. return url.href;
  136. }
  137.  
  138. return { offset, iteratable_url }
  139. }
  140.  
  141. THUMB_IMG_DATA(thumb) {
  142. const img = thumb.querySelector('img');
  143. const privateThumb = thumb.querySelector('.private');
  144. let imgSrc = img?.getAttribute('data-original');
  145. if (privateThumb) {
  146. imgSrc = parseCSSUrl(privateThumb.style.background);
  147. privateThumb.removeAttribute('style');
  148. }
  149. const count = img.getAttribute('data-cnt');
  150. if (!count || count === "6") img.removeAttribute('data-cnt');
  151. img.classList.remove('lazy-load');
  152. img.classList.add('tracking');
  153.  
  154. if (this.IS_PLAYLIST) {
  155. img.onmouseover = img.onmouseout = null;
  156. img.removeAttribute('onmouseover');
  157. img.removeAttribute('onmouseout');
  158. }
  159.  
  160. return { img, imgSrc };
  161. }
  162.  
  163. ITERATE_PREVIEW_IMG(img) {
  164. const count = parseInt(img.getAttribute('data-cnt')) || 6;
  165. img.src = img.getAttribute('src').replace(/(\d+)(?=\.jpg$)/, (_, n) => `${circularShift(parseInt(n), count)}`);
  166. }
  167.  
  168. THUMB_DATA(thumb) {
  169. const title = sanitizeStr(thumb.querySelector('.title').innerText);
  170. const duration = timeToSeconds(thumb.querySelector('.thumb > .duration').textContent);
  171. return { title, duration }
  172. }
  173.  
  174. IS_PRIVATE(thumb) {
  175. return !thumb.querySelector('.private');
  176. }
  177. }
  178.  
  179. const RULES = new THISVID_RULES();
  180.  
  181. //====================================================================================================
  182.  
  183. function friend(id) {
  184. return fetchWith(FRIEND_REQUEST_URL(id));
  185. }
  186.  
  187. const FRIEND_REQUEST_URL = (id) => `https://thisvid.com/members/${id}/?action=add_to_friends_complete&function=get_block&block_id=member_profile_view_view_profile&format=json&mode=async&message=`;
  188.  
  189. const USERS_PER_PAGE = 24;
  190.  
  191. async function getMemberFriends(memberId, start, end) {
  192. const { friendsCount } = await getMemberData(memberId);
  193. const offset = Math.ceil(friendsCount / USERS_PER_PAGE);
  194. const pages = range(offset).slice(start, end).map(o => `https://thisvid.com/members/${memberId}/friends/${o}/`);
  195. const pagesFetched = pages.map(p => fetchHtml(p));
  196. const friends = (await Promise.all(pagesFetched)).flatMap(getMembers);
  197. return friends;
  198. }
  199.  
  200. function getMembers(el) {
  201. const friendsList = el.querySelector('#list_members_friends_items');
  202. return Array.from(friendsList?.querySelectorAll('.tumbpu') || [])
  203. .map(e => e.href.match(/\d+/)?.[0]).filter(_ => _);
  204. }
  205.  
  206. async function friendMemberFriends(orientationFilter) {
  207. const memberId = window.location.pathname.match(/\d+/)[0];
  208. friend(memberId);
  209. const friends = await getMemberFriends(memberId);
  210. const spool = new AsyncPool(60);
  211. friends.map((fid) => {
  212. if (!orientationFilter) () => friend(fid);
  213. return () => getMemberData(fid).then(async ({ orientation, uploadedPrivate }) => {
  214. if (orientation === orientationFilter && uploadedPrivate > 0) {
  215. await friend(fid);
  216. }
  217. });
  218. }).forEach(f => spool.push(f));
  219. await spool.run();
  220. }
  221.  
  222. function initFriendship() {
  223. GM_addStyle('.buttons {display: flex; flex-wrap: wrap} .buttons button, .buttons a {align-self: center; padding: 4px; margin: 5px;}');
  224.  
  225. const buttonAll = parseDom('<button style="background: radial-gradient(red, blueviolet);">friend everyone</button>');
  226. const buttonStraightOnly = parseDom('<button style="background: radial-gradient(red, #a18cb5);">friend straights</button>');
  227. const buttonGayOnly = parseDom('<button style="background: radial-gradient(red, #46baff);">friend gays</button>');
  228. const buttonBisexualOnly = parseDom('<button style="background: radial-gradient(red, #4ebaaf);">friend bisexuals</button>');
  229.  
  230. document.querySelector('.buttons').append(buttonAll, buttonStraightOnly, buttonGayOnly, buttonBisexualOnly);
  231.  
  232. buttonAll.addEventListener('click', (e) => handleClick(e), { once: true });
  233. buttonStraightOnly.addEventListener('click', (e) => handleClick(e, 'Straight'), { once: true });
  234. buttonGayOnly.addEventListener('click', (e) => handleClick(e, 'Gay'), { once: true });
  235. buttonBisexualOnly.addEventListener('click', (e) => handleClick(e, 'Bisexual'), { once: true });
  236.  
  237. function handleClick(e, orientationFilter) {
  238. const button = e.target;
  239. button.style.background = 'radial-gradient(#ff6114, #5babc4)';
  240. button.innerText = 'processing requests';
  241. friendMemberFriends(orientationFilter).then(() => {
  242. button.style.background = 'radial-gradient(blue, lightgreen)';
  243. button.innerText = 'friend requests sent';
  244. });
  245. }
  246. }
  247.  
  248. //====================================================================================================
  249.  
  250. async function getMemberData(id) {
  251. const url = id.includes('member') ? id : `/members/${id}/`;
  252. const doc = await fetchHtml(url);
  253. const data = {};
  254.  
  255. doc.querySelectorAll('.profile span').forEach(s => {
  256. if (s.innerText.includes('Name:')) {
  257. data.name = s.firstElementChild.innerText.trim();
  258. }
  259. if (s.innerText.includes('Orientation:')) {
  260. data.orientation = s.firstElementChild.innerText.trim();
  261. }
  262. if (s.innerText.includes('Videos uploaded:')) {
  263. data.uploadedPublic = parseInt(s.children[0].innerText);
  264. data.uploadedPrivate = parseInt(s.children[1].innerText);
  265. }
  266. });
  267.  
  268. data.friendsCount = parseInt(doc.querySelector('#list_members_friends')?.firstElementChild.innerText.match(/\d+/g).pop()) || 0;
  269.  
  270. return data;
  271. }
  272.  
  273. //====================================================================================================
  274.  
  275. unsafeWindow.requestPrivateAccess = (e, memberid) => {
  276. e.preventDefault();
  277. friend(memberid);
  278. e.target.innerText = e.target.innerText.replace('🚑', '🍆');
  279. }
  280.  
  281. async function checkPrivateVideoAccess(url) {
  282. const html = await fetchHtml(url);
  283. const holder = html.querySelector('.video-holder > p');
  284.  
  285. const access = !holder;
  286.  
  287. const uploaderEl = holder ? holder.querySelector('a') : html.querySelector('a.author');
  288. const uploaderURL = uploaderEl.href.replace(/.*\/(\d+)\/$/, (a, b) => b);
  289. const uploaderName = uploaderEl.innerText;
  290.  
  291. return {
  292. access,
  293. uploaderURL,
  294. uploaderName
  295. }
  296. }
  297.  
  298. function checkPrivateVidsAccess() {
  299. document.querySelectorAll('.tumbpu > .private').forEach(async t => {
  300. const thumb = t.parentElement;
  301. const { access, uploaderURL, uploaderName } = await checkPrivateVideoAccess(thumb.href);
  302.  
  303. thumb.style.background = access ? haveAccessColor : haveNoAccessColor;
  304. thumb.querySelector('.title').innerText += access ? ' ✅ ' : ' ❌ ';
  305. thumb.querySelector('.title').appendChild(parseDom(access ? `<span>${uploaderName}</span>` :
  306. `<span onclick="requestPrivateAccess(event, ${uploaderURL});"> 🚑 ${uploaderName}</span>`));
  307. });
  308. }
  309.  
  310. //====================================================================================================
  311.  
  312. const createDownloadButton = () => downloader({
  313. append: '',
  314. after: '.share_btn',
  315. button: '<li><a href="#" style="text-decoration: none;font-size: 2rem;">📼</a></li>',
  316. cbBefore: () => $('.fp-ui').click()
  317. })
  318.  
  319. //====================================================================================================
  320.  
  321. class PreviewAnimation {
  322. constructor(element, delay = ANIMATION_DELAY) {
  323. $('img[alt!="Private"]').off();
  324. this.tick = new Tick(delay);
  325. listenEvents(element, ['mouseover', 'touchstart'], this.animatePreview);
  326. }
  327.  
  328. animatePreview = (e) => {
  329. const { target: el, type } = e;
  330. if (!el.classList.contains('tracking') || !el.getAttribute("src")) return;
  331. this.tick.stop();
  332. if (type === 'mouseover' || type === 'touchstart') {
  333. const orig = el.getAttribute("src");
  334. this.tick.start(
  335. () => { RULES.ITERATE_PREVIEW_IMG(el); },
  336. () => { el.src = orig; });
  337. el.addEventListener(type === 'mouseover' ? 'mouseleave' : 'touchend', () => this.tick.stop(), { once: true });
  338. }
  339. };
  340. }
  341.  
  342. //====================================================================================================
  343.  
  344. function highlightMessages() {
  345. for (const member of document.querySelectorAll('.user-avatar > a')) {
  346. getMemberData(member.href).then(({ uploadedPublic, uploadedPrivate }) => {
  347. if (uploadedPrivate > 0) {
  348. const success = !member.parentElement.nextElementSibling.innerText.includes('declined');
  349. member.parentElement.parentElement.style.background = success ? succColor : failColor;
  350. }
  351. member.parentElement.parentElement.querySelector('.user-comment p').innerText +=
  352. ` | videos: ${uploadedPublic} public, ${uploadedPrivate} private`;
  353. });
  354. }
  355. }
  356.  
  357. //====================================================================================================
  358.  
  359. const lskdb = new LSKDB();
  360.  
  361. async function getMemberVideos(id, type = 'private') {
  362. const { uploadedPrivate, uploadedPublic, name } = await getMemberData(id);
  363. const videosCount = type === 'private' ? uploadedPrivate : uploadedPublic;
  364. const paginationLast = Math.ceil(videosCount / 48);
  365. const { iteratable_url } = RULES.URL_DATA(new URL(`https://thisvid.com/members/${id}/${type}_videos/`));
  366. const memberVideosGenerator = PaginationManager.createPaginationGenerator(0, paginationLast, iteratable_url);
  367. return { name, videosCount, memberVideosGenerator };
  368. }
  369.  
  370. async function getMembersVideos(id, friendsCount, memberGeneratorCallback, type = 'private') {
  371. let skipFlag = false;
  372. let skipCount = 1;
  373. let minVideosCount = 1;
  374.  
  375. const skipCurrentMember = (n = 1) => { skipFlag = true; skipCount = n; }
  376. const filterVideosCount = (n = 1) => { minVideosCount = n; }
  377.  
  378. let membersIds = await getMemberFriends(id, 0, 1);
  379. getMemberFriends(id, 1).then(r => { membersIds = membersIds.concat(r) });
  380.  
  381. async function* pageGenerator() {
  382. let currentGenerator = null;
  383. for (let c = 0; c < friendsCount - 1; c++) {
  384. if (lskdb.hasKey(membersIds[c])) continue;
  385.  
  386. if (!currentGenerator) {
  387. const { memberVideosGenerator, name, videosCount } = await getMemberVideos(membersIds[c], type);
  388.  
  389. if (memberVideosGenerator && videosCount >= minVideosCount) {
  390. currentGenerator = memberVideosGenerator;
  391. memberGeneratorCallback(name, videosCount, membersIds[c]);
  392. } else continue;
  393. }
  394.  
  395. const { value: { url } = {}, done } = await currentGenerator.next();
  396.  
  397. if (done || skipFlag) {
  398. c += skipCount - 1;
  399. skipCount = 1;
  400. currentGenerator = null;
  401. skipFlag = false;
  402. } else {
  403. yield { url, offset: c };
  404. }
  405. }
  406.  
  407. }
  408.  
  409. return {
  410. pageGenerator: () => pageGenerator(membersIds, type),
  411. skipCurrentMember,
  412. filterVideosCount
  413. };
  414. }
  415.  
  416. function createPrivateFeedButton() {
  417. const container = document.querySelectorAll('.sidebar ul')[1];
  418. const buttonPrv = parseDom(`<li><a href="https://thisvid.com/my_wall/#private_feed" class="selective"><i class="ico-arrow"></i>My Friends Private Videos</a></li>`);
  419. const buttonPub = parseDom(`<li><a href="https://thisvid.com/my_wall/#public_feed" class="selective"><i class="ico-arrow"></i>My Friends Public Videos</a></li>`);
  420. container.append(buttonPub, buttonPrv);
  421. }
  422.  
  423. async function createPrivateFeed() {
  424. createPrivateFeedButton();
  425. if (!window.location.hash.includes('feed')) return;
  426. const isPubKey = window.location.hash === '#public_feed';
  427.  
  428. const container = parseDom('<div class="thumbs-items"></div>');
  429. const ignored = parseDom('<div class="ignored"><h2>IGNORED:</h2></div>');
  430.  
  431. Object.assign(defaultSchemeWithPrivateFilter, {
  432. controlsSkip: [
  433. { type: "button", innerText: "skip 10", callback: async () => skip(event, 10) },
  434. { type: "button", innerText: "skip 100", callback: async () => skip(event, 100) },
  435. { type: "button", innerText: "skip 1000", callback: async () => skip(event, 1000) }],
  436. controlsFilter: [
  437. { type: "button", innerText: "filter >10", callback: async () => filterVidsCount(event, 10) },
  438. { type: "button", innerText: "filter >25", callback: async () => filterVidsCount(event, 25) },
  439. { type: "button", innerText: "filter >100", callback: async () => filterVidsCount(event, 100) },
  440. ]
  441. });
  442.  
  443. const containerParent = document.querySelector('.main > .container > .content');
  444. containerParent.innerHTML = '';
  445. containerParent.nextElementSibling.remove()
  446. containerParent.append(container);
  447. container.before(ignored);
  448. GM_addStyle(`.content { width: auto; }
  449. .member-videos, .ignored { background: #b3b3b324; min-height: 3rem; margin: 1rem 0px; color: #fff; font-size: 1.24rem; display: flex; flex-wrap: wrap; justify-content: center;
  450. padding: 10px; width: 100%; }
  451. .member-videos * { padding: 5px; margin: 4px; }
  452. .member-videos h2 a { font-size: 1.24rem; margin: 0; padding: 0; display: inline; }
  453. .ignored * { padding: 4px; margin: 5px; }
  454. .thumbs-items { display: flex; flex-wrap: wrap; }`);
  455.  
  456. const { friendsCount } = await getMemberData(RULES.MY_ID);
  457.  
  458. RULES.INTERSECTION_OBSERVABLE = document.querySelector('.footer');
  459. RULES.PAGINATION_LAST = friendsCount;
  460. RULES.CONTAINER = container;
  461.  
  462. const { pageGenerator, skipCurrentMember, filterVideosCount } = await getMembersVideos(RULES.MY_ID, friendsCount, (name, videosCount, id) => {
  463. container.append(parseDom(`
  464. <div class="member-videos" id="mem-${id}">
  465. <h2><a href="/members/${id}/">${name}</a> ${videosCount} videos</h2>
  466. <button onClick="hideMemberVideos(event)">ignore 🗡</button>
  467. <button onClick="hideMemberVideos(event, false)">skip</button>
  468. </div>`));
  469. }, isPubKey ? 'public' : 'private');
  470.  
  471. const ignoredMembers = lskdb.getAllKeys();
  472. ignoredMembers.forEach(im => {
  473. document.querySelector('.ignored').append(parseDom(`<button id="#ir-${im}" onClick="unignore(event)">${im} 🗡</button>`));
  474. });
  475.  
  476. const skip = (e, n) => {
  477. skipCurrentMember(n);
  478. document.querySelector('.thumbs-items').innerHTML = '';
  479. }
  480.  
  481. unsafeWindow.hideMemberVideos = (e, ignore = true) => {
  482. let id = e.target.parentElement.id;
  483. if (!document.querySelector(`#${id} ~ div`)) {
  484. skipCurrentMember();
  485. }
  486. const box = document.getElementById(id);
  487. const toDelete = [box];
  488. let curr = box.nextElementSibling;
  489. while (curr?.classList.contains('tumbpu')) {
  490. toDelete.push(curr);
  491. curr = curr.nextElementSibling;
  492. }
  493. toDelete.forEach(e => e.remove());
  494. id = id.slice(4)
  495. if (ignore) {
  496. document.querySelector('.ignored').append(parseDom(`<button id="irm-${id}" onClick="unignore(event)">${id} X</button>`));
  497. lskdb.setKey(id);
  498. }
  499. }
  500.  
  501. unsafeWindow.unignore = (e) => {
  502. const id = e.target.id.slice(4);
  503. lskdb.removeKey(id);
  504. e.target.remove();
  505. }
  506.  
  507. const filterVidsCount = (e, count) => {
  508. filterVideosCount(count);
  509. }
  510.  
  511. new PaginationManager(state, stateLocale, RULES, handleLoadedHTML, SCROLL_RESET_DELAY, pageGenerator);
  512. new PreviewAnimation(document.body);
  513. new JabroniOutfitUI(store, defaultSchemeWithPrivateFilter);
  514. }
  515.  
  516. //====================================================================================================
  517.  
  518. async function clearMessages() {
  519. const sortMsgs = (doc) => {
  520. doc.querySelectorAll('.entry').forEach(e => {
  521. const id = e.querySelector('input[name="delete[]"]').value;
  522. const msg = e.querySelector('.user-comment').innerText;
  523. if (/has confirmed|declined your|has removed/g.test(msg)) deleteMsg(id);
  524. });
  525. }
  526.  
  527. const deleteMsg = id => {
  528. const url = `https://thisvid.com/my_messages/inbox/?mode=async&format=json&action=delete&function=get_block&block_id=list_messages_my_conversation_messages&delete[]=${id}`;
  529. fetch(url).then(res => console.log(url, res?.status));
  530. }
  531.  
  532. await Promise.all(Array.from({ length: RULES.PAGINATION_LAST }, (_, i) =>
  533. fetchHtml(`https://thisvid.com/my_messages/inbox/${i + 1}/`).then(html => sortMsgs(html))));
  534. }
  535.  
  536. function clearMessagesButton() {
  537. const btn = parseDom('<button>clear messages</button>');
  538. btn.addEventListener('click', clearMessages);
  539. document.querySelector('.headline').append(btn);
  540. }
  541.  
  542. //====================================================================================================
  543.  
  544. function route() {
  545. console.log(SponsaaLogo);
  546.  
  547. if (RULES.LOGGED_IN) {
  548. defaultSchemeWithPrivateFilter.privateFilter.push(
  549. { type: "button", innerText: "check access 🔓", callback: checkPrivateVidsAccess });
  550. }
  551.  
  552. if (RULES.IS_MY_MEMBER_PAGE) {
  553. createPrivateFeed();
  554. }
  555.  
  556. if (RULES.IS_MESSAGES_PAGE) {
  557. clearMessagesButton();
  558. highlightMessages();
  559. }
  560.  
  561. if (RULES.IS_VIDEO_PAGE) createDownloadButton();
  562.  
  563. if (!RULES.PAGE_HAS_VIDEO) return;
  564.  
  565. const containers = Array.from(RULES.IS_WATCHLATER_KIND ? [RULES.CONTAINER] : document.querySelectorAll('.thumbs-items:not(.thumbs-members)'));
  566. if (containers.length > 1 && !RULES.IS_MEMBER_PAGE) RULES.CONTAINER = containers[0];
  567. containers.forEach(c => {
  568. handleLoadedHTML(c, RULES.IS_MEMBER_PAGE ? c : RULES.CONTAINER, true);
  569. });
  570.  
  571. new PreviewAnimation(document.body);
  572. new JabroniOutfitUI(store, defaultSchemeWithPrivateFilter);
  573.  
  574. if (RULES.IS_OTHER_MEMBER_PAGE) {
  575. initFriendship();
  576. }
  577.  
  578. if (RULES.PAGINATION_ALLOWED) {
  579. stateLocale.pagIndexLast = RULES.PAGINATION_LAST;
  580. if (!RULES.PAGINATION) return;
  581. new PaginationManager(state, stateLocale, RULES, handleLoadedHTML, SCROLL_RESET_DELAY);
  582. }
  583. }
  584.  
  585. //====================================================================================================
  586.  
  587. const SCROLL_RESET_DELAY = 350;
  588. const ANIMATION_DELAY = 750;
  589.  
  590. const store = new JabroniOutfitStore(defaultStateWithDurationAndPrivacy);
  591. const { state, stateLocale } = store;
  592. const { applyFilters, handleLoadedHTML } = new DataManager(RULES, state);
  593. store.subscribe(applyFilters);
  594.  
  595. route();