Threads.net Image Downloader

Add a download button to a specific div on Threads.net to download all images in the <picture> tag of the post.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         Threads.net Image Downloader
// @namespace    http://tampermonkey.net/
// @version      1.8
// @license      MIT
// @description  Add a download button to a specific div on Threads.net to download all images in the <picture> tag of the post.
// @author       StevenJon0826
// @match        https://www.threads.net/*
// @match        https://www.threads.com/*
// @grant        GM_download
// ==/UserScript==

(function() {
    'use strict';

    // ⭐ 自動點開 Spoiler 遮罩
    const openSpoilers = () => {
        document.querySelectorAll(".x5yr21d.x1n2onr6.xh8yej3").forEach(el => {
            if (el.__openedSpoiler) return; // 避免重複點擊

            const text = el.textContent?.trim();
            if (text !== "Spoiler") return;

            el.__openedSpoiler = true;
            el.click(); // 🔥 Threads 原生行為:click 即可解除 spoiler
        });
    };

    function addButtonToElement(element) {
        // 檢查該元素是否已經存在按鈕,避免重複加入
        if (!element.querySelector('button.my-custom-button')) {
            // 建立按鈕
            const button = document.createElement('button');
            button.textContent = 'Download';
            button.classList.add('my-custom-button');
            button.style.position = 'relative';
            // 當按鈕被點擊時,往上找兩層並統計<picture>元素內的<img>數量
            button.addEventListener('click', function() {
                // 阻止事件的預設行為和冒泡
                event.preventDefault();
                event.stopPropagation();

                // 往上找兩層 //x1s688f
                const grandparentElement = element.parentElement?.parentElement?.parentElement?.parentElement?.parentElement;
                if (grandparentElement) {
                    // 在祖先層級中尋找所有的<picture>元素
                    const pictures = grandparentElement.querySelectorAll('picture img');
                    // 找到 class 包含 x1s688f 的 <span> 並取得其文字內容
                    const spanElement = grandparentElement.querySelector('span[class*="x1s688f"]');
                    let spanText;
                    if (spanElement) {
                        spanText = spanElement.textContent; // 取得文字內容
                    }
                    const timeElement = grandparentElement.querySelector('time'); // 假設只有一個time元素
                    let formattedTime;
                    if (timeElement) {
                        const datetimeValue = timeElement.getAttribute('datetime'); // 取得datetime屬性
                        const dateObject = new Date(datetimeValue); // 將datetime轉換為Date物件

                        // 格式化日期為YYYYMMDD_hhmmss
                        const year = dateObject.getFullYear();
                        const month = String(dateObject.getMonth() + 1).padStart(2, '0'); // 月份從0開始,所以加1
                        const day = String(dateObject.getDate()).padStart(2, '0');
                        const hours = String(dateObject.getHours()).padStart(2, '0');
                        const minutes = String(dateObject.getMinutes()).padStart(2, '0');
                        const seconds = String(dateObject.getSeconds()).padStart(2, '0');

                        formattedTime = `${year}${month}${day}_${hours}${minutes}${seconds}`;
                    }
                    pictures.forEach((img, index) => {
                        const imageUrl = img.src;
                        const filename = `Threads-${spanText}-${formattedTime}-${index + 1}.jpg`;
                        GM_download(imageUrl, filename);
                    });

                    // 找到 aria-label="讚" 的元素並模擬點擊
                    const likeButton = grandparentElement.querySelector('[aria-label="讚"]');
                    if (likeButton) {
                        if (typeof likeButton.click === 'function') {
                            likeButton.click(); // 如果元素有 click 方法,模擬點擊
                        } else {
                            // 如果該元素是SVG,則創建一個事件手動觸發
                            const event = new MouseEvent('click', {
                                bubbles: true,
                                cancelable: true
                            });
                            likeButton.dispatchEvent(event); // 模擬點擊事件
                        }
                        console.log('Like button clicked');
                    } else {
                        console.log('No like button with aria-label "讚" found');
                    }

                } else {
                    console.log('Could not find grandparent element');
                }
            });
            // 將按鈕加入到該元素中
            element.appendChild(button);
        }
    }

    function scanForElements() {
        // 定期掃描符合條件的元素
        const elements = document.querySelectorAll('div[class*="x1fc57z9"]');
        elements.forEach(addButtonToElement);
    }

    // 使用setInterval每隔一段時間檢查畫面上是否有符合條件的元素
    setInterval(scanForElements, 500); // 每秒檢查一次
    setInterval(openSpoilers, 500); // 每秒檢查一次
})();