Letterboxd Custom Images

Customize letterboxd posters and backdrops without letterboxd PATRON

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Letterboxd Custom Images
// @description  Customize letterboxd posters and backdrops without letterboxd PATRON
// @author       Tetrax-10
// @namespace    https://github.com/Tetrax-10/letterboxd-custom-images
// @version      4.6
// @license      MIT
// @match        *://*.letterboxd.com/*
// @connect      themoviedb.org
// @homepageURL  https://github.com/Tetrax-10/letterboxd-custom-images
// @supportURL   https://github.com/Tetrax-10/letterboxd-custom-images/issues
// @icon         https://tetrax-10.github.io/letterboxd-custom-images/assets/icon.png
// @run-at       document-start
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_listValues
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

;(() => {
    // Register menu command to open settings popup
    GM_registerMenuCommand("Settings", showSettingsPopup)

    let currentPage = null

    // Retrieve logged-in username from cookies
    const loggedInAs =
        document.cookie
            ?.split("; ")
            ?.find((row) => row.startsWith("letterboxd.signed.in.as="))
            ?.split("=")[1]
            ?.toLowerCase() || null // Add null check for safety

    // Retrieve useMobileSite setting from cookies
    const isMobile =
        document.cookie
            ?.split("; ")
            ?.find((row) => row.startsWith("useMobileSite"))
            ?.split("=")[1]
            ?.toLowerCase() === "yes" || false

    // Default configuration settings
    const defaultConfig = {
        TMDB_API_KEY: "",

        FILM_DISPLAY_MISSING_BACKDROP: true,
        FILM_SHORT_BACKDROP: false,

        LIST_AUTO_SCRAPE: false,
        LIST_SHORT_BACKDROP: false,

        USER_AUTO_SCRAPE: false,
        USER_SHORT_BACKDROP: false,
        OTHER_USER_NO_AUTO_SCRAPE: true,

        PERSON_AUTO_SCRAPE: false,
        PERSON_SHORT_BACKDROP: false,

        REVIEW_SHORT_BACKDROP: true,
        REPLACE_OTHER_NON_PATRON_REVIEW: false,
        REPLACE_OTHER_PATRON_REVIEW: false,
    }

    // Initialize configuration with defaults if not already set
    try {
        const currentConfig = GM_getValue("CONFIG", {})
        if (currentConfig.FILM_SHORT_BACKDROP === undefined) {
            GM_setValue("CONFIG", defaultConfig)
            console.debug("Configuration initialized with default values.")
        } else {
            Object.entries(defaultConfig).forEach(([key, value]) => {
                if (currentConfig[key] === undefined) {
                    currentConfig[key] = value
                    console.debug("Configuration updated with default value for", key)
                }
            })
            GM_setValue("CONFIG", currentConfig)
        }
    } catch (error) {
        console.error("Error initializing configuration:", error)
    }

    // Function to get a specific configuration value
    function getConfigData(configId) {
        try {
            const config = GM_getValue("CONFIG", {})
            return config[configId]
        } catch (error) {
            console.error(`Error getting config data for ${configId}:`, error)
            return null
        }
    }

    // Function to set a specific configuration value
    function setConfigData(configId, value) {
        try {
            const config = GM_getValue("CONFIG", {})
            config[configId] = value
            GM_setValue("CONFIG", config)
            console.debug(`Config data for ${configId} updated.`)
        } catch (error) {
            console.error(`Error setting config data for ${configId}:`, error)
        }
    }

    // IndexedDB database variables
    let db = null
    let upgradeNeeded = false

    // Function to open the IndexedDB database
    function openDb() {
        return new Promise((resolve, reject) => {
            const request = indexedDB.open("ItemDataDB", 1)

            request.onupgradeneeded = (event) => {
                db = event.target.result
                if (!db.objectStoreNames.contains("itemData")) {
                    db.createObjectStore("itemData", { keyPath: "itemId" })
                    upgradeNeeded = true
                    console.debug("Database upgrade needed, object store created.")
                }
            }

            request.onsuccess = (event) => {
                db = event.target.result
                console.debug("Database connection established.")
                resolve(db)
            }

            request.onerror = (event) => {
                console.error("Error opening database:", event.target.errorCode)
                reject(event.target.errorCode)
            }
        })
    }

    // Function to get the database instance
    async function getDatabase() {
        if (!db)
            db = await openDb().catch((error) => {
                console.error("Failed to open database:", error)
                throw error
            })
        return db
    }

    // Initialize the database and migrate old data if needed
    getDatabase()
        .then(async () => {
            if (upgradeNeeded) {
                const ITEM_DATA = GM_getValue("ITEM_DATA", {})
                if (Object.keys(ITEM_DATA).length) {
                    await setItemData(ITEM_DATA).catch((error) => {
                        console.error("Failed to migrate old item data:", error)
                    })
                    console.debug("Old item data migrated.")
                }
            }

            // Clean up old stored values except for the configuration
            let allKeys = GM_listValues()
            for (let i = 0; i < allKeys.length; i++) {
                const key = allKeys[i]
                if (key !== "CONFIG") {
                    GM_deleteValue(key)
                    console.debug("Deleted old stored value:", key)
                }
            }
        })
        .catch((error) => {
            console.error("Failed to initialize database and migrate data:", error)
        })

    // Function to get item data from the database
    async function getItemData(itemId, dataType) {
        try {
            const db = await getDatabase()
            return new Promise((resolve, reject) => {
                const transaction = db.transaction("itemData", "readonly")
                const store = transaction.objectStore("itemData")

                if (!itemId) {
                    // Get all items if no itemId is provided
                    const request = store.getAll()
                    request.onsuccess = (event) => {
                        const items = event.target.result
                        const result = {}

                        items.forEach((item) => {
                            const id = item.itemId
                            delete item.itemId
                            result[id] = item
                        })
                        resolve(result)
                        console.debug("Retrieved all item data.")
                    }

                    request.onerror = (event) => {
                        console.error("Error retrieving all item data:", event.target.error)
                        reject(event.target.error)
                    }
                    return
                }

                const request = store.get(itemId)
                request.onsuccess = (event) => {
                    const itemData = event.target.result || {}
                    let value = itemData[dataType] ?? ""

                    // Handle specific data transformations based on dataType
                    switch (dataType) {
                        case "pu":
                        case "bu":
                            if (value.startsWith("t/")) {
                                value = `https://image.tmdb.org/t/p/original/${value.slice(2)}.jpg`
                            }
                            break
                        case "ty":
                            if (value === "m") {
                                value = "movie"
                            } else if (value === "t") {
                                value = "tv"
                            }
                            break
                    }

                    resolve(value)
                    console.debug(`Retrieved item data for ${itemId}, type: ${dataType}`)
                }

                request.onerror = (event) => {
                    console.error(`Error retrieving item data for ${itemId}:`, event.target.error)
                    reject(event.target.error)
                }
            })
        } catch (error) {
            console.error(`Error in getItemData for itemId ${itemId} and dataType ${dataType}:`, error)
            throw error
        }
    }

    // Function to set item data in the database
    async function setItemData(itemId, dataType, value) {
        try {
            const db = await getDatabase()
            return new Promise((resolve, reject) => {
                const transaction = db.transaction("itemData", "readwrite")
                const store = transaction.objectStore("itemData")

                store.get(typeof itemId === "object" ? "" : itemId).onsuccess = (event) => {
                    const itemData = event.target.result || {}

                    if (typeof itemId === "object") {
                        // If itemId is an object, assume it's a full data object to be inserted
                        Object.keys(itemId).forEach((id) => {
                            store.put({ itemId: id, ...itemId[id] })
                        })
                        console.debug("Bulk item data inserted.")
                        resolve()
                        return
                    }

                    const data = itemData || {}

                    // Handle specific data transformations based on dataType
                    if (!value) {
                        delete data[dataType]
                    } else {
                        switch (dataType) {
                            case "pu":
                            case "bu":
                                if (value.includes(".org/t/p/")) {
                                    const id = value.match(/\/([^\/]+)\.jpg$/)?.[1] ?? ""
                                    if (id) data[dataType] = `t/${id}`
                                } else {
                                    data[dataType] = value
                                }
                                break
                            case "ty":
                                if (value === "movie") {
                                    data[dataType] = "m"
                                } else {
                                    data[dataType] = "t"
                                }
                                break
                            default:
                                data[dataType] = value
                                break
                        }
                    }

                    store.put({ itemId, ...data })
                    console.debug(`Item data set for ${itemId}, type: ${dataType}`)
                    resolve()
                }

                transaction.onerror = (event) => {
                    console.error(`Error setting item data for ${itemId}:`, event.target.error)
                    reject(event.target.error)
                }
            })
        } catch (error) {
            console.error(`Error in setItemData for itemId ${itemId} and dataType ${dataType}:`, error)
            throw error
        }
    }

    GM_addStyle(`
        #lci-settings-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.5);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 10000;
            overflow: hidden;
        }
        #lci-settings-popup {
            background-color: rgb(32, 36, 44);
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            z-index: 10001;
            font-family: Source Sans Pro, Arial, sans-serif;
            font-feature-settings: normal;
            font-variation-settings: normal;
            font-size: 100%;
            font-weight: inherit;
            line-height: 1.5;
            letter-spacing: normal;
            width: ${isMobile ? "80%" : "50%"};
            max-height: 80vh;
            overflow-y: auto;
            display: flex;
            flex-direction: column;
            -webkit-overflow-scrolling: touch;
        }
        #lci-settings-popup[type="imageurlpopup"] {
            width: 80%;
        }
        body.lci-no-scroll {
            overflow: hidden;
        }
        #lci-settings-popup label {
            color: rgb(207, 207, 207);
            font-weight: bold;
            font-size: 1.2em;
            margin-bottom: 10px;
        }
        #lci-settings-popup input {
            background-color: rgb(32, 36, 44);
            border: 1px solid rgb(207, 207, 207);
            color: rgb(207, 207, 207);
            padding: 10px;
            border-radius: 8px;
            margin-bottom: 10px;
        }
        #lci-settings-popup button {
            background-color: rgb(76, 175, 80);
            color: white;
            padding: 10px;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-size: 16px;
            margin-bottom: 10px;
        }
        #lci-settings-popup .import-export-container {
            display: flex;
            justify-content: space-between;
            margin-top: 20px;
        }
        .lci-checkbox-container {
            display: flex;
            align-items: center;
        }
        .lci-checkbox-container input[type="checkbox"] {
            appearance: none;
            background-color: rgb(32, 36, 44);
            border: 1px solid rgb(207, 207, 207);
            border-radius: 4px;
            width: 20px;
            height: 20px;
            cursor: pointer;
            position: relative;
            margin-right: 10px;
            outline: none;
        }
        .lci-checkbox-container input[type="checkbox"]:checked {
            background-color: rgb(76, 175, 80);
            border: none;
        }
        .lci-checkbox-container input[type="checkbox"]:checked::after {
            content: '\\2714'; /* Unicode checkmark */
            color: white;
            font-size: 1em;
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
        }
        .lci-checkbox-container label {
            color: rgb(207, 207, 207);
            font-weight: bold;
            font-size: 1.2em;
        }
        #lci-image-grid {
            display: grid;
            grid-template-columns: repeat(${isMobile ? "1" : "3"}, 1fr);
            gap: 15px;
            margin-top: 20px;
        }
        #lci-image-grid.lci-poster-grid {
            grid-template-columns: repeat(${isMobile ? "2" : "5"}, 1fr);
        }
        .lci-image-item {
            cursor: pointer;
            border-radius: 8px;
            overflow: hidden;
            border: 2px solid transparent;
            transition: border-color 0.3s;
            position: relative;
        }
        .lci-image-item img {
            width: 100%;
            height: auto;
            display: block;
        }
        .lci-image-item:hover {
            border-color: rgb(76, 175, 80);
        }
        .lci-tooltip {
            visibility: hidden;
            background-color: rgba(32, 36, 44, 0.8);
            color: white;
            text-align: center;
            padding: 5px 10px;
            border-radius: 4px;
            position: absolute;
            bottom: 5px;
            left: 50%;
            transform: translateX(-50%);
            width: auto;
            white-space: nowrap;
            font-size: 0.9em;
            opacity: 0;
            transition: opacity 0.3s ease-in-out;
            box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.3);
        }
        .lci-image-item:hover .lci-tooltip {
            visibility: visible;
            opacity: 1;
        }
        #lci-loading-spinner {
            border: 4px solid rgba(255, 255, 255, 0.3);
            border-radius: 50%;
            border-top: 4px solid rgb(76, 175, 80);
            width: 40px;
            height: 40px;
            animation: spin 1s linear infinite;
            margin: 20px auto;
        }

        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        `)

    async function showImageUrlPopup({ itemId, targetedFilmId, filmElementSelector, mode = "backdrop" } = {}) {
        const modeName = mode === "poster" ? "Poster" : "Backdrop"
        const imageUrlKey = mode === "poster" ? "pu" : "bu"
        let hasInputValueChanged = false

        // Add the no-scroll class to the body
        document.body.classList.add("lci-no-scroll")

        // Create overlay for the popup
        const overlay = document.createElement("div")
        overlay.id = "lci-settings-overlay"
        overlay.onclick = (e) => {
            if (e.target === overlay) closePopup(overlay)
        }

        // Create popup container
        const popup = document.createElement("div")
        popup.id = "lci-settings-popup"
        popup.setAttribute("type", "imageurlpopup")

        // Add label for the input field
        const label = document.createElement("label")
        label.textContent = `Enter ${modeName} Image URL:`
        popup.appendChild(label)

        // Create input field for the URL
        const input = document.createElement("input")
        input.type = "text"
        try {
            input.value = await getItemData(itemId, imageUrlKey) // Retrieve existing image URL
        } catch (error) {
            console.error(`Failed to retrieve ${modeName} URL:`, error) // Log error if retrieval fails
            input.value = ""
        }
        input.placeholder = `${modeName} Image URL`
        if (!isMobile) input.autofocus = true
        input.oninput = () => {
            hasInputValueChanged = true
        }
        popup.appendChild(input)

        overlay.appendChild(popup)
        document.body.appendChild(overlay)

        // Focus on the input field after a short delay
        setTimeout(() => {
            if (!isMobile) input.focus()
        }, 100)

        async function updateImage(imageUrl, mode) {
            if (mode === "poster") {
                document.querySelectorAll(`.film-poster[data-item-link*="film/${itemId.slice(2)}"] .image`).forEach((posterImageElement) => {
                    injectPoster(posterImageElement, imageUrl)
                })
            } else if (mode === "backdrop" && currentPage !== "other") {
                const header = await waitForElement("#header")
                injectBackdrop(header, imageUrl, getConfigData(`${currentPage.toUpperCase()}_SHORT_BACKDROP`) ? ["shortbackdropped", "-crop"] : [])
            }
        }

        function closePopup(overlay) {
            if (hasInputValueChanged) {
                const imageUrl = document.querySelector(`input[placeholder="${modeName} Image URL"]`)?.value?.trim() || ""
                if (imageUrl) updateImage(imageUrl, mode)
                setItemData(itemId, imageUrlKey, imageUrl).catch((err) => {
                    console.error(`Failed to set ${modeName} URL:`, err)
                })
            }

            document.body.removeChild(overlay)
            // Remove the no-scroll class from the body
            document.body.classList.remove("lci-no-scroll")
        }

        // Exit if TMDB API key is not configured
        if (!getConfigData("TMDB_API_KEY")) return

        // Show loading spinner
        const spinner = document.createElement("div")
        spinner.id = "lci-loading-spinner"
        popup.appendChild(spinner)

        let filmId, tmdbIdType, tmdbId

        try {
            if (targetedFilmId) {
                // any item if but with targetedFilmId
                // "Set as item backdrop" context menu
                const targetedFilmTmdbId = await getItemData(targetedFilmId, "tId")
                if (targetedFilmTmdbId) {
                    filmId = targetedFilmId
                } else {
                    await scrapeFilmPage(targetedFilmId.slice(2))
                    filmId = targetedFilmId
                }
            } else if (itemId.startsWith("f/")) {
                // "Set film backdrop/poster" context menu
                const itemTmdbId = await getItemData(itemId, "tId")
                if (itemTmdbId) {
                    filmId = itemId
                } else {
                    await scrapeFilmPage(itemId.slice(2))
                    filmId = itemId
                }
            } else if (!itemId.startsWith("f/")) {
                // Set item backdrop menu
                const itemFilmId = await getItemData(itemId, "fId")
                const itemFilmTmdbId = await getItemData(itemFilmId, "tId")

                if (itemFilmTmdbId) {
                    filmId = itemFilmId
                } else {
                    await scrapeFilmLinkElement(filmElementSelector, true, itemId)
                    filmId = itemFilmId
                }
            }

            // Retrieve TMDB ID type and ID
            tmdbIdType = await getItemData(filmId, "ty")
            tmdbId = await getItemData(filmId, "tId")

            if (!tmdbIdType || !tmdbId) {
                console.error("TMDB ID or ID type is missing for filmId:", filmId) // Log missing ID error
                return
            }

            const imageGrid = document.createElement("div")
            imageGrid.id = "lci-image-grid"
            if (mode === "poster") imageGrid.className = "lci-poster-grid"
            popup.appendChild(imageGrid)

            async function getAllTmdbImages(tmdbIdType, tmdbId) {
                try {
                    const tmdbRawRes = await fetch(
                        `https://api.themoviedb.org/3/${tmdbIdType}/${tmdbId}/images?api_key=${getConfigData("TMDB_API_KEY")}`
                    )

                    if (!tmdbRawRes.ok) {
                        console.error(`Failed to fetch images from TMDB: ${tmdbRawRes.status} ${tmdbRawRes.statusText}`)
                        return []
                    }

                    const tmdbRes = await tmdbRawRes.json()

                    const images = tmdbRes[mode === "poster" ? "posters" : "backdrops"] || []

                    const localeImages = []
                    const nonLocaleImages = []

                    // Separate images into locale and non-locale
                    images.forEach((image) => {
                        if (!image.iso_639_1) {
                            nonLocaleImages.push(image)
                        } else {
                            localeImages.push(image)
                        }
                    })

                    // Group images by language
                    const postersByLanguage = localeImages.reduce((acc, image) => {
                        const language = image.iso_639_1
                        if (!acc[language]) acc[language] = []
                        acc[language].push(image)
                        return acc
                    }, {})

                    // Sort images by number of images in each language
                    const sortedLanguages = Object.keys(postersByLanguage).sort((a, b) => {
                        return postersByLanguage[b].length - postersByLanguage[a].length
                    })

                    const sortedLocaleImages = sortedLanguages.flatMap((language) => postersByLanguage[language])

                    return mode === "poster" ? [...sortedLocaleImages, ...nonLocaleImages] : [...nonLocaleImages, ...sortedLocaleImages]
                } catch (error) {
                    console.error("Error in getAllTmdbImages:", error)
                    return []
                }
            }

            let allImageUrls = await getAllTmdbImages(tmdbIdType, tmdbId)
            let currentRow = 0
            const columnsToLoad = isMobile ? 1 : mode === "poster" ? 5 : 3
            const rowsToLoad = 15 / columnsToLoad

            // Remove spinner and load initial images
            await loadImages()
            spinner.remove()

            async function loadImages() {
                const nextImages = allImageUrls.slice(currentRow * columnsToLoad, (currentRow + rowsToLoad) * columnsToLoad)
                nextImages.forEach((image) => {
                    const imageUrl = `https://image.tmdb.org/t/p/original${image.file_path}`

                    const imageItem = document.createElement("div")
                    imageItem.className = "lci-image-item"
                    if (imageUrl === input.value) imageItem.style.borderColor = "#40bcf4"

                    const img = document.createElement("img")
                    img.src = imageUrl.replace("original", mode === "poster" ? "w342" : "w780")
                    imageItem.appendChild(img)

                    // Create tooltip with image metadata
                    const tooltip = document.createElement("div")
                    tooltip.className = "lci-tooltip"
                    tooltip.textContent = `${image.width && image.height ? `${image.width} × ${image.height}` : ""}${
                        image.iso_639_1 ? ` • ${image.iso_639_1}` : ""
                    }`
                    if (tooltip.textContent) imageItem.appendChild(tooltip)

                    imageItem.onclick = () => {
                        hasInputValueChanged = false
                        updateImage(imageUrl, mode)
                        setItemData(itemId, imageUrlKey, imageUrl).catch((err) => {
                            console.error(`Failed to set ${modeName} URL:`, err)
                        })
                        closePopup(overlay)
                    }
                    imageGrid.appendChild(imageItem)
                })

                currentRow += rowsToLoad
            }

            // Auto-load more images when scrolling to the bottom using IntersectionObserver
            const observer = new IntersectionObserver((entries) => {
                if (entries[0].isIntersecting) {
                    if (currentRow * columnsToLoad < allImageUrls.length) {
                        loadImages()
                    } else {
                        // Disconnect the observer when all images are loaded
                        observer.disconnect()
                    }
                }
            })

            // Create a sentinel element at the bottom of the image grid to trigger loading
            const sentinel = document.createElement("div")
            sentinel.id = "lci-sentinel"
            popup.appendChild(sentinel)

            observer.observe(sentinel)
        } catch (error) {
            console.error("An error occurred while setting up the image URL popup:", error)
        }
    }

    function showSettingsPopup() {
        // Add the no-scroll class to the body
        document.body.classList.add("lci-no-scroll")

        // Create overlay for the settings popup
        const overlay = document.createElement("div")
        overlay.id = "lci-settings-overlay"
        overlay.onclick = (e) => {
            if (e.target === overlay) closePopup(overlay)
        }

        const popup = document.createElement("div")
        popup.id = "lci-settings-popup"

        // Helper function to create label elements
        function createLabelElement(text) {
            const label = document.createElement("label")
            label.textContent = text
            popup.appendChild(label)
        }

        // Helper function to create input elements
        function createInputElement(name, id, placeholder) {
            createLabelElement(name)

            const input = document.createElement("input")
            input.type = "text"
            input.value = getConfigData(id)
            input.placeholder = placeholder
            input.oninput = (e) => {
                const value = e.target.value?.trim()
                setConfigData(id, value).catch((err) => {
                    console.error(`Failed to set config data for ${id}:`, err) // Log error if setting data fails
                })
            }
            popup.appendChild(input)
        }

        // Helper function to create checkbox elements
        function createCheckboxElement(labelText, id) {
            const container = document.createElement("div")
            container.className = "lci-checkbox-container"

            const checkbox = document.createElement("input")
            checkbox.type = "checkbox"
            checkbox.checked = getConfigData(id)
            checkbox.onchange = (e) => {
                setConfigData(id, e.target.checked).catch((err) => {
                    console.error(`Failed to set config data for ${id}:`, err) // Log error if setting data fails
                })
            }
            container.appendChild(checkbox)

            const label = document.createElement("label")
            label.textContent = labelText
            container.appendChild(label)

            popup.appendChild(container)
        }

        function createSpaceComponent() {
            const space = document.createElement("div")
            space.style.marginBottom = "10px"
            popup.appendChild(space)
        }

        // Export settings to a JSON file
        async function exportSettings() {
            try {
                const settings = {
                    CONFIG: GM_getValue("CONFIG", {}),
                    ITEM_DATA: await getItemData(),
                }

                // Create a data URL for the JSON file
                const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(settings, null, 2))
                const downloadAnchor = document.createElement("a")
                downloadAnchor.setAttribute("href", dataStr)
                downloadAnchor.setAttribute("download", "lciSettings.json")
                document.body.appendChild(downloadAnchor)
                downloadAnchor.click()
                document.body.removeChild(downloadAnchor)
            } catch (error) {
                console.error("Failed to export settings:", error) // Log error if export fails
            }
        }

        // Import settings from a JSON file
        function importSettings(event) {
            const file = event.target.files[0]
            if (!file) return

            const reader = new FileReader()
            reader.onload = (e) => {
                const content = e.target.result
                try {
                    const settings = JSON.parse(content)

                    GM_setValue("CONFIG", settings.CONFIG || {})
                    setItemData(settings.ITEM_DATA || {}).catch((err) => {
                        console.error("Failed to import item data:", err) // Log error if importing data fails
                    })

                    // Refresh the popup to reflect imported settings
                    closePopup(overlay)
                    showSettingsPopup()
                } catch (err) {
                    console.error("Failed to import settings:", err) // Log error if JSON parsing fails
                    alert("Failed to import settings: Invalid JSON file.")
                }
            }
            reader.onerror = (err) => {
                console.error("Error reading import file:", err) // Log error if file reading fails
                alert("Failed to read import file.")
            }
            reader.readAsText(file)
        }

        // UI Elements
        createInputElement(
            "Enter your TMDB API key to display missing film backdrops and get the ability to select backdrops from UI:",
            "TMDB_API_KEY",
            "TMDB API Key"
        )
        createSpaceComponent()

        createLabelElement("Film Page:")
        createCheckboxElement("Display missing backdrop for less popular films", "FILM_DISPLAY_MISSING_BACKDROP")
        createCheckboxElement("Short backdrops", "FILM_SHORT_BACKDROP")
        createSpaceComponent()

        createLabelElement("List Page:")
        createCheckboxElement("Auto scrape backdrops", "LIST_AUTO_SCRAPE")
        createCheckboxElement("Short backdrops", "LIST_SHORT_BACKDROP")
        createSpaceComponent()

        createLabelElement("User Page:")
        createCheckboxElement("Auto scrape backdrops", "USER_AUTO_SCRAPE")
        createCheckboxElement("Short backdrops", "USER_SHORT_BACKDROP")
        createCheckboxElement("Don't auto scrape backdrops for other users", "OTHER_USER_NO_AUTO_SCRAPE")
        createSpaceComponent()

        createLabelElement("Person Page:")
        createCheckboxElement("Auto scrape backdrops", "PERSON_AUTO_SCRAPE")
        createCheckboxElement("Short backdrops", "PERSON_SHORT_BACKDROP")
        createSpaceComponent()

        createLabelElement("Review Page:")
        createCheckboxElement("Short backdrops", "REVIEW_SHORT_BACKDROP")
        createCheckboxElement("Use my custom backdrop on non-patron user's review", "REPLACE_OTHER_NON_PATRON_REVIEW")
        createCheckboxElement("Use my custom backdrop on patron user's review", "REPLACE_OTHER_PATRON_REVIEW")
        createSpaceComponent()

        // Import/Export Buttons
        const importExportContainer = document.createElement("div")
        importExportContainer.className = "import-export-container"

        const exportButton = document.createElement("button")
        exportButton.textContent = "Export Settings"
        exportButton.onclick = exportSettings

        const importButton = document.createElement("button")
        importButton.textContent = "Import Settings"
        importButton.onclick = () => {
            const fileInput = document.createElement("input")
            fileInput.type = "file"
            fileInput.accept = ".json"
            fileInput.onchange = importSettings
            fileInput.click()
        }

        importExportContainer.appendChild(exportButton)
        importExportContainer.appendChild(importButton)
        popup.appendChild(importExportContainer)

        overlay.appendChild(popup)
        document.body.appendChild(overlay)

        function closePopup(overlay) {
            document.body.removeChild(overlay)
            // Remove the no-scroll class from the body
            document.body.classList.remove("lci-no-scroll")
        }
    }

    async function waitForElement(selector, timeout = null, nthElement = 1) {
        // wait till document body loads
        while (!document.body) {
            await new Promise((resolve) => setTimeout(resolve, 10))
        }

        nthElement -= 1

        return new Promise((resolve) => {
            if (document.querySelectorAll(selector)?.[nthElement]) {
                return resolve(document.querySelectorAll(selector)?.[nthElement])
            }

            const observer = new MutationObserver(async () => {
                if (document.querySelectorAll(selector)?.[nthElement]) {
                    resolve(document.querySelectorAll(selector)?.[nthElement])
                    observer.disconnect()
                } else {
                    if (timeout) {
                        async function timeOver() {
                            return new Promise((resolve) => {
                                setTimeout(() => {
                                    observer.disconnect()
                                    resolve(false)
                                }, timeout)
                            })
                        }
                        resolve(await timeOver())
                    }
                }
            })

            observer.observe(document.body, {
                childList: true,
                subtree: true,
            })
        })
    }

    async function getTmdbBackdrop(tmdbIdType, tmdbId) {
        if (!getConfigData("TMDB_API_KEY")) {
            console.error("TMDB API key is not configured.") // Log missing API key
            return null
        }

        try {
            const tmdbRawRes = await fetch(`https://api.themoviedb.org/3/${tmdbIdType}/${tmdbId}/images?api_key=${getConfigData("TMDB_API_KEY")}`)
            if (!tmdbRawRes.ok) {
                console.error(`Failed to fetch TMDB backdrops: ${tmdbRawRes.statusText}`) // Log HTTP error
                return null
            }

            const tmdbRes = await tmdbRawRes.json()
            const imageId = tmdbRes.backdrops?.[0]?.file_path

            return imageId ? `https://image.tmdb.org/t/p/original${imageId}` : null
        } catch (error) {
            console.error("Error fetching TMDB backdrop:", error) // General error catch
            return null
        }
    }

    async function isDefaultBackdropAvailable(dom) {
        let defaultBackdropElement
        if (dom) {
            defaultBackdropElement = dom.querySelector("#backdrop")
        } else {
            defaultBackdropElement = document.querySelector("#backdrop")
            if (!defaultBackdropElement) {
                try {
                    defaultBackdropElement = await waitForElement("#backdrop", 100)
                } catch (error) {
                    console.error("Failed to find default backdrop element:", error) // Log element not found
                    return false
                }
            }
        }

        const defaultBackdropUrl =
            defaultBackdropElement?.dataset?.backdrop2x ||
            defaultBackdropElement?.dataset?.backdrop ||
            defaultBackdropElement?.dataset?.backdropMobile

        if (defaultBackdropUrl?.includes("https://a.ltrbxd.com/resized/sm/upload")) {
            return defaultBackdropUrl
        }
        return false
    }

    async function extractBackdropUrlFromLetterboxdFilmPage(filmId, dom, shouldScrape = true) {
        try {
            const filmBackdropUrl = await isDefaultBackdropAvailable(dom)

            // Get TMDB ID and type
            let tmdbElement
            if (dom) {
                tmdbElement = dom.querySelector(`.micro-button.track-event[data-track-action="TMDB"]`)
            } else {
                tmdbElement = await waitForElement(`.micro-button.track-event[data-track-action="TMDB"]`, 5000)
            }

            const tmdbIdType = tmdbElement.href?.match(/\/(movie|tv)\/(\d+)\//)?.[1] ?? null
            const tmdbId = tmdbElement.href?.match(/\/(movie|tv)\/(\d+)\//)?.[2] ?? null

            if (tmdbIdType && tmdbId) {
                await setItemData(filmId, "ty", tmdbIdType)
                await setItemData(filmId, "tId", tmdbId)
            }

            if (!filmBackdropUrl && !document.querySelector(`#lci-settings-popup[type="imageurlpopup"]`) && shouldScrape) {
                return await getTmdbBackdrop(tmdbIdType, tmdbId)
            }

            return filmBackdropUrl
        } catch (error) {
            console.error("Error extracting backdrop URL from Letterboxd film page:", error) // General error catch
            return null
        }
    }

    function scrapeFilmPage(filmName) {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: `https://letterboxd.com/film/${filmName}/`,
                onload: async function (response) {
                    try {
                        const parser = new DOMParser()
                        const dom = parser.parseFromString(response.responseText, "text/html")

                        // Resolve with URL and cache status
                        resolve([await extractBackdropUrlFromLetterboxdFilmPage(`f/${filmName}`, dom), false])
                    } catch (error) {
                        console.error("Error parsing or extracting backdrop from Letterboxd page:", error) // General error catch
                        resolve([null, false])
                    }
                },
                onerror: function (error) {
                    console.error(`Can't scrape Letterboxd page: ${filmName}`, error) // Log scraping error
                    resolve([null, false])
                },
            })
        })
    }

    async function scrapeFilmLinkElement(selector, shouldScrape, itemId) {
        try {
            const firstPosterElement = await waitForElement(selector, 2000)
            if (!firstPosterElement) return [null, false]

            const filmName = firstPosterElement.href?.match(/\/film\/([^\/]+)/)?.[1]
            const filmId = `f/${filmName}`

            if (!itemId.startsWith("f/")) await setItemData(itemId, "fId", filmId)

            const cacheBackdrop = await getItemData(filmId, "bu")

            if (cacheBackdrop) {
                return [cacheBackdrop, true]
            } else if (!shouldScrape) {
                return [null, false]
            } else {
                return await scrapeFilmPage(filmName)
            }
        } catch (error) {
            console.error("Error scraping film link element:", error) // General error catch
            return [null, false]
        }
    }

    function injectPoster(posterImageElement, imageUrl) {
        let posterSize = posterImageElement.src.includes("0-70-0-105-crop") ? "w154" : "original"
        posterSize = posterImageElement.src.includes("0-150-0-225-crop") ? "w342" : posterSize
        posterSize = posterImageElement.src.includes("0-230-0-345-crop") ? "w500" : posterSize

        imageUrl = imageUrl.replace("original", posterSize)
        posterImageElement.src = imageUrl
        posterImageElement.srcset = imageUrl
    }

    function injectBackdrop(header, backdropUrl, attributes = []) {
        try {
            // Get or inject backdrop containers
            const backdropContainer =
                // For patron users who already have a backdrop
                document.querySelector(".backdrop-container") ||
                // For non-patron users
                Object.assign(document.createElement("div"), { className: "backdrop-container" })

            // Inject necessary classes
            document.body.classList.add("backdropped", "backdrop-loaded", ...attributes)
            document.getElementById("content")?.classList.add("-backdrop")

            // Ensure .-backdrop is added to #content if missed before
            const intervalId = setInterval(() => document.getElementById("content")?.classList.add("-backdrop"), 100)
            setTimeout(() => clearInterval(intervalId), 5000)

            // Inject backdrop child
            backdropContainer.innerHTML = `
                <div id="backdrop" class="backdrop-wrapper -loaded" data-backdrop="${backdropUrl}" data-backdrop2x="${backdropUrl}" data-backdrop-mobile="${backdropUrl}" data-offset="0">
                    <div class="backdropimage js-backdrop-image" style="background-image: url(${backdropUrl}); background-position: center 0px;"></div>
                    <div class="backdropmask js-backdrop-fade"></div>
                </div>`

            header.before(backdropContainer)
        } catch (error) {
            console.error("Error injecting backdrop:", error) // General error catch
        }
    }

    async function injectContextMenuToAllFilmPosterItems({ itemId, name } = {}) {
        if (isMobile) return

        function addFilmOption({ contextmenu, className, name, onClick = () => {}, itemId = undefined } = {}) {
            try {
                const activityMenuElement = contextmenu.querySelector(`.popmenu-textitem:has(> a[href$="/activity/"])`)
                const filmName = activityMenuElement?.firstElementChild?.href?.match(/\/film\/([^\/]+)/)?.[1]

                const imageMenuElement = document.createElement("li")
                imageMenuElement.classList.add(className, "popmenu-textitem", "-centered")

                const imageMenuLinkElement = document.createElement("a")
                imageMenuLinkElement.style.cursor = "pointer"
                imageMenuLinkElement.textContent = name
                imageMenuElement.onclick = () => {
                    contextmenu.setAttribute("hidden", "")
                    onClick(filmName, itemId)
                }

                imageMenuElement.appendChild(imageMenuLinkElement)
                activityMenuElement.parentNode.insertBefore(imageMenuElement, activityMenuElement)
            } catch (error) {
                console.error("Error adding film option to context menu:", error) // General error catch
            }
        }

        try {
            const observer = new MutationObserver(() => {
                const contextmenuSelector = "body > div > .popmenu.poster-popmenu:not([contextmenu-processed])"

                if (!document.querySelector(contextmenuSelector)) return

                const allContextmenu = document.querySelectorAll(contextmenuSelector)
                for (const contextmenu of allContextmenu) {
                    contextmenu.setAttribute("contextmenu-processed", "")
                    if (itemId) {
                        addFilmOption({
                            contextmenu,
                            className: "fm-set-as-item-backdrop",
                            name: `Set as ${name} backdrop`,
                            onClick: (filmName, itemId) => showImageUrlPopup({ itemId: itemId, targetedFilmId: `f/${filmName}` }),
                            itemId: itemId,
                        })
                    }
                    addFilmOption({
                        contextmenu,
                        className: "fm-set-film-backdrop",
                        name: "Set film backdrop",
                        onClick: (filmName) => showImageUrlPopup({ itemId: `f/${filmName}` }),
                    })
                    addFilmOption({
                        contextmenu,
                        className: "fm-set-film-poster",
                        name: "Set film poster",
                        onClick: (filmName) => showImageUrlPopup({ itemId: `f/${filmName}`, mode: "poster" }),
                    })
                }
            })

            await waitForElement("body")
            observer.observe(document.body, { childList: true })
        } catch (error) {
            console.error("Error injecting context menu to all film poster items:", error) // General error catch
        }
    }

    async function filmPageMenuInjector({ filmId, mode } = {}) {
        const yourActivityMenuItem = await waitForElement(`ul.js-actions-panel > li:has(a[href*="/activity/"])`, 5000)

        const setFilmImageMenuItem = document.createElement("li")

        const anchor = document.createElement("a")
        anchor.textContent = `Set film ${mode}`
        anchor.style.cursor = "pointer"
        anchor.onclick = () => showImageUrlPopup({ itemId: filmId, mode })

        setFilmImageMenuItem.appendChild(anchor)
        yourActivityMenuItem.parentNode.insertBefore(setFilmImageMenuItem, yourActivityMenuItem)
    }

    async function filmPageInjector() {
        try {
            const filmId = `f/${location.pathname.split("/")?.[2]}`

            const header = await waitForElement("#header")
            filmPageMenuInjector({ filmId, mode: "backdrop" })
            filmPageMenuInjector({ filmId, mode: "poster" })
            injectContextMenuToAllFilmPosterItems()

            const cacheBackdrop = await getItemData(filmId, "bu")

            async function scrapeTmdbIdAndType() {
                try {
                    // Extracts TMDB ID and type
                    const tmdbElement = await waitForElement(`.micro-button.track-event[data-track-action="TMDB"]`, 5000)
                    const tmdbIdType = tmdbElement.href?.match(/\/(movie|tv)\/(\d+)\//)?.[1] ?? null
                    const tmdbId = tmdbElement.href?.match(/\/(movie|tv)\/(\d+)\//)?.[2] ?? null

                    if (tmdbIdType && tmdbId) {
                        await setItemData(filmId, "ty", tmdbIdType)
                        await setItemData(filmId, "tId", tmdbId)
                    }
                } catch (error) {
                    console.error("Error scraping TMDB ID and type:", error) // General error catch
                }
            }

            if (cacheBackdrop) {
                // Inject backdrop
                injectBackdrop(header, cacheBackdrop, getConfigData("FILM_SHORT_BACKDROP") ? ["shortbackdropped", "-crop"] : [])
                scrapeTmdbIdAndType()
                return
            }

            // If original backdrop is available then return
            if (await isDefaultBackdropAvailable()) {
                scrapeTmdbIdAndType()
                return
            }

            if (getConfigData("TMDB_API_KEY") && getConfigData("FILM_DISPLAY_MISSING_BACKDROP")) {
                const backdropUrl = await extractBackdropUrlFromLetterboxdFilmPage(filmId)

                // Inject backdrop
                if (backdropUrl) {
                    injectBackdrop(header, backdropUrl, getConfigData("FILM_SHORT_BACKDROP") ? ["shortbackdropped", "-crop"] : [])
                    await setItemData(filmId, "bu", backdropUrl)
                }
            } else {
                await extractBackdropUrlFromLetterboxdFilmPage(filmId, undefined, false)
            }
        } catch (error) {
            console.error("Error in film page injector:", error) // General error catch
        }
    }

    async function userPageMenuInjector(userId, filmElementSelector) {
        const copyLinkMenuItem = await waitForElement(`.menuitem:has(> button[data-menuitem-trigger="clipboard"])`, 5000)

        const setUserBackdropMenuItem = document.createElement("div")
        setUserBackdropMenuItem.classList.add("menuitem", "-trigger", "-has-icon", "js-menuitem")
        setUserBackdropMenuItem.role = "none"

        const setUserBackdropMenuButton = document.createElement("button")
        setUserBackdropMenuButton.type = "button"
        setUserBackdropMenuButton.role = "menuitem"
        setUserBackdropMenuButton.setAttribute("data-dismiss", "dropdown")
        setUserBackdropMenuButton.onclick = () => showImageUrlPopup({ itemId: userId, filmElementSelector: filmElementSelector })
        setUserBackdropMenuButton.innerHTML = `
            <svg class="glyph" role="presentation" width="8" height="8" viewBox="0 0 16 16" style="margin-bottom: 6px">
                <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
                    <g id="Icon-Set" sketch:type="MSLayerGroup" transform="translate(-360.000000, -99.000000)" fill="currentColor">
                        <path
                            d="M368,109 C366.896,109 366,108.104 366,107 C366,105.896 366.896,105 368,105 C369.104,105 370,105.896 370,107 C370,108.104 369.104,109 368,109 L368,109 Z M368,103 C365.791,103 364,104.791 364,107 C364,109.209 365.791,111 368,111 C370.209,111 372,109.209 372,107 C372,104.791 370.209,103 368,103 L368,103 Z M390,116.128 L384,110 L374.059,120.111 L370,116 L362,123.337 L362,103 C362,101.896 362.896,101 364,101 L388,101 C389.104,101 390,101.896 390,103 L390,116.128 L390,116.128 Z M390,127 C390,128.104 389.104,129 388,129 L382.832,129 L375.464,121.535 L384,112.999 L390,118.999 L390,127 L390,127 Z M364,129 C362.896,129 362,128.104 362,127 L362,126.061 L369.945,118.945 L380.001,129 L364,129 L364,129 Z M388,99 L364,99 C361.791,99 360,100.791 360,103 L360,127 C360,129.209 361.791,131 364,131 L388,131 C390.209,131 392,129.209 392,127 L392,103 C392,100.791 390.209,99 388,99 L388,99 Z"
                            id="image-picture"
                            sketch:type="MSShapeGroup"
                        ></path>
                    </g>
                </g>
            </svg>
            <span class="label">Set user backdrop</span>
            `

        setUserBackdropMenuItem.appendChild(setUserBackdropMenuButton)
        copyLinkMenuItem.parentNode.insertBefore(setUserBackdropMenuItem, copyLinkMenuItem.nextSibling)
    }

    async function userPageInjector() {
        try {
            const userName = location.pathname.split("/")?.[1]?.toLowerCase()
            const userId = `u/${userName}`
            const filmElementSelector = "#favourites .poster-list > li:first-child a"

            const isCurrentUser = userName === loggedInAs
            const cacheBackdrop = await getItemData(userId, "bu")
            const header = await waitForElement("#header")
            userPageMenuInjector(userId, filmElementSelector)
            injectContextMenuToAllFilmPosterItems({ itemId: userId, name: "user" })

            if (cacheBackdrop) {
                injectBackdrop(header, cacheBackdrop, getConfigData("USER_SHORT_BACKDROP") ? ["shortbackdropped", "-crop"] : [])
                await scrapeFilmLinkElement(filmElementSelector, false, userId)
                return
            }

            if (await isDefaultBackdropAvailable()) {
                await scrapeFilmLinkElement(filmElementSelector, false, userId)
                return
            }

            const [scrapedImage, isCached] = await scrapeFilmLinkElement(
                filmElementSelector,
                isCurrentUser ? getConfigData("USER_AUTO_SCRAPE") : !getConfigData("OTHER_USER_NO_AUTO_SCRAPE"),
                userId
            )

            if (scrapedImage && (isCurrentUser || (!isCurrentUser && !getConfigData("OTHER_USER_NO_AUTO_SCRAPE")))) {
                injectBackdrop(header, scrapedImage, getConfigData("USER_SHORT_BACKDROP") ? ["shortbackdropped", "-crop"] : [])

                if (!isCached) {
                    await setItemData(userId, "bu", scrapedImage)
                }
            }
        } catch (error) {
            console.error("Error in userPageInjector:", error)
        }
    }

    async function listPageMenuInjector(listId, filmElementSelector) {
        const likeMenuItem = await waitForElement("li.like-link-target", 5000)

        const setListBackdropMenuItem = document.createElement("li")

        const setListBackdropLink = document.createElement("a")
        setListBackdropLink.textContent = "Set list backdrop"
        setListBackdropLink.style.cursor = "pointer"
        setListBackdropLink.onclick = () => showImageUrlPopup({ itemId: listId, filmElementSelector: filmElementSelector })

        setListBackdropMenuItem.appendChild(setListBackdropLink)
        likeMenuItem.parentNode.insertBefore(setListBackdropMenuItem, likeMenuItem.nextSibling)
    }

    async function listPageInjector() {
        try {
            const listId = `l/${location.pathname.split("/")?.[1]?.toLowerCase()}/${location.pathname.split("/")?.[3]}`
            const filmElementSelector = ".poster-list > li:first-child a"

            const cacheBackdrop = await getItemData(listId, "bu")
            const header = await waitForElement("#header")
            listPageMenuInjector(listId, filmElementSelector)
            injectContextMenuToAllFilmPosterItems({ itemId: listId, name: "list" })

            if (!getConfigData("LIST_SHORT_BACKDROP")) {
                document.body.classList.remove("shortbackdropped", "-crop")
            }

            if (cacheBackdrop) {
                injectBackdrop(header, cacheBackdrop, getConfigData("LIST_SHORT_BACKDROP") ? ["shortbackdropped", "-crop"] : [])
                await scrapeFilmLinkElement(filmElementSelector, false, listId)
                return
            }

            if (await isDefaultBackdropAvailable()) {
                await scrapeFilmLinkElement(filmElementSelector, false, listId)
                return
            }

            const [scrapedImage, isCached] = await scrapeFilmLinkElement(filmElementSelector, getConfigData("LIST_AUTO_SCRAPE"), listId)

            if (scrapedImage) {
                injectBackdrop(header, scrapedImage, getConfigData("LIST_SHORT_BACKDROP") ? ["shortbackdropped", "-crop"] : [])

                if (!isCached) {
                    await setItemData(listId, "bu", scrapedImage)
                }
            }
        } catch (error) {
            console.error("Error in listPageInjector:", error)
        }
    }

    async function personPageMenuInjector(personId, filmElementSelector) {
        const personImageElement = await waitForElement(".person-image", 5000)

        const setPersonBackdropButton = document.createElement("button")
        setPersonBackdropButton.style.borderRadius = "4px"
        setPersonBackdropButton.style.width = "100%"
        setPersonBackdropButton.style.border = "1px solid hsla(0,0%,100%,0.25)"
        setPersonBackdropButton.style.backgroundColor = "transparent"
        setPersonBackdropButton.style.color = "#9ab"
        setPersonBackdropButton.style.height = "40px"
        setPersonBackdropButton.style.cursor = "pointer"
        setPersonBackdropButton.style.fontFamily = "Graphik-Regular-Web, sans-serif"
        setPersonBackdropButton.textContent = "Set person backdrop"
        setPersonBackdropButton.addEventListener("mouseenter", () => {
            setPersonBackdropButton.style.color = "#def"
        })
        setPersonBackdropButton.addEventListener("mouseleave", () => {
            setPersonBackdropButton.style.color = "#9ab"
        })
        setPersonBackdropButton.onclick = () => showImageUrlPopup({ itemId: personId, filmElementSelector: filmElementSelector })

        personImageElement.parentNode.insertBefore(setPersonBackdropButton, personImageElement.nextSibling)
    }

    async function personPageInjector() {
        try {
            const personId = `p/${location.pathname.split("/")?.[2]}`
            const filmElementSelector = ".grid > li:first-child a"

            const cacheBackdrop = await getItemData(personId, "bu")
            const header = await waitForElement("#header")
            personPageMenuInjector(personId, filmElementSelector)
            injectContextMenuToAllFilmPosterItems({ itemId: personId, name: "person" })

            if (cacheBackdrop) {
                injectBackdrop(header, cacheBackdrop, getConfigData("PERSON_SHORT_BACKDROP") ? ["shortbackdropped", "-crop"] : [])
                await scrapeFilmLinkElement(filmElementSelector, false, personId)
                return
            }

            if (await isDefaultBackdropAvailable()) {
                await scrapeFilmLinkElement(filmElementSelector, false, personId)
                return
            }

            const [scrapedImage, isCached] = await scrapeFilmLinkElement(filmElementSelector, getConfigData("PERSON_AUTO_SCRAPE"), personId)

            if (scrapedImage) {
                injectBackdrop(header, scrapedImage, getConfigData("PERSON_SHORT_BACKDROP") ? ["shortbackdropped", "-crop"] : [])

                if (!isCached) {
                    await setItemData(personId, "bu", scrapedImage)
                }
            }
        } catch (error) {
            console.error("Error in personPageInjector:", error)
        }
    }

    async function reviewPageInjector() {
        try {
            const filmName = location.pathname.match(/\/film\/([^\/]+)/)?.[1]
            const filmId = `f/${filmName}`
            const filmElementSelector = `.film-poster a[href^="/film/"]`

            const userName = location.pathname.split("/")?.[1]?.toLowerCase()
            const isCurrentUser = userName === loggedInAs

            const cacheBackdrop = await getItemData(filmId, "bu")
            const header = await waitForElement("#header")
            filmPageMenuInjector({ filmId, mode: "backdrop" })
            filmPageMenuInjector({ filmId, mode: "poster" })
            injectContextMenuToAllFilmPosterItems()

            const defaultBackdropUrl = await isDefaultBackdropAvailable()

            if (
                cacheBackdrop &&
                (isCurrentUser ||
                    (!isCurrentUser && getConfigData("REPLACE_OTHER_NON_PATRON_REVIEW") && !defaultBackdropUrl) ||
                    (!isCurrentUser && getConfigData("REPLACE_OTHER_PATRON_REVIEW") && defaultBackdropUrl))
            ) {
                injectBackdrop(header, cacheBackdrop, getConfigData("REVIEW_SHORT_BACKDROP") ? ["shortbackdropped", "-crop"] : [])
                return
            }

            if (defaultBackdropUrl && !getConfigData("REPLACE_OTHER_PATRON_REVIEW")) return

            const [scrapedImage, isCached] = await scrapeFilmLinkElement(filmElementSelector, false, filmId)

            if (
                scrapedImage &&
                (isCurrentUser ||
                    (!isCurrentUser &&
                        ((getConfigData("REPLACE_OTHER_NON_PATRON_REVIEW") && !defaultBackdropUrl) ||
                            (getConfigData("REPLACE_OTHER_NON_PATRON_REVIEW") && defaultBackdropUrl))))
            ) {
                injectBackdrop(header, scrapedImage, ["shortbackdropped", "-crop"])

                if (!isCached) {
                    await setItemData(filmId, "bu", scrapedImage)
                }
            }
        } catch (error) {
            console.error("Error in reviewPageInjector:", error)
        }
    }

    async function injectPosters() {
        await waitForElement("body")

        const observer = new MutationObserver(async () => {
            if (!document.querySelector(".film-poster:not([poster-processed])")) return

            const allPosterImageElements = document.querySelectorAll(`.film-poster:not([poster-processed]) .image`)
            for (const posterImageElement of allPosterImageElements) {
                // Get the film name
                const posterElement = posterImageElement.parentElement?.parentElement
                const filmPath = posterImageElement.nextElementSibling?.href || posterElement?.getAttribute("data-item-link")
                const filmName = filmPath?.match(/\/film\/([^\/]+)/)?.[1] || ""
                if (!filmName) continue

                // Mark the element as processed to avoid reprocessing
                posterElement?.setAttribute("poster-processed", "")

                const filmId = `f/${filmName}`
                const cachePoster = await getItemData(filmId, "pu")

                if (cachePoster) injectPoster(posterImageElement, cachePoster)
            }
        })

        observer.observe(document.body, {
            childList: true,
            subtree: true,
        })
    }

    // MAIN

    try {
        const currentURL = location.protocol + "//" + location.hostname + location.pathname

        const filmPageRegex = /^(https?:\/\/letterboxd\.com\/film\/[^\/]+\/?(crew|details|releases|genres)?\/)$/
        const userPageRegex = /^(https?:\/\/letterboxd\.com\/[^\/]+(?:\/\?.*)?\/?)$/
        const listPageRegex =
            /^(https?:\/\/letterboxd\.com\/[A-Za-z0-9-_]+\/list\/[A-Za-z0-9-_]+(?:\/(by|language|country|decade|genre|on|detail|year)\/[A-Za-z0-9-_\/]+)?\/(?:(detail|page\/\d+)\/?)?)$/
        const personPageRegex =
            /^(https?:\/\/letterboxd\.com\/(actor|additional-directing|additional-photography|art-direction|assistant-director|camera-operator|casting|choreography|cinematography|co-director|composer|costume-design|director|editor|executive-producer|hairstyling|lighting|makeup|original-writer|producer|production-design|set-decoration|songs|sound|story|stunts|visual-effects|writer)\/[A-Za-z0-9-_]+(?:\/(by|language|country|decade|genre|on|year|popular)\/[A-Za-z0-9-_\/]+)?\/(?:page\/\d+\/?)?)$/
        const reviewPageRegex = /^(https?:\/\/letterboxd\.com\/[A-Za-z0-9-_]+\/film\/[A-Za-z0-9-_]+\/(\d+\/)?(?:reviews\/?)?(?:page\/\d+\/?)?)$/

        injectPosters()

        if (filmPageRegex.test(currentURL)) {
            currentPage = "film"
            filmPageInjector()
        } else if (
            userPageRegex.test(currentURL) &&
            ![
                "/settings/",
                "/films/",
                "/lists/",
                "/members/",
                "/journal/",
                "/sign-in/",
                "/create-account/",
                "/pro/",
                "/search/",
                "/activity/",
                "/countries/",
            ].some((ending) => currentURL.toLowerCase().endsWith(ending))
        ) {
            currentPage = "user"
            userPageInjector()
        } else if (listPageRegex.test(currentURL)) {
            currentPage = "list"
            listPageInjector()
        } else if (personPageRegex.test(currentURL)) {
            currentPage = "person"
            personPageInjector()
        } else if (reviewPageRegex.test(currentURL)) {
            currentPage = "review"
            reviewPageInjector()
        } else {
            currentPage = "other"
            injectContextMenuToAllFilmPosterItems({ itemId: `u/${loggedInAs}`, name: "user" })
        }
    } catch (error) {
        console.error("Error in main function:", error)
    }
})()