X.com/Twitter Automation

Automatically loads new posts on X.com/Twitter and scrolls back to the reading position.

目前為 2024-11-23 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name               X.com/Twitter Automation
// @name:de            X.com/Twitter Automatik
// @name:es            Automatización de X.com/Twitter
// @name:fr            Automatisation de X.com/Twitter
// @name:it            Automazione di X.com/Twitter
// @name:pt            Automação do X.com/Twitter
// @name:ru            Автоматизация X.com/Twitter
// @name:zh            X.com/Twitter 自动化
// @name:ja            X.com/Twitter 自動化
// @name:ko            X.com/Twitter 자동화
// @name:hi            X.com/Twitter स्वचालन
// @name:ar            أتمتة X.com/Twitter
// @namespace          http://tampermonkey.net/
// @description        Automatically loads new posts on X.com/Twitter and scrolls back to the reading position.
// @description:de     Lädt automatisch neue Beiträge auf X.com/Twitter und scrollt zur Leseposition zurück.
// @description:es     Carga automáticamente nuevos tweets en X.com/Twitter y vuelve a la posición de lectura.
// @description:fr     Charge automatiquement de nouveaux posts sur X.com/Twitter et revient à la position de lecture.
// @description:it     Carica automaticamente nuovi post su X.com/Twitter e torna alla posizione di lettura.
// @description:pt     Carrega automaticamente novos posts no X.com/Twitter e retorna à posição de leitura.
// @description:ru     Автоматически загружает новые посты на X.com/Twitter и возвращает к позиции чтения.
// @description:zh     自动加载 X.com/Twitter 上的新帖子,并返回到阅读位置。
// @description:ja     X.com/Twitterで新しい投稿を自動的に読み込み、読書位置に戻ります.
// @description:ko     X.com/Twitter에서 새 게시물을 자동으로 로드하고 읽던 위치로 돌아갑니다.
// @description:hi     X.com/Twitter पर नए पोस्ट स्वचालित रूप से लोड करता है और पढ़ने की स्थिति पर वापस ले जाता है.
// @description:ar     يقوم بتحميل المنشورات الجديدة تلقائيًا على X.com/Twitter ويعيدك إلى موضع القراءة.
// @icon               https://cdn-icons-png.flaticon.com/128/14417/14417460.png
// @supportURL         https://www.paypal.com/paypalme/Coopiis?country.x=DE&locale.x=de_DE
// @author             Copiis
// @version            2024.12.24
// @license            MIT
// @match              https://x.com/home
// @grant              GM_setValue
// @grant              GM_getValue
// ==/UserScript==

