您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Automatically scroll to the last viewed or marked Facebook story
// ==UserScript== // @name Facebook last post scroller // @namespace https://github.com/soufianesakhi/facebook-last-post-scroller // @description Automatically scroll to the last viewed or marked Facebook story // @author https://github.com/soufianesakhi // @copyright 2016-2017, Soufiane Sakhi // @license MIT; https://opensource.org/licenses/MIT // @homepage https://github.com/soufianesakhi/facebook-last-post-scroller // @supportURL https://github.com/soufianesakhi/facebook-last-post-scroller/issues // @icon https://cdn3.iconfinder.com/data/icons/watchify-v1-0-80px/80/arrow-down-80px-128.png // @require http://code.jquery.com/jquery.min.js // @require https://greasyfork.org/scripts/19857-node-creation-observer/code/node-creation-observer.js?version=174436 // @include https://www.facebook.com/* // @include https://web.facebook.com/* // @version 1.3.1 // @grant GM_setValue // @grant GM_getValue // ==/UserScript== /// <reference path="../typings/index.d.ts" /> var storySelector = "[id^='hyperfeed_story_id']"; var subStorySelector = ".userContentWrapper"; var scrollerBtnPredecessorSelector = "#pagelet_composer"; var storyLinkSelector = "div._5pcp span > span > a._5pcq[target]"; var lastPostButtonAppendSelector = "div._5pcp"; var blueBarId = "pagelet_bluebar"; var timestampAttribute = "data-timestamp"; var loadedStoryByPage = 10; var fbUrlPatterns = [ new RegExp("https?:\/\/(web|www)\.facebook\.com\/\\?sk\=h_chr", "i"), new RegExp("https?:\/\/(web|www)\.facebook\.com\/?$", "i"), new RegExp("https?:\/\/(web|www)\.facebook\.com\/\\?ref\=logo", "i")]; var lastPostIconLink = "https://cdn3.iconfinder.com/data/icons/watchify-v1-0-80px/80/arrow-down-80px-128.png"; var iconStyle = "vertical-align: middle; height: 20px; width: 20px; cursor: pointer;"; var lastPostSeparatorTitle = "End of new posts"; var scriptId = "FBLastPost"; var menuId = "FBLastPostMenu"; var lastPostSeparatorId = scriptId + "Separator"; var lastPostURIKey = scriptId + "URI"; var lastPostTimestampKey = scriptId + "Timestamp"; var lastPostScrollerId = getId("Scroller"); var reverseSortLoaderId = getId("ReverseSortLoader"); var lastPostURI = GM_getValue(lastPostURIKey, null); var lastPostTimestamp = GM_getValue(lastPostTimestampKey, 0); var storyCount = 0; /** @type {MutationObserver[]} */ var storyLoadObservers = []; /** @type {Element[]} */ var loadedStories = []; /** @type {Element[]} */ var checkedStories = []; /** @type {Number} */ var previousScrollHeight; var stopped = false; var isMostRecentMode = false; var isHome = false; var currentURL = null; /** @type {JQuery} */ var loadingToolbar; /** @type {JQuery} */ var loadingProgress; /** @type {Number} */ var timeSinceLastpost; $(document).ready(function () { initLastPostButtonObserver(); initButtons(); }); function initLastPostButtonObserver() { NodeCreationObserver.onCreation(lastPostButtonAppendSelector, function (storyDetailsElement) { checkURLChange(); if (!isHomeMostRecent()) { return; } var storyElement = $(storyDetailsElement).closest(storySelector); var storyId = storyElement.attr('id'); var lastPostIconId = getId(storyId); $(storyDetailsElement).append('<span id="' + lastPostIconId + '" > <abbr title="Set as last post"><img src="' + lastPostIconLink + '" style="' + iconStyle + '" /></abbr></span>'); $("#" + lastPostIconId).click(function () { if (confirm("Set this post as the last ?")) { var storyElement = $(this).closest(storySelector); setLastPost(storyElement); } }); }); } function initLoadingToolbar() { timeSinceLastpost = Math.floor(Date.now() / 1000) - lastPostTimestamp; console.log('timeSinceLastpost: ' + timeSinceLastpost); loadingToolbar = $("<div>", { id: getId("LoadingToolbar"), style: "position: fixed; top: 50px; left: 300px; width: 400px; z-index: 9999; background-color: beige; padding: 10px; border: 1px solid grey; border-radius: 2px;" }).insertAfter(scrollerBtnPredecessorSelector); $("<img>", { src: lastPostIconLink, style: "position: absolute; top: 5px; right: 10px; height: 30px; width: 30px; " }).appendTo(loadingToolbar); var stopLoadingBtn = $("<button>", { id: getId("StopLoading"), type: "submit", style: "cursor: pointer; color: buttontext; background-color: buttonface;" }).text("Stop loading & scrolling").appendTo(loadingToolbar); loadingProgress = $("<progress>", { value: 0, max: 100, style: "margin-left: 40px;" }).appendTo(loadingToolbar); stopLoadingBtn.click(stopLoading); } function updateProgress(currentTimestamp) { var progress = 100 - 100 * (currentTimestamp - lastPostTimestamp) / timeSinceLastpost; loadingProgress.attr("value", progress); } function getButton(id, title) { return '<button id="' + id + '" type="submit" style="margin-left: 2%; cursor: pointer;"><img src="' + lastPostIconLink + '" style="' + iconStyle + '" />' + title + '</button>'; } function getMenu(children) { return '<div id="' + menuId + '" style="text-align: center;" >' + children + '</div>'; } function initButtons() { NodeCreationObserver.onCreation(scrollerBtnPredecessorSelector, function (predecessor) { checkURLChange(); if (!isHomeMostRecent()) { return; } var children = getButton(lastPostScrollerId, "Scroll to last post"); children += getButton(reverseSortLoaderId, "Load last post and revese sort stories"); $(predecessor).after(getMenu(children)); $("#" + lastPostScrollerId).click(startLoading); $("#" + reverseSortLoaderId).click(startLoading); }); } /** * @param {JQueryEventObject} eventObject */ function startLoading(eventObject) { var reverseSort = eventObject.target.id === reverseSortLoaderId; $("#" + menuId).hide(); initLoadingToolbar(); NodeCreationObserver.onCreation(storySelector, function (element) { if (stopped) { return; } storyCount++; if (loadedStories.indexOf(element) == -1) { loadedStories.push(element); } if (storyCount % loadedStoryByPage == 0) { waitForStoriesToLoad(element.id, storyCount, reverseSort); return; } if (storyCount == 1) { if (lastPostURI == null) { NodeCreationObserver.remove(storySelector); stopped = true; return; } searchForStory(reverseSort); } else if (storyCount == 2) { searchForStory(reverseSort); scrollToBottom(); storyCount = 10; } }); } function checkURLChange() { var url = document.URL; if (url !== currentURL) { currentURL = url; isHome = matchesFBHomeURL(); if (isHome) { checkMostRecentMode(); } } } function isHomeMostRecent() { return isHome && isMostRecentMode; } function checkMostRecentMode() { var element = $("#stream_pagelet a[href^='/?sk=h_nor']"); var elementExist = element.length == 1; isMostRecentMode = elementExist && element.is(':visible'); } function matchesFBHomeURL() { var isHome = false; fbUrlPatterns.forEach(function (pattern) { if (pattern.test(currentURL)) { isHome = true; } }); return isHome; } function setLastPost(storyElement) { var uri = getStoryURI(storyElement); var timestamp = getStoryTimestamp(storyElement); GM_setValue(lastPostURIKey, uri); GM_setValue(lastPostTimestampKey, timestamp); console.log("Setting last post: " + uri + " (timestamp: " + timestamp + ")"); } function getId(elementId) { return scriptId + "-" + elementId; } /** * @param {string} id * @param {number} count * @param {boolean} reverseSort */ function waitForStoriesToLoad(id, count, reverseSort) { var mutationObserver = new MutationObserver(function (elements, observer) { var loadedStories = storyCount - count; if (stopped) { observer.disconnect(); return; } if (loadedStories > loadedStoryByPage - 1) { observer.disconnect(); storyLoadObservers = removeFromArray(storyLoadObservers, observer); searchForStory(reverseSort); } else { scrollToBottom(); } }); storyLoadObservers.push(mutationObserver); mutationObserver.observe(document.documentElement, { childList: true, subtree: true }); } /** * @param {boolean} reverseSort */ function searchForStory(reverseSort) { loadedStories.forEach(function (element) { if (!stopped && checkedStories.indexOf(element) == -1) { var uri = getStoryURI(element); if (uri != null) { checkedStories.push(element); var ts = getStoryTimestamp(element); updateProgress(ts); var notSuggested = notSuggestedStory(element); if (uri === lastPostURI) { stopSearching(element.id, reverseSort); } else if (ts < lastPostTimestamp && notSuggested) { stopSearching(element.id, reverseSort); console.log("The last post was not found: " + lastPostURI + " (" + lastPostTimestamp + ")"); console.log("Stopped at the story: " + uri + " (" + ts + ")" + (notSuggested ? "" : " (suggested story)")); } } } }); } function notSuggestedStory(storyElement) { if ($(storyElement).find("img[alt=explore]").length > 0) { return false; } var div = $(storyElement).find("._5g-l"); var notSuggested = div.length == 0 || div.find(".profileLink").length > 0; if (notSuggested) { notSuggested = $(storyElement).find("span:not([class]) > span[class]:not(:has(*))").length == 0; } return notSuggested; } function getStoryTimestamp(storyElement) { return Number($(storyElement).attr(timestampAttribute)); } function getStoryURI(storyElement) { var aLink = $(storyElement).find(storyLinkSelector); if (aLink != null) { return aLink.attr("href"); } return null; } function stopLoading() { loadingToolbar.hide(); stopped = true; NodeCreationObserver.remove(storySelector); storyLoadObservers.forEach(function (observer) { observer.disconnect(); }); storyLoadObservers = []; } /** * @param {string} id * @param {boolean} reverseSort */ function stopSearching(id, reverseSort) { setLastPost(checkedStories[0]); stopLoading(); var lastPostSeparator = $("<div>", { id: lastPostSeparatorId, style: 'margin-bottom: 10px; text-align: center;' }).text(lastPostSeparatorTitle); $("#" + id).before(lastPostSeparator); if (reverseSort) { window.scrollTo(0, 0); var timestamps = []; var parent = $(checkedStories[0]).parent(); for (var i = 1; i < checkedStories.length - 1; i++) { timestamps.push(getStoryTimestamp(checkedStories[i])); $(checkedStories[i]).detach().prependTo(parent); } } else { var offsetHeight = document.getElementById(blueBarId).offsetHeight; var height = lastPostSeparator[0].offsetTop; var y = height - offsetHeight; window.scrollTo(0, y > 0 ? y : 0); } } function removeFromArray(array, element) { var index = array.indexOf(element); if (index > -1) { return array.splice(index, 1); } return array; } function scrollToBottom() { var currentScrollHeight = document.body.scrollHeight; if (previousScrollHeight !== currentScrollHeight) { previousScrollHeight = currentScrollHeight; window.scrollTo(0, currentScrollHeight); } }