SB100 / PTP Show Profile Film in Forums

// ==UserScript==
// @namespace    https://openuserjs.org/users/SB100
// @name         PTP Show Profile Film in Forums
// @description  Show a user's profile film when you click on their display picture
// @updateURL    https://openuserjs.org/meta/SB100/PTP_Show_Profile_Film_in_Forums.meta.js
// @version      1.1.3
// @author       SB100
// @copyright    2021, SB100 (https://openuserjs.org/users/SB100)
// @license      MIT
// @match        https://passthepopcorn.me/forums.php?*action=viewthread*
// @match        https://passthepopcorn.me/torrents.php?*id=*
// @match        https://passthepopcorn.me/requests.php?*action=view*id=*
// @match        https://passthepopcorn.me/comments.php?*action=requests_b*
// @match        https://passthepopcorn.me/comments.php?*action=my_torrents*
// @grant        GM_xmlhttpRequest
// ==/UserScript==

// ==OpenUserJS==
// @author SB100
// ==/OpenUserJS==

/* jshint esversion: 6 */

/**
 * Turn a HTML string into a HTML element so that we can run querySelector calls against it
 */
function htmlToElement(html) {
  var template = document.createElement('template');
  html = html.trim();
  template.innerHTML = html;
  return template.content;
}

/**
 * Traverse a node until we find a parent who matches the selector
 */
function getParent(elem, selector) {
  if (elem === document.documentElement) {
    return null;
  }

  if (elem.matches(selector)) {
    return elem;
  }

  return getParent(elem.parentNode, selector);
}

/**
 * Adds styling to div to show it nicely as an overlay
 */
function addStylingToLoadingDiv(div, height = '100%') {
  div.style.position = 'absolute';
  div.style.left = '5px';
  div.style.top = '5px';
  div.style.width = 'calc(100% - 9px)';
  div.style.height = height.toString().includes('%') ? `calc(${height} - 5px)` : `${height}px`;
  div.style.background = 'rgba(0, 0, 0, 0.5)';
  div.style.display = 'flex';
  div.style.alignItems = 'center';
  div.style.justifyContent = 'center';
  div.classList.add('forum-post__avatar-profile-film');
}

/**
 * Load a user profile and return traversable nodes
 */
function loadUserProfile(url, avatarParent, height) {
  let resolver;
  let rejecter;
  const p = new Promise((resolveFn, rejectFn) => {
    resolver = resolveFn;
    rejecter = rejectFn;
  });

  const div = document.createElement('div');

  GM_xmlhttpRequest({
    method: 'get',
    url: url,
    timeout: 10000,
    onloadstart: () => {
      addStylingToLoadingDiv(div, height);
      div.innerText = 'Loading Profile Film ...';
      avatarParent.appendChild(div);
    },
    onload: (response) => {
      if (response.status !== 200) {
        div.innerText = "Couldn't load User Profile =(";
        div.onclick = () => {
          div.remove()
        }
        rejecter(new Error('Not OK'));
        return;
      }

      div.remove();
      resolver(htmlToElement(response.response));
    },
    onerror: (response) => {
      div.innerText = "Unknown Error =(";
      div.onclick = () => {
        div.remove()
      }
      rejecter(response)
    },
    ontimeout: (response) => {
      div.innerText = "Timed out =(";
      div.onclick = () => {
        div.remove()
      }
      rejecter(response)
    }
  });

  return p;
}

/**
 * Find a user profile link by traversing from their avatar image
 */
function getUserProfileLink(avatarElem) {
  const forumPost = getParent(avatarElem, '.forum-post');
  if (!forumPost) {
    return;
  }

  const usernameElem = forumPost.querySelector('a.username');
  if (!usernameElem) {
    return;
  }

  return usernameElem.href;
}

/**
 * Find the profile film image on the user page
 */
function findProfileFilmFromUserPage(userPageDoc) {
  const profileFilmHeader = Array.from(userPageDoc.querySelectorAll('.panel__heading__title')).filter(title => title.innerText === 'Profile Film');
  if (!profileFilmHeader || profileFilmHeader.length !== 1) {
    return;
  }

  const panel = getParent(profileFilmHeader[0], '.panel');
  if (!panel) {
    return;
  }

  return panel.querySelector('img');
}

/**
 * Swap the avatar and the profile film
 */
function swapImage(parentNode) {
  parentNode.querySelector('.forum-post__avatar__image').classList.toggle('hidden');
  parentNode.querySelector('.sidebar-cover-image').classList.toggle('hidden');
}

/**
 * Main runner to do all the things
 */
function showProfileFilm(event) {
  // only interested in forum avatars
  if (event.target.classList.contains('forum-post__avatar__image') === false && event.target.classList.contains('sidebar-cover-image') === false) {
    return;
  }

  const avatarParent = getParent(event.target, '.forum-post__avatar');

  // if we've already fetched the profile film, just swap the images
  if (avatarParent.childElementCount === 2) {
    swapImage(avatarParent);
    return;
  }

  // find height of avatar image so we don;t experience jank on an image swap
  const avatarHeight = event.target.height;

  // make relative to we can add a loading spinner to the image
  // set height to reduce jank
  avatarParent.style.position = 'relative';
  avatarParent.style.minHeight = `${avatarHeight + 10}px`;
  avatarParent.parentNode.style.minHeight = `${avatarHeight + 10}px`;

  // find the user link on the page
  const userProfileLink = getUserProfileLink(event.target);
  if (!userProfileLink) {
    return;
  }

  // load it, find the profile film, add it to the current page, and show it
  return loadUserProfile(userProfileLink, avatarParent, avatarHeight)
    .then((doc) => findProfileFilmFromUserPage(doc))
    .then((profileImg) => {
      if (!profileImg) {
        const div = document.createElement('div');
        addStylingToLoadingDiv(div, avatarHeight);
        div.innerText = 'No Profile Film Found';
        div.onclick = () => {
          div.remove()
        }
        avatarParent.appendChild(div);

        return;
      }

      profileImg.style.border = '2px dashed #C8AF4B';
      avatarParent.appendChild(profileImg);

      if (profileImg.height > 100 && profileImg.height < avatarHeight) {
        avatarParent.style.minHeight = `${profileImg.height + 10}px`;
        avatarParent.parentNode.style.minHeight = `${profileImg.height + 10}px`;
      }

      event.target.classList.add('hidden');
    });
}

/**
 * Event listener to run the main runner. Even works with infinite scrolling!
 */
function attachEventListeners() {
  document.querySelector('.thin').addEventListener('dblclick', showProfileFilm);
}

/**
 * Run the script
 */
(function () {
  'use strict';

  attachEventListeners();
})();