TitleList

Common functions for working on lists of titles

目前為 2019-09-20 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/390248/734489/TitleList.js

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

//{
// Common functions for working on lists of titles, loading them, highlighting
// titles based on these lists.
//
// Copyright (c) 2019, Guido Villa
// Most of the code is taken from IMDb 'My Movies' enhancer:
// Copyright (c) 2008-2018, Ricardo Mendonça Ferreira ([email protected])
// Released under the GPL license - http://www.gnu.org/copyleft/gpl.html
//
// --------------------------------------------------------------------
//
// ==UserScript==
// @name          TitleList
// @description   Common functions for working on lists of titles
// @namespace     https://greasyfork.org/en/scripts/390248-titlelist
// @updateURL     about:blank
// @homepageURL   https://greasyfork.org/en/scripts/390248-titlelist
// @copyright     2019, Guido Villa
// @license       GPL-3.0-or-later
// @oujs:author   Guido
// @date          18.09.2019
// @version       0.1
// ==/UserScript==
//
// To-do (priority: [H]igh, [M]edium, [L]ow):
//   - [H] Everything: extract functions from Netflix Hide Titles script and insert them below
//
// History:
// --------
// 2019.09.18  [0.1] First test version, private use only
//
//}

/*jshint esversion: 6 */

const TITLELIST_Version = '0.1';

// FUNCTIONS *************************************************************************************************************	

function TL() {
    'use strict';
    var mainContext;

    /* XXX var private_stuff = function() {  // Only visible inside Restaurant()
        myPrivateVar = "I can set this here!";
    }*/

    this.getLoggedUser = function(ctx) {
        //
        // Return name of user currently logged on <ctx> site
        // Return last saved value and log error if no user is found
        //
        var user = ctx.getUser();

        if (!user) {
            console.error(ctx.name + ": user not logged in (or couldn't get user info) on URL " + document.URL);
            user = GM_getValue(ctx.name + '-lastUser', '');
            console.error("Using last user: " + user);
        }
        GM_setValue(ctx.name + '-lastUser', user);
        ctx.user = user;
        return user;
    };


    function loadSavedList(listName) {
        //
        // Load a single saved lists
        //
        var list;
        var userData = GM_getValue(listName, null);
        if (userData) {
            try {
                list = JSON.parse(userData);
            } catch(err) {
                alert("Error loading saved list named '" + listName + "'!\n" + err.message);
            }
        }
        return list;
    }


    this.loadSavedLists = function(ctx) {
        //
        // Load lists saved for the current user
        //
        var lists = {};

        var listNames = loadSavedList('TitleLists-' + ctx.user);
        if (!listNames) return lists;

        for (var listName in listNames) {
            lists[listName] = loadSavedList('TitleList-' + ctx.user + '-' + listName);
        }
        return lists;
    };


    this.saveList = function(ctx, list, name) {
        //
        // Save single list for the current user
        //
        var listNames = loadSavedList('TitleLists-' + ctx.user);
        if (!listNames) listNames = {};

        listNames[name] = 1;
        var userData = JSON.stringify(listNames);
        GM_setValue('TitleLists-' + ctx.user, userData);

        userData = JSON.stringify(list);
        GM_setValue('TitleList-' + ctx.user + '-' + name, userData);
    };


    this.manageTitlePage = function(ctx) {
        var we_are_in_a_title_page = ctx.isTitlePage(document);
        if (!we_are_in_a_title_page) return;

        // find current logged in user, or quit script
        if (!getLoggedUser(ctx)) return;

        mainContext = ctx;

        // Load lists data for this user from local storage
        ctx.allLists = loadSavedLists(dest);

        // start the title processing function
        processTitles(ctx);
        if (ctx.interval >= 100) {
            ctx.timer = setInterval(function() {processTitles(ctx);}, ctx.interval);
        }
    };


    this.inLists = function(ctx, tt, entry) {
        //
        // Receives a title (and corresponding entry) and finds all lists title is in.
        // Argument "entry" is for "virtual" lists determined by attributes in the DOM
        //
        var lists = ( ctx.getListsFromEntry && ctx.getListsFromEntry(tt, entry) || {} );

        for (var list in ctx.allLists) {
            if (ctx.allLists[list][tt.id]) lists[list] = true;
        }

        return lists;
    };


    this.processTitles = function(ctx) {
        //
        // Process all title cards in current page
        //
        var entries = ctx.getTitleEntries(document);
        if (!entries) return;

        var entry, tt, lists, processingType;
        for (var i = 0; i < entries.length; i++) {
            entry = entries[i];

            // if entry has already been previously processed, skip it
            if (entry.TLProcessed) continue;

            tt = ctx.getIdFromEntry(entry);
            if (!tt) continue;

            ctx.modifyEntry(entry);
            lists = inLists(tt, entry);

            processingType = ctx.determineType(lists, tt, entry);

            if (processingType) {
                ctx.processItem(entry, tt, processingType);
                entry.TLProcessingType = processingType;
            }

            entry.TLProcessed = true; // set to "true" after processing (so we skip it on next pass)
        }
    };


    this.toggleTitle = function(evt) {
        var data = evt.target.dataset;

        // get title entry
        var entry = evt.target;
        if (Number.isInteger(Number(data.howToFindEntry))) {
            for (var i = 0; i < Number(data.howToFindEntry); i++) entry = entry.parentNode;
        } else {
            entry = entry.closest(data.howToFindEntry);
        }

        var tt = mainContext.getIdFromEntry(entry);
        if (!tt) return;

        // check if item is in list
        var list = mainContext.allLists[data.toggleList];
        if (list[tt.id]) {
            delete list[tt.id];
            mainContext.unProcessItem(entry, tt, data.toggleType);
            entry.TLProcessingType = "-" + data.toggleType;
        } else {
            list[tt.id] = tt.title;
            mainContext.processItem(entry, tt, data.toggleType);
            entry.TLProcessingType = data.toggleType;
        }
        saveList(mainContext, list, data.toggleList);
    };



    this.addToggleEventOnClick = function(button, toggleType, toggleList, howToFindEntry) {
        button.dataset.toggleType     = toggleType;
        button.dataset.toggleList     = toggleList;
        button.dataset.howToFindEntry = howToFindEntry;
        button.addEventListener('click', toggleTitle, false);
    };


}









