ČSFD Compare

Show your own ratings on other users ratings list

当前为 2022-10-03 提交的版本,查看 最新版本

// ==UserScript==
// @name         ČSFD Compare
// @version      0.5.12.1
// @namespace    csfd.cz
// @description  Show your own ratings on other users ratings list
// @author       Jan Verner <[email protected]>
// @include      *csfd.cz/*
// @include      *csfd.sk/*
// @icon         http://img.csfd.cz/assets/b1733/images/apple_touch_icon.png
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @require      https://gf.qytechs.cn/scripts/449554-csfd-compare-utils/code/csfd-compare-utils.js?version=1100309
// @license      GNU GPLv3
// ==/UserScript==


const VERSION = 'v0.5.12.1';
const SCRIPTNAME = 'CSFD-Compare';
const SETTINGSNAME = 'CSFD-Compare-settings';
const GREASYFORK_URL = 'https://gf.qytechs.cn/cs/scripts/425054-%C4%8Dsfd-compare';

const SETTINGSNAME_HIDDEN_BOXES = 'CSFD-Compare-hiddenBoxes';


let defaultSettings = {
  // HOME PAGE
  hiddenSections: [],
  // GLOBAL
  showControlPanelOnHover: true,
  clickableHeaderBoxes: true,
  clickableMessages: true,
  addStars: true,
  // USER
  displayMessageButton: true,
  displayFavoriteButton: true,
  hideUserControlPanel: true,
  compareUserRatings: true,
  // FILM/SERIES
  addRatingsDate: false,
  showLinkToImage: true,
  ratingsEstimate: true,
  ratingsFromFavorites: true,
  addRatingsComputedCount: true,
  hideSelectedUserReviews: false,
  hideSelectedUserReviewsList: [],
  // ACTORS
  showOnOneLine: false,
};


/**
 * Check if settings are valid. If not, reset them.
 * Return either unmodified or modified settings
 * @param {*} settings - LocalStorage settings current value
 * @param {string} settingsName - Settings Name
 */
async function checkSettingsValidity(settings, settingsName) {

  if (settingsName === SETTINGSNAME_HIDDEN_BOXES) {
    const isArray = Array.isArray(settings);
    let keysValid = true;
    settings.forEach(element => {
      const keys = Object.keys(element);
      if (keys.length !== 2) {
        keysValid = false;
      }
    });

    if (!isArray || !keysValid) {
      settings = defaultSettings.hiddenSections;
      localStorage.setItem(SETTINGSNAME_HIDDEN_BOXES, JSON.stringify(settings));
      return settings;
    }
  }
  return settings;
}

async function delay(t) {
  return new Promise(resolve => {
    setTimeout(resolve, t);
  });
}

async function getSettings(settingsName = SETTINGSNAME) {
  if (!localStorage[settingsName]) {
    if (settingsName === SETTINGSNAME_HIDDEN_BOXES) {
      defaultSettings = [];
    }
    console.log(`ADDDING DEFAULTS: ${defaultSettings}`);
    localStorage.setItem(settingsName, JSON.stringify(defaultSettings));
    return defaultSettings;
  } else {
    return JSON.parse(localStorage[settingsName]);
  }
}

async function refreshTooltips() {
  try {
    tippy('[data-tippy-content]', {
      // interactive: true,
      popperOptions: { modifiers: { computeStyle: { gpuAcceleration: false } } }
    });
  } catch (err) {
    console.log("Error: refreshTooltips():", err);
  }
}

/**
 * Take a list of dictionaries and return merged dictionary
 * @param {*} list
 * @returns
 */
async function mergeDict(list) {
  const merged = list.reduce(function (r, o) {
    Object.keys(o).forEach(function (k) { r[k] = o[k]; });
    return r;
  }, {});
  return merged;
}

async function onHomepage() {
  let check = false;
  if (document.location.pathname === '/') {
    check = true;
  }
  return check;
}

