|
4 | 4 | // @name:zh-TW X-Downloader |
5 | 5 | // @name:ja X-Downloader |
6 | 6 | // @namespace hoothin |
7 | | -// @version 2025-08-09 |
| 7 | +// @version 2025-08-10 |
| 8 | +// @license MIT |
8 | 9 | // @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)上添加一个便捷的下载按钮,一键轻松保存喜欢的媒体内容。 |
10 | 11 | // @description:zh-TW 優化您的 Twitter (X) 瀏覽體驗,直接在圖片及影片(GIF)上新增一個便捷的下載按鈕,一鍵輕鬆儲存喜愛的媒體內容。 |
11 | 12 | // @description:ja Twitter (X) の画像や動画(GIF)に便利なダウンロードボタンを追加し、ワンクリックでお気に入りのメディアを簡単に保存できるようにします。 |
12 | 13 | // @author hoothin |
13 | 14 | // @match https://x.com/* |
14 | 15 | // @match https://twitter.com/* |
15 | 16 | // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== |
16 | 17 | // @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 |
17 | 20 | // ==/UserScript== |
18 | 21 |
|
19 | 22 | (function() { |
20 | 23 | 'use strict'; |
21 | | - let downloadBtn = document.createElement("div"); |
| 24 | + let downloadBtn = document.createElement("a"); |
| 25 | + downloadBtn.target = "_blank"; |
22 | 26 | 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;"; |
23 | 27 | 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 => { |
27 | 29 | let parent = downloadBtn.parentNode; |
28 | 30 | if (!parent) return; |
29 | 31 | let img = parent.querySelector('[data-testid="tweetPhoto"]>img'); |
30 | 32 | 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; |
32 | 34 | if (/\.svg$/.test(newsrc)) return; |
33 | 35 | if (newsrc == img.src) { |
34 | 36 | newsrc=newsrc.replace(/\?format=/i, ".").replace(/\&name=/i, ":").replace(/\.(?=[^\.\/]*$)/, "?format=").replace( /(:large|:medium|:small|:orig|:thumb|:[\dx]+)/i, ""); |
35 | 37 | if (newsrc != img.src) { |
36 | 38 | newsrc = newsrc + "&name=orig"; |
37 | 39 | } |
38 | 40 | } |
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 | + } |
40 | 65 | } else { |
41 | 66 | while(parent) { |
42 | 67 | if (parent.nodeName == "ARTICLE" && parent.dataset && parent.dataset.testid == "tweet") { |
|
45 | 70 | parent = parent.parentNode; |
46 | 71 | } |
47 | 72 | if (parent) { |
| 73 | + downloadBtn.removeAttribute('download'); |
48 | 74 | 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 | + } |
50 | 79 | } |
51 | 80 | } |
52 | 81 | }); |
| 82 | + downloadBtn.addEventListener("click", e => { |
| 83 | + if (e.altKey) { |
| 84 | + e.preventDefault(); |
| 85 | + e.stopPropagation(); |
| 86 | + } |
| 87 | + }); |
53 | 88 | downloadBtn.addEventListener("mouseenter", () => { |
54 | 89 | downloadBtn.style.opacity = 1; |
55 | 90 | }); |
56 | 91 | downloadBtn.addEventListener("mouseleave", () => { |
57 | 92 | downloadBtn.style.opacity = 0.5; |
58 | 93 | }); |
| 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 | + } |
59 | 112 | document.addEventListener("mouseenter", e => { |
60 | 113 | if (e.target.dataset && e.target.dataset.testid == "tweetPhoto") { |
61 | 114 | e.target.parentNode.appendChild(downloadBtn); |
|
0 commit comments