Greasy Fork is available in English.
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.
// ==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);
}
}
}
});
})();