// ==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;
}
// 收藏数 View
function 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 fnColorStars
const $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 forEach
const $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 转换为 Array
function 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")) {
// 记录触发次数到 dataset
const 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;
}
// 遍历并查找 .EntryTitleLink
for (let i = 0; i < $$list.length; i++) {
const $item = $$list[i];
const $title = $item.querySelector(".EntryTitleLink");
if ($title) {
fnItemTitle($title);
}
}
};
fnElChange($n("#root"), () => {
fnItemTitleWrap();
});
})();