您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
Common functions for working on lists of entries
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.icu/scripts/390248/738286/EntryList.js
// EntryList library // Common functions for modifying/hiding/etc. entries in page, based on // entry features or presence in one or more lists. // For instance: hide all YouTube videos that have been watched and highlight // the ones that have been started but not finished, highlight Netflix movies // based on IMDb lists, etc. // // http://greasyfork.icu/scripts/390248-entrylist // Copyright (C) 2019, Guido Villa // Original idea and some 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 // // For instructions, see http://greasyfork.icu/help/installing-user-scripts // // -------------------------------------------------------------------- // // ==UserScript== // @namespace http://greasyfork.icu/users/373199-guido-villa // @exclude * // // ==UserLibrary== // @name EntryList // @description Common functions for working on lists of entries // @version 1.5 // @author guidovilla // @date 03.10.2019 // @copyright 2019, Guido Villa (http://greasyfork.icu/users/373199-guido-villa) // @license GPL-3.0-or-later // @homepageURL http://greasyfork.icu/scripts/390248-entrylist // @supportURL https://gitlab.com/gv-browser/userscripts/issues // @contributionURL https://tinyurl.com/gv-donate-0c // // @downloadURL http://greasyfork.icu/scripts/390248-entrylist/code/EntryList.user.js // @updateURL http://greasyfork.icu/scripts/390248-entrylist/code/EntryList.meta.js // ==/UserScript== // // ==/UserLibrary== // // -------------------------------------------------------------------- // // To-do (priority: [H]igh, [M]edium, [L]ow): // - [H] Extend library to work on all the scripts // - [M] Make private members actually private and not only undocumented // (only after understanding which ones really can be private) // - [M] main context as default context // - [M] do we need that the library is not cached? if so, how? // - [M] changes to a list aren't reflected in page till reload. Change? // - [M] Better handle case without lists (e.g. optimizations) // - [M] Add description of flow in usage documentation // - [M] Add indication of URL to use to @require library itself // - [M] List regeneration function doesn't handle case where lists are missing // // Changelog: // ---------- // 2019.10.03 [1.5] Automatically handle case with only one list // Better handling of list of lists // Add possibility to permanently skip an entry // 2019.10.02 [1.4] More generic: getUser and getIdFromEntry are now optional // Add newContext utility function // 2019.09.30 [1.3] Correct @namespace and other headers (for public use) // 2019.09.27 [1.2] Refactoring and name changing: TitleList -> EntryList // 2019.09.27 [1.1] Code cleanup (string literals, reorder functions) // Check for validity of the context object // Add usage documentation // 2019.09.21 [1.0] First version // 2019.09.18 [0.1] First test version, private use only // /* jshint esversion: 6, supernew: true */ /* exported EL, Library_Version_ENTRYLIST */ const Library_Version_ENTRYLIST = '1.5'; /* How to use the library This library instantitates an EL object with a startup method. Call EL.startup(ctx), passing a "context" object that is specific to the website you are working on. Other functions and variables: - mainContext: the context saved with EL.startup - addToggleEventOnClick(button, howToFindEntry[, toggleList[, toggleType]]): mainly used in ctx.modifyEntry(), adds an event listener that implements a toggle function: - button: the DOM object to attach the event listener to - howToFindEntry: how to go from evt.target to the entry object. It can be: - a number: # of node.parentNode to hop to get from evt.target to to entry - a CSS selector: used with evt.target.closest to get to entry - toggleList: the list where the entry is toggled when the button is pressed (can be omitted if a default list is to be used) - toggleType: the processing type that is toggled by the press of the button (can be omitted if only one processing type is used) It cannot be a false value (0, null, false, undefined, etc.) - newContext(name): utility function that returns a new context, initialized with <name> - markInvalid(entry): marks entry as invalid to skips it in subsequent passes This function returns false so it can be used in isValidEntry() in this way: return condition || EL.markInvalid(entry) This leaves the return value unchanged and marks the entry only if invalid Mandatory callback functions and variables in context: - name: identifier of the site - getPageEntries(): return (usually with querySelectorAll) an array of entries to be treated - processItem(entry, tt, processingType): process the entry based on the processing type or other features of the entry Conditionally mandatory callback functions and variables in context: - getUser(): retrieve and return the username used on the website mandatory if data are to be stored on a per-user basis - getIdFromEntry(entry): return a tt: { id, name } object from the entry mandatory if you want to save entries to lists NOTE: if id is not found, entry is skipped but it is not marked as invalid for subsequent passes (unless you use TL.markInvalid(), see above) - unProcessItem(entry, tt, processingType): like processItem, but it should reverse the action mandatory for entries that have a toggle action added with EL.addToggleEventOnClick() Optional callback functions and variables in context: - interval: interval (in ms) to re-scan links in the DOM won't re-scan if < MIN_INTERVAL dafault: DEFAULT_INTERVAL - isEntryPage(): returns false if page must not be scanned for entries default is always true (all pages contain entries) - isValidEntry(entry): return false if entry must be skipped NOTE: if entry is skipped, it is not however marked as invalid for subsequent passes (unless you use TL.markInvalid(), see above) default is always true (all entries returned by "getPageEntries" are valid) - modifyEntry(entry): optionally modify entry when scanned for the first time (e.g. add a button) see also EL.addToggleEventOnClick() above - determineType(lists, tt, entry): return the processing type for an entry, given the lists it appears in, or a false value (0, null, false, undefined, etc.) if no processing is required "lists" is an object with a true property for each list the entry appears in. The decision can also be taken using name, id and properties of the entry. If there is a single processing type, the function might as well return true/false Default: returns true if entry is in at least one list (especially useful in cases with only one list, so there is no need to tell different lists apart) */ var EL = new (function() { 'use strict'; const STORAGE_SEP = '-'; const FAKE_USER = '_'; const DEFAULT_TYPE = '_DEF_'; const MIN_INTERVAL = 100; const DEFAULT_INTERVAL = 1000; var self = this; this.mainContext = null; /* PRIVATE members */ // Check if "object" has "property" of "type" // used to test if object "implements" a specific interface function checkProperty(object, property, type, optional) { if (typeof object[property] !== type && (!optional || typeof object[property] !== 'undefined')) { console.error((optional ? 'Optionally, c' : 'C') + 'ontext must have a "' + property + '" property of type "' + type + '"'); return false; } else return true; } // check if context has the correct variables and functions function isValidTargetContext(ctx) { var valid = true; valid &= checkProperty(ctx, 'name', 'string'); valid &= checkProperty(ctx, 'getPageEntries', 'function'); valid &= checkProperty(ctx, 'processItem', 'function'); valid &= checkProperty(ctx, 'interval', 'number', true); valid &= checkProperty(ctx, 'isEntryPage', 'function', true); valid &= checkProperty(ctx, 'isValidEntry', 'function', true); valid &= checkProperty(ctx, 'modifyEntry', 'function', true); valid &= checkProperty(ctx, 'determineType', 'function', true); valid &= checkProperty(ctx, 'getUser', 'function', true); valid &= checkProperty(ctx, 'getIdFromEntry', 'function', true); valid &= checkProperty(ctx, 'unProcessItem', 'function', true); return !!valid; } // standardized names for storage variables var storName = { 'lastUser': function(ctx) { return ctx.name + STORAGE_SEP + 'lastUser'; }, 'listOfLists': function(ctx) { return 'EntryLists' + STORAGE_SEP + ctx.user; }, 'listPrefix': function(ctx) { return 'EntryList' + STORAGE_SEP + ctx.user + STORAGE_SEP; }, 'listName': function(ctx, listName) { return this.listPrefix(ctx) + listName; }, }; // Return name of user currently logged on <ctx> site // Return last saved value and log error if no user is found this.getLoggedUser = function(ctx) { if (!ctx.getUser) return FAKE_USER; 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(storName.lastUser(ctx), ''); console.error('Using last user: ' + user); } GM_setValue(storName.lastUser(ctx), user); ctx.user = user; return user; }; // Regenerate and save the list of lists stored object, even if empty // returns the new list function regenerateListOfLists(ctx) { var allVariables = GM_listValues(); var listNames = allVariables.reduce(function(listNames, variable) { if (variable.startsWith(storName.listPrefix(ctx))) { listNames.push(variable.substring(storName.listPrefix(ctx).length)); } return listNames; }, []); var userData = JSON.stringify(listNames); GM_setValue(storName.listOfLists(ctx), userData); return listNames; } // Load a single saved lists function loadSavedList(listName) { 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; } // Load the list of lists, regenerating it if necessary // always returns an array, possibly empty function loadListOfLists(ctx) { var listNames = loadSavedList(storName.listOfLists(ctx)); if (!Array.isArray(listNames)) listNames = regenerateListOfLists(ctx); return listNames; } // Load lists for the current user this.loadSavedLists = function(ctx) { var listNames = loadListOfLists(ctx); var lists = {}; var list; var mustRegenerateListOfLists = false; listNames.forEach(function(listName) { list = loadSavedList(storName.listName(ctx, listName)); if (list) lists[listName] = list; else mustRegenerateListOfLists = true; }); if (mustRegenerateListOfLists) regenerateListOfLists(ctx); return lists; }; // Save single list for the current user this.saveList = function(ctx, list, name) { var listNames = loadListOfLists(ctx); if (listNames.indexOf(name) == -1) { listNames.push(name); var userData = JSON.stringify(listNames); GM_setValue(storName.listOfLists(ctx), userData); } userData = JSON.stringify(list); GM_setValue(storName.listName(ctx, name), userData); }; // Receives an entry tt and finds all lists where tt.id appears this.inLists = function(ctx, tt) { var lists = {}; for (var list in ctx.allLists) { if (ctx.allLists[list][tt.id]) lists[list] = true; } return lists; }; // Wrap ctx.getIdFromEntry and add error logging function _wrap_getIdFromEntry(ctx, entry) { var tt = ctx.getIdFromEntry(entry); if (!tt) console.error('Could not determine id :-( - for entry', entry); return tt; } // Process all entries in current page this.processEntries = function(ctx) { var entries = ctx.getPageEntries(); 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.ELProcessed || entry.ELInvalid) continue; // see if entry is valid if (ctx.isValidEntry && !ctx.isValidEntry(entry)) continue; tt = null; if (ctx.getIdFromEntry) { tt = _wrap_getIdFromEntry(ctx, entry); if (!tt) continue; } if (ctx.modifyEntry) ctx.modifyEntry(entry); lists = ( tt ? self.inLists(ctx, tt) : {} ); processingType = (ctx.determineType ? ctx.determineType(lists, tt, entry) : Object.keys(lists).length > 0); if (processingType) { ctx.processItem(entry, tt, processingType); entry.ELProcessingType = processingType; } entry.ELProcessed = true; // set to "true" after processing (so we skip it on next pass) } }; // handle the toggle event this.handleToggleButton = function(evt) { evt.stopPropagation(); evt.preventDefault(); var data = evt.target.dataset; var toggleList = (typeof data.toggleList === 'undefined' ? DEFAULT_TYPE : data.toggleList); var toggleType = (typeof data.toggleType === 'undefined' ? DEFAULT_TYPE : data.toggleType); // get corresponding 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); } self.toggleEntry(entry, toggleList, toggleType); }; // add/remove entry from a list this.toggleEntry = function(entry, toggleList, toggleType) { var ctx = self.mainContext; var tt = _wrap_getIdFromEntry(ctx, entry); if (!tt) return; // check if item is in list var list = ctx.allLists[toggleList]; if (!list) list = ctx.allLists[toggleList] = {}; if (list[tt.id]) { delete list[tt.id]; ctx.unProcessItem(entry, tt, toggleType); entry.ELProcessingType = '-' + toggleType; } else { list[tt.id] = tt.name; ctx.processItem(entry, tt, toggleType); entry.ELProcessingType = toggleType; } self.saveList(ctx, list, toggleList); }; /* PUBLIC members */ // utility function to create a new context, initialized with <name> this.newContext = function(name) { return { 'name': name }; }; // startup function this.startup = function(ctx) { // check that passed context is good if (!isValidTargetContext(ctx)) { console.log('Invalid context, aborting'); return; } self.mainContext = ctx; //TODO forse salvare una variabile we_are_in_an_entry_page nel contesto? //TODO per altri casi lo startup deve fare anche altro if (!( !ctx.isEntryPage || ctx.isEntryPage() )) return; // find current logged in user, or quit script if (!self.getLoggedUser(ctx)) { console.log('No user is defined, aborting'); return; } // Load list data for this user from local storage ctx.allLists = self.loadSavedLists(ctx); // start the entry processing function self.processEntries(ctx); if (typeof ctx.interval === 'undefined' || ctx.interval >= MIN_INTERVAL) { // TODO we might consider using MutationObserver in the future, instead ctx.timer = setInterval(function() {self.processEntries(ctx);}, ctx.interval || DEFAULT_INTERVAL); } }; this.addToggleEventOnClick = function(button, howToFindEntry, toggleList, toggleType) { button.dataset.howToFindEntry = howToFindEntry; if (typeof toggleList !== 'undefined') button.dataset.toggleList = toggleList; if (typeof toggleType !== 'undefined') button.dataset.toggleType = toggleType; button.addEventListener('click', self.handleToggleButton, false); }; this.markInvalid = function(entry) { entry.ELInvalid = true; return false; } })();