- // ==UserScript==
- // @name Filterboxd
- // @namespace https://github.com/blakegearin/filterboxd
- // @version 1.5.0
- // @description Filter content on Letterboxd
- // @author Blake Gearin <hello@blakeg.me> (https://blakegearin.com)
- // @match https://letterboxd.com/*
- // @require https://openuserjs.org/src/libs/sizzle/GM_config.js
- // @grant GM.getValue
- // @grant GM.setValue
- // @icon https://raw.githubusercontent.com/blakegearin/filterboxd/main/img/logo.png
- // @supportURL https://github.com/blakegearin/filterboxd/issues
- // @license MIT
- // @copyright 2024–2025, Blake Gearin (https://blakegearin.com)
- // ==/UserScript==
-
- /* jshint esversion: 6 */
- /* global GM_config */
-
- (function() {
- 'use strict';
-
- const VERSION = '1.5.0';
- const USERSCRIPT_NAME = 'Filterboxd';
- let GMC = null;
-
- // Log levels
- const SILENT = 0;
- const QUIET = 1;
- const INFO = 2;
- const DEBUG = 3;
- const VERBOSE = 4;
- const TRACE = 5;
-
- // Change to true if you want to clear all your local data; this is irreversible
- const RESET_DATA = false;
-
- const LOG_LEVELS = {
- default: 'quiet',
- options: [
- 'silent',
- 'quiet',
- 'info',
- 'debug',
- 'verbose',
- 'trace',
- ],
- getName: (level) => {
- return {
- 0: 'silent',
- 1: 'quiet',
- 2: 'info',
- 3: 'debug',
- 4: 'verbose',
- 5: 'trace',
- }[level];
- },
- getValue: (name) => {
- return {
- silent: SILENT,
- quiet: QUIET,
- info: INFO,
- debug: DEBUG,
- verbose: VERBOSE,
- trace: TRACE,
- }[name];
- },
- };
-
- function currentLogLevel() {
- if (GMC === null) return LOG_LEVELS.getValue(LOG_LEVELS.default);
-
- return LOG_LEVELS.getValue(GMC.get('logLevel'));
- }
-
- function log (level, message, variable = undefined) {
- if (currentLogLevel() < level) return;
-
- const levelName = LOG_LEVELS.getName(level);
-
- const log = `[${VERSION}] [${levelName}] ${USERSCRIPT_NAME}: ${message}`;
-
- console.groupCollapsed(log);
-
- if (variable !== undefined) console.dir(variable, { depth: null });
-
- console.trace();
- console.groupEnd();
- }
-
- function logError (message, error = undefined) {
- const log = `[${VERSION}] [error] ${USERSCRIPT_NAME}: ${message}`;
-
- console.groupCollapsed(log);
-
- if (error !== undefined) console.error(error);
-
- console.trace();
- console.groupEnd();
- }
-
- log(TRACE, 'Starting');
-
- function gmcGet(key) {
- log(DEBUG, 'gmcGet()');
-
- try {
- return GMC.get(key);
- } catch (error) {
- logError(`Error setting GMC, key=${key}`, error);
- }
- }
-
- function gmcSet(key, value) {
- log(DEBUG, 'gmcSet()');
-
- try {
- return GMC.set(key, value);
- } catch (error) {
- logError(`Error setting GMC, key=${key}, value=${value}`, error);
- }
- }
-
- function gmcSave() {
- log(DEBUG, 'gmcSave()');
-
- try {
- return GMC.save();
- } catch (error) {
- logError('Error saving GMC', error);
- }
- }
-
- function startObserving() {
- log(DEBUG, 'startObserving()');
-
- OBSERVER.observe(
- document.body,
- {
- childList: true,
- subtree: true,
- },
- );
- }
-
- function modifyThenObserve(callback) {
- log(DEBUG, 'modifyThenObserve()');
-
- OBSERVER.disconnect();
- callback();
- startObserving();
- }
-
- function mutationsExceedsLimits() {
- // Fail-safes to prevent infinite loops
- if (IDLE_MUTATION_COUNT > gmcGet('maxIdleMutations')) {
- logError('Max idle mutations exceeded');
- OBSERVER.disconnect();
-
- return true;
- } else if (ACTIVE_MUTATION_COUNT >= gmcGet('maxActiveMutations')) {
- logError('Max active mutations exceeded');
- OBSERVER.disconnect();
-
- return true;
- }
-
- return false;
- }
-
- function observeAndModify(mutationsList) {
- log(VERBOSE, 'observeAndModify()');
-
- if (mutationsExceedsLimits()) return;
-
- log(VERBOSE, 'mutationsList.length', mutationsList.length);
-
- for (const mutation of mutationsList) {
- if (mutation.type !== 'childList') return;
-
- log(TRACE, 'mutation', mutation);
-
- let sidebarUpdated;
- let popMenuUpdated;
- let filtersApplied;
-
- modifyThenObserve(() => {
- sidebarUpdated = maybeAddListItemToSidebar();
- log(VERBOSE, 'sidebarUpdated', sidebarUpdated);
-
- popMenuUpdated = addListItemToPopMenu();
- log(VERBOSE, 'popMenuUpdated', popMenuUpdated);
-
- filtersApplied = applyFilters();
- log(VERBOSE, 'filtersApplied', filtersApplied);
- });
-
- const activeMutation = sidebarUpdated || popMenuUpdated || filtersApplied;
- log(DEBUG, 'activeMutation', activeMutation);
-
- if (activeMutation) {
- ACTIVE_MUTATION_COUNT++;
- log(VERBOSE, 'ACTIVE_MUTATION_COUNT', ACTIVE_MUTATION_COUNT);
- } else {
- IDLE_MUTATION_COUNT++;
- log(VERBOSE, 'IDLE_MUTATION_COUNT', IDLE_MUTATION_COUNT);
- }
-
- if (mutationsExceedsLimits()) break;
- }
- }
-
- // Source: https://stackoverflow.com/a/21144505/5988852
- function countWords(string) {
- var matches = string.match(/[\w\d’'-]+/gi);
- return matches ? matches.length : 0;
- }
-
- function createId(string) {
- log(TRACE, 'createId()');
-
- if (string.startsWith('#')) return string;
-
- if (string.startsWith('.')) {
- logError(`Attempted to create an id from a class: "${string}"`);
- return;
- }
-
- if (string.startsWith('[')) {
- logError(`Attempted to create an id from an attribute selector: "${string}"`);
- return;
- }
-
- return `#${string}`;
- }
-
- const FILM_BEHAVIORS = [
- 'Remove',
- 'Fade',
- 'Blur',
- 'Replace poster',
- 'Custom',
- ];
- const REVIEW_BEHAVIORS = [
- 'Remove',
- 'Fade',
- 'Blur',
- 'Replace text',
- 'Custom',
- ];
- const COLUMN_ONE_WIDTH = '33%';
- const COLUMN_TWO_WIDTH = '64.8%';
- const COLUMN_HALF_WIDTH = '50%';
-
- let IDLE_MUTATION_COUNT = 0;
- let ACTIVE_MUTATION_COUNT = 0;
- let SELECTORS = {
- filmPosterPopMenu: {
- self: '.film-poster-popmenu',
- userscriptListItemClass: 'filterboxd-list-item',
- addToList: '.film-poster-popmenu .menu-item-add-to-list',
- addThisFilm: '.film-poster-popmenu .menu-item-add-this-film',
- },
- filmPageSections: {
- backdropImage: 'body.backdrop-loaded .backdrop-container',
- // Left column
- poster: '#film-page-wrapper section.poster-list a[data-js-trigger="postermodal"]',
- stats: '#film-page-wrapper section.poster-list ul.film-stats',
- whereToWatch: '#film-page-wrapper section.watch-panel',
- // Right column
- userActionsPanel: '#film-page-wrapper section#userpanel',
- ratings: '#film-page-wrapper section.ratings-histogram-chart',
- // Middle column
- releaseYear: '#film-page-wrapper .details .releaseyear',
- director: '#film-page-wrapper .details .credits',
- tagline: '#film-page-wrapper .tagline',
- description: '#film-page-wrapper .truncate',
- castTab: '#film-page-wrapper #tabbed-content ul li:nth-of-type(1),#film-page-wrapper #tab-cast',
- crewTab: '#film-page-wrapper #tabbed-content ul li:nth-of-type(2),#film-page-wrapper #tab-crew',
- detailsTab: '#film-page-wrapper #tabbed-content ul li:nth-of-type(3),#film-page-wrapper #tab-details',
- genresTab: '#film-page-wrapper #tabbed-content ul li:nth-of-type(4),#film-page-wrapper #tab-genres',
- releasesTab: '#film-page-wrapper #tabbed-content ul li:nth-of-type(5),#film-page-wrapper #tab-releases',
- activityFromFriends: '#film-page-wrapper section.activity-from-friends',
- filmNews: '#film-page-wrapper section.film-news',
- reviewsFromFriends: '#film-page-wrapper section#popular-reviews-with-friends',
- popularReviews: '#film-page-wrapper section#popular-reviews',
- recentReviews: '#film-page-wrapper section#recent-reviews',
- relatedFilms: '#film-page-wrapper section#related',
- similarFilms: '#film-page-wrapper section.related-films:not(#related)',
- mentionedBy: '#film-page-wrapper section#film-hq-mentions',
- popularLists: '#film-page-wrapper section:has(#film-popular-lists)',
- },
- filter: {
- filmClass: 'filterboxd-filter-film',
- reviewClass: 'filterboxd-filter-review',
- reviews: {
- ratings: '.film-detail .attribution .rating,.film-detail-meta .rating,.activity-summary .rating,.film-metadata .rating,.-rated .rating,.poster-viewingdata .rating',
- likes: '.film-detail .attribution .icon-liked,.film-metadata .icon-liked,.review .like-link-target,.film-detail-content .like-link-target',
- comments: '.film-detail .attribution .content-metadata,#content #comments',
- withSpoilers: '.film-detail:has(.contains-spoilers)',
- withoutRatings: '.film-detail:not(:has(.rating))',
- },
- },
- homepageSections: {
- friendsHaveBeenWatching: '.person-home h1.title-hero span',
- newFromFriends: '.person-home section#recent-from-friends',
- popularWithFriends: '.person-home section#popular-with-friends',
- discoveryStream: '.person-home section.section-discovery-stream',
- latestNews: '.person-home section#latest-news:not(:has(.teaser-grid))',
- popularReviewsWithFriends: '.person-home section#popular-reviews',
- newListsFromFriends: '.person-home section:has([href="/lists/friends/"])',
- popularLists: '.person-home section:has([href="/lists/popular/this/week/"])',
- recentStories: '.person-home section.stories-section',
- recentShowdowns: '.person-home section:has([href="/showdown/"])',
- recentNews: '.person-home section#latest-news:has(.teaser-grid)',
- },
- processedClass: {
- apply: 'filterboxd-hide-processed',
- remove: 'filterboxd-unhide-processed',
- },
- settings: {
- clear: '.clear',
- favoriteFilms: '.favourite-films-selector',
- filteredTitleLinkClass: 'filtered-title-span',
- note: '.note',
- posterList: '.poster-list',
- removePendingClass: 'remove-pending',
- savedBadgeClass: 'filtered-saved',
- subNav: '.sub-nav',
- subtitle: '.mob-subtitle',
- tabbedContentId: '#tabbed-content',
- },
- userpanel: {
- self: '#userpanel',
- userscriptListItemId: 'filterboxd-userpanel-list-item',
- addThisFilm: '#userpanel .add-this-film',
- },
- };
-
- function addListItemToPopMenu() {
- log(DEBUG, 'addListItemToPopMenu()');
-
- const filmPosterPopMenus = document.querySelectorAll(SELECTORS.filmPosterPopMenu.self);
-
- if (!filmPosterPopMenus) {
- log(`Selector ${SELECTORS.filmPosterPopMenu.self} not found`, DEBUG);
- return false;
- }
-
- let pageUpdated = false;
-
- filmPosterPopMenus.forEach(filmPosterPopMenu => {
- const userscriptListItemPresent = filmPosterPopMenu.querySelector(
- `.${SELECTORS.filmPosterPopMenu.userscriptListItemClass}`,
- );
- if (userscriptListItemPresent) return;
-
- const lastListItem = filmPosterPopMenu.querySelector('li:last-of-type');
-
- if (!lastListItem) {
- logError(`Selector ${SELECTORS.filmPosterPopMenu} li:last-of-type not found`);
- return;
- }
-
- const unorderedList = filmPosterPopMenu.querySelector('ul');
- if (!unorderedList) {
- logError(`Selector ${SELECTORS.filmPosterPopMenu.self} ul not found`);
- return;
- }
-
- modifyThenObserve(() => {
- let userscriptListItem = lastListItem.cloneNode(true);
- userscriptListItem.classList.add(SELECTORS.filmPosterPopMenu.userscriptListItemClass);
-
- userscriptListItem = buildUserscriptLink(userscriptListItem, unorderedList);
- lastListItem.parentNode.append(userscriptListItem);
- });
-
- pageUpdated = true;
- });
-
- return pageUpdated;
- }
-
- function addFilterToFilm({ id, slug }) {
- log(DEBUG, 'addFilterToFilm()');
-
- let pageUpdated = false;
-
- const idMatch = `[data-film-id="${id}"]`;
- let appliedSelector = `.${SELECTORS.processedClass.apply}`;
-
- const replaceBehavior = gmcGet('filmBehaviorType') === 'Replace poster';
- log(VERBOSE, 'replaceBehavior', replaceBehavior);
-
- if (replaceBehavior) appliedSelector = '[data-original-img-src]';
-
- log(VERBOSE, 'Activity page reviews');
- document.querySelectorAll(`section.activity-row ${idMatch}`).forEach(posterElement => {
- applyFilterToFilm(posterElement, 3);
-
- pageUpdated = true;
- });
-
- log(VERBOSE, 'Activity page likes');
- document.querySelectorAll(`section.activity-row .activity-summary a[href*="${slug}"]:not(${appliedSelector})`).forEach(posterElement => {
- applyFilterToFilm(posterElement, 3);
-
- pageUpdated = true;
- });
-
- log(VERBOSE, 'New from friends');
- document.querySelectorAll(`.poster-container ${idMatch}:not(${appliedSelector})`).forEach(posterElement => {
- applyFilterToFilm(posterElement, 1);
-
- pageUpdated = true;
- });
-
- log(VERBOSE, 'Reviews');
- document.querySelectorAll(`.review-tile ${idMatch}:not(${appliedSelector})`).forEach(posterElement => {
- applyFilterToFilm(posterElement, 3);
-
- pageUpdated = true;
- });
-
- log(VERBOSE, 'Diary');
- document.querySelectorAll(`.td-film-details [data-original-img-src]${idMatch}:not(${appliedSelector})`).forEach(posterElement => {
- applyFilterToFilm(posterElement, 2);
-
- pageUpdated = true;
- });
-
- log(VERBOSE, 'Popular with friends, competitions');
- const remainingElements = document.querySelectorAll(
- `div:not(.popmenu):not(.actions-panel) ${idMatch}:not(aside [data-film-id="${id}"]):not(${appliedSelector})`,
- );
- remainingElements.forEach(posterElement => {
- applyFilterToFilm(posterElement, 0);
-
- pageUpdated = true;
- });
-
- return pageUpdated;
- }
-
- function addToHiddenTitles(filmMetadata) {
- log(DEBUG, 'addToHiddenTitles()');
-
- const filmFilter = getFilter('filmFilter');
- filmFilter.push(filmMetadata);
- log(VERBOSE, 'filmFilter', filmFilter);
-
- setFilter('filmFilter', filmFilter);
- }
-
- function applyFilters() {
- log(DEBUG, 'applyFilters()');
-
- let pageUpdated = false;
-
- const filmFilter = getFilter('filmFilter');
- log(VERBOSE, 'filmFilter', filmFilter);
-
- const reviewFilter = getFilter('reviewFilter');
- log(VERBOSE, 'reviewFilter', reviewFilter);
-
- const replaceBehavior = gmcGet('reviewBehaviorType') === 'Replace text';
- log(VERBOSE, 'replaceBehavior', replaceBehavior);
-
- const reviewBehaviorReplaceValue = gmcGet('reviewBehaviorReplaceValue');
- log(VERBOSE, 'reviewBehaviorReplaceValue', reviewBehaviorReplaceValue);
-
- const homepageFilter = getFilter('homepageFilter');
- log(VERBOSE, 'homepageFilter', homepageFilter);
-
- const filmPageFilter = getFilter('filmPageFilter');
- log(VERBOSE, 'filmPageFilter', filmPageFilter);
-
- modifyThenObserve(() => {
- filmFilter.forEach(filmMetadata => {
- const filmUpdated = addFilterToFilm(filmMetadata);
- if (filmUpdated) pageUpdated = true;
- });
-
- const selectorReviewElementsToFilter = [];
- if (reviewFilter.ratings) selectorReviewElementsToFilter.push(SELECTORS.filter.reviews.ratings);
- if (reviewFilter.likes) selectorReviewElementsToFilter.push(SELECTORS.filter.reviews.likes);
- if (reviewFilter.comments) selectorReviewElementsToFilter.push(SELECTORS.filter.reviews.comments);
-
- log(VERBOSE, 'selectorReviewElementsToFilter', selectorReviewElementsToFilter);
-
- if (selectorReviewElementsToFilter.length) {
- document.querySelectorAll(selectorReviewElementsToFilter.join(',')).forEach(reviewElement => {
- reviewElement.style.display = 'none';
-
- pageUpdated = true;
- });
- }
-
- const reviewsToFilterSelectors = [];
- if (reviewFilter.withSpoilers) reviewsToFilterSelectors.push(SELECTORS.filter.reviews.withSpoilers);
- if (reviewFilter.withoutRatings) reviewsToFilterSelectors.push(SELECTORS.filter.reviews.withoutRatings);
-
- log(VERBOSE, 'reviewsToFilterSelectors', reviewsToFilterSelectors);
-
- if (reviewsToFilterSelectors.length) {
- document.querySelectorAll(reviewsToFilterSelectors.join(',')).forEach(review => {
- if (replaceBehavior) {
- review.querySelector('.body-text').innerText = reviewBehaviorReplaceValue;
- }
-
- review.classList.add(SELECTORS.filter.reviewClass);
-
- pageUpdated = true;
- });
- }
-
- if (reviewFilter.byWordCount) {
- const reviewMinimumWordCount = getFilter('reviewMinimumWordCount');
- log(VERBOSE, 'reviewMinimumWordCount', reviewMinimumWordCount);
-
- document.querySelectorAll('.film-detail:not(.filterboxd-filter-review)').forEach(review => {
- const reviewText = review.querySelector('.body-text').innerText;
- log(VERBOSE, 'reviewText', reviewText);
-
- if (countWords(reviewText) >= reviewMinimumWordCount) return;
-
- if (replaceBehavior) {
- review.querySelector('.body-text').innerText = reviewBehaviorReplaceValue;
- }
-
- review.classList.add(SELECTORS.filter.reviewClass);
-
- pageUpdated = true;
- });
- }
-
- const sectionsToFilter = [];
-
- const homepageSectionsToFilter = Object.keys(homepageFilter)
- .filter(key => homepageFilter[key])
- .map(key => SELECTORS.homepageSections[key])
- .filter(Boolean);
- log(VERBOSE, 'homepageSectionToFilter', homepageSectionsToFilter);
-
- const filmPageSectionsToFilter = Object.keys(filmPageFilter)
- .filter(key => filmPageFilter[key])
- .map(key => SELECTORS.filmPageSections[key])
- .filter(Boolean);
- log(VERBOSE, 'filmPageSectionsToFilter', filmPageSectionsToFilter);
-
- sectionsToFilter.push(...homepageSectionsToFilter);
- sectionsToFilter.push(...filmPageSectionsToFilter);
-
- if (sectionsToFilter.length) {
- document.querySelectorAll(sectionsToFilter.join(',')).forEach(filterSection => {
- filterSection.style.display = 'none';
-
- pageUpdated = true;
- });
- }
-
- if (filmPageFilter.backdropImage) {
- document.querySelector('#content.-backdrop')?.classList.remove('-backdrop');
- pageUpdated = true;
- }
- });
-
- return pageUpdated;
- }
-
- function applyFilterToFilm(element, levelsUp = 0) {
- log(DEBUG, 'applyFilterToFilm()');
-
- const replaceBehavior = gmcGet('filmBehaviorType') === 'Replace poster';
- log(VERBOSE, 'replaceBehavior', replaceBehavior);
-
- if (replaceBehavior) {
- const filmBehaviorReplaceValue = gmcGet('filmBehaviorReplaceValue');
- log(VERBOSE, 'filmBehaviorReplaceValue', filmBehaviorReplaceValue);
-
- const elementImg = element.querySelector('img');
- if (!elementImg) return;
-
- const originalImgSrc = elementImg.src;
- if (!originalImgSrc) return;
-
- if (originalImgSrc === filmBehaviorReplaceValue) return;
-
- element.setAttribute('data-original-img-src', originalImgSrc);
-
- element.querySelector('img').src = filmBehaviorReplaceValue;
- element.querySelector('img').srcset = filmBehaviorReplaceValue;
-
- element.classList.add(SELECTORS.processedClass.apply);
- element.classList.remove(SELECTORS.processedClass.remove);
- } else {
- let target = element;
-
- for (let i = 0; i < levelsUp; i++) {
- if (target.parentNode) {
- target = target.parentNode;
- } else {
- break;
- }
- }
-
- log(VERBOSE, 'target', target);
-
- target.classList.add(SELECTORS.filter.filmClass);
- element.classList.add(SELECTORS.processedClass.apply);
- element.classList.remove(SELECTORS.processedClass.remove);
- }
- }
-
- function buildBehaviorFormRows(parentDiv, filterName, selectArrayValues, behaviorsMetadata) {
- const behaviorValue = gmcGet(`${filterName}BehaviorType`);
- log(DEBUG, 'behaviorValue', behaviorValue);
-
- const behaviorChange = (event) => {
- const filmBehaviorType = event.target.value;
- updateBehaviorCSSVariables(filterName, filmBehaviorType);
- };
-
- const behaviorFormRow = createFormRow({
- formRowStyle: `width: ${COLUMN_ONE_WIDTH};`,
- labelText: 'Behavior',
- inputValue: behaviorValue,
- inputType: 'select',
- selectArray: selectArrayValues,
- selectOnChange: behaviorChange,
- });
-
- parentDiv.appendChild(behaviorFormRow);
-
- // Fade amount
- const behaviorFadeAmount = parseInt(gmcGet(behaviorsMetadata.fade.fieldName));
- log(DEBUG, 'behaviorFadeAmount', behaviorFadeAmount);
-
- const fadeAmountFormRow = createFormRow({
- formRowClass: ['update-details'],
- formRowStyle: `width: ${COLUMN_TWO_WIDTH}; float: right; display: var(--filterboxd-${filterName}-behavior-fade);`,
- labelText: 'Opacity',
- inputValue: behaviorFadeAmount,
- inputType: 'number',
- inputMin: 0,
- inputMax: 100,
- inputStyle: 'width: 100px !important;',
- notes: '%',
- notesStyle: 'width: 10px; margin-left: 14px;',
- });
-
- parentDiv.appendChild(fadeAmountFormRow);
-
- // Blur amount
- const behaviorBlurAmount = parseInt(gmcGet(behaviorsMetadata.blur.fieldName));
- log(DEBUG, 'behaviorBlurAmount', behaviorBlurAmount);
-
- const blurAmountFormRow = createFormRow({
- formRowClass: ['update-details'],
- formRowStyle: `width: ${COLUMN_TWO_WIDTH}; float: right; display: var(--filterboxd-${filterName}-behavior-blur);`,
- labelText: 'Amount',
- inputValue: behaviorBlurAmount,
- inputType: 'number',
- inputMin: 1,
- inputStyle: 'width: 100px !important;',
- notes: 'px',
- notesStyle: 'width: 10px; margin-left: 14px;',
- });
-
- parentDiv.appendChild(blurAmountFormRow);
-
- // Replace value
- const behaviorReplaceValue = gmcGet(behaviorsMetadata.replace.fieldName);
- log(DEBUG, 'behaviorReplaceValue', behaviorReplaceValue);
-
- const replaceValueFormRow = createFormRow({
- formRowStyle: `width: ${COLUMN_TWO_WIDTH}; float: right; display: var(--filterboxd-${filterName}-behavior-replace);`,
- labelText: behaviorsMetadata.replace.labelText,
- inputValue: behaviorReplaceValue,
- inputType: 'text',
- });
-
- parentDiv.appendChild(replaceValueFormRow);
-
- // Custom CSS
- const behaviorCustomValue = gmcGet(behaviorsMetadata.custom.fieldName);
- log(DEBUG, 'behaviorCustomValue', behaviorCustomValue);
-
- const cssFormRow = createFormRow({
- formRowStyle: `width: ${COLUMN_TWO_WIDTH}; float: right; display: var(--filterboxd-${filterName}-behavior-custom);`,
- labelText: 'CSS',
- inputValue: behaviorCustomValue,
- inputType: 'text',
- });
-
- parentDiv.appendChild(cssFormRow);
-
- return [
- behaviorFormRow,
- fadeAmountFormRow,
- blurAmountFormRow,
- replaceValueFormRow,
- behaviorCustomValue,
- ];
- }
-
- function buildToggleSectionListItems(filterName, unorderedList, listItemMetadata) {
- log(DEBUG, 'buildListItemToggles()');
-
- const filter = getFilter(filterName);
-
- listItemMetadata.forEach(metadata => {
- const { type, name, description } = metadata;
-
- if (type === 'label') {
- const label = document.createElement('label');
- unorderedList.appendChild(label);
-
- label.innerText = description;
- label.style.cssText = 'margin: 1em 0em;';
-
- return;
- }
-
- const checked = filter[name] || false;
-
- const listItem = document.createElement('li');
- listItem.classList.add('option');
-
- const label = document.createElement('label');
- listItem.appendChild(label);
-
- label.classList.add('option-label', '-toggle', 'switch-control');
-
- const labelSpan = document.createElement('span');
- label.appendChild(labelSpan);
-
- labelSpan.classList.add('label');
- labelSpan.innerText = description;
-
- const labelInput = document.createElement('input');
- label.appendChild(labelInput);
-
- labelInput.classList.add('checkbox');
- labelInput.setAttribute('type', 'checkbox');
- labelInput.setAttribute('role', 'switch');
- labelInput.setAttribute('data-filter-name', filterName);
- labelInput.setAttribute('data-field-name', name);
- labelInput.checked = checked;
-
- const labelCheckboxSpan = document.createElement('span');
- label.appendChild(labelCheckboxSpan);
-
- labelCheckboxSpan.classList.add('state');
-
- const checkboxTrackSpan = document.createElement('span');
- labelCheckboxSpan.appendChild(checkboxTrackSpan);
-
- checkboxTrackSpan.classList.add('track');
-
- const checkboxHandleSpan = document.createElement('span');
- checkboxTrackSpan.appendChild(checkboxHandleSpan);
-
- checkboxHandleSpan.classList.add('handle');
-
- unorderedList.appendChild(listItem);
- });
- }
-
- function buildUserscriptLink(userscriptListItem, unorderedList) {
- log(DEBUG, 'buildUserscriptLink()');
-
- const userscriptLink = userscriptListItem.firstElementChild;
- userscriptListItem.onclick = (event) => {
- event.preventDefault();
-
- log(DEBUG, 'userscriptListItem clicked');
- log(VERBOSE, 'event', event);
-
- const link = event.target;
- log(VERBOSE, 'link', link);
-
- const id = parseInt(link.getAttribute('data-film-id'));
- const slug = link.getAttribute('data-film-slug');
- const name = link.getAttribute('data-film-name');
- const year = link.getAttribute('data-film-release-year');
-
- const filmMetadata = {
- id,
- slug,
- name,
- year,
- };
-
- const titleIsHidden = link.getAttribute('data-title-hidden') === 'true';
-
- modifyThenObserve(() => {
- if (titleIsHidden) {
- removeFilterFromFilm(filmMetadata);
- removeFromFilmFilter(filmMetadata);
- } else {
- addFilterToFilm(filmMetadata);
- addToHiddenTitles(filmMetadata);
- }
-
- const sidebarLink = document.querySelector(createId(SELECTORS.userpanel.userscriptListItemId));
- if (sidebarLink) {
- updateLinkInPopMenu(!titleIsHidden, sidebarLink);
-
- const popupLink = document.querySelector(`.${SELECTORS.filmPosterPopMenu.userscriptListItemClass} a`);
- if (popupLink) updateLinkInPopMenu(!titleIsHidden, popupLink);
- } else {
- updateLinkInPopMenu(!titleIsHidden, link);
- }
- });
- };
-
- let filmPosterSelector;
-
- let titleId = unorderedList.querySelector('[data-film-id]')?.getAttribute('data-film-id');
- log(DEBUG, 'titleId', titleId);
-
- if (titleId) {
- filmPosterSelector = `[data-film-id='${titleId}'].film-poster`;
- } else {
- const titleName = unorderedList.querySelector('[data-film-name]')?.getAttribute('data-film-name');
- log(DEBUG, 'titleName', titleName);
-
- if (titleName) {
- filmPosterSelector = `[data-film-name='${titleName}'].film-poster`;
- } else {
- logError('No film id or name found in unordered list');
- return;
- }
- }
-
- log(DEBUG, 'filmPosterSelector', filmPosterSelector);
- const filmPoster = document.querySelector(filmPosterSelector);
- log(DEBUG, 'filmPoster', filmPoster);
-
- if (!titleId) {
- titleId = filmPoster?.getAttribute('data-film-id');
- log(DEBUG, 'titleId', titleId);
-
- if (!titleId) {
- logError('No film id found on film poster');
- return;
- }
- }
-
- userscriptLink.setAttribute('data-film-id', titleId);
-
- if (!filmPoster) {
- logError('No film poster found');
- log(INFO, 'unorderedList', unorderedList);
- }
-
- const titleSlug =
- unorderedList.querySelector('[data-film-slug]')?.getAttribute('data-film-slug')
- || filmPoster?.getAttribute('data-film-slug');
- log(DEBUG, 'titleSlug', titleSlug);
-
- if (titleSlug) userscriptLink.setAttribute('data-film-slug', titleSlug);
-
- const titleName = unorderedList.querySelector('[data-film-name]')?.getAttribute('data-film-name');
- log(DEBUG, 'titleName', titleName);
- if (titleName) userscriptLink.setAttribute('data-film-name', titleName);
-
- // Title year isn't present in the pop menu list, so retrieve it from the film poster
- const titleYear =
- filmPoster?.querySelector('.has-menu')?.getAttribute('data-original-title')?.match(/\((\d{4})\)/)?.[1]
- || document.querySelector('div.releaseyear a')?.innerText
- || document.querySelector('small.metadata a')?.innerText
- || filmPoster?.querySelector('.frame-title')?.innerText?.match(/\((\d{4})\)/)?.[1];
- log(DEBUG, 'titleYear', titleYear);
- if (titleYear) userscriptLink.setAttribute('data-film-release-year', titleYear);
-
- const filmFilter = getFilter('filmFilter');
- log(DEBUG, 'filmFilter', filmFilter);
-
- const titleIsHidden = filmFilter.some(
- filteredFilm => filteredFilm.id?.toString() === titleId?.toString(),
- );
- log(DEBUG, 'titleIsHidden', titleIsHidden);
-
- updateLinkInPopMenu(titleIsHidden, userscriptLink);
-
- userscriptLink.removeAttribute('class');
-
- return userscriptListItem;
- }
-
- function buildToggleSection(parentElement, sectionTitle, filerName, sectionMetadata) {
- log(DEBUG, 'buildToggleSection()');
-
- const formRowDiv = document.createElement('div');
- parentElement.appendChild(formRowDiv);
-
- formRowDiv.style.cssText = 'margin-bottom: 40px;';
-
- const sectionHeader = document.createElement('h3');
- formRowDiv.append(sectionHeader);
-
- sectionHeader.classList.add('title-3');
- sectionHeader.style.cssText = 'margin-top: 0em;';
- sectionHeader.innerText = sectionTitle;
-
- const unorderedList = document.createElement('ul');
- formRowDiv.append(unorderedList);
-
- unorderedList.classList.add('options-list', '-toggle-list', 'js-toggle-list');
-
- buildToggleSectionListItems(
- filerName,
- unorderedList,
- sectionMetadata,
- );
-
- let formColumnDiv = document.createElement('div');
- formRowDiv.appendChild(formColumnDiv);
-
- formColumnDiv.classList.add('form-columns', '-cols2');
- }
-
- function createFormRow({
- formRowClass = [],
- formRowStyle = '',
- labelText = '',
- helpText = '',
- inputValue = '',
- inputType = 'text',
- inputMin = null,
- inputMax = null,
- inputStyle = '',
- selectArray = [],
- selectOnChange = () => {},
- notes = '',
- notesStyle = '',
- }) {
- log(DEBUG, 'createFormRow()');
-
- const formRow = document.createElement('div');
- formRow.classList.add('form-row');
- formRow.style.cssText = formRowStyle;
- formRow.classList.add(...formRowClass);
-
- const selectList = document.createElement('div');
- formRow.appendChild(selectList);
-
- selectList.classList.add('select-list');
-
- const label = document.createElement('label');
- selectList.appendChild(label);
-
- label.classList.add('label');
- label.textContent = labelText;
-
- if (helpText) {
- const helpIcon = document.createElement('span');
- label.appendChild(helpIcon);
-
- helpIcon.classList.add('s', 'icon-14', 'icon-tip', 'tooltip');
- helpIcon.setAttribute('target', '_blank');
- helpIcon.setAttribute('data-html', 'true');
- helpIcon.setAttribute('data-original-title', helpText);
- helpIcon.innerHTML = '<span class="icon"></span>(Help)';
- }
-
- const inputDiv = document.createElement('div');
- selectList.appendChild(inputDiv);
-
- inputDiv.classList.add('input');
- inputDiv.style.cssText = inputStyle;
-
- if (inputType === 'select') {
- const select = document.createElement('select');
- inputDiv.appendChild(select);
-
- select.classList.add('select');
-
- selectArray.forEach(option => {
- const optionElement = document.createElement('option');
- select.appendChild(optionElement);
-
- optionElement.value = option;
- optionElement.textContent = option;
-
- if (option === inputValue) optionElement.setAttribute('selected', 'selected');
- });
-
- select.onchange = selectOnChange;
- } else if (['text', 'number'].includes(inputType)) {
- const input = document.createElement('input');
- inputDiv.appendChild(input);
-
- input.type = inputType;
- input.classList.add('field');
- input.value = inputValue;
-
- if (inputMin !== null) input.min = inputMin;
- if (inputMax !== null) input.max = inputMax;
- }
-
- if (notes) {
- const notesElement = document.createElement('p');
- selectList.appendChild(notesElement);
-
- notesElement.classList.add('notes');
- notesElement.style.cssText = notesStyle;
- notesElement.textContent = notes;
- }
-
- return formRow;
- }
-
- function displaySavedBadge() {
- log(DEBUG, 'displaySavedBadge()');
-
- const savedBadge = document.querySelector(`.${SELECTORS.settings.savedBadgeClass}`);
-
- savedBadge.classList.remove('hidden');
- savedBadge.classList.add('fade');
-
- setTimeout(() => {
- savedBadge.classList.add('fade-out');
- }, 2000);
-
- setTimeout(() => {
- savedBadge.classList.remove('fade', 'fade-out');
- savedBadge.classList.add('hidden');
- }, 3000);
- }
-
- function getFilter(filterName) {
- log(DEBUG, 'getFilter()');
-
- return JSON.parse(gmcGet(filterName));
- }
-
- function getFilterBehaviorStyle(filterName) {
- log(DEBUG, 'getFilterBehaviorStyle()');
-
- let behaviorStyle;
- let behaviorType = gmcGet(`${filterName}BehaviorType`);
- log(DEBUG, 'behaviorType', behaviorType);
-
- const behaviorFadeAmount = gmcGet(`${filterName}BehaviorFadeAmount`);
- log(VERBOSE, 'behaviorFadeAmount', behaviorFadeAmount);
-
- const behaviorBlurAmount = gmcGet(`${filterName}BehaviorBlurAmount`);
- log(VERBOSE, 'behaviorBlurAmount', behaviorBlurAmount);
-
- const behaviorCustomValue = gmcGet(`${filterName}BehaviorCustomValue`);
- log(VERBOSE, 'behaviorCustomValue', behaviorCustomValue);
-
- switch (behaviorType) {
- case 'Remove':
- behaviorStyle = 'display: none !important;';
- break;
- case 'Fade':
- behaviorStyle = `opacity: ${behaviorFadeAmount}%`;
- break;
- case 'Blur':
- behaviorStyle = `filter: blur(${behaviorBlurAmount}px)`;
- break;
- case 'Custom':
- behaviorStyle = behaviorCustomValue;
- break;
- }
-
- updateBehaviorCSSVariables(filterName, behaviorType);
-
- return behaviorStyle;
- }
-
- function gmcInitialized() {
- log(DEBUG, 'gmcInitialized()');
- log(QUIET, 'Running');
-
- GMC.css.basic = '';
-
- if (RESET_DATA) {
- log(QUIET, 'Resetting GMC');
-
- for (const [key, field] of Object.entries(GMC_FIELDS)) {
- const value = field.default;
- gmcSet(key, value);
- }
-
- log(QUIET, 'GMC reset');
- }
-
- let userscriptStyle = document.createElement('style');
- userscriptStyle.setAttribute('id', 'filterboxd-style');
-
- const filmBehaviorStyle = getFilterBehaviorStyle('film');
- log(VERBOSE, 'filmBehaviorStyle', filmBehaviorStyle);
-
- const reviewBehaviorStyle = getFilterBehaviorStyle('review');
- log(VERBOSE, 'reviewBehaviorStyle', reviewBehaviorStyle);
-
- userscriptStyle.textContent += `
- .${SELECTORS.filter.filmClass}
- {
- ${filmBehaviorStyle}
- }
-
- .${SELECTORS.filter.reviewClass}
- {
- ${reviewBehaviorStyle}
- }
-
- .${SELECTORS.settings.filteredTitleLinkClass}
- {
- cursor: pointer;
- margin-right: 0.3rem !important;
- }
-
- .${SELECTORS.settings.filteredTitleLinkClass}:hover
- {
- background: #303840;
- color: #def;
- }
-
- .${SELECTORS.settings.removePendingClass}
- {
- outline: 1px dashed #ee7000;
- outline-offset: -1px;
- }
-
- .hidden {
- visibility: hidden;
- }
-
- .fade {
- opacity: 1;
- transition: opacity 1s ease-out;
- }
-
- .fade.fade-out {
- opacity: 0;
- }
- `;
- document.body.appendChild(userscriptStyle);
-
- const onSettingsPage = window.location.href.includes('/settings/');
- log(VERBOSE, 'onSettingsPage', onSettingsPage);
-
- if (onSettingsPage) {
- maybeAddConfigurationToSettings();
- }
- else {
- applyFilters();
- startObserving();
- }
- }
-
- function trySelectFilterboxdTab() {
- const maxAttempts = 10;
- let attempts = 0;
- let successes = 0;
-
- log(DEBUG, `Attempting to select Filterboxd tab (attempt ${attempts + 1}/${maxAttempts})`);
-
- const tabLink = document.querySelector('a[data-id="filterboxd"]');
- if (!tabLink) {
- log(DEBUG, 'Filterboxd tab link not found yet');
- if (attempts < maxAttempts) {
- attempts++;
- setTimeout(trySelectFilterboxdTab, 100);
- } else {
- logError('Failed to find Filterboxd tab after maximum attempts');
- }
- return;
- }
-
- try {
- tabLink.click();
-
- setTimeout(() => {
- const tabSelected = document.querySelector('li.selected:has(a[data-id="filterboxd"])') !== null;
- if (tabSelected) {
- log(DEBUG, 'Filterboxd tab selected successfully');
- successes++;
-
- // There's a race condition between the click and the "Profile" tab being loaded and selected
- if (successes < 2) setTimeout(trySelectFilterboxdTab, 500);
- } else {
- log(DEBUG, 'Click didn\'t select the tab properly');
- if (attempts < maxAttempts) {
- attempts++;
- setTimeout(trySelectFilterboxdTab, 100);
- } else {
- logError('Failed to select Filterboxd tab after maximum attempts');
- }
- }
- }, 50);
- } catch (error) {
- logError('Error selecting Filterboxd tab', error);
- if (attempts < maxAttempts) {
- attempts++;
- setTimeout(trySelectFilterboxdTab, 100);
- }
- }
- }
-
- function maybeAddConfigurationToSettings() {
- log(DEBUG, 'maybeAddConfigurationToSettings()');
-
- const userscriptTabId = 'tab-filterboxd';
- const configurationExists = document.querySelector(createId(userscriptTabId));
- log(VERBOSE, 'configurationExists', configurationExists);
-
- if (configurationExists) {
- log(DEBUG, 'Filterboxd configuration tab is present');
- return;
- }
-
- const userscriptTabDiv = document.createElement('div');
-
- const settingsTabbedContent = document.querySelector(SELECTORS.settings.tabbedContentId);
- settingsTabbedContent.appendChild(userscriptTabDiv);
-
- userscriptTabDiv.setAttribute('id', userscriptTabId);
- userscriptTabDiv.classList.add('tabbed-content-block');
-
- const tabTitle = document.createElement('h2');
- userscriptTabDiv.append(tabTitle);
-
- tabTitle.style.cssText = 'margin-bottom: 1em;';
- tabTitle.innerText = 'Filterboxd';
-
- const tabPrimaryColumn = document.createElement('div');
- userscriptTabDiv.append(tabPrimaryColumn);
-
- tabPrimaryColumn.classList.add('col-10', 'overflow');
-
- const asideColumn = document.createElement('aside');
- userscriptTabDiv.append(asideColumn);
-
- asideColumn.classList.add('col-12', 'overflow', 'col-right', 'js-hide-in-app');
-
- // Filter film page
- const filmPageFilterMetadata = [
- {
- type: 'toggle',
- name: 'backdropImage',
- description: 'Remove backdrop image',
- },
- {
- type: 'label',
- description: 'Left column',
- },
- {
- type: 'toggle',
- name: 'poster',
- description: 'Remove poster',
- },
- {
- type: 'toggle',
- name: 'stats',
- description: 'Remove Letterboxd stats',
- },
- {
- type: 'toggle',
- name: 'whereToWatch',
- description: 'Remove "Where to watch" section',
- },
- {
- type: 'label',
- description: 'Right column',
- },
- {
- type: 'toggle',
- name: 'userActionsPanel',
- description: 'Remove user actions panel',
- },
- {
- type: 'toggle',
- name: 'ratings',
- description: 'Remove "Ratings" section',
- },
- {
- type: 'label',
- description: 'Middle column',
- },
- {
- type: 'toggle',
- name: 'releaseYear',
- description: 'Remove release year text',
- },
- {
- type: 'toggle',
- name: 'director',
- description: 'Remove director text',
- },
- {
- type: 'toggle',
- name: 'tagline',
- description: 'Remove tagline text',
- },
- {
- type: 'toggle',
- name: 'description',
- description: 'Remove description text',
- },
- {
- type: 'toggle',
- name: 'castTab',
- description: 'Remove "Cast" tab',
- },
- {
- type: 'toggle',
- name: 'crewTab',
- description: 'Remove "Crew" tab',
- },
- {
- type: 'toggle',
- name: 'detailsTab',
- description: 'Remove "Details" tab',
- },
- {
- type: 'toggle',
- name: 'genresTab',
- description: 'Remove "Genres" tab',
- },
- {
- type: 'toggle',
- name: 'releasesTab',
- description: 'Remove "Releases" tab',
- },
- {
- type: 'toggle',
- name: 'activityFromFriends',
- description: 'Remove "Activity from friends" section',
- },
- {
- type: 'toggle',
- name: 'filmNews',
- description: 'Remove HQ film news section',
- },
- {
- type: 'toggle',
- name: 'reviewsFromFriends',
- description: 'Remove "Reviews from friends" section',
- },
- {
- type: 'toggle',
- name: 'popularReviews',
- description: 'Remove "Popular reviews" section',
- },
- {
- type: 'toggle',
- name: 'recentReviews',
- description: 'Remove "Recent reviews" section',
- },
- {
- type: 'toggle',
- name: 'relatedFilms',
- description: 'Remove "Related films" section',
- },
- {
- type: 'toggle',
- name: 'similarFilms',
- description: 'Remove "Similar films" section',
- },
- {
- type: 'toggle',
- name: 'mentionedBy',
- description: 'Remove "Mentioned by" section',
- },
- {
- type: 'toggle',
- name: 'popularLists',
- description: 'Remove "Popular lists" section',
- },
- ];
-
- buildToggleSection(
- asideColumn,
- 'Film Page Filter',
- 'filmPageFilter',
- filmPageFilterMetadata,
- );
-
- // Advanced Options
- const formRowDiv = document.createElement('div');
- asideColumn.appendChild(formRowDiv);
-
- formRowDiv.style.cssText = 'margin-bottom: 40px;';
-
- const sectionHeader = document.createElement('h3');
- formRowDiv.append(sectionHeader);
-
- sectionHeader.classList.add('title-3');
- sectionHeader.style.cssText = 'margin-top: 0em;';
- sectionHeader.innerText = 'Advanced Options';
-
- const logLevelValue = gmcGet('logLevel');
- log(DEBUG, 'logLevelValue', logLevelValue);
-
- const logLevelFormRow = createFormRow({
- formRowClass: ['update-details'],
- formRowStyle: `width: ${COLUMN_ONE_WIDTH};`,
- labelText: 'Log level ',
- helpText: 'Determines how much logging<br /> is visible in the browser console',
- inputValue: logLevelValue,
- inputType: 'select',
- selectArray: LOG_LEVELS.options,
- });
-
- formRowDiv.appendChild(logLevelFormRow);
-
- const mutationsDiv = document.createElement('div');
- mutationsDiv.style.cssText = 'display: flex; align-items: center;';
- formRowDiv.append(mutationsDiv);
-
- const maxActiveMutationsValue = gmcGet('maxActiveMutations');
- log(DEBUG, 'maxActiveMutationsValue', maxActiveMutationsValue);
-
- const maxActiveMutationsFormRow = createFormRow({
- formRowClass: ['update-details'],
- formRowStyle: `width: ${COLUMN_HALF_WIDTH};`,
- labelText: 'Max active mutations ',
- helpText: 'Safety limit that halts execution<br /> when a certain number of modifications<br /> are performed by the script',
- inputValue: maxActiveMutationsValue,
- inputType: 'number',
- inputMin: 1,
- inputStyle: 'width: 100px !important;',
- });
-
- mutationsDiv.appendChild(maxActiveMutationsFormRow);
-
- const maxIdleMutationsValue = gmcGet('maxIdleMutations');
- log(DEBUG, 'maxIdleMutationsValue', maxIdleMutationsValue);
-
- const maxIdleMutationsFormRow = createFormRow({
- formRowClass: ['update-details'],
- formRowStyle: `width: ${COLUMN_HALF_WIDTH}; float: right;`,
- labelText: 'Max idle mutations ',
- helpText: 'Safety limit that halts execution<br /> when a certain number of modifications<br /> are performed by Letterboxd<br /> that did not result in modifications<br /> from the script',
- inputValue: maxIdleMutationsValue,
- inputType: 'number',
- inputMin: 1,
- inputStyle: 'width: 100px !important;',
- });
-
- mutationsDiv.appendChild(maxIdleMutationsFormRow);
-
- let formColumnDiv = document.createElement('div');
- formRowDiv.appendChild(formColumnDiv);
-
- formColumnDiv.classList.add('form-columns', '-cols2');
-
- // Filter films
- const favoriteFilmsDiv = document.querySelector(SELECTORS.settings.favoriteFilms);
- const filteredFilmsDiv = favoriteFilmsDiv.cloneNode(true);
- tabPrimaryColumn.appendChild(filteredFilmsDiv);
-
- filteredFilmsDiv.style.cssText = 'margin-bottom: 20px;';
-
- const posterList = filteredFilmsDiv.querySelector(SELECTORS.settings.posterList);
- posterList.remove();
-
- filteredFilmsDiv.querySelector(SELECTORS.settings.subtitle).innerText = 'Films Filter';
- filteredFilmsDiv.querySelector(SELECTORS.settings.note).innerText =
- 'Right click to mark for removal.';
-
- let hiddenTitlesDiv = document.createElement('div');
- filteredFilmsDiv.append(hiddenTitlesDiv);
-
- const hiddenTitlesParagraph = document.createElement('p');
- hiddenTitlesDiv.appendChild(hiddenTitlesParagraph);
-
- hiddenTitlesDiv.classList.add('text-sluglist');
-
- const filmFilter = getFilter('filmFilter');
- log(VERBOSE, 'filmFilter', filmFilter);
-
- filmFilter.forEach((filteredFilm, index) => {
- log(VERBOSE, 'filteredFilm', filteredFilm);
-
- let filteredTitleLink = document.createElement('a');
- hiddenTitlesParagraph.appendChild(filteredTitleLink);
-
- if (filteredFilm.slug) filteredTitleLink.href= `/film/${filteredFilm.slug}`;
-
- filteredTitleLink.classList.add(
- 'text-slug',
- SELECTORS.processedClass.apply,
- SELECTORS.settings.filteredTitleLinkClass,
- );
- filteredTitleLink.setAttribute('data-film-id', filteredFilm.id);
- filteredTitleLink.setAttribute('index', index);
-
- let titleLinkText = filteredFilm.name;
- if (['', null, undefined].includes(filteredFilm.name)) {
- log(INFO, 'filteredFilm has no name; marking as broken', filteredFilm);
- titleLinkText = 'Broken, please remove';
- }
-
- if (!['', null, undefined].includes(filteredFilm.year)) {
- titleLinkText += ` (${filteredFilm.year})`;
- }
- filteredTitleLink.innerText = titleLinkText;
-
- filteredTitleLink.oncontextmenu = (event) => {
- event.preventDefault();
-
- filteredTitleLink.classList.toggle(SELECTORS.settings.removePendingClass);
- };
- });
-
- let formColumnsDiv = document.createElement('div');
- filteredFilmsDiv.appendChild(formColumnsDiv);
-
- formColumnsDiv.classList.add('form-columns', '-cols2');
-
- // Filter films behavior
- const filmBehaviorsMetadata = {
- fade: {
- fieldName: 'filmBehaviorFadeAmount',
- },
- blur: {
- fieldName: 'filmBehaviorBlurAmount',
- },
- replace: {
- fieldName: 'filmBehaviorReplaceValue',
- labelText: 'Direct image URL',
- },
- custom: {
- fieldName: 'filmBehaviorCustomValue',
- },
- };
- const filmFormRows = buildBehaviorFormRows(
- formColumnsDiv,
- 'film',
- FILM_BEHAVIORS,
- filmBehaviorsMetadata,
- );
-
- const clearDiv = filteredFilmsDiv.querySelector(SELECTORS.settings.clear);
- clearDiv.remove();
-
- // Filter reviews
- const filteredReviewsFormRow = document.createElement('div');
- tabPrimaryColumn.append(filteredReviewsFormRow);
-
- filteredReviewsFormRow.classList.add('form-row');
-
- const filteredReviewsTitle = document.createElement('h3');
- filteredReviewsFormRow.append(filteredReviewsTitle);
-
- filteredReviewsTitle.classList.add('title-3');
- filteredReviewsTitle.style.cssText = 'margin-top: 0em;';
- filteredReviewsTitle.innerText = 'Reviews Filter';
-
- // First unordered list
- const filteredReviewsUnorderedListFirst = document.createElement('ul');
- filteredReviewsFormRow.append(filteredReviewsUnorderedListFirst);
-
- filteredReviewsUnorderedListFirst.classList.add('options-list', '-toggle-list', 'js-toggle-list');
- filteredReviewsUnorderedListFirst.style.cssText += 'margin-bottom: 5px;';
-
- const reviewFilterItemsFirst = [
- {
- name: 'ratings',
- description: 'Remove ratings from reviews',
- },
- {
- name: 'likes',
- description: 'Remove likes from reviews',
- },
- {
- name: 'comments',
- description: 'Remove comments from reviews',
- },
- {
- name: 'byWordCount',
- description: 'Filter reviews by minimum word count',
- },
- ];
-
- buildToggleSectionListItems(
- 'reviewFilter',
- filteredReviewsUnorderedListFirst,
- reviewFilterItemsFirst,
- );
-
- // Minium word count
- let minimumWordCountDiv = document.createElement('div');
- filteredReviewsFormRow.appendChild(minimumWordCountDiv);
-
- minimumWordCountDiv.classList.add('form-columns', '-cols2');
-
- const minimumWordCountValue = gmcGet('reviewMinimumWordCount');
- log(DEBUG, 'minimumWordCountValue', minimumWordCountValue);
-
- const minimumWordCountFormRow = createFormRow({
- formRowClass: ['update-details'],
- formRowStyle: `width: ${COLUMN_TWO_WIDTH}; float: right; margin-bottom: 10px;`,
- inputValue: minimumWordCountValue,
- inputType: 'number',
- inputStyle: 'width: 100px !important;',
- notes: 'words',
- notesStyle: 'width: 10px; margin-left: 14px;',
- });
-
- minimumWordCountDiv.appendChild(minimumWordCountFormRow);
-
- // Second unordered list
- const filteredReviewsUnorderedListSecond = document.createElement('ul');
- filteredReviewsFormRow.append(filteredReviewsUnorderedListSecond);
-
- filteredReviewsUnorderedListSecond.classList.add('options-list', '-toggle-list', 'js-toggle-list');
- filteredReviewsUnorderedListSecond.style.cssText += 'margin: 0 0 1.53846154rem;';
-
- const reviewFilterItemsSecond = [
- {
- name: 'withSpoilers',
- description: 'Filter reviews that contain spoilers',
- },
- {
- name: 'withoutRatings',
- description: 'Filter reviews that don\'t have ratings',
- },
- ];
-
- buildToggleSectionListItems(
- 'reviewFilter',
- filteredReviewsUnorderedListSecond,
- reviewFilterItemsSecond,
- );
-
- let reviewColumnsDiv = document.createElement('div');
- filteredReviewsFormRow.appendChild(reviewColumnsDiv);
-
- reviewColumnsDiv.classList.add('form-columns', '-cols2');
-
- const reviewBehaviorsMetadata = {
- fade: {
- fieldName: 'reviewBehaviorFadeAmount',
- },
- blur: {
- fieldName: 'reviewBehaviorBlurAmount',
- },
- replace: {
- fieldName: 'reviewBehaviorReplaceValue',
- labelText: 'Text',
- },
- custom: {
- fieldName: 'reviewBehaviorCustomValue',
- },
- };
- const reviewFormRows = buildBehaviorFormRows(
- reviewColumnsDiv,
- 'review',
- REVIEW_BEHAVIORS,
- reviewBehaviorsMetadata,
- );
-
- // Filter homepage
- const homepageFilterMetadata = [
- {
- name: 'friendsHaveBeenWatching',
- description: 'Remove "Here\'s what your friends have been watching..." title text',
- },
- {
- name: 'newFromFriends',
- description: 'Remove "New from friends" films section',
- },
- {
- name: 'popularWithFriends',
- description: 'Remove "Popular with friends" section',
- },
- {
- name: 'discoveryStream',
- description: 'Remove discovery section (e.g. festivals, competitions)',
- },
- {
- name: 'latestNews',
- description: 'Remove "Latest news" section',
- },
- {
- name: 'popularReviewsWithFriends',
- description: 'Remove "Popular reviews with friends" section',
- },
- {
- name: 'newListsFromFriends',
- description: 'Remove "New from friends" lists section',
- },
- {
- name: 'popularLists',
- description: 'Remove "Popular lists" section',
- },
- {
- name: 'recentStories',
- description: 'Remove "Recent stories" section',
- },
- {
- name: 'recentShowdowns',
- description: 'Remove "Recent showdowns" section',
- },
- {
- name: 'recentNews',
- description: 'Remove "Recent news" section',
- },
- ];
-
- buildToggleSection(
- tabPrimaryColumn,
- 'Homepage Filter',
- 'homepageFilter',
- homepageFilterMetadata,
- );
-
- // Save changes
- let buttonsRowDiv = document.createElement('div');
- userscriptTabDiv.appendChild(buttonsRowDiv);
-
- buttonsRowDiv.style.cssText = 'display: flex; align-items: center;';
- buttonsRowDiv.classList.add('buttons', 'clear', 'row');
-
- let saveInput = document.createElement('input');
- buttonsRowDiv.appendChild(saveInput);
-
- saveInput.classList.add('button', 'button-action');
- saveInput.setAttribute('value', 'Save Changes');
- saveInput.setAttribute('type', 'submit');
- saveInput.onclick = (event) => {
- event.preventDefault();
-
- const pendingRemovals = hiddenTitlesParagraph.querySelectorAll(`.${SELECTORS.settings.removePendingClass}`);
- pendingRemovals.forEach(removalLink => {
- const id = parseInt(removalLink.getAttribute('data-film-id'));
- const filteredFilm = filmFilter.find(filteredFilm => filteredFilm.id === id);
-
- if (filteredFilm) {
- removeFilterFromFilm(filteredFilm);
- removeFromFilmFilter(filteredFilm);
- } else {
- const index = removalLink.getAttribute('index');
- removeFromFilmFilter(null, index);
- }
- removalLink.remove();
- });
-
- const minimumWordCountValue = parseInt(minimumWordCountFormRow.querySelector('input').value || 0);
- log(DEBUG, 'minimumWordCountValue', minimumWordCountValue);
-
- gmcSet('reviewMinimumWordCount', minimumWordCountValue);
-
- saveBehaviorSettings('film', filmFormRows);
- saveBehaviorSettings('review', reviewFormRows);
-
- const inputToggles = userscriptTabDiv.querySelectorAll('input[type="checkbox"]');
- inputToggles.forEach(inputToggle => {
- const filterName = inputToggle.getAttribute('data-filter-name');
- const filter = getFilter(filterName);
-
- const fieldName = inputToggle.getAttribute('data-field-name');
- const checked = inputToggle.checked;
-
- filter[fieldName] = checked;
- setFilter(filterName, filter);
- });
-
- const logLevel = logLevelFormRow.querySelector('select').value;
- gmcSet('logLevel', logLevel);
-
- const maxIdleMutationsValue = parseInt(maxIdleMutationsFormRow.querySelector('input').value || 0);
- log(DEBUG, 'maxIdleMutationsValue', maxIdleMutationsValue);
-
- gmcSet('maxIdleMutations', maxIdleMutationsValue);
-
- const maxActiveMutationsValue = parseInt(maxActiveMutationsFormRow.querySelector('input').value || 0);
- log(DEBUG, 'maxActiveMutationsValue', maxActiveMutationsValue);
-
- gmcSet('maxActiveMutations', maxActiveMutationsValue);
-
- gmcSave();
-
- displaySavedBadge();
- };
-
- let checkContainerDiv = document.createElement('div');
- buttonsRowDiv.appendChild(checkContainerDiv);
-
- checkContainerDiv.classList.add('check-container');
- checkContainerDiv.style.cssText = 'margin-left: 10px;';
-
- let usernameAvailableParagraph = document.createElement('p');
- checkContainerDiv.appendChild(usernameAvailableParagraph);
-
- usernameAvailableParagraph.classList.add(
- 'username-available',
- 'has-icon',
- 'hidden',
- SELECTORS.settings.savedBadgeClass,
- );
- usernameAvailableParagraph.style.cssText = 'float: left;';
-
- let iconSpan = document.createElement('span');
- usernameAvailableParagraph.appendChild(iconSpan);
-
- iconSpan.classList.add('icon');
-
- const savedText = document.createTextNode('Saved');
- usernameAvailableParagraph.appendChild(savedText);
-
- const settingsSubNav = document.querySelector(SELECTORS.settings.subNav);
-
- const userscriptSubNabListItem = document.createElement('li');
- settingsSubNav.appendChild(userscriptSubNabListItem);
-
- const userscriptSubNabLink = document.createElement('a');
- userscriptSubNabListItem.appendChild(userscriptSubNabLink);
-
- const userscriptSettingsLink = '/settings/?filterboxd';
- userscriptSubNabLink.setAttribute('href', userscriptSettingsLink);
- userscriptSubNabLink.setAttribute('data-id', 'filterboxd');
- userscriptSubNabLink.innerText = 'Filterboxd';
- userscriptSubNabLink.onclick = (event) => {
- event.preventDefault();
-
- Array.from(settingsSubNav.children).forEach(listItem => {
- const link = listItem.querySelector('a');
-
- if (link.getAttribute('data-id') === 'filterboxd') {
- listItem.classList.add('selected');
- } else {
- listItem.classList.remove('selected');
- }
- });
-
- Array.from(settingsTabbedContent.children).forEach(tab => {
- if (!tab.id) return;
-
- const display = tab.id === userscriptTabId ? 'block' : 'none';
- tab.style.cssText = `display: ${display};`;
- });
-
- window.history.replaceState(null, '', `https://letterboxd.com${userscriptSettingsLink}`);
- };
-
- Array.from(settingsSubNav.children).forEach(listItem => {
- listItem.onclick = (event) => {
- const link = event.target;
- if (link.getAttribute('href') === userscriptSettingsLink) return;
-
- userscriptSubNabListItem.classList.remove('selected');
- userscriptTabDiv.style.display = 'none';
- };
- });
- }
-
- function maybeAddListItemToSidebar() {
- log(DEBUG, 'maybeAddListItemToSidebar()');
-
- const isListPage = document.querySelector('body.list-page');
- if (isListPage) return;
-
- const userscriptListItemFound = document.querySelector(createId(SELECTORS.userpanel.userscriptListItemId));
- if (userscriptListItemFound) {
- log(DEBUG, 'Userscript list item already exists');
- return false;
- }
-
- const userpanel = document.querySelector(SELECTORS.userpanel.self);
-
- if (!userpanel) {
- log(INFO, 'Userpanel not found');
- return false;
- }
-
- const secondLastListItem = userpanel.querySelector('li:nth-last-child(3)');
- if (!secondLastListItem ) {
- log(INFO, 'Second last list item not found');
- return false;
- }
-
- if (secondLastListItem.classList.contains('loading-csi')) {
- log(INFO, 'Second last list item is loading');
- return false;
- }
-
- let userscriptListItem = document.createElement('li');
- const userscriptListLink = document.createElement('a');
- userscriptListItem.appendChild(userscriptListLink);
- userscriptListLink.href = '#';
-
- userscriptListItem.setAttribute('id', SELECTORS.userpanel.userscriptListItemId);
-
- const unorderedList = userpanel.querySelector('ul');
- userscriptListItem = buildUserscriptLink(userscriptListItem, unorderedList);
-
- // Text: "Go PATRON to change images"
- const upsellLink = userpanel.querySelector('[href="/pro/"]');
-
- // If the upsell link is present, insert above
- // Otherwise, inset above "Share"
- const insertBeforeElementIndex = upsellLink ? 2 : 1;
- const insertBeforeElement = userpanel.querySelector(`li:nth-last-of-type(${insertBeforeElementIndex})`);
-
- secondLastListItem.parentNode.insertBefore(userscriptListItem, insertBeforeElement);
-
- return true;
- }
-
- function removeFilterFromElement(element, levelsUp = 0) {
- log(DEBUG, 'removeFilterFromElement()');
-
- const replaceBehavior = gmcGet('filmBehaviorType') === 'Replace poster';
- log(VERBOSE, 'replaceBehavior', replaceBehavior);
-
- if (replaceBehavior) {
- const originalImgSrc = element.getAttribute('data-original-img-src');
- if (!originalImgSrc) {
- log(DEBUG, 'data-original-img-src attribute not found', element);
- return;
- }
-
- element.querySelector('img').src = originalImgSrc;
- element.querySelector('img').srcset = originalImgSrc;
-
- element.removeAttribute('data-original-img-src');
- element.classList.add(SELECTORS.processedClass.remove);
- element.classList.remove(SELECTORS.processedClass.apply);
- } else {
- let target = element;
-
- for (let i = 0; i < levelsUp; i++) {
- if (target.parentNode) {
- target = target.parentNode;
- } else {
- break;
- }
- }
-
- log(VERBOSE, 'target', target);
-
- target.classList.remove(SELECTORS.filter.filmClass);
- element.classList.add(SELECTORS.processedClass.remove);
- element.classList.remove(SELECTORS.processedClass.apply);
- }
- }
-
- function removeFromFilmFilter(filmMetadata, index) {
- log(DEBUG, 'removeFromFilmFilter()');
-
- let filmFilter = getFilter('filmFilter');
- if (filmMetadata) {
- filmFilter = filmFilter.filter(filteredFilm => filteredFilm.id !== filmMetadata.id);
- } else {
- filmFilter.splice(index, 1);
- }
-
- setFilter('filmFilter', filmFilter);
- }
-
- function removeFilterFromFilm({ id, slug }) {
- log(DEBUG, 'removeFilterFromFilm()');
-
- const idMatch = `[data-film-id="${id}"]`;
- let removedSelector = `.${SELECTORS.processedClass.remove}`;
-
- log(VERBOSE, 'Activity page reviews');
- document.querySelectorAll(`section.activity-row ${idMatch}`).forEach(posterElement => {
- removeFilterFromElement(posterElement, 3);
- });
-
- log(VERBOSE, 'Activity page likes');
- document.querySelectorAll(`section.activity-row .activity-summary a[href*="${slug}"]:not(${removedSelector})`).forEach(posterElement => {
- removeFilterFromElement(posterElement, 3);
- });
-
- log(VERBOSE, 'New from friends');
- document.querySelectorAll(`.poster-container ${idMatch}:not(${removedSelector})`).forEach(posterElement => {
- removeFilterFromElement(posterElement, 1);
- });
-
- log(VERBOSE, 'Reviews');
- document.querySelectorAll(`.review-tile ${idMatch}:not(${removedSelector})`).forEach(posterElement => {
- removeFilterFromElement(posterElement, 3);
- });
-
- log(VERBOSE, 'Diary');
- document.querySelectorAll(`.td-film-details [data-original-img-src]${idMatch}:not(${removedSelector})`).forEach(posterElement => {
- removeFilterFromElement(posterElement, 2);
- });
-
- log(VERBOSE, 'Popular with friends, competitions');
- const remainingElements = document.querySelectorAll(
- `div:not(.popmenu):not(.actions-panel) ${idMatch}:not(aside [data-film-id="${id}"]):not(#backdrop):not(${removedSelector})`,
- );
- remainingElements.forEach(posterElement => {
- removeFilterFromElement(posterElement, 0);
- });
- }
-
- function saveBehaviorSettings(filterName, formRows) {
- log(DEBUG, 'saveBehaviorSettings()');
-
- const behaviorType = formRows[0].querySelector('select').value;
- log(DEBUG, 'behaviorType', behaviorType);
-
- gmcSet(`${filterName}BehaviorType`, behaviorType);
-
- updateBehaviorCSSVariables(filterName, behaviorType);
-
- if (behaviorType === 'Fade') {
- const behaviorFadeAmount = formRows[1].querySelector('input').value;
- log(DEBUG, 'behaviorFadeAmount', behaviorFadeAmount);
-
- gmcSet(`${filterName}BehaviorFadeAmount`, behaviorFadeAmount);
- } else if (behaviorType === 'Blur') {
- const behaviorBlurAmount = formRows[2].querySelector('input').value;
- log(DEBUG, 'behaviorBlurAmount', behaviorBlurAmount);
-
- gmcSet(`${filterName}BehaviorBlurAmount`, behaviorBlurAmount);
- } else if (behaviorType.includes('Replace')) {
- const behaviorReplaceValue = formRows[3].querySelector('input').value;
- log(DEBUG, 'behaviorReplaceValue', behaviorReplaceValue);
-
- gmcSet(`${filterName}BehaviorReplaceValue`, behaviorReplaceValue);
- } else if (behaviorType === 'Custom') {
- const behaviorCustomValue = formRows[4].querySelector('input').value;
- log(DEBUG, 'behaviorCustomValue', behaviorCustomValue);
-
- gmcSet(`${filterName}BehaviorCustomValue`, behaviorCustomValue);
- }
- }
-
- function setFilter(filterName, filterValue) {
- log(DEBUG, 'setFilter()');
-
- gmcSet(filterName, JSON.stringify(filterValue));
- return gmcSave();
- }
-
- function updateBehaviorCSSVariables(filterName, behaviorType) {
- log(DEBUG, 'updateBehaviorTypeVariable()');
- log(DEBUG, 'behaviorType', behaviorType);
-
- const fadeValue = behaviorType === 'Fade' ? 'block' : 'none';
- document.documentElement.style.setProperty(
- `--filterboxd-${filterName}-behavior-fade`,
- fadeValue,
- );
-
- const blurValue = behaviorType === 'Blur' ? 'block' : 'none';
- document.documentElement.style.setProperty(
- `--filterboxd-${filterName}-behavior-blur`,
- blurValue,
- );
-
- const replaceValue = behaviorType.includes('Replace') ? 'block' : 'none';
- document.documentElement.style.setProperty(
- `--filterboxd-${filterName}-behavior-replace`,
- replaceValue,
- );
-
- const customValue = behaviorType === 'Custom' ? 'block' : 'none';
- document.documentElement.style.setProperty(
- `--filterboxd-${filterName}-behavior-custom`,
- customValue,
- );
- }
-
- function updateLinkInPopMenu(titleIsHidden, link) {
- log(DEBUG, 'updateLinkInPopMenu()');
-
- link.setAttribute('data-title-hidden', titleIsHidden);
-
- const innerText = titleIsHidden ? 'Remove from filter' : 'Add to filter';
- link.innerText = innerText;
- }
-
- const urlParams = new URLSearchParams(window.location.search);
- const tabSelected = urlParams.get('filterboxd') !== null;
- log(DEBUG, 'tabSelected', tabSelected);
-
- if (tabSelected) trySelectFilterboxdTab();
-
- let OBSERVER = new MutationObserver(observeAndModify);
-
- const GMC_FIELDS = {
- filmBehaviorType: {
- type: 'select',
- options: FILM_BEHAVIORS,
- default: 'Fade',
- },
- filmBehaviorBlurAmount: {
- type: 'int',
- default: 3,
- },
- filmBehaviorCustomValue: {
- type: 'text',
- default: '',
- },
- filmBehaviorFadeAmount: {
- type: 'int',
- default: 10,
- },
- filmBehaviorReplaceValue: {
- type: 'text',
- default: 'https://raw.githubusercontent.com/blakegearin/filterboxd/main/img/bee-movie.jpg',
- },
- filmFilter: {
- type: 'text',
- default: JSON.stringify([]),
- },
- filmPageFilter: {
- type: 'text',
- default: JSON.stringify({}),
- },
- homepageFilter: {
- type: 'text',
- default: JSON.stringify({}),
- },
- logLevel: {
- type: 'select',
- options: LOG_LEVELS.options,
- default: LOG_LEVELS.default,
- },
- reviewBehaviorType: {
- type: 'select',
- options: REVIEW_BEHAVIORS,
- default: 'Fade',
- },
- reviewBehaviorBlurAmount: {
- type: 'int',
- default: 3,
- },
- reviewBehaviorCustomValue: {
- type: 'text',
- default: '',
- },
- reviewBehaviorFadeAmount: {
- type: 'int',
- default: 10,
- },
- reviewBehaviorReplaceValue: {
- type: 'text',
- default: 'According to all known laws of aviation, there is no way a bee should be able to fly.',
- },
- reviewFilter: {
- type: 'text',
- default: JSON.stringify({}),
- },
- reviewMinimumWordCount: {
- type: 'int',
- default: 10,
- },
- maxIdleMutations: {
- type: 'int',
- default: 10000,
- },
- maxActiveMutations: {
- type: 'int',
- default: 10000,
- },
- };
-
- GMC = new GM_config({
- id: 'gmc-frame',
- events: {
- init: gmcInitialized,
- },
- fields: GMC_FIELDS,
- });
- })();