tronix42 / IMDb-Lists-Highlighter

// ==UserScript==
// @name          IMDb-Lists-Highlighter
// @author        tronix42
// @copyright     2025, tronix42
// @copyright     2008-2024, Ricardo Mendonca Ferreira (original script - IMDb 'My Movies' enhancer)
// @namespace     http://example.com/
// @version       1.1
// @description   highlights movie titles, series titles, and people from your lists
// @include       *://*.imdb.com/*
// @grant         none
// @run-at        document-idle
// @license       GPL-3.0-or-later
// @updateURL     https://openuserjs.org/install/tronix42/IMDb-Lists-Highlighter.meta.js
// ==/UserScript==
//
// --------------------------------------------------------------------
//
// Thanks to AltoRetrato and his work with the great "IMDb 'My Movies' enhancer" Userscript.
// https://openuserjs.org/scripts/AltoRetrato/IMDb_My_Movies_enhancer
//
// This userscript highlights movie titles, series titles, and people from your lists. This way, you can immediately see which
// movies or series you've already seen or have on your watchlist while browsing IMDb. If you have lists of your
// favorite actors/actresses, you can see them highlighted in the calendar when they appear in a new film.
//
// This all works so far, with a small limitation. Custom lists and watchlist working fine. Unfortunately,
// the ratings list and check-in list don't work via automatic import. You have to take a detour for that.
// You can either manually import the ratings CSV file (which you downloaded previously) or create a custom list
// and add all rated films to it, which you then import via the script.
//
// A "Configure List" button will appear on the list page. All recognized lists will then be in the configuration,
// where you can assign each list a unique color. If you simply check the box next to the list WITHOUT uploading
// a CSV file, the lists will be imported automatically (as mentioned, this unfortunately doesn't work for
// ratings or check-ins). If you check the box AND upload a CSV file, the import will be done manually.
//
// As soon as you click Start Import, all lists to be imported will be displayed, along with a progress circle.
// When the import of a list is complete, the number of imported entries will be displayed next to it.
// After the import is finished, reload the page, and all imported entries should be highlighted.

// All custom colors will be saved. You don't have to import all lists at once — nothing will be lost if you import
// another list later. Clear Data deletes all data!
//
//
// History:
// --------
// 2025.05.19  [1.1] Added Fallback for GM_addStyle (Greasemonkey)
// 2025.05.12  [1.0] Public Release
// --------------------------------------------------------------------