(function () {
    let isAutomationActive = false;
    let isAutoScrolling = false;
    let savedTopPostData = null;

    window.onload = () => {
        console.log("Seite vollständig geladen. Initialisiere Script...");
        initializeScript();
    };

    function initializeScript() {
        loadSavedData();
        if (savedTopPostData) {
            console.log(`Gespeicherte Daten gefunden. Versuche zum gespeicherten Beitrag zu scrollen: Handler: ${savedTopPostData.authorHandler}, Timestamp: ${savedTopPostData.timestamp}`);
            scrollToSavedPost();
        } else {
            console.log("Keine gespeicherten Daten gefunden. Automatik startet erst, wenn der Benutzer manuell scrollt.");
        }

        const observer = new MutationObserver(() => {
            if (isNearTopOfPage() && !isAutomationActive) {
                activateAutomation();
            }

            if (isAutomationActive) {
                const newPostsButton = getNewPostsButton();
                if (newPostsButton) {
                    newPostsButton.click();
                    waitForNewPostsToLoad(() => scrollToSavedPost());
                }
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });

        window.addEventListener('scroll', () => {
            if (isAutoScrolling) return;

            if (isNearTopOfPage() && !isAutomationActive) {
                activateAutomation();
            } else if (window.scrollY > 3 && isAutomationActive) {
                deactivateAutomation();
            }
        });
    }

    function waitForNewPostsToLoad(callback) {
        const interval = setInterval(() => {
            const posts = document.querySelectorAll("article");
            if (posts.length > 0) {
                console.log("Neue Beiträge geladen.");
                clearInterval(interval);
                callback();
            } else {
                console.log("Warte auf das Laden neuer Beiträge...");
            }
        }, 500);
    }

    function isNearTopOfPage() {
        return window.scrollY <= 3;
    }

    function isPageFullyLoaded() {
        return document.readyState === "complete";
    }

    function loadSavedData() {
        const savedData = GM_getValue("topPostData", null);
        if (savedData) {
            savedTopPostData = JSON.parse(savedData);
            console.log("Daten geladen:", savedTopPostData);
        }
    }

    function scrollToSavedPost() {
        const interval = setInterval(() => {
            if (!isPageFullyLoaded()) {
                console.log("Warte auf vollständiges Laden der Seite...");
                return;
            }

            const matchedPost = findPostByData(savedTopPostData);

            if (matchedPost) {
                clearInterval(interval);
                console.log("Gespeicherter Beitrag gefunden. Warte auf vollständiges Laden...");
                waitForPostToLoad(matchedPost, () => {
                    console.log("Gespeicherter Beitrag vollständig geladen. Scrollen...");
                    scrollToPost(matchedPost, "center");
                });
            } else if (!isAtBottomOfPage()) {
                console.log("Scrollen nach unten, um mehr Beiträge zu laden...");
                scrollWithLazyLoading();
            } else {
                console.log("Gespeicherter Beitrag konnte nicht gefunden werden. Weitere Beiträge werden geladen...");
            }
        }, 1000);
    }

    function waitForPostToLoad(post, callback) {
        const interval = setInterval(() => {
            if (isPostFullyLoaded(post)) {
                clearInterval(interval);
                callback();
            } else {
                console.log("Warte darauf, dass der Beitrag vollständig geladen wird...");
            }
        }, 500);
    }

    function isPostFullyLoaded(post) {
        const timeElement = post.querySelector("time");
        const isTimeLoaded = timeElement && timeElement.getAttribute("datetime");

        const authorElement = post.querySelector(".css-1jxf684.r-bcqeeo.r-1ttztb7.r-qvutc0.r-poiln3");
        const isAuthorLoaded = authorElement && authorElement.textContent.trim();

        const images = Array.from(post.querySelectorAll("img"));
        const areImagesLoaded = images.every(img => img.complete && img.naturalWidth > 0);

        return isTimeLoaded && isAuthorLoaded && areImagesLoaded;
    }

    function scrollWithLazyLoading() {
        const interval = setInterval(() => {
            const posts = Array.from(document.querySelectorAll("article"));
            const lastPost = posts[posts.length - 1];

            if (!lastPost || !isPostFullyLoaded(lastPost)) {
                console.log("Warte darauf, dass der letzte Beitrag vollständig geladen wird...");
                return;
            }

            console.log("Letzter Beitrag vollständig geladen. Scrolle weiter...");
            clearInterval(interval);
            window.scrollBy({ top: 500, behavior: "smooth" });
        }, 1000);
    }

    function getNewPostsButton() {
        return Array.from(document.querySelectorAll("button, span")).find((button) =>
            /neue Posts anzeigen|Post anzeigen/i.test(button.textContent.trim())
        );
    }

    function scrollToPost(post, position = "center") {
        isAutoScrolling = true;
        post.scrollIntoView({ behavior: "smooth", block: position });
        setTimeout(() => {
            isAutoScrolling = false;
        }, 1000);
    }

    function isAtBottomOfPage() {
        return window.innerHeight + window.scrollY >= document.body.scrollHeight - 1;
    }

    function activateAutomation() {
        isAutomationActive = true;
        console.log("Automatik aktiviert.");
        saveTopPostData();
    }

    function deactivateAutomation() {
        isAutomationActive = false;
        console.log("Automatik deaktiviert.");
    }

    function saveTopPostData() {
        const topPost = getTopVisiblePost();
        if (topPost) {
            savedTopPostData = {
                timestamp: getPostTimestamp(topPost),
                authorHandler: getPostAuthorHandler(topPost),
            };
            saveData(savedTopPostData);
        }
    }

    function getTopVisiblePost() {
        const posts = Array.from(document.querySelectorAll("article"));
        return posts.length > 0 ? posts[0] : null;
    }

    function getPostTimestamp(post) {
        const timeElement = post.querySelector("time");
        return timeElement?.getAttribute("datetime") || null;
    }

    function getPostAuthorHandler(post) {
        const authorElement = post.querySelector(".css-1jxf684.r-bcqeeo.r-1ttztb7.r-qvutc0.r-poiln3");
        return authorElement?.textContent.trim() || null;
    }

    function saveData(data) {
        GM_setValue("topPostData", JSON.stringify(data));
        console.log(`Daten dauerhaft gespeichert: Handler: ${data.authorHandler}, Timestamp: ${data.timestamp}`);
    }

    function findPostByData(data) {
        if (!data || !data.timestamp || !data.authorHandler) return null;

        const posts = Array.from(document.querySelectorAll("article"));
        return posts.find((post) => {
            const postTimestamp = getPostTimestamp(post);
            const postAuthorHandler = getPostAuthorHandler(post);
            return (
                postTimestamp &&
                postAuthorHandler &&
                isTimestampMatching(postTimestamp, data.timestamp) &&
                postAuthorHandler === data.authorHandler
            );
        });
    }

    function isTimestampMatching(postTimestamp, savedTimestamp) {
        if (!postTimestamp || !savedTimestamp) return false;

        const postDate = new Date(postTimestamp);
        const savedDate = new Date(savedTimestamp);

        return (
            postDate.getFullYear() === savedDate.getFullYear() &&
            postDate.getMonth() === savedDate.getMonth() &&
            postDate.getDate() === savedDate.getDate() &&
            postDate.getHours() === savedDate.getHours() &&
            postDate.getMinutes() === savedDate.getMinutes()
        );
    }
})();