Greasy Fork

LetterBoxd Roulette

Pick a random movie on any LetterBoxd page. Press R to roll a movie, and Shift+R to exit the roulette. Works for watchlist, any user list, and anywhere you can find a gallery of film posters on the site. Turn on the "Fade watched films" switch (the one already on the vanilla LetterBoxd site) to make the roulette skip watched films.

当前为 2023-09-19 提交的版本,查看 最新版本

// ==UserScript==
// @name         LetterBoxd Roulette
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  Pick a random movie on any LetterBoxd page. Press R to roll a movie, and Shift+R to exit the roulette. Works for watchlist, any user list, and anywhere you can find a gallery of film posters on the site. Turn on the "Fade watched films" switch (the one already on the vanilla LetterBoxd site) to make the roulette skip watched films.
// @author       Gatleos
// @match        https://letterboxd.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=letterboxd.com
// @grant        none
// ==/UserScript==

(function () {
  "use strict";
  window.LETTERBOXD_ROULETTE = {
    filmIndex: -1,
    active: false,
    shuffledIndexList: [],
    shuffledIndexListCounter: 0,
  };
  const ROULETTE_SELECTED_CLASS = "letterboxd-roulette-chosen";

  function shuffle(array) {
    for (let i = array.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [array[i], array[j]] = [array[j], array[i]];
    }
    return array;
  }

  function isTextInput(el) {
    if (el.tagName != "INPUT") {
      return false;
    }
    const typeAttr = el.attributes.getNamedItem("type");
    return typeAttr && typeAttr.textContent == "text";
  }

  function select(el) {
    el.classList.add(ROULETTE_SELECTED_CLASS);
  }

  function deselect(el) {
    el.classList.remove(ROULETTE_SELECTED_CLASS);
  }

  function injectStylesheet() {
    const css = `
li.poster-container.${ROULETTE_SELECTED_CLASS}>* {
  opacity: 1 !important;
  transition: all .1s linear;
}

li.poster-container.${ROULETTE_SELECTED_CLASS}>.poster .frame .overlay {
  border-width: 3px !important;
  bottom: 0 !important;
  left: 0 !important;
  right: 0 !important;
  top: 0 !important;
  border-color: rgb(0, 56, 112) !important;
  box-shadow: rgba(16, 19, 22, 0.25) 0px 0px 1px 1px inset !important;
}

li.poster-container:not(.${ROULETTE_SELECTED_CLASS})>* {
  opacity: .2 !important;
  transition: all .1s linear;
}

body.hide-films-seen li.poster-container.film-watched:not(.${ROULETTE_SELECTED_CLASS})>* {
  opacity: 0 !important;
  transition: all .1s linear;
}
`;
    const head = document.head || document.getElementsByTagName("head")[0],
      style = document.createElement("style");
    head.appendChild(style);
    style.type = "text/css";
    style.id = "letterboxd-roulette-style";
    if (style.styleSheet) {
      style.styleSheet.cssText = css;
    } else {
      style.appendChild(document.createTextNode(css));
    }
  }

  function removeStylesheet() {
    const stylesheet = document.head.querySelector(
      "style#letterboxd-roulette-style"
    );
    if (stylesheet) {
      stylesheet.remove();
    }
  }

  function createShuffledIndexList(size) {
    window.LETTERBOXD_ROULETTE.shuffledIndexList = [];
    for (let i = 0; i < size; i++) {
      window.LETTERBOXD_ROULETTE.shuffledIndexList.push(i);
    }
    shuffle(window.LETTERBOXD_ROULETTE.shuffledIndexList);
    window.LETTERBOXD_ROULETTE.shuffledIndexListCounter = 0;
  }

  function activateRoulette() {
    injectStylesheet();
    window.LETTERBOXD_ROULETTE.active = true;
  }

  function deactivateRoulette() {
    removeStylesheet();
    window.LETTERBOXD_ROULETTE.filmIndex = -1;
    window.LETTERBOXD_ROULETTE.shuffledIndexList = [];
    window.LETTERBOXD_ROULETTE.shuffledIndexListCounter = 0;
    window.LETTERBOXD_ROULETTE.active = false;
  }

  function roulette(scrollToSelection) {
    // run one-time setup
    if (!window.LETTERBOXD_ROULETTE.active) {
      activateRoulette();
    }
    // get list of posters, and filter out watched films if
    // "Fade watched films" switch is on
    let posters = [];
    const hideWatchedFilms =
      document.body.classList.contains("hide-films-seen");
    if (hideWatchedFilms) {
      posters = document.querySelectorAll(
        "li.poster-container.film-not-watched"
      );
    } else {
      posters = document.querySelectorAll("li.poster-container");
    }
    // if our list of shuffled indices doesn't match poster list size, generate it
    if (window.LETTERBOXD_ROULETTE.shuffledIndexList.length != posters.length) {
      createShuffledIndexList(posters.length);
    }
    // deselect existing pick
    let chosen = [...posters].find((el) =>
      el.classList.contains(ROULETTE_SELECTED_CLASS)
    );
    if (chosen) {
      deselect(chosen);
      window.LETTERBOXD_ROULETTE.filmIndex = -1;
    }
    // select a new poster
    const count = posters.length;
    const randomPick =
      window.LETTERBOXD_ROULETTE.shuffledIndexList[
        window.LETTERBOXD_ROULETTE.shuffledIndexListCounter
      ];
    window.LETTERBOXD_ROULETTE.shuffledIndexListCounter += 1;
    if (window.LETTERBOXD_ROULETTE.shuffledIndexListCounter >= posters.length) {
      window.LETTERBOXD_ROULETTE.shuffledIndexListCounter %= posters.length;
    }
    const toWatch = posters[randomPick];
    select(toWatch);
    window.LETTERBOXD_ROULETTE.filmIndex = randomPick;
    // scroll to the selected poster
    if (scrollToSelection) {
      toWatch.scrollIntoView({
        behavior: "smooth",
        block: "center",
        inline: "center",
      });
    }
  }

  // run roulette when R is pressed
  window.addEventListener("keydown", (ev) => {
    if (ev.code == "KeyR") {
      const focusedElement = document.activeElement;
      if (
        ev.ctrlKey == true ||
        ev.altKey == true ||
        (ev.metaKey == true && isTextInput(focusedElement))
      ) {
        // only act on keypress without modifiers,
        // and if a text field is not focused
        return;
      }
      if (ev.shiftKey) {
        deactivateRoulette();
      } else {
        const scrollToSelection = true;
        if (!isTextInput(focusedElement)) {
          roulette(scrollToSelection);
        }
      }
    }
  });
})();