(async () => {
  "use strict";
  /* globals jQuery, $, waitForKeyElements */
  /* jshint -W069 */
  /* jshint -W083 */
  /* jshint -W075 */


  class Csfd {

    constructor(csfdPage) {
      this.csfdPage = csfdPage;
      this.stars = {};
      this.storageKey = undefined;
      this.userUrl = undefined;
      this.endPageNum = 0;
      this.userRatingsCount = 0;
      this.userRatingsUrl = undefined;
      this.localStorageRatingsCount = 0;
      this.settings = undefined;

      this.RESULT = {};

      // Ignore the ads... Make 'hodnoceni' table wider.
      // TODO: Toto do hodnoceni!
      $('.column.column-80').attr('class', '.column column-90');
    }

    getEndPageNum(data) {
      console.log("fn: getEndPageNum()");
      let $pagination = $(data).find('.box-content').find('.box-more-bar').find('.pagination')[0];
      let lastPageHref = $($pagination).find('a:nth-last-child(2)').attr('href');
      let foundMatch = lastPageHref.match(new RegExp("page=(.*)$"));

      let endPageNum = 0;
      if (foundMatch.length == 2) {
        endPageNum = parseInt(foundMatch[1]);
      }
      return endPageNum;
    }

    async isLoggedIn() {
      const $profile = $('.profile.initialized');
      return $profile.length > 0;
    }

    async getCurrentUser() {
      let loggedInUser = $('.profile.initialized').attr('href');
      if (loggedInUser !== undefined) {
        if (loggedInUser.length == 1) {
          loggedInUser = loggedInUser[0];
        }
      }

      if (typeof loggedInUser === 'undefined') {
        console.log("Trying again...");

        // [OLD Firefox] workaround (the first returns undefined....?)
        let profile = document.querySelectorAll('.profile');
        if (profile.length == 0) {
          return undefined;
        }
        loggedInUser = profile[0].getAttribute('href');

        if (typeof loggedInUser === 'undefined') {
          console.error(`${SCRIPTNAME}: Can't find logged in username...`);
          throw (`${SCRIPTNAME}: exit`);  // TODO: Popup informing user
        }
      }
      return loggedInUser;
    }

    getStars() {
      if (localStorage[this.storageKey]) {
        let stars = JSON.parse(localStorage[this.storageKey]);
        return stars;
      } else {
        return {};
      }
    }

    async getLocalStorageRatings() {
      if (localStorage[this.storageKey]) {
        let stars = JSON.parse(localStorage[this.storageKey]);
        return stars;
      } else {
        return {};
      }
    }

    async getLocalStorageRatingsCount() {
      const ratings = await this.getLocalStorageRatings();
      return Object.keys(ratings).length;
    }

    /**
     *
     * @returns {str} Current movie: <MovieId>-<MovieUrlTitle>
     *
     * Example:
     * - https://www.csfd.sk/film/739784-star-trek-lower-decks/prehlad/ --> 739784-star-trek-lower-decks
     * - https://www.csfd.cz/film/1032817-naomi/1032819-don-t-believe-everything-you-think/recenze/ --> 1032819-don-t-believe-everything-you-think
     */
    getCurrentFilmUrl() {
      let foundMatch = $('a[href$="/diskuze/"]:first').attr('href');
      foundMatch = foundMatch.match(new RegExp("film/" + "(.*)" + "/diskuze"));
      if (foundMatch == null) {
        console.error("TODO: nenaslo to... vyhledat jinym zpusobem!");
        throw (`${SCRIPTNAME} Exiting...`);
      }

      let filmUrl = `/film/${foundMatch[1]}/`;
      return filmUrl;
    }

    updateInLocalStorage(ratingsObject) {
      // Check if film is in LocalStorage
      let filmUrl = this.getCurrentFilmUrl();
      let myRating = this.stars[filmUrl] || undefined;

      // Item not in LocalStorage, add it then!
      if (myRating === undefined) {
        // Item not in LocalStorage, add
        this.stars[filmUrl] = ratingsObject;
        localStorage.setItem(this.storageKey, JSON.stringify(this.stars));
        return true;
      }

      if (myRating.rating !== ratingsObject.rating) {
        // LocalStorage rating != current rating, update
        this.stars[filmUrl] = ratingsObject;
        localStorage.setItem(this.storageKey, JSON.stringify(this.stars));
        return true;
      }

      return true;
    }

    removeFromLocalStorage() {
      // Check if film is in LocalStorage
      let filmUrl = this.getCurrentFilmUrl();
      let item = this.stars[filmUrl];

      // Item not in LocalStorage, everything is fine
      if (item === undefined) {
        return null;
      }

      // Item in LocalStorage, delete it from local dc
      delete this.stars[filmUrl];

      // And resave it to LocalStorage
      localStorage.setItem(this.storageKey, JSON.stringify(this.stars));

      return true;
    }

    async getCurrentFilmRating() {
      // let $activeStars = this.csfdPage.find(".star.active:not('.computed')");
      let $activeStars = this.csfdPage.find(".star.active");

      // No rating
      if ($activeStars.length === 0) { return null; }

      // Rating "odpad" or "1"
      if ($activeStars.length === 1) {
        if ($activeStars.attr('data-rating') === "0") {
          return 0;
        }
      }

      // Rating "1" to "5"
      return $activeStars.length;
    }

    async getCurrentUserRatingsCount2() {
      return $.get(this.userRatingsUrl)
        .then(function (data) {
          const count = $(data).find('.box-user-rating span.count').text().replace(/[\s()]/g, '');
          if (count) {
            return parseInt(count);
          }
          return 0;
        });
    }

    async fillMissingSettingsKeys() {
      let settings = await getSettings();

      let currentKeys = Object.keys(settings);
      let defaultKeys = Object.keys(defaultSettings);
      for (const defaultKey of defaultKeys) {
        let exists = currentKeys.includes(defaultKey);
        if (!exists) {
          settings[defaultKey] = defaultSettings[defaultKey];
        }
      }
      localStorage.setItem(SETTINGSNAME, JSON.stringify(settings));
    }

    async loadInitialSettings() {

      // GLOBAL
      $('#chkControlPanelOnHover').attr('checked', settings.showControlPanelOnHover);
      $('#chkClickableHeaderBoxes').attr('checked', settings.clickableHeaderBoxes);
      $('#chkClickableMessages').attr('checked', settings.clickableMessages);
      $('#chkAddStars').attr('checked', settings.addStars);

      // USER
      $('#chkDisplayMessageButton').attr('checked', settings.displayMessageButton);
      $('#chkDisplayFavoriteButton').attr('checked', settings.displayFavoriteButton);
      $('#chkHideUserControlPanel').attr('checked', settings.hideUserControlPanel);
      $('#chkCompareUserRatings').attr('checked', settings.compareUserRatings);

      // FILM/SERIES
      $('#chkAddRatingsDate').attr('checked', settings.addRatingsDate);
      $('#chkShowLinkToImage').attr('checked', settings.showLinkToImage);
      $('#chkRatingsEstimate').attr('checked', settings.ratingsEstimate);
      $('#chkRatingsFromFavorites').attr('checked', settings.ratingsFromFavorites);
      $('#chkAddRatingsComputedCount').attr('checked', settings.addRatingsComputedCount);
      $('#chkHideSelectedUserReviews').attr('checked', settings.hideSelectedUserReviews);
      settings.hideSelectedUserReviews || $('#txtHideSelectedUserReviews').parent().hide();
      // if (settings.hideSelectedUserReviews === false) { $('#txtHideSelectedUserReviews').parent().hide(); }
      if (settings.hideSelectedUserReviewsList !== undefined) { $('#txtHideSelectedUserReviews').val(settings.hideSelectedUserReviewsList.join(', ')); }

      // ACTORS
      $('#chkShowOnOneLine').attr('checked', settings.showOnOneLine);
    }

    async addSettingsEvents() {
      // HOME PAGE

      // GLOBAL
      $('#chkControlPanelOnHover').change(function () {
        settings.showControlPanelOnHover = this.checked;
        localStorage.setItem(SETTINGSNAME, JSON.stringify(settings));
        Glob.popup("Nastavení uloženo (obnovte stránku)", 2);
      });

      $('#chkClickableHeaderBoxes').change(function () {
        settings.clickableHeaderBoxes = this.checked;
        localStorage.setItem(SETTINGSNAME, JSON.stringify(settings));
        Glob.popup("Nastavení uloženo (obnovte stránku)", 2);
      });

      $('#chkClickableMessages').change(function () {
        settings.clickableMessages = this.checked;
        localStorage.setItem(SETTINGSNAME, JSON.stringify(settings));
        Glob.popup("Nastavení uloženo (obnovte stránku)", 2);
      });

      $('#chkAddStars').change(function () {
        settings.addStars = this.checked;
        localStorage.setItem(SETTINGSNAME, JSON.stringify(settings));
        Glob.popup("Nastavení uloženo (obnovte stránku)", 2);
      });

      // USER
      $('#chkDisplayMessageButton').change(function () {
        settings.displayMessageButton = this.checked;
        localStorage.setItem(SETTINGSNAME, JSON.stringify(settings));
        Glob.popup("Nastavení uloženo (obnovte stránku)", 2);
      });

      $('#chkDisplayFavoriteButton').change(function () {
        settings.displayFavoriteButton = this.checked;
        localStorage.setItem(SETTINGSNAME, JSON.stringify(settings));
        Glob.popup("Nastavení uloženo (obnovte stránku)", 2);
      });

      $('#chkHideUserControlPanel').change(function () {
        settings.hideUserControlPanel = this.checked;
        localStorage.setItem(SETTINGSNAME, JSON.stringify(settings));
        Glob.popup("Nastavení uloženo (obnovte stránku)", 2);
      });

      $('#chkCompareUserRatings').change(function () {
        settings.compareUserRatings = this.checked;
        localStorage.setItem(SETTINGSNAME, JSON.stringify(settings));
        Glob.popup("Nastavení uloženo (obnovte stránku)", 2);
      });

      // FILM/SERIES
      $('#chkShowLinkToImage').change(function () {
        settings.showLinkToImage = this.checked;
        localStorage.setItem(SETTINGSNAME, JSON.stringify(settings));
        Glob.popup("Nastavení uloženo (obnovte stránku)", 2);
      });

      $('#chkRatingsEstimate').change(function () {
        settings.ratingsEstimate = this.checked;
        localStorage.setItem(SETTINGSNAME, JSON.stringify(settings));
        Glob.popup("Nastavení uloženo (obnovte stránku)", 2);
      });

      $('#chkRatingsFromFavorites').change(function () {
        settings.ratingsFromFavorites = this.checked;
        localStorage.setItem(SETTINGSNAME, JSON.stringify(settings));
        Glob.popup("Nastavení uloženo (obnovte stránku)", 2);
      });

      $('#chkAddRatingsDate').change(function () {
        settings.addRatingsDate = this.checked;
        localStorage.setItem(SETTINGSNAME, JSON.stringify(settings));
        Glob.popup("Nastavení uloženo (obnovte stránku)", 2);
      });

      $('#chkAddRatingsComputedCount').change(function () {
        settings.addRatingsComputedCount = this.checked;
        localStorage.setItem(SETTINGSNAME, JSON.stringify(settings));
        Glob.popup("Nastavení uloženo (obnovte stránku)", 2);
      });

      $('#chkHideSelectedUserReviews').change(function () {
        settings.hideSelectedUserReviews = this.checked;
        localStorage.setItem(SETTINGSNAME, JSON.stringify(settings));
        Glob.popup("Nastavení uloženo (obnovte stránku)", 2);
        $('#txtHideSelectedUserReviews').parent().toggle();
      });

      $('#txtHideSelectedUserReviews').change(function () {
        let ignoredUsers = this.value.replace(/\s/g, '').split(",");
        settings.hideSelectedUserReviewsList = ignoredUsers;
        localStorage.setItem(SETTINGSNAME, JSON.stringify(settings));
        Glob.popup(`Ignorovaní uživatelé:\n${ignoredUsers.join(', ')}`, 4);
      });

      // ACTORS
      $('#chkShowOnOneLine').change(function () {
        settings.showOnOneLine = this.checked;
        localStorage.setItem(SETTINGSNAME, JSON.stringify(settings));
        Glob.popup("Nastavení uloženo (obnovte stránku)", 2);
      });
    }

    async onOtherUserHodnoceniPage() {
      if ((location.href.includes('/hodnoceni') || location.href.includes('/hodnotenia')) && location.href.includes('/uzivatel/')) {
        if (!location.href.includes(this.userUrl)) {
          return true;
        }
      }
      return false;
    }

    async onOtherUserPage() {
      if (location.href.includes('/uzivatel/')) {
        if (!location.href.includes(this.userUrl)) {
          return true;
        }
      }
      return false;
    }

    async onPersonalFavorite() {
      if (location.href.includes('/soukromne/oblubene/') || location.href.includes('/soukrome/oblibene/')) {
        if (!location.href.includes(this.userUrl)) {
          return true;
        }
      }
      return false;
    }

    async notOnUserPage() {
      if (location.href.includes('/uzivatel/') && location.href.includes(this.userUrl)) {
        return false;
      }
      return true;
    }

    exportRatings() {
      localStorage.setItem(this.storageKey, JSON.stringify(this.stars));
    }

    importRatings() {
      if (localStorage[this.storageKey]) {
        this.stars = JSON.parse(localStorage[this.storageKey]);
      }
    }

    async addStars() {
      if (location.href.includes('/zebricky/') || location.href.includes('/rebricky/')) {
        return;
      }
      let starsCss = { marginLeft: "5px" };
      if (await this.onOtherUserPage() || await this.onPersonalFavorite()) {
        starsCss = {
          marginLeft: "5px",
          borderWidth: "1px",
          borderStyle: "solid",
          borderColor: "#c78888",
          borderRadius: "5px",
          padding: "0px 5px",
        };
      }

      let $links = $('a.film-title-name');
      for (const $link of $links) {
        let href = $($link).attr('href');
        let res = this.stars[href];
        if (res === undefined) {
          continue;
        }
        let $sibl = $($link).closest('td').siblings('.rating,.star-rating-only');
        if ($sibl.length !== 0) {
          continue;
        }
        let starClass = res.rating !== 0 ? `stars-${res.rating}` : `trash`;
        let starText = res.rating !== 0 ? "" : "odpad!";
        let $starSpan = $("<span>", {
          'class': `star-rating`,
          html: `<span class="stars ${starClass}" title="${res.date}">${starText}</span>`
        }).css(starsCss);
        $($link).after($starSpan);
      }
    }

    addRatingsColumn() {
      if (this.userRatingsCount === 0) { return; }

      let $page = this.csfdPage;

      let $tbl = $page.find('#snippet--ratings table tbody');
      let starsDict = this.getStars();

      $tbl.find('tr').each(function () {
        let $row = $(this);
        let url = $($row).find('.name').find('a').attr('href');
        const myRating = starsDict[url] || {};

        let $span = "";
        if (myRating.rating === 0) {
          $span = `<span class="stars trash">odpad!</span>`;
        } else {
          $span = `<span class="stars stars-${myRating.rating}" title="${myRating.date}"></span>`;
        }

        $row.find('td:nth-child(2)').after(`
                    <td class="star-rating-only">
                        <span class="star-rating">
                            ${$span}
                        </span>
                    </td>
                `);
      });
    }

    async openControlPanelOnHover() {
      let btn = $('.button-control-panel');
      let panel = $('#dropdown-control-panel');
      $(btn).on('mouseover', () => {
        if (!panel.hasClass('active')) {
          panel.addClass('active');
          let windowWidth = $(window).width();
          if (windowWidth <= 635) {
            panel.appendTo(document.body);
            panel.css("top", "133px");
            panel.css("right", "15px");
          }
        }
      });
      $(btn).on('mouseleave', () => {
        if (panel.hasClass('active')) panel.removeClass('active');
      });
      $(panel).on('mouseover', () => {
        if (!panel.hasClass('active')) panel.addClass('active');
      });
      $(panel).on('mouseleave', () => {
        if (panel.hasClass('active')) panel.removeClass('active');
      });

    }

    addWarningToUserProfile() {
      $(".csfd-compare-menu").append(`
                <div class='counter'>
                    <span><b>!</b></span>
                </div>
            `);
    }

    async showRefreshRatingsButton(ratingsInLS, curUserRatings) {
      let $button = $('<button>', {
        id: 'refr-ratings-button',
        "class": 'csfd-compare-reload',
        html: `
                    <center>
                        <b> >> Načíst hodnocení << </b> <br>
                        Uložené: ${ratingsInLS} / ${curUserRatings}
                    </center>
                `,
      }).css({
        textTransform: "initial",
        fontSize: "0.9em",
        padding: "5px",
        border: "4px solid whitesmoke",
        width: "-moz-available",
        width: "-webkit-fill-available",
        width: "100%",
      });
      let $div = $('<div>', {
        html: $button,
      });
      $('.csfd-compare-settings').after($div);

      let forceUpdate = ratingsInLS > curUserRatings ? true : false;

      $($button).on("click", async function () {
        let csfd = new Csfd($('div.page-content'));
        csfd.refreshAllRatings(csfd, forceUpdate);
      });
    }

    displayMessageButton() {
      let userHref = $('#dropdown-control-panel li a.ajax').attr('href');
      if (userHref === undefined) {
        console.log("fn displayMessageButton(): can't find user href, exiting function...");
        return;
      }

      let button = document.createElement("button");
      button.setAttribute("data-tippy-content", $('#dropdown-control-panel li a.ajax')[0].text);
      button.setAttribute("style", "float: right; border-radius: 5px;");
      button.innerHTML = `
                <a class="ajax"
                    rel="contentModal"
                    data-mfp-src="#panelModal"
                    href="${userHref}"><i class="icon icon-messages"></i></a>
            `;
      $(".user-profile-content > h1").append(button);
    }

    async displayFavoriteButton() {
      let favoriteButton = $('#snippet--menuFavorite > a');
      if (favoriteButton.length !== 1) {
        console.log("fn displayFavoriteButton(): can't find user href, exiting function...");
        return;
      }
      let tooltipText = favoriteButton[0].text;
      let addRemoveIndicator = "+";
      if (tooltipText.includes("Odebrat") || tooltipText.includes("Odobrať")) {
        addRemoveIndicator = "-";
      }

      let button = document.createElement("button");
      button.setAttribute("style", "float: right; border-radius: 5px; margin: 0px 5px;");
      button.setAttribute("data-tippy-content", tooltipText);
      button.innerHTML = `
                <a class="ajax"
                    rel="contentModal"
                    data-mfp-src="#panelModal"
                    href="${favoriteButton.attr('href')}">
                        <span id="add-remove-indicator" style="font-size: 1.5em; color: white;">${addRemoveIndicator}</span>
                        <i class="icon icon-favorites"></i>
                </a>
            `;
      $(".user-profile-content > h1").append(button);

      $(button).on('click', async function () {
        if (addRemoveIndicator == "+") {
          $('#add-remove-indicator')[0].innerText = '-';
          button._tippy.setContent("Odebrat z oblíbených");
        } else {
          $('#add-remove-indicator')[0].innerText = '+';
          button._tippy.setContent("Přidat do oblíbených");
        }
        await refreshTooltips();
      });
    }

    hideUserControlPanel() {
      let panel = $('.button-control-panel:not(.small)');
      if (panel.length !== 1) { return; }
      panel.hide();
    }

    async showLinkToImageOnSmallMoviePoster() {
      let $film = this.csfdPage.find('.film-posters');
      let $img = $film.find('img');
      let src = $img.attr('src');
      let width = $img.attr('width');

      let $div = $(`<div>`, { "class": 'link-to-image' })
        .css({
          position: 'absolute',
          right: '0px',
          bottom: '0px',
          display: 'none',
          'z-index': '999',
          'padding-left': '0.5em',
          'padding-right': '0.5em',
          'margin-bottom': '0.5em',
          'margin-right': '0.5em',
          'background-color': 'rgba(255, 245, 245, 0.85)',
          'border-radius': '5px 0px',
          'font-weight': 'bold'
        })
        .html(`<a href="${src}">w${width}</a>`);

      $film.find('a').after($div);

      $film.on('mouseover', () => {
        $div.show("fast");
      });
      $film.on('mouseleave', () => {
        $div.hide("fast");
      });
    }

    /**
     * Show link for all possible picture sizes
     */
    async showLinkToImageOnOtherGalleryImages() {

      let $pictures = this.csfdPage.find('.gallery-item picture');

      let pictureIdx = 0;
      for (const $picture of $pictures) {
        console.log("$picture", $picture);
        let obj = {};

        let src = $($picture).find('img').attr('src').replace(/cache[/]resized[/]w\d+[/]/g, '');
        console.log("src: ", src);

        obj['100 %'] = src;

        let $sources = $($picture).find('source');
        console.log("$sources: ", $sources);

        for (const $source of $sources) {

          const srcset = $($source).attr('srcset');
          console.log("srcset: ", srcset);

          if (srcset === undefined) { continue; }

          let attributeText = srcset.replace(/\dx/g, '').replace(/\s/g, '');
          let links = attributeText.split(',');

          for (const link of links) {

            const match = link.match(/[/]w(\d+)/);

            if (match !== null) {
              if (match.length === 2) {
                const width = match[1];
                obj[width] = link;
              }
            }
          }
        }

        let idx = 0;
        for (const item in obj) {

          let $div = $(`<div>`, { "class": `link-to-image-gallery picture-idx-${pictureIdx}` })
            .css({
              position: 'absolute',
              right: '0px',
              bottom: '0px',
              display: 'none',
              'z-index': '999',
              'padding-left': '0.5em',
              'padding-right': '0.5em',
              'margin-bottom': `${0.5 + (idx * 2)}em`,
              'margin-right': '0.5em',
              'background-color': 'rgba(255, 245, 245, 0.75)',
              'border-radius': '5px 0px',
              'font-weight': 'bold'
            })
            .html(`<a href="${obj[item]}">${item}</a>`);

          $($picture).find('img').after($div);
          $($picture).attr('data-idx', pictureIdx);
          $($picture).parent().css({ position: 'relative' });  // need to have this for absolute position to work

          idx += 1;
        }

        pictureIdx += 1;

        $($picture).on('mouseover', () => {
          const pictureIdx = $($picture).attr('data-idx');
          $(`.link-to-image-gallery.picture-idx-${pictureIdx}`).show("fast");
        });
        $($picture).on('mouseleave', () => {
          const pictureIdx = $($picture).attr('data-idx');
          $(`.link-to-image-gallery.picture-idx-${pictureIdx}`).hide("fast");
        });
      }
    }

    /**
     * If film has been rated by user favorite people, make an averate and display it
     * under the normal rating as: oblíbení: X %
     *
     * @returns null
     */
    async ratingsFromFavorites() {
      let $ratingSpans = this.csfdPage.find('li.favored:not(.current-user-rating) .star-rating .stars');

      // No favorite people ratings found
      if ($ratingSpans.length === 0) { return; }

      let ratingNumbers = [];
      for (let $span of $ratingSpans) {
        let num = this.getNumberFromRatingSpan($($span));
        num = num * 20;
        ratingNumbers.push(num);
      }
      let average = (array) => array.reduce((a, b) => a + b) / array.length;
      const ratingAverage = Math.round(average(ratingNumbers));

      let $ratingAverage = this.csfdPage.find('.box-rating-container div.film-rating-average');
      $ratingAverage.html(`
                <span style="position: absolute;">${$ratingAverage.text()}</span>
                <span style="position: relative; top: 25px; font-size: 0.3em; font-weight: 600;">oblíbení: ${ratingAverage} %</span>
            `);

    }
    /**
     * When there is less than 10 ratings on a movie, csfd waits with the rating.
     * This computes the rating from those less than 10 and shows it.
     *
     * @returns null
     */
    async ratingsEstimate() {

      // Find rating-average element
      let $ratingAverage = this.csfdPage.find('.box-rating-container .film-rating-average');

      // Not found, exit fn()
      if ($ratingAverage.length !== 1) { return; }

      // Get the text
      let curRating = $ratingAverage.text().replace(/\s/g, '');

      // If the text if anything than '?%', exit fn()
      if (!curRating.includes('?%')) { return; }

      // Get all other users ratings
      let $userRatings = this.csfdPage.find('section.others-rating .star-rating');

      // If no ratings in other ratings, exit fn()
      if ($userRatings.length === 0) { return; }

      // Fill the list with ratings as numbers
      let ratingNumbers = [];
      for (const $userRating of $userRatings) {
        let $ratingSpan = $($userRating).find('.stars');
        let num = this.getNumberFromRatingSpan($ratingSpan);
        // Transform number to percentage (0 -> 0 %, 1 -> 20 %, 2 -> 40 %...)
        num = num * 20;
        ratingNumbers.push(num);
      }

      // Compute the average
      let average = (array) => array.reduce((a, b) => a + b) / array.length;
      const ratingAverage = Math.round(average(ratingNumbers));

      // Rewrite the displayed rating
      const bgcolor = this.getRatingColor(ratingAverage);
      $ratingAverage
        .text(`${ratingAverage} %`)
        .css({ color: '#fff', backgroundColor: bgcolor })
        .attr('title', `spočteno z hodnocení: ${$userRatings.length}`);
    }
    /**
     * Depending on the percent number, return a color as a string representation
     * 0-29 black; 30-69 blue; 70-100 red
     *
     * @param {int} ratingPercent
     * @returns {string} representation of colour
     */
    getRatingColor(ratingPercent) {
      switch (true) {
        case (ratingPercent < 29):
          return "#535353";
        case (ratingPercent >= 30 && ratingPercent < 69):
          return "#658db4";
        case (ratingPercent >= 70):
          return "#ba0305";
        default:
          return "#d2d2d2";
      }
    }
    /**
     * From jquery! $span csfd element class (.stars stars-4) return the ratings number (4)
     *
     * @param {jquery} $span
     * @returns int in range 0-5
     */
    getNumberFromRatingSpan($span) {
      // if ($span instanceof jQuery === false) {
      //     $span = $($span)
      // }

      // TODO: využít tuto funkci i při načítání hodnocení do LS
      let rating = 0;
      for (let stars = 0; stars <= 5; stars++) {
        if ($span.hasClass('stars-' + stars)) {
          rating = stars;
        }
      }
      return rating;
    }
    /**
     * Show clickable link to the absolute url of the image mouse is hovering above.
     *
     * Works with:
     * - Small Movie Poster
     * - Movie Gallery Images
     */
    async showLinkToImage() {
      this.showLinkToImageOnSmallMoviePoster();
      this.showLinkToImageOnOtherGalleryImages();
    }

    async doSomething(idx, url) {
      let data = await $.get(url);
      let $rows = $(data).find('#snippet--ratings tr');
      let dc = {};
      for (const $row of $rows) {
        let name = $($row).find('td.name a').attr('href');
        let $ratings = $($row).find('span.stars');
        let rating = 0;
        for (let stars = 0; stars <= 5; stars++) {
          if ($ratings.hasClass('stars-' + stars)) {
            rating = stars;
          }
        }
        let date = $($row).find('td.date-only').text().replace(/[\s]/g, '');
        dc[name] = { 'rating': rating, 'date': date };
      }
      return dc;
      // web workers - vyšší dívčí - více vláken z browseru
    }

    async getAllPages(force = false) {
      const url = location.origin.endsWith('sk') ? `${this.userUrl}hodnotenia` : `${this.userUrl}hodnoceni`;
      const $content = await $.get(url);
      const $href = $($content).find(`.pagination a:not(.page-next):not(.page-prev):last`);
      const maxPageNum = $href.text();
      this.userRatingsCount = await this.getCurrentUserRatingsCount2();
      let dict = this.stars;
      let ls = force ? [] : [dict];
      for (let idx = 1; idx <= maxPageNum; idx++) {
        if (!force) if (Object.keys(dict).length === this.userRatingsCount) break;
        console.log(`Načítám hodnocení ${idx}/${maxPageNum}`);
        Glob.popup(`Načítám hodnocení ${idx}/${maxPageNum}`, 1, 200, 0);
        const url = location.origin.endsWith('sk') ? `${this.userUrl}hodnotenia/?page=${idx}` : `${this.userUrl}hodnoceni/?page=${idx}`;
        const res = await this.doSomething(idx, url);
        ls.push(res);
        if (!force) dict = await mergeDict(ls);
      }
      if (force) dict = await mergeDict(ls);
      return dict;
    }

    async refreshAllRatings(csfd, force = false) {
      await csfd.initializeClassVariables();
      csfd.stars = await csfd.getAllPages(force);
      csfd.exportRatings();
      Glob.popup(`Vaše hodnocení byla načtena.<br>Obnovte stránku.`, 4, 200);
    }

    async removableHomeBoxes() {
      const boxSettingsName = 'CSFD-Compare-hiddenBoxes';
      let settings = await getSettings(boxSettingsName);

      $('.box-header').each(async function (index, value) {
        let $section = $(this).closest('section');
        $section.attr('data-box-id', index);

        if (settings.some(x => x.boxId == index)) {
          $section.hide();
        }

        let $btnHideBox = $('<a>', {
          'class': 'hide-me button',
          href: 'javascript:void(0)',
          html: `Skrýt`
        }).css({
          margin: 'auto',
          marginLeft: '10px',
          backgroundColor: '#7b0203',
          display: 'none',
        });

        // $btnHideBox.wrap(`<div class="box-header-action"></div>`);  // TODO: important?

        let $h2 = $(this).find('h2');
        if ($h2.length === 0) {
          $h2 = $(this).find('p');
          // if ($h2.text().includes('Partnerem')) {
          $(this).css({ 'padding-right': '0px' });
          $h2.after($btnHideBox[0]);
          return;
          // }
        }

        $h2.append($btnHideBox[0]);
      });

      $('.box-header').on('mouseover', async function () {
        $(this).find('.hide-me').show();
      }).on('mouseout', async function () {
        $(this).find('.hide-me').hide();
      });

      $('.hide-me').on('click', async function (event) {
        let $section = $(event.target).closest('section');
        let boxId = $section.data('box-id');
        let boxName = $section.find('h2').first().text().replace(/\n|\t|Skrýt/g, "");  // clean from '\t', '\n'
        if (boxName === '') {
          boxName = $section.find('p').first().text().replace(/\n|\t|Skrýt/g, "");
        }
        let dict = { boxId: boxId, boxName: boxName };
        let settings = await getSettings(SETTINGSNAME_HIDDEN_BOXES);
        if (!settings.includes(dict)) {
          settings.push(dict);
          localStorage.setItem(boxSettingsName, JSON.stringify(settings));
          csfd.addHideSectionButton(boxId, boxName);
        }
        $section.hide();
      });
    }

    showOnOneLine() {
      const $sections = $(`div.creator-filmography`).find(`section`);
      let $nooverflowH3 = $sections.find(`h3.film-title-nooverflow`);
      $nooverflowH3.css({
        "display": "inline-block",
        "white-space": "nowrap",
        "text-overflow": "ellipsis",
        "overflow": "hidden",
        "max-width": "230px",
      });
      const $filmTitleNameA = $nooverflowH3.find(`a.film-title-name`);
      $filmTitleNameA.css({
        "white-space": "nowrap",
      });
      $filmTitleNameA.each(function () {
        const $this = $(this);
        $this.attr("title", $this.text());
      });
    }

    addHideSectionButton(boxId, boxName) {
      let $button = `
                <button class="restore-hidden-section" data-box-id="${boxId}" title="${boxName}"
                    style="border-radius: 4px;
                           margin: 1px;
                           max-width: 60px;
                           text-transform: capitalize;
                           overflow: hidden;
                           text-overflow: ellipsis;"
                >${boxName}</button>
            `;
      let $div = $(`div.hidden-sections`);
      $div.append($button);
    }


    async addSettingsPanel() {
      let dropdownStyle = 'right: 150px; width: max-content;';
      let disabled = '';
      let needToLoginTooltip = '';
      let needToLoginStyle = '';

      if (!await this.isLoggedIn()) {
        dropdownStyle = 'right: 50px; width: max-content;';
        disabled = 'disabled';
        needToLoginTooltip = `data-tippy-content="Funguje jen po přihlášení"`;
        needToLoginStyle = 'color: grey;';
      }

      let button = document.createElement('li');
      let resetLabelStyle = "-webkit-transition: initial; transition: initial; font-weight: initial; display: initial !important;";

      // Add box-id attribute to .box-header(s)
      $('.box-header').each(async function (index, value) {
        let $section = $(this).closest('section');
        $section.attr('data-box-id', index);
      });

      // Build array of buttons for un-hiding sections
      let resultDisplayArray = [];
      let hiddenBoxesArray = await getSettings(SETTINGSNAME_HIDDEN_BOXES);
      hiddenBoxesArray = await checkSettingsValidity(hiddenBoxesArray, SETTINGSNAME_HIDDEN_BOXES);
      hiddenBoxesArray.sort((a, b) => a - b);  // sort by numbers
      hiddenBoxesArray.forEach(element => {
        let boxId = element.boxId;
        let boxName = element.boxName.replace(/\n|\t/g, "");  // clean text of '\n' and '\t';
        resultDisplayArray.push(`
                    <button class="restore-hidden-section" data-box-id="${boxId}" title="${boxName}"
                            style="border-radius: 4px;
                                   margin: 1px;
                                   max-width: 60px;
                                   text-transform: capitalize;
                                   overflow: hidden;
                                   text-overflow: ellipsis;"
                    >
                        ${boxName}
                    </button>
                `);
      });

      button.innerHTML = `
                <a href="javascript:void()" class="user-link initialized csfd-compare-menu">CC</a>
                <div class="dropdown-content notifications" style="${dropdownStyle}">

                    <div class="dropdown-content-head csfd-compare-settings">
                        <h2>CSFD-Compare nastavení</h2>
                        <span style="float: right; font-size: 0.7rem; margin-top: 0.2rem;">
                            <a id="script-version" href="${GREASYFORK_URL}">${VERSION}</a>
                        </span>
                    </div>

                    <article class="article">
                        <h2 class="article-header">Domácí stránka - skryté panely</h2>
                        <section>
                            <div class="article-content">
                                <div class="hidden-sections" style="max-width: fit-content;">${resultDisplayArray.join("")}</div>
                            </div>
                        </section>
                    </article>
                    <article class="article">
                        <h2 class="article-header">Globální</h2>
                        <section>
                            <div class="article-content">
                                <input type="checkbox" id="chkControlPanelOnHover" name="control-panel-on-hover">
                                <label for="chkControlPanelOnHover" style="${resetLabelStyle}">Otevřít ovládací panel přejetím myší</label>
                            </div>
                            <div class="article-content">
                                <input type="checkbox" id="chkClickableHeaderBoxes" name="clickable-header-boxes">
                                <label for="chkClickableHeaderBoxes" style="${resetLabelStyle}">Boxy s tlačítkem "VÍCE" jsou klikatelné celé</label>
                            </div>
                            <div class="article-content">
                                <input type="checkbox" id="chkClickableMessages" name="clickable-messages" ${disabled}>
                                <label for="chkClickableMessages" style="${resetLabelStyle} ${needToLoginStyle}" ${needToLoginTooltip}>Klikatelné zprávy (bez tlačítka "více...")</label>
                            </div>
                            <div class="article-content">
                                <input type="checkbox" id="chkAddStars" name="add-stars" ${disabled}>
                                <label for="chkAddStars" style="${resetLabelStyle} ${needToLoginStyle}" ${needToLoginTooltip}>Přidat hvězdičky hodnocení u viděných filmů/seriálů</label>
                            </div>
                        </section>
                    </article>

                    <article class="article">
                        <h2 class="article-header">Uživatelé</h2>
                        <section>
                            <div class="article-content">
                                <input type="checkbox" id="chkHideUserControlPanel" name="chide-user-control-panel">
                                <label for="chkHideUserControlPanel" style="${resetLabelStyle}">Skrýt ovládací panel</label>
                            </div>
                            <div class="article-content">
                                <input type="checkbox" id="chkDisplayMessageButton" name="display-message-button" ${disabled}>
                                <label for="chkDisplayMessageButton" style="${resetLabelStyle} ${needToLoginStyle}" ${needToLoginTooltip}> ↳ Přidat tlačítko odeslání zprávy</label>
                            </div>
                            <div class="article-content">
                                <input type="checkbox" id="chkDisplayFavoriteButton" name="display-favorite-button" ${disabled}>
                                <label for="chkDisplayFavoriteButton" style="${resetLabelStyle} ${needToLoginStyle}" ${needToLoginTooltip}> ↳ Přidat tlačítko přidat/odebrat z oblíbených</label>
                            </div>
                            <div class="article-content">
                                <input type="checkbox" id="chkCompareUserRatings" name="compare-user-ratings" ${disabled}>
                                <label for="chkCompareUserRatings" style="${resetLabelStyle} ${needToLoginStyle}" ${needToLoginTooltip}>Porovnat uživatelská hodnocení s mými</label>
                            </div>
                        </section>
                    </article>

                    <article class="article">
                        <h2 class="article-header">Film/Seriál</h2>
                        <section>
                            <div class="article-content">
                                <input type="checkbox" id="chkShowLinkToImage" name="show-link-to-image" ${disabled}>
                                <label for="chkShowLinkToImage" style="${resetLabelStyle}"}>Zobrazit odkazy na obrázcích</label>
                            </div>
                            <div class="article-content">
                                <input type="checkbox" id="chkRatingsEstimate" name="ratings-estimate" ${disabled}>
                                <label for="chkRatingsEstimate" style="${resetLabelStyle}">Vypočtení % při počtu hodnocení pod 10</label>
                            </div>
                            <div class="article-content">
                                <input type="checkbox" id="chkRatingsFromFavorites" name="ratings-from-favorites" ${disabled}>
                                <label for="chkRatingsFromFavorites" style="${resetLabelStyle}">Zobrazit hodnocení z průměru oblíbených</label>
                            </div>
                            <div class="article-content">
                                <input type="checkbox" id="chkAddRatingsComputedCount" name="compare-user-ratings" ${disabled}>
                                <label for="chkAddRatingsComputedCount" style="${resetLabelStyle} ${needToLoginStyle}" ${needToLoginTooltip}>Zobrazit spočteno ze sérií</label>
                            </div>
                            <div class="article-content">
                                <input type="checkbox" id="chkAddRatingsDate" name="add-ratings" ${disabled}>
                                <label for="chkAddRatingsDate" style="${resetLabelStyle} ${needToLoginStyle}" ${needToLoginTooltip}>Zobrazit datum hodnocení</label>
                            </div>
                            <div class="article-content">
                                <input type="checkbox" id="chkHideSelectedUserReviews" name="hide-selected-user-reviews">
                                <label for="chkHideSelectedUserReviews" style="${resetLabelStyle}">Skrýt recenze lidí</label>
                                <div>
                                    <input type="textbox" id="txtHideSelectedUserReviews" name="hide-selected-user-reviews-list">
                                    <label style="${resetLabelStyle}">(např: POMO, golfista)</label>
                                </div>
                            </div>
                        </section>
                    </article>

                    <article class="article">
                        <h2 class="article-header">Herci</h2>
                        <section>
                            <div class="article-content">
                                <input type="checkbox" id="chkShowOnOneLine" name="show-on-one-line" ${disabled}>
                                <label for="chkShowOnOneLine" style="${resetLabelStyle}"}>Filmy na jednom řádku (experimental)</label>
                            </div>
                        </section>
                    </article>

                </div>
            `;
      $('.header-bar').prepend(button);

      await refreshTooltips();

      // Show() the section and remove the number from localStorage
      $(".hidden-sections").on("click", ".restore-hidden-section", async function () {
        let $element = $(this);
        let sectionId = $element.attr("data-box-id");

        // Remove from localStorage
        let hiddenBoxesArray = await getSettings(SETTINGSNAME_HIDDEN_BOXES);
        hiddenBoxesArray = hiddenBoxesArray.filter(item => item.boxId !== parseInt(sectionId));
        let settingsName = "CSFD-Compare-hiddenBoxes";
        localStorage.setItem(settingsName, JSON.stringify(hiddenBoxesArray));

        // Show section
        let $section = $(`section[data-box-id="${sectionId}"`);
        $section.show();

        // Remove button
        $element.remove();
      });

      // Don't hide settings popup when mouse leaves within interval of 0.2s
      let timer;
      $(button).on("mouseover", function () {
        if (timer) {
          clearTimeout(timer);
          timer = null;
        }
        if (!$(button).hasClass("active")) {
          $(button).addClass("active");
        }
      });

      $(button).on("mouseleave", function () {
        if ($(button).hasClass("active")) {
          timer = setTimeout(() => {
            $(button).removeClass("active");
          }, 200);
        }
      });
    }

    async checkAndUpdateRatings() {
      let currentFilmRating = await this.getCurrentFilmRating();
      let currentFilmDateAdded = await this.getCurrentFilmDateAdded();

      if (currentFilmRating === null) {
        // Check if record exists, if yes, remove it
        this.removeFromLocalStorage();
      } else {
        // Check if current page rating corresponds with that in LocalStorage, if not, update it
        const ratingsObject = {
          rating: currentFilmRating,
          date: currentFilmDateAdded
        };
        this.updateInLocalStorage(ratingsObject);
      }
    }

    clickableMessages() {
      let $messagesBox = $('.dropdown-content.messages');
      let $more = $messagesBox.find('.span-more-small');
      if ($more.length < 1) { return; }

      for (const $span of $more) {

        // Hide "... více" button
        $($span).hide();

        let $content = $($span).closest('.article-content');
        let $article = $content.closest('article');
        $content.hover(function () {
          // $(this).css('background-color', '#e1e0e0');
          $article.css('background-color', '#e1e0e0');
        }, function () {
          // $(this).css('background-color', 'initial');
          $article.css('background-color', 'initial');
        });

        let href = $($span).find('a').attr('href');
        $content.wrap(`<a href="${href}"></a>`);
      }
    }
    async clickableHeaderBoxes() {
      // CLICKABLE HEADER BUTTONS
      $(".user-link.wantsee").on("click", function () {
        location.href = "/chci-videt/";
      });
      $(".user-link.favorites").on("click", function () {
        location.href = "/soukrome/oblibene/";  // TODO: Toto pry nefunguje
      });
      $(".user-link.messages").on("click", function () {
        location.href = "/posta/";
      });

      // CLICKABLE HEADER DIVS
      let headers = $('.dropdown-content-head,.box-header');
      for (const div of headers) {
        let btn = $(div).find('a.button');

        if (btn.length === 0) { continue; }
        if (!["více", "viac"].includes(btn[0].text.toLowerCase())) { continue; }

        $(div).wrap(`<a href="${btn.attr('href')}"></a>`);

        let h2 = $(div).find('h2');
        let spanCount = h2.find('span.count');
        $(div)
          .mouseover(() => {
            $(div).css({ backgroundColor: '#ba0305' });
            $(h2[0]).css({ backgroundColor: '#ba0305', color: '#fff' });
            if (spanCount.length == 1) { spanCount[0].style.color = '#fff'; }
          })
          .mouseout(() => {
            if ($(div).hasClass('dropdown-content-head')) {
              $(div).css({ backgroundColor: '#ececec' });
            } else {
              $(div).css({ backgroundColor: '#e3e3e3' });
            }
            $(h2[0]).css({ backgroundColor: 'initial', color: 'initial' });
            if (spanCount.length == 1) { spanCount[0].style.color = 'initial'; }
          });
      }
    }

    hideSelectedUserReviews() {
      let articleHeaders = $('.article-header-review-name');
      for (const element of articleHeaders) {
        let userTitle = $(element).find('.user-title-name');
        if (userTitle.length != 1) { continue; }
        let ignoredUser = settings.hideSelectedUserReviewsList.includes(userTitle[0].text);
        if (!ignoredUser) { continue; }
        $(element).closest('article').hide();
      }
    }
    async getCurrentFilmDateAdded() {
      let ratingText = this.csfdPage.find('span.stars-rating.initialized').attr('title');
      if (ratingText === undefined) {
        // Grab the rating date from mobile-rating
        ratingText = this.csfdPage.find('.mobile-film-rating-detail a span').attr('title');
        if (ratingText === undefined) {
          return;
        }
      }
      let match = ratingText.match("[0-9]{2}[.][0-9]{2}[.][0-9]{4}");
      if (match !== null) {
        let ratingDate = match[0];
        return ratingDate;
        // let $myRatingCaption = $('.my-rating h3');
        // $myRatingCaption.html(`${$myRatingCaption.text()}<br>${ratingDate}`);
      }
      return undefined;
    }

    async addRatingsDate() {
      // Grab the rating date from stars-rating
      let ratingText = $('span.stars-rating.initialized').attr('title');
      if (ratingText === undefined) {
        // Grab the rating date from mobile-rating
        ratingText = $('.mobile-film-rating-detail a span').attr('title');
        if (ratingText === undefined) {
          return;
        }
      }
      let match = ratingText.match("[0-9]{2}[.][0-9]{2}[.][0-9]{4}");
      if (match !== null) {
        let ratingDate = match[0];
        let $myRatingCaption = $('.my-rating h3');
        $myRatingCaption.html(`${$myRatingCaption.text()}<br>${ratingDate}`);
      }
    }

    /**
     * From the title of .current-user-rating span get 'spocteno ze serii: x'
     * and add it bellow the 'Moje hodnoceni' text
     */
    async addRatingsComputedCount() {
      let $computedStars = $('.star.active.computed');
      let isComputed = $computedStars.length != 0;
      if (!isComputed) { return; }
      let fromRatingsText = this.csfdPage.find('.current-user-rating > span').attr('title');
      if (fromRatingsText === undefined) {
        return;
      }
      let $myRatingCaption = $('.my-rating h3');
      $myRatingCaption.html(`${$myRatingCaption.text()}<br>${fromRatingsText}`);
    }

    async checkForUpdate() {
      let pageHtml = await $.get(GREASYFORK_URL);
      let version = $(pageHtml).find('dd.script-show-version > span').text();
      return version;
    }

    async getChangelog() {
      let pageHtml = await $.get(`${GREASYFORK_URL}/versions`);
      let versionDateTime = $(pageHtml).find('.version-date').first().attr('datetime');
      let versionNumber = $(pageHtml).find('.version-number a').first().text();
      let versionDate = versionDateTime.substring(0, 10);
      let versionTime = versionDateTime.substring(11, 16);
      let changelogText = `
                <div style="font-size: 0.8rem; line-height: 1.5;">${versionDate} ${versionTime} (${versionNumber})<br>
                    <hr>
                    ${$(pageHtml).find('.version-changelog').html()}
                </div>
            `;
      return changelogText;
    }

    async initializeClassVariables() {
      this.userUrl = await this.getCurrentUser();
      this.storageKey = `${SCRIPTNAME}_${this.userUrl.split("/")[2].split("-")[1]}`;
      this.userRatingsUrl = location.origin.endsWith('sk') ? `${this.userUrl}/hodnotenia` : `${this.userUrl}/hodnoceni`;
      this.stars = this.getStars();
    }
  }

  // $(document).on('click', '#refr-ratings-button', function () {
  //     alert("hihi");
  // });

  // ============================================================================================
  // SCRIPT START
  // ============================================================================================
  await delay(20);  // Greasemonkey workaround, wait a little bit for page to somehow load
  let csfd = new Csfd($('div.page-content'));


  // =================================
  // LOAD SETTINGS
  // =================================
  await csfd.fillMissingSettingsKeys();

  const settings = await getSettings();
  await csfd.addSettingsPanel();
  await csfd.loadInitialSettings();
  await csfd.addSettingsEvents();


  // =================================
  // GLOBAL
  // =================================
  if (settings.clickableHeaderBoxes) { csfd.clickableHeaderBoxes(); }
  if (settings.showControlPanelOnHover) { csfd.openControlPanelOnHover(); }

  // Film/Series page
  if (location.href.includes('/film/') || location.href.includes('/tvurce/') || location.href.includes('/tvorca/')) {
    if (settings.hideSelectedUserReviews) { csfd.hideSelectedUserReviews(); }
    // csfd.showLinkToImageOnSmallMoviePoster();
    if (settings.showLinkToImage) { csfd.showLinkToImage(); }
    if (settings.ratingsEstimate) { csfd.ratingsEstimate(); }
    if (settings.ratingsFromFavorites) { csfd.ratingsFromFavorites(); }
  }

  // Actor page
  if (location.href.includes('/tvurce/') || location.href.includes('/tvorca/')) {
    if (settings.showOnOneLine) { csfd.showOnOneLine(); }
  }
  // // Any Gallery page
  // if (location.href.includes('/galerie/') || location.href.includes('/galeria/')) {
  //     csfd.showLinkToImageOnOtherGalleryImages();
  // }

  if (await onHomepage()) { csfd.removableHomeBoxes(); }

  // if (settings.removeVideoPanel) { csfd.removeBox_VideoPanel(); }
  // if (settings.removeMotivationPanel) { csfd.removeBox_MotivationPanel(); }
  // if (settings.removeContestPanel) { csfd.removeBox_ContestPanel(); }
  // if (settings.removeCsfdCinemaPanel) { csfd.removeBox_CsfdCinemaPanel(); }
  // if (settings.removeMoviesOfferPanel) { csfd.removeBox_MoviesOfferPanel(); }


  // =================================
  // NOT LOGGED IN
  // =================================
  if (!await csfd.isLoggedIn()) {
    // User page
    if (location.href.includes('/uzivatel/')) {
      if (settings.hideUserControlPanel) { csfd.hideUserControlPanel(); }
    }
  }


  // =================================
  // LOGGED IN
  // =================================
  if (await csfd.isLoggedIn()) {

    // Global settings without category
    await csfd.initializeClassVariables();

    if (settings.addStars && await csfd.notOnUserPage()) { csfd.addStars(); }

    let ratingsInLocalStorage = 0;
    let currentUserRatingsCount = 0;
    if (settings.addStars || settings.compareUserRatings) {
      ratingsInLocalStorage = await csfd.getLocalStorageRatingsCount();
      currentUserRatingsCount = await csfd.getCurrentUserRatingsCount2();
      if (ratingsInLocalStorage !== currentUserRatingsCount) {
        csfd.showRefreshRatingsButton(ratingsInLocalStorage, currentUserRatingsCount);
        csfd.addWarningToUserProfile();
      } else {
        csfd.userRatingsCount = currentUserRatingsCount;
      }
    }

    // Header modifications
    if (settings.clickableMessages) { csfd.clickableMessages(); }

    // Film page
    if (location.href.includes('/film/')) {
      if (settings.addRatingsDate) { csfd.addRatingsDate(); }
      if (settings.addRatingsComputedCount) { csfd.addRatingsComputedCount(); }

      // Dynamic LocalStorage update on Film/Series in case user changes ratings
      await csfd.checkAndUpdateRatings();
    }

    // Ratings DB - check if number of ratings saved and current are the same
    if (settings.compareUserRatings || settings.addStars) {

      let spanContent = { html: "✔️", title: "Přenačíst všechna hodnocení" };
      if (ratingsInLocalStorage !== currentUserRatingsCount) {
        spanContent = { html: "⚠️", title: "Nejsou načtena všechna hodnocení! \nPřenačíst VŠECHNA hodnocení" };
      }

      const $span = $("<span>", spanContent).css({ cursor: "pointer" });
      $span.on("click", async function () {
        let csfd = new Csfd($('div.page-content'));
        csfd.refreshAllRatings(csfd, true);
      });
      // OK or WARN icon for compareUserRatings
      if (settings.compareUserRatings) {
        $('#chkCompareUserRatings').parent().append($span.clone(true));
      }
      // OK or WARN icon for addStars
      if (settings.addStars) {
        $('#chkAddStars').parent().append($span.clone(true));
      }
    }

    // User page
    if (await csfd.onOtherUserPage()) {
      if (settings.displayMessageButton) { csfd.displayMessageButton(); }
      if (settings.displayFavoriteButton) { csfd.displayFavoriteButton(); }
      if (settings.hideUserControlPanel) { csfd.hideUserControlPanel(); }
      if (await csfd.onOtherUserHodnoceniPage()) {
        if (settings.compareUserRatings) { csfd.addRatingsColumn(); }
      }
    }
  }

  // let t0 = performance.now();
  // const $siteHtml = await $.get(GREASYFORK_URL);
  // let t1 = performance.now();
  // console.log("Call to 'await $.get(GREASYFORK_URL)' took " + (t1 - t0) + " ms.");

  // If not already in session storage, get new version from greasyfork and display changelog over version link
  let updateCheckJson = sessionStorage.updateChecked !== undefined ? JSON.parse(sessionStorage.updateChecked) : {};
  let $verLink = $('#script-version');
  if (Object.keys(updateCheckJson).length !== 0) {
    const difference = (Date.now() - updateCheckJson.lastCheck) / 60 / 60 / 60;
    const curVersion = VERSION.replace('v', '');
    // If more than 5 minutes, check for update
    if (difference >= 5) {
      let version = await csfd.checkForUpdate();
      let changelogText = await csfd.getChangelog();
      updateCheckJson.changelogText = changelogText;
      $verLink.attr("data-tippy-content", changelogText);
      if (version !== curVersion) {
        updateCheckJson.newVersion = true;
        updateCheckJson.newVersionNumber = version;

        let versionText = `${$verLink.text()} (Update v${version})`;
        $verLink.text(versionText);
        updateCheckJson.versionText = versionText;
      } else {
        updateCheckJson.newVersion = false;
        updateCheckJson.versionText = VERSION;
      }
      updateCheckJson.lastCheck = Date.now();
      sessionStorage.updateChecked = JSON.stringify(updateCheckJson);
    } else {
      if (updateCheckJson.newVersion === true) {
        if (updateCheckJson.newVersionNumber === curVersion) {
          $verLink.text(`v${curVersion}`);
        } else {
          const versionText = `${$verLink.text()} (Update v${updateCheckJson.newVersionNumber})`;
          $verLink.text(versionText);
        }
        $verLink.attr("data-tippy-content", updateCheckJson.changelogText);
      } else {
        $verLink.attr("data-tippy-content", updateCheckJson.changelogText);
      }
      // $('#script-version')
      //     .text(updateCheckJson.versionText)
      //     .attr("data-tippy-content", updateCheckJson.changelogText);
    }

  } else {
    let version = await csfd.checkForUpdate();
    let curVersion = VERSION.replace('v', '');
    if (version !== curVersion) {
      updateCheckJson.newVersion = true;
      let $verLink = $('#script-version');
      let versionText = `${$verLink.text()} (Update v${version})`;
      updateCheckJson.versionText = versionText;
      updateCheckJson.newVersionNumber = version;
      let changelogText = await csfd.getChangelog();
      $verLink.text(versionText);
      updateCheckJson.changelogText = changelogText;
      $verLink.attr("data-tippy-content", changelogText);
    } else {
      updateCheckJson.changelogText = await csfd.getChangelog();
      updateCheckJson.newVersion = false;
      updateCheckJson.versionText = VERSION;
      $('#script-version').attr("data-tippy-content", updateCheckJson.changelogText);
    }
    updateCheckJson.lastCheck = Date.now();
    sessionStorage.updateChecked = JSON.stringify(updateCheckJson);
  }

  // Call TippyJs constructor
  await refreshTooltips();

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址