Skip to content

Commit 5f57be1

Browse files
committed
Update X-Downloader.user.js
1 parent 2e54cf9 commit 5f57be1

File tree

1 file changed

+62
-9
lines changed

1 file changed

+62
-9
lines changed

X-Downloader/X-Downloader.user.js

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,64 @@
44
// @name:zh-TW X-Downloader
55
// @name:ja X-Downloader
66
// @namespace hoothin
7-
// @version 2025-08-09
7+
// @version 2025-08-10
8+
// @license MIT
89
// @description Enhances your Twitter (X) experience by adding a convenient download button to images and videos (GIFs), enabling easy, one-click saving of media.
9-
// @description:zh-CN 优化你的 Twitter (X) 浏览体验,直接在图片和视频(GIF)上添加一个便捷的下载按钮,一键轻松保存喜欢的媒体内容。
10+
// @description:zh-CN 优化你的推特 (X) 浏览体验,直接在图片和视频(GIF)上添加一个便捷的下载按钮,一键轻松保存喜欢的媒体内容。
1011
// @description:zh-TW 優化您的 Twitter (X) 瀏覽體驗,直接在圖片及影片(GIF)上新增一個便捷的下載按鈕,一鍵輕鬆儲存喜愛的媒體內容。
1112
// @description:ja Twitter (X) の画像や動画(GIF)に便利なダウンロードボタンを追加し、ワンクリックでお気に入りのメディアを簡単に保存できるようにします。
1213
// @author hoothin
1314
// @match https://x.com/*
1415
// @match https://twitter.com/*
1516
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
1617
// @grant none
18+
// @downloadURL https://update.greasyfork.org/scripts/545186/X-Downloader.user.js
19+
// @updateURL https://update.greasyfork.org/scripts/545186/X-Downloader.meta.js
1720
// ==/UserScript==
1821

1922
(function() {
2023
'use strict';
21-
let downloadBtn = document.createElement("div");
24+
let downloadBtn = document.createElement("a");
25+
downloadBtn.target = "_blank";
2226
downloadBtn.style.cssText = "background: #000000aa; border-radius: 50%; transition: opacity ease 0.3s; position: absolute; top: 0; right: 0px; cursor: pointer; opacity: 0.5; padding: 5px;";
2327
downloadBtn.innerHTML = `<svg width="25" height="25" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>`;
24-
downloadBtn.addEventListener("click", e => {
25-
e.preventDefault();
26-
e.stopPropagation();
28+
downloadBtn.addEventListener("mousedown", e => {
2729
let parent = downloadBtn.parentNode;
2830
if (!parent) return;
2931
let img = parent.querySelector('[data-testid="tweetPhoto"]>img');
3032
if (img) {
31-
let newsrc = img.src.replace("_normal.",".").replace("_200x200.",".").replace("_mini.",".").replace("_bigger.",".").replace(/_x\d+\./,".");
33+
let newsrc = img.src.replace("_normal.",".").replace("_200x200.",".").replace("_mini.",".").replace("_bigger.",".").replace(/_x\d+\./,"."), imgname;
3234
if (/\.svg$/.test(newsrc)) return;
3335
if (newsrc == img.src) {
3436
newsrc=newsrc.replace(/\?format=/i, ".").replace(/\&name=/i, ":").replace(/\.(?=[^\.\/]*$)/, "?format=").replace( /(:large|:medium|:small|:orig|:thumb|:[\dx]+)/i, "");
3537
if (newsrc != img.src) {
3638
newsrc = newsrc + "&name=orig";
3739
}
3840
}
39-
window.open(newsrc, "_blank");
41+
while(parent) {
42+
if (parent.nodeName == "ARTICLE" && parent.dataset && parent.dataset.testid == "tweet") {
43+
break;
44+
}
45+
parent = parent.parentNode;
46+
}
47+
if (parent) {
48+
const time = parent.querySelector('time[datetime]');
49+
const user = parent.querySelector('[role="link"]>div>div>span>span');
50+
let formatMatch = img.src.match(/format=(\w+)/), ext = "jpg";
51+
if (formatMatch) {
52+
ext = formatMatch[1];
53+
} else {
54+
formatMatch = newsrc.match(/\.(\w+)/);
55+
if (formatMatch) {
56+
ext = formatMatch[1];
57+
}
58+
}
59+
imgname = `${user.innerText} ${time.innerText.replace(/(.*) · (.*)/, "$2 $1")}.${ext}`;
60+
}
61+
downloadBtn.href = newsrc;
62+
if (e.altKey) {
63+
downloadByFetch(newsrc, imgname);
64+
}
4065
} else {
4166
while(parent) {
4267
if (parent.nodeName == "ARTICLE" && parent.dataset && parent.dataset.testid == "tweet") {
@@ -45,17 +70,45 @@
4570
parent = parent.parentNode;
4671
}
4772
if (parent) {
73+
downloadBtn.removeAttribute('download');
4874
let link = parent.querySelector('a[role="link"][aria-label]');
49-
window.open(`https://twitter.hoothin.com/?url=${encodeURIComponent(link ? link.href : document.location.href)}`, "_blank");
75+
downloadBtn.href = `https://twitter.hoothin.com/?url=${encodeURIComponent(link ? link.href : document.location.href)}`;
76+
if (e.altKey) {
77+
window.open(downloadBtn.href, "_blank");
78+
}
5079
}
5180
}
5281
});
82+
downloadBtn.addEventListener("click", e => {
83+
if (e.altKey) {
84+
e.preventDefault();
85+
e.stopPropagation();
86+
}
87+
});
5388
downloadBtn.addEventListener("mouseenter", () => {
5489
downloadBtn.style.opacity = 1;
5590
});
5691
downloadBtn.addEventListener("mouseleave", () => {
5792
downloadBtn.style.opacity = 0.5;
5893
});
94+
async function downloadByFetch(imageUrl, filename) {
95+
try {
96+
const response = await fetch(imageUrl);
97+
if (!response.ok) throw new Error('CORS request failed');
98+
const blob = await response.blob();
99+
const blobUrl = URL.createObjectURL(blob);
100+
const tempLink = document.createElement('a');
101+
tempLink.href = blobUrl;
102+
tempLink.setAttribute('download', filename);
103+
document.body.appendChild(tempLink);
104+
tempLink.click();
105+
document.body.removeChild(tempLink);
106+
URL.revokeObjectURL(blobUrl);
107+
} catch (error) {
108+
console.error('error:', error);
109+
window.open(imageUrl, '_blank');
110+
}
111+
}
59112
document.addEventListener("mouseenter", e => {
60113
if (e.target.dataset && e.target.dataset.testid == "tweetPhoto") {
61114
e.target.parentNode.appendChild(downloadBtn);

0 commit comments

Comments
 (0)