(function() {
    'use strict';

     // ——— Fallback GM_addStyle (Grease­monkey 4+) ———
    var GM_addStyle = (typeof GM_addStyle === 'function')
      ? GM_addStyle
      : function(css) {
          const style = document.createElement('style');
          style.textContent = css;
          document.head.appendChild(style);
        };
    // ———————————————————————————————————————————————

    let myLists = [],
        listOrder = [];
    let progressModal = null;
    let progressItems = [];

    function getCurrentUser() {
        const el = document.querySelector('[data-testid="user-menu-toggle-name"]') ||
            document.querySelector('.navbar__user-menu-toggle__name') ||
            document.querySelector('#nbpersonalize strong');
        return el ? el.textContent.trim() : null;
    }

    function getStorageUser() {
        for (let i = 0; i < localStorage.length; i++) {
            const k = localStorage.key(i);
            if (k.startsWith('myMovies-')) return k.slice(9);
        }
        return null;
    }

    function getUserId() {
        const link = document.querySelector('a[href*="/user/ur"]');
        if (link) {
            const m = link.href.match(/\/user\/(ur\d+)\//);
            if (m) return m[1];
        }
        console.error('IMDb User-ID not found');
        return null;
    }

    const user = getCurrentUser() || getStorageUser();
    // Check language Regex
    const pathParts = window.location.pathname.split('/');
    const langSegment = pathParts[1];
    const langRegex = /^[a-z]{2}(?:-[A-Z]{2})?$/;
    const countryPath = langRegex.test(langSegment) ? `/${langSegment}` : '';
    if (!user) return;

    // 1) Load lists from LocalStorage
    const listsLoaded = loadLists();

    // 2) Config-Button (Lists URL)
    if (/^\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?user\/ur\d+\/lists/.test(location.pathname)) {
        // (a) Load Database
        let savedListsMap = {};
        if (loadLists()) {
            myLists.forEach(l => {
                savedListsMap[l.id] = {
                    ids: JSON.parse(JSON.stringify(l.ids)),
                    names: l.names ? JSON.parse(JSON.stringify(l.names)) : {},
                    color: l.color,
                    selected: l.selected
                };
            });
        }

        // (b) Show all Lists
        collectLists();
        addConfigButton();

        // (c) Overwrite Data
        Object.entries(savedListsMap).forEach(([id, data]) => {
            const lst = myLists.find(x => x.id === id);
            if (lst) {
                lst.ids = data.ids;
                lst.names = data.names;
                lst.color = data.color;
                lst.selected = data.selected;
            }
        });

        // (d) Highlight on lists-URL with CSS
        let css = '';
        myLists.forEach(list => {
            Object.keys(list.ids).forEach(code36 => {
                const num = parseInt(code36, 36);
                css += `
        a[href*="/title/tt${num}/?ref_"] {
          color: ${list.color} !important;
          font-weight: bold !important;
        }
      `;
            });
        });
        GM_addStyle(css);
        highlightLinks();
    }

    // 3) CSS and Search-Highlight
    if (listsLoaded) {
        // (a) CSS-String
        let css = '';
        myLists.forEach(list => {
            Object.keys(list.ids).forEach(code36 => {
                const num = parseInt(code36, 36);
                css += `
         a[href*="/title/tt${num}/?ref_"] {
           color: ${list.color} !important;
           font-weight: bold !important;
         }
       `;
            });
        });
        GM_addStyle(css);

        // (b) Search-Highlight-Function
        highlightTitle();
        highlightLinks();
        if (/^\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?calendar/.test(location.pathname)) {
            highlightCalendarPeople();
            // Observer for Calendar URL
            new MutationObserver(() => highlightCalendarPeople())
                .observe(document.body, {
                    childList: true,
                    subtree: true
                });
        }
        const observer = new MutationObserver(() => highlightLinks());
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    function collectLists() {
        // 1) Load savedColors
        let savedColors = {};
        if (loadLists()) {
            savedColors = myLists.reduce((map, l) => {
                map[l.id] = l.color;
                return map;
            }, {});
        }
        const customColors = {
            "Your Watchlist": "DarkGoldenRod",
            "Your Ratings": "Green"
        };
        const defaultColor = 'Red';
        myLists = [];
        listOrder = [];
        const seen = new Set();
        [
            ["Your Watchlist", "watchlist"],
            ["Your Ratings", "ratings"],
            ["Your check-ins", "checkins"]
        ].forEach(([name, id], i) => {
            // 2a) check savedColors, customColors and defaultColor
            myLists.push({
                name,
                id,
                color: savedColors[id] || customColors[name] || defaultColor,
                ids: {},
                names: {},
                selected: false,
                csvFile: null
            });
            listOrder.push(i);
            seen.add(id);
        });
        document.querySelectorAll('a[href*="/list/ls"]').forEach(a => {
            const m = a.href.match(/\/list\/(ls\d+)/);
            if (!m) return;
            const id = m[1];
            if (seen.has(id)) return;
            seen.add(id);
            const raw = a.getAttribute('aria-label') || a.title || a.textContent.trim();
            const name = raw.replace(/^View list page for\s*/i, '').trim();
            // 2b) check savedColors and defaultColor
            myLists.push({
                name,
                id,
                color: savedColors[id] || defaultColor,
                ids: {},
                names: {},
                selected: false,
                csvFile: null
            });
            listOrder.push(myLists.length - 1);
        });
    }

    function addConfigButton() {
        const h1 = document.querySelector('h1');
        if (!h1) return;
        const btn = document.createElement('button');
        btn.textContent = 'Configure lists';
        btn.style.margin = '0 10px';
        btn.onclick = openConfig;
        h1.parentNode.insertBefore(btn, h1.nextSibling);
    }

    function openConfig() {
        // --- LOAD & MERGE PREVIOUS STATE ---
        let savedListsMap = {};
        if (loadLists()) {
            myLists.forEach(l => {
                savedListsMap[l.id] = {
                    ids: JSON.parse(JSON.stringify(l.ids)),
                    color: l.color,
                    selected: l.selected
                };
            });
        }
        collectLists();
        const modal = document.createElement('div');
        modal.style = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;';
        const box = document.createElement('div');
        box.style = 'background:#fff;padding:20px;max-height:80%;overflow:auto;';

        const header = document.createElement('div');
        header.style = 'display:flex;align-items:left;margin-bottom:1px;font-weight:bold;';
        const hChk = document.createElement('span');
        hChk.style = 'width:1px;';
        const hLists = document.createElement('span');
        hLists.textContent = 'Lists:';
        hLists.style = 'margin-right:112px;';
        const hCsv = document.createElement('span');
        hCsv.textContent = 'CSV file:';
        hCsv.style = 'margin-right:1px;';
        const hColor = document.createElement('span');
        hColor.textContent = 'Color (HEX or Name):';
        hColor.style = 'margin-right:18px;';
        header.append(hChk, hLists, hColor, hCsv);
        box.appendChild(header);

        myLists.forEach((lst, i) => {
            const sav = savedListsMap[lst.id];
            if (sav) {
                lst.ids = sav.ids;
                lst.color = sav.color;
                lst.selected = sav.selected;
            }
            const row = document.createElement('div');
            row.style = 'margin:4px 0; display:flex; align-items:center;';

            // Checkbox automatic Import
            const chk = document.createElement('input');
            chk.type = 'checkbox';
            chk.style = 'margin-right:8px;';
            chk.checked = lst.selected;
            chk.onchange = e => {
                lst.selected = e.target.checked;
                if (lst.selected) lst.csvFile = null;
            };

            // Label
            const lbl = document.createElement('span');
            lbl.textContent = ' ' + lst.name + ' ';
            lbl.style = 'margin-right:8px;';
            lbl.style.fontWeight = 'normal';
            // color label list, when imported
            if (Object.keys(lst.ids).length > 0) {
                lbl.style.color = lst.color;
                lbl.style.fontWeight = 'bold';
            }

            // File-Input (local list CSV-file import)
            const fileInput = document.createElement('input');
            fileInput.type = 'file';
            fileInput.accept = '.csv';
            fileInput.style = 'margin-left:8px;';
            fileInput.onchange = e => {
                lst.csvFile = e.target.files[0];

            };

            // Color-Picker & Hex
            const col = document.createElement('input');
            col.type = 'color';
            col.value = nameToHex(lst.color);
            col.style = 'margin-left:8px; margin-right:10px;';
            col.oninput = e => {
                lst.color = e.target.value;
                txt.value = e.target.value;
                if (Object.keys(lst.ids).length > 0) {
                    lbl.style.color = lst.color;
                    lbl.style.fontWeight = 'bold';
                }
            };

            // Input Color-Textbox
            const txt = document.createElement('input');
            txt.type = 'text';
            txt.value = lst.color.toLowerCase();
            txt.placeholder = '#Hex or Name';
            txt.style = 'width:100px; margin-left:auto;';
            txt.oninput = e => {
                const v = e.target.value.trim().toLowerCase();
                lst.color = v;
                if (/^#([0-9A-Fa-f]{6})$/.test(v)) {
                    col.value = v;
                } else {
                    try {
                        col.value = nameToHex(v);
                    } catch {}
                }
                if (Object.keys(lst.ids).length > 0) {
                    lbl.style.color = lst.color;
                    lbl.style.fontWeight = 'bold';
                }
            };

            row.append(chk, lbl, txt, col, fileInput);
            box.appendChild(row);
        });

        const imp = document.createElement('button');
        imp.textContent = 'Start Import';
        imp.style.margin = '10px';
        imp.onclick = () => {
            startImport();
            document.body.removeChild(modal);
        };

        const clr = document.createElement('button');
        clr.textContent = 'Clear Data';
        clr.style.margin = '10px';
        clr.onclick = () => {
            eraseData();
            alert('Data cleared');
            document.body.removeChild(modal);
        };

        const cxl = document.createElement('button');
        cxl.textContent = 'Cancel';
        cxl.style.margin = '10px';
        cxl.onclick = () => document.body.removeChild(modal);

        box.append(imp, clr, cxl);
        modal.appendChild(box);
        document.body.appendChild(modal);
    }

    // startImport: CSV import vs. automatic import
    function startImport() {
        const tasks = [];
        myLists.forEach((l, i) => {
            if (l.selected && l.csvFile) {
                // CSV import, only if Checkbox is seleced
                tasks.push({
                    type: 'csv',
                    idx: i
                });
            } else if (l.selected) {
                // automatic import only if Checkbox is selected and no CSV-file loaded
                tasks.push({
                    type: 'auto',
                    idx: i
                });
            }
        });
        if (!tasks.length) {
            alert('No Lists selected!');
            return;
        }
        // eraseData()
        createProgressModal(tasks.map(o => o.idx));
        let rem = tasks.length;
        tasks.forEach(({
            type,
            idx
        }) => {
            if (type === 'csv') {
                importCsv(idx, () => {
                    updateListProgress(idx, Object.keys(myLists[idx].ids).length);
                    if (--rem === 0) {
                        // 1) clear all Checkbox
                        myLists.forEach(l => l.selected = false);
                        // 2) save changes
                        saveLists();
                        // 3) close progress pop-up
                        finishProgress();
                    }
                });
            } else {
                downloadList(idx, () => {
                    const cnt = Object.keys(myLists[idx].ids).length;
                    updateListProgress(idx, cnt);
                    if (--rem === 0) {
                        myLists.forEach(l => l.selected = false);
                        saveLists();
                        finishProgress();
                    }
                });
            }
        });
    }

    // CSV-Import function
    function importCsv(idx, cb) {
        const lst = myLists[idx];
        lst.ids = {};
        const file = lst.csvFile;
        const reader = new FileReader();
        reader.onload = e => {
            const text = e.target.result;
            text.split(/\r?\n/).forEach(line => {
                // search tt-Code
                const match = line.match(/tt(\d+)/i);
                if (!match) return;
                const num = match[1]; // for example "1234567"
                const id36 = parseInt(num, 10).toString(36);
                lst.ids[id36] = {};
            });
            cb();
        };
        reader.onerror = () => {
            console.error('CSV Import error', reader.error);
            cb();
        };
        reader.readAsText(file);
    }

    function createProgressModal(selectedIndices) {
        progressModal = document.createElement('div');
        progressModal.style = 'position:fixed;top:0;left:0;width:100%;height:100%;' +
            'background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;';
        const box = document.createElement('div');
        box.style = 'background:#fff;padding:20px;max-height:80%;overflow:auto;min-width:300px;';

        const header = document.createElement('h2');
        header.id = 'progressHeader';
        header.textContent = `Import ${selectedIndices.length} Lists`;
        box.appendChild(header);
        box.appendChild(document.createElement('br'));

        progressItems = selectedIndices.map((listIdx, idx) => {
            const row = document.createElement('div');
            row.style = 'display:flex;align-items:center;margin:4px 0;';
            const label = document.createElement('span');
            label.textContent = `${idx+1}. Import ${myLists[listIdx].name}`;
            label.style = 'flex:1;';
            const spinner = document.createElement('div');
            spinner.className = 'item-spinner';
            spinner.style = 'margin-left:8px;border:4px solid #ccc;border-top:4px solid #3498db;' +
                'border-radius:50%;width:16px;height:16px;animation:spin 1s linear infinite;';
            row.append(label, spinner);
            box.appendChild(row);
            return {
                listIdx,
                row,
                label,
                spinner
            };
        });

        const style = document.createElement('style');
        style.id = 'spinStyle';
        style.textContent = '@keyframes spin {0%{transform:rotate(0deg);}100%{transform:rotate(360deg);}}';
        document.head.appendChild(style);

        progressModal.appendChild(box);
        document.body.appendChild(progressModal);
    }

    function updateListProgress(listIdx, count) {
        const item = progressItems.find(i => i.listIdx === listIdx);
        if (!item) return;
        if (item.spinner) item.row.removeChild(item.spinner);
        item.label.textContent = `${item.label.textContent}: ${count} items imported`;
    }

    function finishProgress() {
        const spinEl = document.getElementById('spinStyle');
        if (spinEl) {
            spinEl.parentNode.removeChild(spinEl);
        }
        const box = progressModal.querySelector('div');
        const footer = document.createElement('div');
        footer.style = 'margin-top:12px;text-align:center;font-weight:bold;';
        footer.textContent = 'Import finished!';
        box.appendChild(footer);
        const btn = document.createElement('button');
        btn.textContent = 'OK';
        btn.style = 'margin-top:10px;padding:6px 12px;';
        btn.onclick = () => document.body.removeChild(progressModal);
        box.appendChild(btn);
    }

    function eraseData() {
        localStorage.removeItem('myMovies-' + user);
    }

    function saveLists() {
        localStorage.setItem('myMovies-' + user, JSON.stringify({
            myLists,
            listOrder
        }));
    }

    function loadLists() {
        const d = localStorage.getItem('myMovies-' + user);
        if (!d) return false;
        const o = JSON.parse(d);
        myLists = o.myLists;
        listOrder = o.listOrder;
        return true;
    }

    function downloadList(idx, cb) {
        const lst = myLists[idx];
        lst.ids = {};
        if (lst.id === 'watchlist') {
            // Basis-URL + language regex
            const BASE = `${window.location.origin}${countryPath}/user/${getUserId()}/watchlist/`;
            let page = 1,
                seen = new Set();
            (async function fetchPage() {
                const iframe = document.createElement('iframe');
                iframe.style.display = 'none';
                // Detail-View + Paginierung
                iframe.src = `${BASE}?view=detail&page=${page}`;
                document.body.appendChild(iframe);
                await new Promise(r => iframe.onload = r);
                await new Promise(r => setTimeout(r, 2000));
                const doc = iframe.contentDocument;
                const sel1 = Array.from(doc.querySelectorAll('a.ipc-title-link-wrapper[href*="/title/tt"]'));
                const sel2 = Array.from(doc.querySelectorAll('.lister-item-header a[href*="/title/tt"]'));
                const anchors = sel1.length ? sel1 : sel2;
                let newFound = false;
                anchors.forEach(a => {
                    const m = a.href.match(/tt(\d+)\//);
                    if (!m) return;
                    const code36 = parseInt(m[1], 10).toString(36);
                    if (!seen.has(code36)) {
                        seen.add(code36);
                        lst.ids[code36] = {};
                        newFound = true;
                    }
                });

                document.body.removeChild(iframe);
                // load next page if at least one new item found
                if (newFound) {
                    page++;
                    fetchPage();
                } else {
                    cb();
                }
            })();
            return;
        }
        // per JSON-LD for Custom lists, ratings... exepct Watchlist
        // automatic didfference People and Titles Lists
        // JSON-LD-Pagination for Custom lists (Titles and People)
        (async () => {
            const base = `https://www.imdb.com/list/${lst.id}/?mode=detail`;
            let page = 1;
            let isPeople = null;
            while (true) {
                // load page with  &page=
                const resp = await fetch(`${base}&page=${page}`, {
                    credentials: 'same-origin'
                });
                const html = await resp.text();
                const d = new DOMParser().parseFromString(html, 'text/html');
                const sc = d.querySelector('script[type="application/ld+json"]');
                if (!sc) break;
                let data;
                try {
                    data = JSON.parse(sc.textContent);
                } catch (err) {
                    console.error('JSON-LD parse error', err);
                    break;
                }

                // Detect list type
                if (page === 1) {
                    const first = data.itemListElement[0];
                    isPeople = (first['@type'] === 'Person') ||
                        (first.item && first.item['@type'] === 'Person');
                }

                const items = data.itemListElement || [];
                if (!items.length) break;

                // extract ID
                items.forEach(e => {
                    const l = e.url ||
                        e['@id'] ||
                        (e.item && (e.item.url || e.item['@id'])) ||
                        '';
                    const re = isPeople ? /name\/nm(\d+)\// : /tt(\d+)\//;
                    const m = l.match(re);
                    if (m) {
                        const code36 = parseInt(m[1], 10).toString(36);
                        lst.ids[code36] = {};
                        if (isPeople && e.item && e.item.name) {
                            lst.names[e.item.name.trim()] = code36;
                        }
                    }
                });

                // load 250 entries per page, if entries lower than 250 end import
                if (items.length < 250) break;
                page++;
            }
            cb();
        })();
    }

    function highlightTitle() {
        // Titels-URLs
        let m = location.href.match(/tt(\d+)\//);
        if (m) {
            const c = movieColor(parseInt(m[1], 10).toString(36));
            if (c) document.querySelectorAll('h1').forEach(h => h.style.color = c);
        }
        // People-URLs
        m = location.href.match(/name\/nm(\d+)\//);
        if (m) {
            const c = movieColor(parseInt(m[1], 10).toString(36));
            if (c) document.querySelectorAll('h1').forEach(h => h.style.color = c);
        }
    }

    function highlightLinks() {
        // 1) highlight standard titles-links on all URLs
        document.querySelectorAll('a[href*="/title/tt"]').forEach(a => {
            const m = a.href.match(
                /^https?:\/\/(?:www\.)?imdb\.com\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?title\/tt(\d+)\/\?ref_=[^/]+/
            );
            if (!m) return;
            const code36 = parseInt(m[1], 10).toString(36);
            const c = movieColor(code36);
            if (c) {
                a.style.color = c;
                a.style.fontWeight = 'bold';
            }
        });
        // highlight standard people-links on all URLs
        const peopleLinkRe = /^https:\/\/www\.imdb\.com\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?name\/nm(\d+)\/\?ref_=[^&#]+$/;

        document.querySelectorAll('a[href]').forEach(a => {
            const href = a.href;
            const m = peopleLinkRe.exec(href);
            if (!m) return;
            const code36 = parseInt(m[1], 10).toString(36);
            const c = movieColor(code36);
            if (c) {
                a.style.color = c;
                a.style.fontWeight = 'bold';
            }
        });

        // 2) highlight Suggestion-Items
        document
            .querySelectorAll('li[id^="react-autowhatever-navSuggestionSearch"]')
            .forEach(li => {
                let link = li.querySelector('a[href*="/title/tt"]');
                if (!link) link = li.querySelector('[data-testid="search-result--link"]');
                if (!link || !link.href) return;
                const m = link.href.match(
                    /^https?:\/\/(?:www\.)?imdb\.com\/title\/tt(\d+)\/\?ref_=[^/]+/
                );
                if (!m) return;
                const code36 = parseInt(m[1], 10).toString(36);
                const c = movieColor(code36);
                if (!c) return;

                // Titles span in suggestion item
                const titleSpan =
                    li.querySelector('.searchResult__constTitle') ||
                    li.querySelector('span');
                if (titleSpan) {
                    titleSpan.style.color = c;
                    titleSpan.style.fontWeight = 'bold';
                }
            });
        // 3) highlight people-Suggestions
        document
            .querySelectorAll('li[id^="react-autowhatever-navSuggestionSearch"]')
            .forEach(li => {
                let link = li.querySelector('a[href*="/name/nm"]');
                if (!link) link = li.querySelector('[data-testid="search-result--link"]');
                if (!link || !link.href) return;
                const m = link.href.match(
                    /^https?:\/\/(?:www\.)?imdb\.com\/name\/nm(\d+)\/\?ref_=[^/]+/
                );
                if (!m) return;
                const code36 = parseInt(m[1], 10).toString(36);
                const c = movieColor(code36);
                if (!c) return;

                // Titles span in suggestion item
                const nameSpan =
                    li.querySelector('.searchResult__actorName') ||
                    li.querySelector('.searchResult__constTitle') ||
                    li.querySelector('span');
                if (nameSpan) {
                    nameSpan.style.color = c;
                    nameSpan.style.fontWeight = 'bold';
                }
            });
    }

    function highlightCalendarPeople() {
        document
            .querySelectorAll('ul.ipc-metadata-list-summary-item__stl span.ipc-metadata-list-summary-item__li')
            .forEach(span => {
                const name = span.textContent.trim();
                for (const i of listOrder) {
                    const lst = myLists[i];
                    if (lst.names && lst.names[name]) {
                        span.style.color = lst.color;
                        span.style.fontWeight = 'bold';
                        break;
                    }
                }
            });
    }

    function movieColor(code) {
        for (const i of listOrder)
            if (myLists[i].ids[code]) return myLists[i].color;
        return '';
    }

    function nameToHex(name) {
        const ctx = document.createElement('canvas').getContext('2d');
        ctx.fillStyle = name;
        return ctx.fillStyle;
    }

})();