function xxx() {
   var WATCHLIST  = "watchlist";
   var RATINGLIST = "ratings";
   var CHECKINS   = "checkins";

   var TITLES = "Titles";
   var PEOPLE = "People";
   var IMAGES = "Images";
   // Lists can be about Titles, People & Images (no Characters lists anymore?)
   // Comment out a list type to disable highlighting for it.
   var listTypes = {};
   listTypes[TITLES] = true;
   listTypes[PEOPLE] = true;
   //listTypes[IMAGES] = true; // To-do: highlight images using colored borders?

   var listOrderIdx = [];

   var myLists = [];
   var neededLists = {};   //GUIDO NF


   // Modified version of Michael Leigeber's code, from:
   // http://sixrevisions.com/tutorials/javascript_tutorial/create_lightweight_javascript_tooltip/
   // http://userscripts.org/scripts/review/91851 & others
   var injectJs = 'function tooltipClass(msg) {this.msg = msg;this.id = "tt";this.top = 3;this.left = 15;this.maxw = 500;this.speed = 10;this.timer = 20;this.endalpha = 95;this.alpha = 0;this.tt == null;this.c;this.h = 0;this.moveFunc = null;this.fade = function (d) {var a = this.alpha;if (a != this.endalpha && d == 1 || a != 0 && d == -1) {var i = this.speed;if (this.endalpha - a < this.speed && d == 1) {i = this.endalpha - a;} else if (this.alpha < this.speed && d == -1) {i = a;}this.alpha = a + i * d;this.tt.style.opacity = this.alpha * 0.01;} else {clearInterval(this.tt.timer);if (d == -1) {this.tt.style.display="none";document.removeEventListener("mousemove", this.moveFunc, false);this.tt = null;}}};this.pos = function (e, inst) {inst.tt.style.top = e.pageY - inst.h + "px";inst.tt.style.left = e.pageX + inst.left + "px";};this.show = function (msg) {if (this.tt == null) {this.tt = document.createElement("div");this.tt.setAttribute("id", this.id);c = document.createElement("div");c.setAttribute("id", this.id + "cont");this.tt.appendChild(c);document.body.appendChild(this.tt);this.tt.style.opacity = 0; this.tt.style.zIndex=100000; var inst = this;this.moveFunc = function (e) {inst.pos(e, inst);};document.addEventListener("mousemove", this.moveFunc, false);}this.tt.style.display = "block";c.innerHTML = msg || this.msg;this.tt.style.width = "auto";if (this.tt.offsetWidth > this.maxw) {this.tt.style.width = this.maxw + "px";}h = parseInt(this.tt.offsetHeight) + this.top;clearInterval(this.tt.timer);var inst = this;this.tt.timer = setInterval(function () {inst.fade(1);}, this.timer);};this.hide = function () {if (this.tt) {clearInterval(this.tt.timer);var inst = this;this.tt.timer = setInterval(function () {inst.fade(-1);}, this.timer);}};} tooltip = new tooltipClass("default txt");';

   var newJs = document.createElement('script');
   newJs.setAttribute('type', 'text/javascript');
   newJs.innerHTML = injectJs;
   document.getElementsByTagName('head')[0].appendChild(newJs);

   var myName = 'Netflix hide titles'; // Name & version of this script
   var user   = '';      // Current user name/alias
   var IMDbUser = '';
   var interval = 1000;  // Interval (in ms, >= 100) to re-scan links in the DOM
                         // Won't re-scan if < 100
                         // (I might consider using MutationObserver in the future, instead)

   function getCurrentNetflixUser() {
      //
      // Return name of user currently logged on IMDb (log on console if failed)
      //
      var loggedUser = null;

      var account = document.querySelector('div.account-menu-item div.account-dropdown-button > a');
      if (account) {
         var accountString = account.getAttribute("aria-label");
         if (accountString) {
            loggedUser = accountString.replace(/ - Account & Settings$/, '');
            if (loggedUser == accountString) loggedUser == null;
         }
      }
      if (!loggedUser) {
         console.error(document.URL + "\nUser not logged in (or couldn't get user info)"); // responseDetails.responseText
         loggedUser = GM_getValue('NetflixHide_lastUser', '');
         console.error("Using last user: " + loggedUser);
      }
      GM_setValue("NetflixHide_lastUser", loggedUser);
      return loggedUser;
   }

   function getCurrentIMDbUser() {
      //
      // Return name of user currently logged on IMDb (log on console if failed)
      //
      var loggedIn = '';
      var account = document.getElementById('consumer_user_nav') ||
                    document.getElementById('nbpersonalize');
      if (account) {
         var                 result = account.getElementsByTagName('strong');
         if (!result.length) result = account.getElementsByClassName("navCategory");
         if (!result.length) result = account.getElementsByClassName("singleLine");
         if (!result.length) result = account.getElementsByTagName("p");
         if (result)
            loggedIn = result[0].textContent.trim();
      }
      if (!loggedIn)
         console.error(document.URL + "\nUser not logged in (or couldn't get user info)"); // responseDetails.responseText
      return loggedIn;
   }

   var myLocalList = {};
   var myNetflixList = {};

   function loadMyLocalList() {
      //
      // Load data for the current user
      //
      var userData = GM_getValue("NetflixHideList-"+user, null);
      if (userData) {
         try {
            myLocalList = JSON.parse(userData);
            return true;
         } catch(err) {
            alert("Error loading Netflix local data!\n" + err.message);
         }
      }
   }

   function loadMyNetflixList() {
      //
      // Load data for the current user
      //
      var userData = GM_getValue("NetflixMyList-"+user, null);
      if (userData) {
         try {
            myNetflixList = JSON.parse(userData);
            return true;
         } catch(err) {
            alert("Error loading Netflix My List data!\n" + err.message);
         }
      }

      return false;
   }

   function getMyIMDbLists() {
      //
      // Get all lists (name & id) for current user into myLists array
      // and set default colors for them (if not previously defined)
      //

      // You can customize your lists colors.
      // See also the listOrder variable below.
      // After any change in the code: save the script, reload the lists page,
      // clear the highlight data and refresh the highlight data!
      var customColors = [];
      customColors["Your Watchlist"] = "DarkGoldenRod";
      customColors["Your ratings"  ] = "Green";
      customColors["Your check-ins"] = "DarkGreen";
//GUIDO      customColors["DefaultColor"  ] = "DarkCyan";
      customColors["DefaultColor"  ] = "Maroon";
      customColors["DefaultPeople" ] = "DarkMagenta";
//GUIDO      customColors["Filmes Netflix Brasil"] = "Red";
      customColors["Visti"]   = "seagreen";
      customColors["Parzialmente visti"]   = "yellowgreen";
      customColors["no"]   = "darkgrey";

      // You can set the search order for the highlight color when a title is in multiple lists.
      // The script will choose the color of the the first list found in the variable below.
      // Uncomment the line below and enter the names of any lists you want to give preference over the others.
      var listOrder = ["Your Watchlist", "Your ratings"];

      myLists.length = 0; // Clear arrays and insert the two defaults
      myLists.push({"name":"Your Watchlist", "id":WATCHLIST,  "color":customColors["Your Watchlist"] || "", "ids":{}, "type":TITLES });
      myLists.push({"name":"Your ratings",   "id":RATINGLIST, "color":customColors["Your ratings"]   || "", "ids":{}, "type":TITLES });
      myLists.push({"name":"Your check-ins", "id":CHECKINS,   "color":customColors["Your check-ins"] || "", "ids":{}, "type":TITLES });
      var lists = document.getElementsByClassName('user-list');
      if (!lists || lists.length < 1) {
         console.error("Error getting lists (or no lists exist)!");
         return false;
      }
      for (var i = 0; i < lists.length; i++) {
         var listType = lists[i].getAttribute("data-list-type");
         if (listType in listTypes) {
            var tmp   = lists[i].getElementsByClassName("list-name");
            if (!tmp) {
               console.error("Error reading information from list #"+i);
               continue;
            }
            tmp = tmp[0]; // <a class="list-name" href="/list/ls003658871/">Filmes Netflix Brasil</a>
            var name  = tmp.text;
            var id    = tmp.href.match(/\/list\/([^\/\?]+)\/?/)[1];
            var colorType = listType == PEOPLE ? "DefaultPeople" : "DefaultColor";
            var color     = customColors[name] || customColors[colorType] || "";
            myLists.push({"name":name, "id":id, "color":color, "ids":{}, "type":listType });
         }
      }
      setListOrder(listOrder);
      return true;
   }

   function loadMyIMDbLists() {
      //
      // Load data for the current user
      //
//      var userData = localStorage.getItem("myMovies-"+user);   // GUIDO NF
      var userData = GM_getValue("myIMDbMovies-"+user, null);   // GUIDO NF
      if (userData) {
         try {
            myLists = JSON.parse(userData);
            if ("myLists" in myLists) {
               listOrderIdx = myLists["listOrder"];
               myLists      = myLists["myLists"  ];

               // GUIDO NF
               for (var i = 0; i < myLists.length; i++) {
                   if (myLists[i].type != TITLES) continue;
                   switch (myLists[i].name) {
                       case 'no':             neededLists.no    = i; break;
                       case 'Visti':          neededLists.visti = i; break;
                       case 'Your Watchlist': neededLists.watch = i; break;
                       case 'tbd':            neededLists.tbd   = i; break;
                   }
               }
               // FINE GUIDO NF
            }
            return true;
         } catch(err) {
            alert("Error loading previous data!\n" + err.message);
         }
      }
      return false;
   }

   function saveMyLocalList() {
      //
      // Save data for the current user
      //
      var userData = JSON.stringify(myLocalList);
      GM_setValue("NetflixHideList-"+user, userData);
   }

   function saveMyIMDbLists() {
      //
      // Save data for the current user
      //
      var userData = JSON.stringify(myLocalList);
      GM_setValue("NetflixHideList-"+user, userData);

      userData = {"listOrder": listOrderIdx, "myLists": myLists};
      userData = JSON.stringify(userData);
      GM_setValue("myIMDbMovies-"+user, userData);
   }

   function getIdFromDiv(div) {
       var a = div.querySelector('a[href^="/watch/"]');
       var tt = null;
       if (a) {
           tt = a.href.match(/\/watch\/([^/?&]+)[/?&]/);
           if (tt && tt.length >= 2) tt = tt[1];
       }
       if (!tt) console.error('Could not determine title id :-(');
       return tt;
   }

   function toggleTitle(evt) {
       // get title id
       var div = evt.target.parentNode.parentNode;
       var tt = getIdFromDiv(div);

       // check if item is in list
       if (myLocalList[tt]) {
           delete myLocalList[tt];
           showItem(div, tt);
       } else {
           var movie = div.querySelector("div.fallback-text");
           var movieTitle = '';
           if (movie) movieTitle = movie.innerText;
           if (!movieTitle) movieTitle = tt;
           myLocalList[tt] = movieTitle;
           hideItem(div, tt, movieTitle);
       }
       saveMyLocalList();
       console.log('TOGGLE: ' + tt + ', t: ' + movieTitle);
   }


   var hideTypes = {
       "H": { "name": 'Hidden',    "colour": 'white' },
       "D": { "name": 'Disliked',  "colour": 'black' },
       "W": { "name": 'Watchlist', "colour": 'darkgoldenrod', "visible": true },
       "T": { "name": 'TBD',       "colour": 'Maroon',        "visible": true },
       "S": { "name": 'Watched',   "colour": 'seagreen' },
       "N": { "name": 'NO',        "colour": 'darkgrey' },
       "M": { "name": 'My list',   "colour": 'yellow' },
       "MISSING": { "name": 'Hide type not known',   "colour": 'red' },
   };


   function hideItem(div, id, title, hideType) {
      //console.log('hideItem', id, title, hideType);
      if (!hideType) hideType = 'H';

      if (!hideTypes[hideType]) hideType = 'MISSING';
      var triangle = document.createElement('div');
      triangle.className = 'NHT-triangle'
      triangle.style.cssText =
          'border-right: 20px solid ' + hideTypes[hideType].colour + '; ' +
          'border-bottom: 20px solid transparent;' +
          'height: 0; ' +
          'width: 0; ' +
          'position: absolute; ' +
          'top: 0; ' +
          'right: 0; ' +
          'z-index: 2;'
      triangle.title = hideTypes[hideType].name;
      div.parentNode.appendChild(triangle);

      if (!hideTypes[hideType].visible) div.parentNode.style.opacity = .1;
/*
       var parent = div.parentNode;
       parent.parentNode.style.width = '5%';

       var field = parent.querySelector('fieldset#hideTitle' + id);
       if (!field) {
           field = document.createElement('fieldset');
           field.id = 'hideTitle' + id;
           field.style.border = 0;
           field.appendChild(document.createTextNode(title));
           parent.appendChild(field);
       } else {
           field.style.display = 'block';
       }
*/
   }

   function showItem(div, id) {
       div.parentNode.style.opacity = 1;
       var triangle = div.parentNode.querySelector('.NHT-triangle');
       if (triangle) triangle.parentNode.removeChild(triangle);
/*
       div.parentNode.parentNode.style.width = null;
       div.parentNode.querySelector('fieldset#hideTitle' + id).style.display = 'none';
*/
   }

/* FROM IMDB MY MOVIES ENHANCER */
   function eraseMyData() {
      //
      // Erase just the movies and lists information for the user
      //
//      localStorage.removeItem("myMovies-"+user);   // GUIDO NF
      GM_deleteValue("myMovies-"+user);   // GUIDO NF
      for (var i = 0; i < myLists.length; i++)
         myLists[i].ids = {};
   }

   function parseCSV(str) {
      // Simple CSV parsing function, by Trevor Dixon:
      // https://stackoverflow.com/questions/1293147/javascript-code-to-parse-csv-data
      var arr = [];
      var quote = false;  // true means we're inside a quoted field

      // iterate over each character, keep track of current row and column (of the returned array)
      var row, col, c;
      for (row = col = c = 0; c < str.length; c++) {
         var cc = str[c], nc = str[c+1];        // current character, next character
         arr[row] = arr[row] || [];             // create a new row if necessary
         arr[row][col] = arr[row][col] || '';   // create a new column (start with empty string) if necessary

         // If the current character is a quotation mark, and we're inside a
         // quoted field, and the next character is also a quotation mark,
         // add a quotation mark to the current column and skip the next character
         if (cc == '"' && quote && nc == '"') { arr[row][col] += cc; ++c; continue; }

         // If it's just one quotation mark, begin/end quoted field
         if (cc == '"') { quote = !quote; continue; }

         // If it's a comma and we're not in a quoted field, move on to the next column
         if (cc == ',' && !quote) { ++col; continue; }

         // If it's a newline (CRLF) and we're not in a quoted field, skip the next character
         // and move on to the next row and move to column 0 of that new row
         if (cc == '\r' && nc == '\n' && !quote) { ++row; col = 0; ++c; continue; }

         // If it's a newline (LF or CR) and we're not in a quoted field,
         // move on to the next row and move to column 0 of that new row
         if (cc == '\n' && !quote) { ++row; col = 0; continue; }
         if (cc == '\r' && !quote) { ++row; col = 0; continue; }

         // Otherwise, append the current character to the current column
         arr[row][col] += cc;
      }
      return arr;
   }

   var downloadedLists = 0;
   var listsNotDownloaded = [];


   function advanceProgressBar() {
      //
      // Update progress bar
      //
      downloadedLists += 1;
      var total = myLists.length;
      var p = Math.round(downloadedLists*(100/total));
      updateProgressBar(p, "Loaded "+downloadedLists+"/"+total);
      if (downloadedLists >= total) {
         updateProgressBar(0, "");
         if (listsNotDownloaded.length > 0) {
            var msg = "Done, but could not load list(s):";
            listsNotDownloaded.forEach(function(l) { msg += "\n * " + l;} );
            msg += "\n\nThis script can only read public lists.";
            alert(msg);
         } else
            alert("OK, we're done!");
      }
   }

   function downloadOK(idx, request, link) {
      //
      // Process a downloaded list
      //
      if (request.status != 200) {
          console.error("Error "+request.status+" downloading "+link+": " + request.statusText);
      } else
      if (request.responseText.indexOf("<!DOCTYPE html") >= 0) {
         console.error("Received HTML instead of CSV file from "+link);
      } else {
         var data = parseCSV(request.responseText);
         var res, entryCode;
         var fields = {};
         var type   = myLists[idx].type;
         for (var i=1; i < data.length; i++) {
            if (type == TITLES) {
               //            ___0___   _____1_____  ____2_____  ___3____  _____4_____  ____5_____  _____6_____  ______7_______  _____8_____  _______9______  ____10___  _____11_____  ___12____   _____13_____  ____14____
               // ratings  : Const,    Your Rating, Date Added, Title,    URL,         Title Type, IMDb Rating, Runtime (mins), Year,        Genres,         Num Votes, Release Date, Directors
               // others   : Position, Const,       Created,    Modified, Description, Title,      URL,         Title Type,     IMDb Rating, Runtime (mins), Year,      Genres,       Num Votes,  Release Date,  Directors
               for (var f=0; f < data[0].length; f++)
                  { fields[data[0][f]] = data[i][f]; }
//               var tt = fields["Const"];  // GUIDO NF
               var tt = fields["Title"];  // GUIDO NF
               var ratingMine = fields["Your Rating"];
               var ratingIMDb = fields["IMDb Rating"];
               if (typeof tt === "undefined")   console.error("Error processing line "+i+" of "+idx);
//               else if (tt.substr(0,2) != 'tt') console.error("Error getting IMDb const from: "+data[i]);  // GUIDO NF
               else {
//                  var ttNum = parseInt(tt.substr(2));  // GUIDO NF
                  // Encode the movie number with "base 36" to save memory
//                  entryCode = ttNum.toString(36);  // GUIDO NF
                  entryCode = tt;  // GUIDO NF
                  myLists[idx].ids[entryCode] = {m:ratingMine, i:ratingIMDb};
               }
            } else if (type == PEOPLE) {
               // ___0___   __1__  ___2___  ___3____  _____4_____  __5__  ____6____  ____7_____
               // Position, Const, Created, Modified, Description, Name,  Known For, Birth Date
               for (var f=0; f < data[0].length; f++)
                  { fields[data[0][f]] = data[i][f]; }
               var nm   = fields["Const"];
             //var name = fields["Name"];
               if (typeof nm === "undefined")   console.error("Error processing line "+i+" of "+idx);
               else if (nm.substr(0,2) != 'nm') console.error("Error getting IMDb const from: "+data[i]);
               else {
                  var nmNum = parseInt(nm.substr(2));
                  // Encode the entry with "base 36" to save memory
                  entryCode = nmNum.toString(36);
                //myLists[idx].ids[entryCode] = {n: name};
                  myLists[idx].ids[entryCode] = {};
               }
            } else if (type == IMAGES) {
               // Do nothing for now
            }
         }
         // Save data into browser
         saveMyIMDbLists();
      }

      advanceProgressBar() ;

      // Try to free some memory
      delete request.responseText;
   }

   var createFunction = function( func, p1, p2, p3 ) {
      return function() {
         func(p1, p2, p3);
      };
   };

   function downloadError(name, request, link) {
      //
      // Alert user about a download error
      //
      var msg = "Error downloading your list "+name+":\n"+
                "Status: "  +request.status + " - " + request.statusText +":\n"+
                "Source: "  +link +"\n" +
                "Headers: " +request.getAllResponseHeaders();
      alert(msg);
      console.error(msg);
      updateProgressBar(0, "");
   }

   function downloadAsync(name, idx, exportLink) {
      var request = new XMLHttpRequest();
      request.onload  = createFunction(downloadOK,     idx, request, exportLink);
      request.onerror = createFunction(downloadError, name, request, exportLink);
      request.open("GET", exportLink, true);
    //request.setRequestHeader("Accept-Encoding","gzip"); // Browser does this already? (I get 'Refused to set unsafe header "Accept-Encoding"')...
      request.send();
   }

   function downloadAsyncWatchlist(name, idx, url) {
      var request = new XMLHttpRequest();
      request.onload  = function() {
         var exportLink;
         var id = request.responseText.match('<meta property="pageId" content="(ls.+?)"/>');
         if (id && id.length > 1)
            exportLink = document.location.protocol + "//www.imdb.com/list/"+id[1]+"/export";
         else {
            id = request.responseText.match('"list":{"id":"(ls.+?)"');
            if (id && id.length > 1)
               exportLink = document.location.protocol + "//www.imdb.com/list/"+id[1]+"/export";
         }
         if (exportLink)
            downloadAsync(name, idx, exportLink);
         else {
            console.error("Could not find id of the '"+name+"' list! Try to make it public (you can make it private again right after).");
            listsNotDownloaded.push(name);
            advanceProgressBar();
         }
      };
      request.onerror = createFunction(downloadError, name, request, url);
      request.open("GET", url, true);
      request.send();
   }

   function downloadList(idx) {
      //
      // Download a list
      //
      var ur = document.location.pathname.match(/\/(ur\d+)/);
      if (ur && ur[1])
         ur = ur[1];
      else {
         alert("Sorry, but I could not find your user ID (required to download your lists). :(");
         return;
      }

      var name = myLists[idx].name;
      var id   = myLists[idx].id;
      // Watchlist & check-ins are not easily available (requires another fetch to find export link)
      // http://www.imdb.com/user/ur???????/watchlist/export                   | shows old HTML format
      // http://www.imdb.com/list/export?list_id=watchlist&author_id=ur??????? | 404 error
      // http://www.imdb.com/user/ur???????/watchlist                          | HTML page w/ "export link" at the bottom
      if (id == WATCHLIST || id == CHECKINS) {
         var url = document.location.protocol + "//www.imdb.com/user/"+ur+"/"+id;
         downloadAsyncWatchlist(name, idx, url);
      } else {
         var exportLink;
         if (id == RATINGLIST)
              exportLink = document.location.protocol + "//www.imdb.com/user/"+ur+"/"+id+"/export";
         else exportLink = document.location.protocol + "//www.imdb.com/list/"+id+"/export";
         downloadAsync(name, idx, exportLink);
      }
   }

   function downloadLists() {
      //
      // Begin to download all user lists at once (asynchronously)
      //
      downloadedLists = 0;
      for (var idx=0; idx < myLists.length; idx++)
         downloadList(idx);
      // With 10.000 items in 5 lists, the approx. time to download them (on Chrome 29) was:
      //  -  synchronously: 1:50s
      //  - asynchronously:   30s
      // Results might vary - a lot! - depending on number of lists and browser
      // Connections per hostname seems to be around 6: http://www.browserscope.org/?category=network&v=top
   }

   // Really simple progress bar...
   var pb;
   var pbBox;
   var pbTxt;

   function createProgressBar(p, msg) {
      var top_  = Math.round(window.innerHeight / 2)  -15;
      var left  = Math.round(window.innerWidth  / 2) -100;
      pbBox = document.createElement('div');
      pbBox.style.cssText  = "background-color: white; border: 2px solid black; "+
         "position: fixed; height: 30px; width: 200px; top: "+top_+"px; left: "+left+"px;";
      document.body.appendChild(pbBox);

      pb = document.createElement('div');
      pb.style.cssText = "background-color: green; border: none; height: 100%; width: "+p+"%;";
      pbBox.appendChild(pb);

      pbTxt = document.createElement('div');
      pbTxt.textContent   = msg;
      pbTxt.style.cssText = "text-align: center; margin-top: -25px; font-family: verdana,sans-serif;";
      pbBox.appendChild(pbTxt);
   }

   function updateProgressBar(p, msg) {
      if (p <= 0) {
         pbBox.style.display = "none";
         return;
      }
      pbTxt.textContent = msg;
      pb.style.width    = p+"%";
   }

   function setListOrder(listOrder) {
      //
      // Set color highlight order using lists indices, after variable listOrder (containing lists names).
      //
      if (typeof listOrder == "undefined")
         listOrder = []; // array of lists names

      listOrderIdx = []; // array of lists indices

      // First add indices set by user in listOrder
      for (var j = 0; j < listOrder.length; j++)
         for (var i = 0; i < myLists.length; i++)
            if (myLists[i].name == listOrder[j]) {
               listOrderIdx.push(i);
               break;
            }
      // Add remaining indices
      for (var i = 0; i < myLists.length; i++)
         if (!listOrderIdx.includes(i))
            listOrderIdx.push(i);
   }




   function inLists(num, type) {
      //
      // Receives an IMDb code and return the names of lists containing it.
      // Argument "num" : entry number encoded in base 36
      // Argument "type": optional, if set, limits search to a specific type of list
      //
      var num_l = 0;
      var lists = "";
      var pos   = -1;
      var rated = false;
      var imdbRating = "";
      var header     = "";
      var movie, name;
      for (var i = 0; i < myLists.length; i++) {
         if (type && myLists[i].type != type)
            continue;
         movie = myLists[i].ids[num];
         if (movie) {
            if (num_l)
               lists += "<br>";
            name = myLists[i].name;
            imdbRating = movie.i;
            if (imdbRating && name == "Your ratings") {
               name = "Your ratings: " + movie.m + " (IMDb: " + imdbRating + ")";
               rated = true;
            }
            lists += name;
            num_l += 1;
         }
      }
      if (imdbRating && !rated)
           imdbRating = "IMDb rating: " + imdbRating + "<br>";
      else imdbRating = "";
      if (num_l == 1)
           header = "<b>In your list:</b><br>";
      else header = "<b>In "+num_l+" of your lists:</b><br>";

      return imdbRating + header + '<div style="margin-left: 15px">' + lists + '</div>';
   }

/* END */


   function addHideBtn(div, func, txt, help) {
      var b           = document.createElement('a');
      b.className     = "nf-svg-button simpleround";
      b.textContent   = txt;
      b.title         = help;
      var d           = document.createElement('div');
      d.className     = "nf-svg-button-wrapper";
      d.style.cssText = 'bottom: 0; position: absolute; z-index: 10';
      d.appendChild(b);
      b.addEventListener('click', func, false);
      div.appendChild(d);
      return d;
   }

   function refreshMovieData() {
      alert(myName+"\n\n"+IMDbUser+", I'll get some info from IMDb to be able to highlight your movies,\nplease click [OK] and wait a bit...");
      eraseMyData();
      createProgressBar(0, "Loading 1/"+myLists.length+"...");
      downloadLists();
   }

   function eraseNetflixMyListData() {
      GM_deleteValue("NetflixMyList-"+user);
      myNetflixList = {};
   }

   function refreshNetflixMyListData() {
      eraseNetflixMyListData();
      var list = {};

      var gallery = document.querySelector('div.mainView div.gallery');
      var titles = gallery.querySelectorAll('div.title-card');

      for (var i=0; i < titles.length; i++) {
         var a = titles[i];

         var tt = getIdFromDiv(a);
         var movie = a.querySelector("div.fallback-text");
         var movieTitle = '';
         if (movie) movieTitle = movie.innerText;
         if (!movieTitle) movieTitle = tt;
         list[tt] = movieTitle;
      }

      var userData = JSON.stringify(list);
      GM_setValue("NetflixMyList-"+user, userData);
      alert('Netflix My list saved');
   }

   //IMDb
   var btn1; // refresh
   var btn2; // clear
   var btn4; // help
   //Netflix
   var btn8; // refresh
   var btn16; // clear

   function btnRefresh() {
      refreshMovieData();
   }

   function btnClear() {
      eraseMyData();
      alert(myName+"\n\nDone! Information cleared, so highlighting is now disabled.");
      window.location.reload();
   }

   function btnNFRefresh() {
      refreshNetflixMyListData();
   }

   function btnNFClear() {
      eraseNetflixMyListData();
      alert(myName+"\n\nDone! Information cleared.");
      window.location.reload();
   }

   function btnHelp () {
      alert(myName+"\n\nThis is a user script that:\n"+
            " • highlights links for entries in your lists (e.g., movies, series & people)\n"+
            " • shows in which of your lists an entry is (in a tooltip)\n"+
            "\nIn order to highlight the entries "+
            "in all IMDb pages as fast as possible, we need to download "+
            "the data from your lists into your browser. Unfortunately " +
            "this can be slow, so it is not done automatically. I suggest "+
            "you to update this information at most once a day.\n\n" +
            "[Refresh highlight data] updates the data in your browser.\n" +
            "[Clear highlight data] disables color highlighting.\n"
      );
   }

   function addBtn(div, func, txt, help, style) {
      var b = document.createElement('button');
      b.className     = "btn";
      if (!style) style = "margin-right: 10px; font-size: 11px;"
      b.style.cssText = style;
//      b.textContent   = txt;   // GUIDO NF
      b.textContent   = 'NF ' + txt;   // GUIDO NF
      b.title         = help;
      b.addEventListener('click', func, false);
      div.appendChild(b);
      return b;
   }

   function addIMDbButtons() {
      var main = document.getElementById("main");
      if (!main)
         console.error('Could not find "main <div>" to insert buttons!');
      else {
         var h1 = main.getElementsByTagName("h1");
         if (h1) {
            var div  = document.createElement('div');
            div.className      = "aux-content-widget-2";
            div.style.cssText  = "margin-top: 10px;";
            btn1 = addBtn(div, btnRefresh, "Refresh highlight data", "Reload information from your lists - might take a few seconds");
            btn2 = addBtn(div, btnClear,   "Clear highlight data",   "Disable color highlighting of your lists");
            btn4 = addBtn(div, btnHelp,    "What's this?",           "Click for help on these buttons");
            h1[0].appendChild(div);
         } else console.error('Could not find "<h1>Your Lists</h1>" to insert buttons!');
      }
   }
   function addNetflixButtons() {
      var main = document.querySelector('div.mainView');
      if (!main)
         console.error('Could not find "main <div>" to insert buttons!');
      else {
          var div  = document.createElement('div');
          var btnStyle = 'margin-left: 20px; margin-bottom: 20px; font-size: 13px; padding: .5em; background: 0 0; color: grey; border: solid 1px grey;';
          btn8 = addBtn(div, btnNFRefresh, "Refresh My List data", "Reload information from your list - might take a few seconds", btnStyle);
          btn16 = addBtn(div, btnNFClear,  "Clear My List data",   "Empty the data", btnStyle);
          main.appendChild(div);
      }
   }

/*
   function addButtons() {
      var div = document.querySelector(".jawbone-actions");
      if (!div)
         console.error('Could not find "button <div>" to insert buttons!');
      else {
            btn1 = addBtn(div, toggleTitle, "HIDE", "Hide this title");
      }
   }
*/

   //-------- "main" --------
   var we_are_in_a_title_page = false;
   var we_are_in_the_imdb_list_page = false;
   var we_are_in_the_netflix_list_page = false;

   if (document.location.href.match(/\.netflix\..{2,3}\//)) {
      we_are_in_a_title_page = true;
   }

   if (document.location.href == 'https://www.netflix.com/browse/my-list') {
      we_are_in_the_netflix_list_page = true;
   }
   if (document.location.href.match(/\.imdb\..{2,3}\/user\/[^\/]+\/lists/)) {
      we_are_in_the_imdb_list_page = true;
      getMyIMDbLists();
   }

   // Find current logged in user, or quit script
   user = getCurrentNetflixUser();
   if (!user) return;


   // Allow user to manually update his/her lists
   if (we_are_in_the_imdb_list_page) {
      // Find current logged in user, or quit script
      IMDbUser = getCurrentIMDbUser();
      if (!IMDbUser) return;  // FIX-ME: to support external sites: set/get LAST user to/from browser storage

      addIMDbButtons();
      return; // Nothing else to do on the lists page - goodbye!
   }
   if (we_are_in_the_netflix_list_page) {
      addNetflixButtons();
   }

   if (we_are_in_a_title_page) {
      // Load lists data for this user from localStorage
      loadMyLocalList();
      loadMyNetflixList();
      loadMyIMDbLists();
   }



// THIS IS THE NEW PART


   function hideTitleCards() {
//       console.log('waitnlp',we_are_in_the_netflix_list_page);
      //
      // Highlight all title cards in the current Netflix page
      //

      var num, color, lists, movie;
      var anchors = document.querySelectorAll('div.title-card');

      for (var i=0; i < anchors.length; i++) {
         var a = anchors[i];
         if (!a.GVhide) {
            addHideBtn(a, toggleTitle, 'H', 'Hide/show this title');

            var tt = getIdFromDiv(a);
            var title, movieTitle;
            var hideType = null;
            if (a.className.indexOf('is-disliked') != -1) hideType = 'D';
            else {
                movie = a.querySelector(".fallback-text");
                if (movie) movieTitle = movie.innerText;

                if (movieTitle) {
                    num   = movieTitle;
                    //lists = inLists(num, TITLES);
                    if        (myLists[neededLists.watch].ids[num]) {
                        hideType = 'W';
                    } else if (myLists[neededLists.tbd].ids[num]) {
                        hideType = 'T';
                    } else if (myLists[neededLists.visti].ids[num]) {
                        hideType = 'S';
                    } else if (myLists[neededLists.no].ids[num]) {
                        hideType = 'N';
                    }
                }
            }
            if (!hideType && (title = myLocalList[tt])) hideType = 'H';
            if ((!hideType || hideType == 'W') && !we_are_in_the_netflix_list_page && (title = myNetflixList[tt])) {
//                console.log('ht',hideType,'tt',tt,'mt',movieTitle);
                var row = a.closest('div.lolomoRow');
                if (!row || (row.dataset.listContext != 'queue' && row.dataset.listContext != 'continueWatching')) hideType = 'M';
//                console.log('rownull',!row,'lt',row.dataset.listContext);
            }

            if (hideType) hideItem(a, tt, title, hideType);
            a.GVhide = true; // set to "true" when "enhanced" (so we skip it on next pass)
         }
      }
   }



   // start the hiding title function
//   if (myLists.length) {
      hideTitleCards();
      if (interval >= 100) setInterval(hideTitleCards, interval);
//   }

}