Greasy Fork is available in English.
新标签页打开条目时自动标记为已读,收藏计数
// ==UserScript==// @name 「Feedly」中键标记已读 + 收藏导出为*.url// @namespace https://www.wdssmq.com/// @version 1.0.7// @author 沉冰浮水// @description 新标签页打开条目时自动标记为已读,收藏计数// @license MIT// @null ----------------------------// @contributionURL https://github.com/wdssmq#%E4%BA%8C%E7%BB%B4%E7%A0%81// @contributionAmount 5.93// @null ----------------------------// @link https://github.com/wdssmq/userscript// @link https://afdian.com/@wdssmq// @link https://greasyfork.org/zh-CN/users/6865-wdssmq// @null ----------------------------// @noframes// @run-at document-end// @match https://feedly.com/*// @grant GM_openInTab// @grant GM_setClipboard// @grant GM_addStyle// ==/UserScript==/* jshint esversion: 6 *//* eslint-disable */(function () {'use strict';const gm_name = "feedly";const curDate = new Date();// ---------------------------------------------------const _curUrl = () => { return window.location.href };const _getDateStr = (date = curDate) => {const options = { year: "numeric", month: "2-digit", day: "2-digit" };return date.toLocaleDateString("zh-CN", options).replace(/\//g, "-");};// ---------------------------------------------------const _log = (...args) => console.log(`[${gm_name}]|`, ...args);const _warn = (...args) => console.warn(`[${gm_name}]|`, ...args);// ---------------------------------------------------// const $ = window.$ || unsafeWindow.$;function $n(e) {return document.querySelector(e);}function $na(e) {return document.querySelectorAll(e);}// ---------------------------------------------------const fnElChange = (el, fn = () => { }) => {const observer = new MutationObserver((mutationRecord, mutationObserver) => {// _log('mutationRecord = ', mutationRecord);// _log('mutationObserver === observer', mutationObserver === observer);fn(mutationRecord, mutationObserver);// mutationObserver.disconnect();});observer.observe(el, {// attributes: false,// attributeFilter: ["class"],childList: true,// characterData: false,subtree: true,});};function fnFindDom(el, selector) {return el.querySelectorAll(selector);}// 原生 js 实现 jquery 的 closest 方法function fnFindDomUp(el, selector) {const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector;while (el) {if (matchesSelector.call(el, selector)) {break;}el = el.parentElement;}return el;}const curTime = Math.floor(curDate.getTime() / 1000);const curHours = Math.floor(curTime / 3600);// const cur4Hours = Math.floor(curTime / (60 * 60 * 4));const cur4Minutes = Math.floor(curTime / 240);// localStorage 封装const lsObj = {setItem: (key, value) => {localStorage.setItem(key, JSON.stringify(value));},getItem: (key, def = "") => {const item = localStorage.getItem(key);if (item) {return JSON.parse(item);}return def;},};// 数据读写封装const gobInfo = {// key: [默认值, 是否记录至 ls]$$Stars: [null, 0],bolUpd: [false, 0],bolReset: [false, 1],cntStars: [0, 0],diffStars: [{ decr: 0, incr: 0 }, 1],lstStars: [0, 1],// 记录输出过的日志logHistory: [[], 0],};// decrease 减少// increase 增加const gob = {_lsKey: `${gm_name}_data`,_bolLoaded: false,_time: {cycle: 0,rem: 0,},data: {},// 初始init() {// 根据 gobInfo 设置 gob 属性for (const key in gobInfo) {if (Object.hasOwnProperty.call(gobInfo, key)) {const item = gobInfo[key];this.data[key] = item[0];Object.defineProperty(this, key, {// value: item[0],// writable: true,get() { return this.data[key] },set(value) { this.data[key] = value; },});}}return this;},// 读取load() {if (this._bolLoaded) {return;}const lsData = lsObj.getItem(this._lsKey, this.data);_log("[log]gob.load()\n", lsData);for (const key in lsData) {if (Object.hasOwnProperty.call(lsData, key)) {const item = lsData[key];this.data[key] = item;}}this._bolLoaded = true;},// 保存save() {const lsData = {};for (const key in gobInfo) {if (Object.hasOwnProperty.call(gobInfo, key)) {const item = gobInfo[key];if (item[1]) {lsData[key] = this.data[key];}}}_log("[log]gob.save()", lsData);lsObj.setItem(this._lsKey, lsData);},};// 初始化gob.init().load();// 星标条目获取gob.GetStarItems = () => {const $listWrap = $n("div.StreamPage");// _log("gob.GetStarItems", $listWrap);if ($listWrap) {gob.$$Stars = $listWrap.querySelectorAll("article a.EntryTitleLink");gob.cntStars = gob.$$Stars.length;// _log("gob.GetStarItems", gob.$$Stars, gob.cntStars);}};// 获取星标条目 nodeList, 用于交换位置gob.GetStarNodes = () => {return $na(".StreamPage .entry.titleOnly");};// 输出日志,只输出一次gob.LogOnce = (key, value) => {if (gob.logHistory.includes(key)) {return;}gob.logHistory.push(key);_log(key, value);};// 判断当前地址是否是收藏页const fnCheckUrl = () => {if ("https://feedly.com/i/saved" === _curUrl()) {return true;}return false;};const fnCheckControl = (diff) => {const iTime = curHours;const modTime = iTime % 4;gob._time.cycle = iTime;gob._time.rem = modTime;// _log("fnCheckControl", JSON.stringify(diff), iTime, modTime);// diff.decr 累计已读// diff.incr 累计新增if (diff.decr >= 17 && diff.decr - diff.incr >= 4) {if (modTime === 0) {return "reset";} else {return "lock";}}return "default";};// 星标变动控制function fnControl() {const $end = $n(".list-entries > h2");if (!$end) {gob.LogOnce("fnControl_34", "页面加载中");return false;}// 读取 gob 值备用const strReset = gob.bolReset.toString();let diffStars = gob.diffStars;// 解锁重置判断if (gob.bolReset) {diffStars = { decr: 0, incr: 0 };}// decrease 减少// increase 增加// 星标变化计数const diff = gob.cntStars - gob.lstStars;// 写入新的星标数gob.lstStars = gob.cntStars;// 初始化时直接返回if (diff === gob.cntStars) {gob.save();return true;}// 判断变化状态if (diff > 0) {// 新增星标计数diffStars.incr += Math.abs(diff);} else {// 已读星标计数diffStars.decr += Math.abs(diff);}gob.bolReset = ((diff) => {if (fnCheckControl(diff) === "reset") {return true;}return false;})(diffStars);// 更新 localStorage 存储if (diff !== 0 || strReset !== gob.bolReset.toString()) {gob.diffStars = diffStars;gob.save();}return true;}// 收藏数 Viewfunction fnViewStars() {// gob.cntStars = fnGetItems(gob);gob.GetStarItems();// _log("fnViewStars", gob.cntStars);const strText = `Read later(${gob.cntStars} 丨 -${gob.diffStars.decr} 丨 +${gob.diffStars.incr})(${gob._time.cycle} - ${gob._time.rem})`;$n("h1 #header-title").innerHTML = strText;if ($n("header.header h2")) {$n("header.header h2").innerHTML = strText;}$n("#header-title").innerHTML = strText;}gob.pickRule = {forMod: 13,minPick: 4,maxPick: 7,lstPick: 0,pickList: [],随机次数: 0,};GM_addStyle(`.pick,.pick:hover {background-color: #ddd !important;}.un-mark {background-color: #f6f7f8 !important;}.lock {color: transparent !important;}`);// 随机交换两个节点的位置function fnRndNodeList(nodeList) {let lstNode = nodeList[0];const parent = nodeList[0].parentNode;// console.log("fnRndNodeList", parent);for (let i = 0; i < nodeList.length; i++) {const node = nodeList[i];if (Math.random() > 0.5) {parent.insertBefore(node, lstNode);} else {parent.insertBefore(lstNode, node);}lstNode = node;}}// 按规则给星标条目着色function fnColorStars(offset = 0) {// begin fnColorStarsconst $stars = gob.$$Stars;const isLock = "lock" === fnCheckControl(gob.data.diffStars) ? true : false;// console.log("fnColorStars", fnCheckControl(gob.data.diffStars), isLock);const oConfig = gob.pickRule;const $$pick = $na(".pick");// ----------------------------let pickCount = $$pick.length;if (pickCount >= oConfig.minPick) {gob.LogOnce("fnColorStars_37", "已经选够了");return;}// ----------------------------if ($stars.length <= 39 && oConfig.随机次数 <= 3) {// 遍历 dom 节点,随机交换位置fnRndNodeList(gob.GetStarNodes());gob.pickRule.随机次数 += 1;return;}// ----------------------------const fnPick = ($item, i) => {if (i > 1 && i - oConfig.lstPick < oConfig.minPick / 2) {return;}oConfig.lstPick = i;$item.classList.add("pick");pickCount += 1;};// ----------------------------[].forEach.call($stars, ($e, i) => {// begin forEachconst $ago = fnFindDom(fnFindDomUp($e, "div.SelectedEntryScroller"), ".ago");const href = $e.href;const hash = parseInt((href + $ago.innerHTML).replace(/\D/g, ""));// _log("fnColorStars", $ago, href, hash);const $item = fnFindDomUp($e, ".entry");if ($item.classList.contains("pick")) {oConfig.lstPick = i;return;}// ----------------------------const $saved = fnFindDom($item, ".EntryReadLaterButton--saved");if (!$saved) {$item.classList.add("un-mark");}// ----------------------------if (oConfig.pickList.includes(hash)) {// fnPick($item);return;}// ----------------------------const bolPick = (() => {let rlt = true;if (i >= 37) {rlt = false;}if (pickCount >= oConfig.maxPick) {rlt = false;}return rlt;})();// ----------------------------let intNum = parseInt(hash + cur4Minutes) + offset;if (intNum % oConfig.forMod == 0 && bolPick) {oConfig.pickList.push(hash);fnPick($item, i);} else {if (isLock || i >= 37) {$item.classList.add("lock");[].forEach.call(fnFindDom($item, "a, span, div>svg, .summary"), ($ite) => {$ite.classList.add("lock");});}}// end forEach});// ----------------------------if (pickCount <= oConfig.minPick) {if (offset < oConfig.forMod) {fnColorStars(offset + 1);return;} else {_log("fnColorStars", "未能选够");_log("fnColorStars", oConfig);oConfig.pickList = [];oConfig.lstPick = 0;}}// end fnColorStars}// 滚动条滚动时触发function fnOnScroll() {fnViewStars();fnColorStars();}// 处理器函数function fnHandler(e = null) {if (!fnCheckUrl()) {return;}// 判断是否为滚动事件if (e && e.type === "scroll") {fnOnScroll();}// 更新 ls 记录let bolUpd = false;if (!gob.bolUpd) {bolUpd = fnControl();}if (bolUpd) {_warn("fnHandler", gob.data);gob.bolUpd = true;}}// 星标部分入口函数function fnMain(record, observer) {if (!fnCheckUrl()) {return;}// 随机直接返回if (Math.random() > 0.6) {return;}gob.GetStarItems();if (gob.cntStars === 0) {return;}gob.LogOnce("_laterCtrl fnMain", { cntStars: gob.cntStars });// observer.disconnect();// 绑定事件if ($n("#feedlyFrame") && $n("#feedlyFrame").dataset.addEL !== "done") {// 加载后尝试执行一次fnHandler();$n("#feedlyFrame").addEventListener("scroll", fnHandler);$n("#feedlyFrame").dataset.addEL = "done";_log("fnMain", "绑定滚动监听");observer.disconnect();}}fnElChange($n("#root"), fnMain);// nodeList 转换为 Arrayfunction fnNodeListToArray(nodeList) {return Array.prototype.slice.call(nodeList);}// 构造 Bash Shell 脚本function fnMKShell(arrList, prefix = "") {const curDateStr = _getDateStr();const _lenTitle = (title) => {// 获取长度,中文算两个字符const len = title.length;const len2 = title.replace(/[\u4e00-\u9fa5]/g, "").length;return len2 + (len - len2) * 2;};let strRlt ="if [ ! -d \"prefix-date\" ]; then\n" +"mkdir prefix-date\n" +"fi\n" +"cd prefix-date\n\n";strRlt = strRlt.replace(/prefix/g, prefix);strRlt = strRlt.replace(/date/g, curDateStr);/*** e {title:"", href:""}*/arrList.forEach(function (e, i) {const serial = i + 1;// _log(e);// 移除不能用于文件名的字符let title = e.title || e.innerText;title = title.replace(/\\|\/|:|\*|!|\?]|<|>/g, "");title = title.replace(/["'\s]/g, "");// _log(title);const lenTitle = _lenTitle(title);// 判断太长时截取if (lenTitle >= 137) {title = title.substring(0, 69); // 截取前 69 个字符}// 获取文章链接const href = e.href || e.url;// url 文件名const urlFileName = `${serial}丨${title}.url`;strRlt += `echo [InternetShortcut] > "${urlFileName}"\n`;strRlt += `echo "URL=${href}" >> "${urlFileName}"\n`;strRlt += "\n";});{strRlt += "exit\n\n";}return strRlt;}// 星标文章导出为 *.url 文件$n("#root").addEventListener("mouseup", function (event) {gob.GetStarItems();const $target = event.target;// 判断是 h2 标签if ($target.tagName !== "H2") {return;}// console.log($target,$target.innerText);if ($target.innerText.indexOf("END OF FEED") > -1) {const listItems = fnNodeListToArray(gob.$$Stars);GM_setClipboard(fnMKShell(listItems, "feedly"));$target.innerText = "已复制到剪贴板";}}, false);// 拿回订阅源地址// 绑定监听事件到 div#box 上$n("#root").addEventListener("mouseup", function (event) {// 输出触发事件的元素// 根据内容判断是否执行相应操作const elText = event.target.innerHTML;if (// elText.indexOf("Feed not found") > -1 ||elText.indexOf("Wrong feed URL") > -1) {// 内部再输出一次确定判断条件正确_log("elText", elText);const curUrl = decodeURIComponent(_curUrl()).replace("https://feedly.com/i/subscription/feed/", "");_log("curUrl", curUrl);// 拿到解码后的订阅源地址// const curUrl = ((url) => {// return url.replace("https://feedly.com/i/subscription/feed/", "");// })(decodeURIComponent(curUrl));// 输出到页面中$n("#feedlyPageFX h2").insertAdjacentHTML("beforeend",`<div class="sub">${curUrl}</div>`,);}}, false);// 自动标记已读(() => {if (!$n("#root") || $n("#root").dataset.MarkRead === "bind") {return;}// _log("fnAutoMark", "自动标记已读");$n("#root").dataset.MarkRead = "bind";// 根据事件返回需要的 dom 元素const fnEventFilter = (eType, eTgt) => {// _log("fnEventFilter", eType, eTgt);let pick = false;let objRlt = null, objDef = {$entry: null,$btn: null,};if (eType === "mouseup") {if (eTgt.classList.contains("EntryTitle") && eTgt.nodeName === "DIV") {const $entry = fnFindDomUp(eTgt, "article.entry");const $btn = $entry.querySelector("button.EntryMarkAsReadButton");// _log("fnEventFilter", $entry, $btn);objRlt = {// 当前条目元素$entry,// 标记已读的按钮$btn,};pick = true;}} else if (eType === "mouseover") {if (eTgt.nodeName === "ARTICLE" && eTgt.className.indexOf("entry") > -1) {objRlt = {// 当前内容条目元素$entry: eTgt,// 标记已读的按钮$btn: eTgt.querySelector("button.EntryMarkAsReadButton"),};// _log("fnEventFilter", "移入");// _log("fnEventFilter", eTgt.dataset.leaveCount, typeof eTgt.dataset.leaveCount);const intLeaveCount = parseInt(eTgt.dataset.leaveCount);// 已经触发过 leave 事件时才通过pick = intLeaveCount >= 1 ? true : false;if (pick) {return objRlt;}// _log("fnEventFilter", intLeaveCount);if (!isNaN(intLeaveCount)) {// _log("fnEventFilter", "已绑定移出事件");return objDef;}// 绑定移出事件eTgt.addEventListener("mouseleave", () => {// _log("fnEventFilter", "移出");const intLeaveCount = parseInt(eTgt.dataset.leaveCount);if (intLeaveCount === 0) {// await _sleep(1000);eTgt.dataset.leaveCount = "1";}}, false);// 设置初始值eTgt.dataset.leaveCount = 0;}}if (pick) {return objRlt;}return objDef;};// 事件处理函数const fnEventHandler = (event) => {// 限制鼠标在元素右侧移入才会触发if (event.type === "mouseover") {const intDiff = Math.abs(event.offsetX - event.target.offsetWidth);if (intDiff > 17) {return;}}const { $entry, $btn } = fnEventFilter(event.type, event.target);if (!$entry || !$btn) {return;}// 判断是否含有指定类名if ($entry.className.indexOf("entry--read") === -1) {_log("fnMarRead", event.button, "自动标记已读");$btn.click();}};// 绑定监听事件$n("#root").addEventListener("mouseup", fnEventHandler, false);$n("#root").addEventListener("mouseover", fnEventHandler, false);})();// 防止误点const fnStopSource = (e) => {const $target = e.target;if ($target.classList.contains("entry__source")) {// 记录触发次数到 datasetconst intCount = $target.dataset.clickCount || 0;if (intCount === 0) {$target.dataset.clickCount = intCount + 1;e.preventDefault();e.stopPropagation();// e.stopImmediatePropagation();// alert("entry__source");return;}}};$n("#root").addEventListener("click", fnStopSource);// 条目标题处理const fnItemTitle = ($e) => {// _log("fnItemTitle", $e);if ($e.dataset.ptDone) {return;}const origTitle = $e.innerText;// _log("origTitle", origTitle);// 定义一个函数用于获取年度及分辨率const fnGetVideoLabel = (videoTitle) => {videoTitle = videoTitle.replace("4K.", "2160p.");// 定义一个正则数组,用于匹配年度及分辨率const arrRegexp = [/(?<year>\d{4})\.(?<res>\d+p)\./,/(?<year>\d{4})\.S\d+.*?(?<res>\d+p)\./,/(?<year>\d{4})\.Complete\.(?<res>\d+p)\./,/(?<year>\d{4})\..+?(?<res>\d+p)\./i,];// 遍历正则数组,匹配年度及分辨率let objLabel = null;for (let i = 0; i < arrRegexp.length; i++) {const regexp = arrRegexp[i];const match = videoTitle.match(regexp);if (match) {objLabel = match.groups;break;}}return objLabel;};const arrMatch = origTitle.match(/(?<cate>\[[^\]]+\])[^[]+-(?<group>[^[]+)(?<title>\[.+\])$/);if (arrMatch) {const strCate = arrMatch.groups.cate.replace(/^\[[^)]+\(([^)]+)\)\]/, "[$1]");const strGroup = arrMatch.groups.group;const strTitle = arrMatch.groups.title;// 提取年度及分辨率const objLabel = fnGetVideoLabel(origTitle);const strNewTitle = `${strTitle} - ${strCate}[${objLabel?.year}][${objLabel?.res}][${strGroup}]`;$e.innerText = strNewTitle;$e.dataset.ptDone = "1";}};const fnItemTitleWrap = (e) => {const $$list = $na("#feedlyPageFX .entry");if (!$$list.length) {return;}// 遍历并查找 .EntryTitleLinkfor (let i = 0; i < $$list.length; i++) {const $item = $$list[i];const $title = $item.querySelector(".EntryTitleLink");if ($title) {fnItemTitle($title);}}};fnElChange($n("#root"), () => {fnItemTitleWrap();});})();