// ==UserScript== // @name XEnhancer // @name:ar XEnhancer // @name:bg XEnhancer // @name:ckb XEnhancer // @name:cs XEnhancer // @name:da XEnhancer // @name:de XEnhancer // @name:el XEnhancer // @name:en XEnhancer // @name:eo XEnhancer // @name:es XEnhancer // @name:es-419 XEnhancer // @name:fi XEnhancer // @name:fr XEnhancer // @name:fr-CA XEnhancer // @name:he XEnhancer // @name:hr XEnhancer // @name:hu XEnhancer // @name:id XEnhancer // @name:it XEnhancer // @name:ja XEnhancer // @name:ka XEnhancer // @name:ko XEnhancer // @name:nb XEnhancer // @name:nl XEnhancer // @name:pl XEnhancer // @name:pt-BR XEnhancer // @name:ro XEnhancer // @name:ru XEnhancer // @name:sk XEnhancer // @name:sr XEnhancer // @name:sv XEnhancer // @name:th XEnhancer // @name:tr XEnhancer // @name:uk XEnhancer // @name:ug XEnhancer // @name:vi XEnhancer // @description XEnhancer empowers your Twitter/X browsing — save media in one click, format timestamps for clarity, and streamline your social feed with ease. // @description:ar XEnhancer يعزز تجربتك على تويتر (X) — احفظ الوسائط بنقرة واحدة، نسق الطوابع الزمنية لتوضيح أفضل، ونظّم تغذيتك الاجتماعية بسهولة. // @description:bg XEnhancer подобрява разглеждането на Twitter (X) — запазвайте медии с едно кликване, форматирайте времевите отметки за яснота и оптимизирайте социалния си поток лесно. // @description:ckb XEnhancer بەرز دەکاتەوە بەرەوپێشگای تیوتر (X) — وێنە و ڤیدیۆکان بە یەک کرتە پاشکەوت بکە، کاتی ڕووداوەکان ڕوون بکەرەوە، و فیدەکانی کۆمەڵایەتی بە ئاسانیدا ڕێکبخە. // @description:cs XEnhancer vylepšuje prohlížení Twitteru (X) — ukládejte média jedním kliknutím, formátujte časová razítka pro přehlednost a snadno optimalizujte svůj sociální feed. // @description:da XEnhancer forbedrer din oplevelse på Twitter (X) — gem medier med et enkelt klik, formater tidsstempler for klarhed, og strømlin din sociale feed med lethed. // @description:de XEnhancer verbessert Ihr Twitter/X-Erlebnis — speichern Sie Medien mit einem Klick, formatieren Sie Zeitstempel zur besseren Übersicht und optimieren Sie Ihren Social-Feed mühelos. // @description:el Το XEnhancer ενισχύει την περιήγησή σας στο Twitter (X) — αποθηκεύστε πολυμέσα με ένα κλικ, μορφοποιήστε χρονικές σημάνσεις για μεγαλύτερη σαφήνεια και οργανώστε εύκολα το κοινωνικό σας feed. // @description:en XEnhancer empowers your Twitter/X browsing — save media in one click, format timestamps for clarity, and streamline your social feed with ease. // @description:eo XEnhancer plibonigas vian retumadon de Twitter (X) — konservu amaskomunikilojn per unu klako, formatu tempstampon por pli granda klareco, kaj faciligu vian socian fluon. // @description:es XEnhancer mejora tu experiencia en Twitter (X): guarda medios con un clic, formatea las marcas de tiempo para mayor claridad y optimiza tu feed social con facilidad. // @description:es-419 XEnhancer mejora tu navegación en Twitter (X) — guarda medios con un clic, formatea las marcas de tiempo para mayor claridad y agiliza tu feed social fácilmente. // @description:fi XEnhancer parantaa Twitter (X) -selailuasi — tallenna media yhdellä napsautuksella, muotoile aikaleimat selkeyden lisäämiseksi ja tehosta sosiaalista syötettäsi vaivattomasti. // @description:fr XEnhancer améliore votre navigation sur Twitter (X) — enregistrez des médias en un clic, formatez les horodatages pour plus de clarté et optimisez facilement votre flux social. // @description:fr-CA XEnhancer améliore votre expérience sur Twitter (X) — sauvegardez les médias en un clic, formatez les horodatages pour plus de clarté et simplifiez votre flux social facilement. // @description:he XEnhancer משדרג את הגלישה שלך ב-Twitter (X) — שמור מדיה בלחיצה אחת, עצב חותמות זמן להבהרה וייעל את הפיד החברתי שלך בקלות. // @description:hr XEnhancer poboljšava pregledavanje Twittera (X) — spremite medije jednim klikom, formatirajte vremenske oznake radi preglednosti i pojednostavite svoj društveni feed. // @description:hu Az XEnhancer fokozza a Twitter/X böngészést — egy kattintással mentheted a médiát, formázhatod az időbélyegeket az átláthatóság érdekében, és könnyedén optimalizálhatod a közösségi hírcsatornát. // @description:id XEnhancer meningkatkan pengalaman menjelajahi Twitter/X — simpan media dengan satu klik, format timestamp untuk kejelasan, dan permudah feed sosial Anda. // @description:it XEnhancer potenzia la navigazione su Twitter/X — salva i media con un clic, formatta i timestamp per maggiore chiarezza e ottimizza il tuo feed sociale con facilità. // @description:ja XEnhancer は Twitter (X) の閲覧を強化します — メディアをワンクリックで保存し、タイムスタンプを見やすくフォーマットし、ソーシャルフィードを簡単に整理できます。 // @description:ka XEnhancer აძლიერებს Twitter/X-ს — დაარეგისტრირე მედია ერთ ক্লიკზე, დააწყობ დროის ნიშანებს გასაგებად და გამარტივე სოციალური ფიდი. // @description:ko XEnhancer는 Twitter/X 탐색을 강화합니다 — 미디어를 한 번의 클릭으로 저장하고, 타임스탬프를 명확하게 포맷하며, 소셜 피드를 쉽게 정리할 수 있습니다. // @description:nb XEnhancer forbedrer din Twitter/X-opplevelse — lagre medier med ett klikk, formater tidsstempler for klarhet, og strømlin feeden din enkelt. // @description:nl XEnhancer verbetert je Twitter/X-ervaring — sla media op met één klik, formatteer tijdstempels voor duidelijkheid en stroomlijn je sociale feed eenvoudig. // @description:pl XEnhancer usprawnia przeglądanie Twittera (X) — zapisuj media jednym kliknięciem, formatuj znaczniki czasu dla przejrzystości i usprawnij swój feed społecznościowy. // @description:pt-BR XEnhancer potencializa sua navegação no Twitter/X — salve mídias com um clique, formate os timestamps para maior clareza e organize seu feed social com facilidade. // @description:ro XEnhancer îmbunătățește navigarea pe Twitter/X — salvează media cu un singur clic, formatează timestamp-urile pentru claritate și optimizează-ți feedul social cu ușurință. // @description:ru XEnhancer улучшает просмотр Twitter/X — сохраняйте медиа в один клик, форматируйте временные метки для наглядности и упрощайте вашу социальную ленту. // @description:sk XEnhancer zlepšuje prehliadanie Twitteru (X) — ukladajte médiá jedným kliknutím, formátujte časové značky pre prehľadnosť a zjednodušte svoj sociálny feed. // @description:sr XEnhancer unapređuje pregledanje Twittera (X) — sačuvajte medije jednim klikom, formatirajte vremenske oznake radi preglednosti i olakšajte svoj društveni feed. // @description:sv XEnhancer förbättrar din Twitter/X-upplevelse — spara media med ett klick, formatera tidsstämplar för tydlighet och effektivisera ditt sociala flöde enkelt. // @description:th XEnhancer ช่วยเพิ่มประสิทธิภาพการใช้งาน Twitter/X — บันทึกสื่อด้วยคลิกเดียว, จัดรูปแบบเวลาสำหรับความชัดเจน, และปรับปรุงฟีดโซเชียลของคุณอย่างง่ายดาย // @description:tr XEnhancer, Twitter/X deneyiminizi güçlendirir — medyaları tek tıkla kaydedin, zaman damgalarını netlik için biçimlendirin ve sosyal akışınızı kolayca düzenleyin. // @description:uk XEnhancer покращує перегляд Twitter/X — зберігайте медіа одним кліком, форматування часових міток для наочності та спрощення вашої соціальної стрічки. // @description:ug XEnhancer Twitter/X تەجرىبىسىڭىزنى كۈچەيتىدۇ — ۋىدىئولارنى ۋە رەسىملەرنى بىر قېتىملىق چېكىش بىلەن ساقلاڭ، ۋاقىت بەلگىلىرىنى ئاچچىق-ئاشكارا قىلىپ بەلگىلەڭ، ۋە ئىجتىمائىي فېدڭىزنى ئاسانلاشتۇرۇڭ. // @description:vi XEnhancer nâng cao trải nghiệm duyệt Twitter/X — lưu phương tiện chỉ với một cú nhấp, định dạng dấu thời gian rõ ràng và tối ưu hóa luồng xã hội của bạn dễ dàng. // @namespace levivi_myself // @version 1.0.1 // @author PeterParker, Levivi // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAEBklEQVR4AeyZXVbjMAyFy2wJ3ikro6wMeIc1de7nOh7LlmM7yRwOHDhR4h9ZuleSnbb8OX3zv18CX53An5WBh4eHywY5H5kF+T9LXByeH5OB6/X6eL1en6+T4hneMgZw+X6VVBhk701SXYbA5+fnkzRcRY23rvP9/f1ra3J0PIJ/9vTv7u5ePj4+XFyGAItR5jkpIe2Ta5K6wJ+JehrIGuAR+Es2ZJoVASm/scho3TqMP2lukZfb8O0OAIDcenN3rXUzKF9EvgkeLxUBBkWCRWXKwmbV3FuUSkdA3BLAZkta5TcCHpsuASa8/VACxAm6mUztB2WMIITAZDZoEiTmaK9KkwCregDJhKcTgWGiKeiUAVmUY/CW7upzlcAIQOlcShIAE0AvsgGM5i7ohE5xky1OwmK03V0lwLIRgOhI1+yZFsAeeNkydmR39eoSYLWMUo/GsACak0ORM6eS1lX7oQOeE8f4kI3uNUQAKw7AU36CiCTHbEUC0KzXc/NZz/qWDBOIAMv6NC8w6TT3Q5mxBRCBYd3Sn30OE8CwHFVRFrBnoss8Ip1uuaGH7AWPjSkCLGgBzEkADN2ODJ/1a3amCWDMO6fJBHOISJKpstyYSuLZSJMTjU0EsK8olwDL/QCJclOz9OSsDeNbbhMErPkYZQOQLKiU2ANBWTq0q6NR49VYWLDhtpkAvgSkAhhJpLewom1Isi4/funvkV0EcBxr2UQUEswhIkkprZYbeltlNwEcO1Hu7gdIqtxSprCzRQ4hEKNsSiUCpMQCLunQLjP1upfEIQRAKMCPPHPRmHnJOZk6oZOvmW0fQkBRJLpuOeQAlYXD98NuAoDPQToRNJ9KIwmv3NwAOPbM0C4CA+AXZ+WmJmOH7IfNBNbAU+vIgp4nWdKaFOV4/DKVBJ3UGWxsIgCQljOAq0wuiDBUUdZYuqS7+/2wiYDAm29jCyIB4lsV5RGG6IdGdsvfwiLJpt61H6YJ5AAyXHxAM+CZiwBXoywdCFeZIsvY6MkUgQg+1XFmvPnZXgC7Ud6zH4YJKCJEygN/8gBk5E4iwdrVKKvcVjOV28vbQwQAr7p3fzZ0HOf2U9sjmdsUyVamIJ/slI0ugR54HJdGW32H7Mj7wXwcKW2vEuiAZ9OasiiNl33IioR36qQox0wZu3mmSptNAgL/X37HEQnAVgDlL/1bqQSpvsmU+ulqEhDrobM+WZpotKIsn+FfSzJVHRbMQVJz5nIJxOPSKNIh/TGCdHcJtmYNRBKGXEUggjdK0VHzrI/z/x4DLQWiOnUGllXfHwwBpeisyLxLXkqJaR/xMawjEuGnyNJXp/+eOzAEZDBEWc/wYSx/5ouObOc+Rtu5f0Mgn/gu7V8CX52pvwAAAP//pQZ+UwAAAAZJREFUAwBCEOZ/U8dgbwAAAABJRU5ErkJggg== // @include https://x.com/* // @include https://twitter.com/* // @exclude *://x.com/i/flow/* // @license MIT // @run-at document-start // @noframes // @grant GM_registerMenuCommand // @grant GM_openInTab // @grant GM.openInTab // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_xmlhttpRequest // @grant GM_download // @downloadURL none // ==/UserScript== (function () { 'use strict'; /*! * Copyright (c) 2026 - 2026, Levivi. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ var css_248z$1 = "li[role=listitem]>div>div>div>div:not(:last-child){filter:none}li[role=listitem]>div>div>div>div+div:last-child{display:none}"; var css_248z = ".x-master-dl{margin-left:12px;order:99}.x-master-dl:hover>div>div>div>div{color:#1da1f2}.x-master-dl:hover>div>div>div>div>div{background-color:rgba(29,161,242,.1)}.x-master-dl:active>div>div>div>div>div{background-color:rgba(29,161,242,.2)}.x-master-dl:hover svg{color:#1da1f2}.x-master-dl:hover div:first-child:not(:last-child){background-color:rgba(29,161,242,.1)}.x-master-dl:active div:first-child:not(:last-child){background-color:rgba(29,161,242,.2)}.x-master-dl.tmd-media{position:absolute;right:0}.x-master-dl.tmd-media>div{border-radius:99px;display:flex;margin:2px}.x-master-dl.tmd-media>div>div{color:#fff;display:flex;margin:6px}.x-master-dl.tmd-media:hover>div{background-color:hsla(0,0%,100%,.6)}.x-master-dl.tmd-media:hover>div>div{color:#1da1f2}.x-master-dl.tmd-media:not(:hover)>div>div{filter:drop-shadow(0 0 1px #000)}.x-master-dl g{display:none}.x-master-dl.completed g.completed,.x-master-dl.download g.download,.x-master-dl.failed g.failed,.x-master-dl.loading g.loading{display:unset}.x-master-dl.loading svg{animation:spin 1s linear infinite}.x-master-dl.download g.download{color:#1da1f2}.tmd-btn{background-color:#1da1f2;border-radius:99px;color:#fff;padding:0 20px}.tmd-btn,.tmd-tag{display:inline-block}.tmd-tag{background-color:#fff;border:1px solid #1da1f2;border-radius:10px;color:#1da1f2;font-weight:700;margin:5px;padding:0 10px}.tmd-btn:hover{background-color:rgba(29,161,242,.9)}.tmd-tag:hover{background-color:rgba(29,161,242,.1)}.tmd-notifier{background:#fff;border:1px solid #ccc;border-radius:8px;bottom:16px;color:#000;display:none;left:16px;padding:4px;position:fixed}.tmd-notifier.running{align-items:center;display:flex}.tmd-notifier label{align-items:center;display:inline-flex;margin:0 8px}.tmd-notifier label:before{background-position:50%;background-repeat:no-repeat;content:\" \";height:16px;width:32px}.tmd-notifier label:first-child:before{background-image:url(\"data:image/svg+xml;charset=utf8,\")}.tmd-notifier label:nth-child(2):before{background-image:url(\"data:image/svg+xml;charset=utf8,\")}.tmd-notifier label:nth-child(3):before{background-image:url(\"data:image/svg+xml;charset=utf8,\")}.x-master-dl.tmd-img{bottom:0;display:none!important;position:absolute;right:0}.x-master-dl.tmd-img>div{background-color:hsla(0,0%,100%,.6);border-radius:99px;display:flex;margin:2px}.x-master-dl.tmd-img>div>div{color:#fff!important;display:flex;margin:6px}.x-master-dl.tmd-img:not(:hover)>div>div{filter:drop-shadow(0 0 1px #000)}.x-master-dl.tmd-img:hover>div>div{color:#1da1f2}.tmd-img.completed,.tmd-img.failed,.tmd-img.loading,:hover>.x-master-dl.tmd-img{display:block!important}.tweet-detail-action-item{width:20%!important}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}"; const language = { "zh": { "dateFormat": { "week": ["日", "一", "二", "三", "四", "五", "六"] }, "download": { "download": "下载", "completed": "下载完成", "tip": "点击下载视频", "preparing": "正在准备下载(如果失败,请手动操作)" }, "menuCommand": { "settings": "设置", "titleDateFormat": "时间格式设置:", "buttonClose": "关闭", "simplifyMode": "简化模式", "turnOn": "打开", "turnOff": "关闭" } }, "en": { "dateFormat": { "week": ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] }, "download": { "download": "Download", "completed": "Download Completed", "tip": "Click to download video", "preparing": "Preparing to download (if failed, please do it manually)" }, "menuCommand": { "settings": "Settings", "titleDateFormat": "Time format settings:", "buttonClose": "Close", "simplifyMode": "Simplify Mode", "turnOn": "Turn on", "turnOff": "Turn off" } }, "ja": { "dateFormat": { "week": ["日", "月", "火", "水", "木", "金", "土"] }, "download": { "download": "ダウンロード", "completed": "ダウンロード完了", "tip": "クリックしてビデオをダウンロード", "preparing": "ダウンロードの準備中(失敗する場合は手動で行ってください)" }, "menuCommand": { "settings": "設定", "titleDateFormat": "時刻形式の設定:", "buttonClose": "閉鎖", "simplifyMode": "簡易モード", "turnOn": "オン", "turnOff": "オフ" } }, "fr": { "dateFormat": { "week": ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam"] }, "download": { "download": "télécharger", "completed": "éléchargement terminé", "tip": "Cliquez pour télécharger la vidéo", "preparing": "Préparation du téléchargement (en cas d'échec, veuillez le faire manuellement)" }, "menuCommand": { "settings": "installation", "titleDateFormat": "Paramètres du format de l'heure :", "buttonClose": "fermeture", "simplifyMode": "Mode simplifié", "turnOn": "Activer", "turnOff": "Désactiver" } }, "de": { "dateFormat": { "week": ["Son", "Mon", "Die", "Mit", "Don", "Fre", "Sam"] }, "download": { "download": "herunterladen", "completed": "Download abgeschlossen", "tip": "Klicken Sie hier, um das Video herunterzuladen", "preparing": "Vorbereitung für den Download (falls der Download fehlschlägt, führen Sie ihn bitte manuell durch)" }, "menuCommand": { "settings": "aufstellen", "titleDateFormat": "Einstellungen für das Zeitformat:", "buttonClose": "Schließung", "simplifyMode": "Vereinfachter Modus", "turnOn": "Einschalten", "turnOff": "Ausschalten" } }, "it": { "dateFormat": { "week": ["Dom", "Lun", "Mar", "Mer", "Gio", "Ven", "Sab"] }, "download": { "download": "scaricamento", "completed": "Download completato", "tip": "Fare clic per scaricare il video", "preparing": "Preparazione per il download (se fallisce, eseguilo manualmente)" }, "menuCommand": { "settings": "impostare", "titleDateFormat": "Impostazioni del formato dell'ora:", "buttonClose": "chiusura", "simplifyMode": "Modalità semplificata", "turnOn": "Attiva", "turnOff": "Disattiva" } }, "ko": { "dateFormat": { "week": ["일", "월", "화", "수", "목", "금", "토"] }, "download": { "download": "다운로드", "completed": "다운로드 완료", "tip": "비디오를 다운로드하려면 클릭하세요", "preparing": "다운로드 준비 중 (실패할 경우 수동으로 진행해주세요)" }, "menuCommand": { "settings": "설정", "titleDateFormat": "시간 형식 설정:", "buttonClose": "폐쇄", "simplifyMode": "간소화 모드", "turnOn": "켜기", "turnOff": "끄기" } }, "ru": { "dateFormat": { "week": ["ВС", "ПН", "ВТ", "СР", "ЧТ", "ПТ", "СБ"] }, "download": { "download": "скачать", "completed": "Загрузка завершена", "tip": "Нажмите, чтобы скачать видео", "preparing": "Подготовка к загрузке (если не получается, сделайте это вручную)" }, "menuCommand": { "settings": "настраивать", "titleDateFormat": "Настройки формата времени:", "buttonClose": "закрытие", "simplifyMode": "Упрощенный режим", "turnOn": "Включить", "turnOff": "Выключить" } }, "pt": { "dateFormat": { "week": ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"] }, "download": { "download": "descargar", "completed": "Descarga completa", "tip": "Clique para baixar o vídeo", "preparing": "Preparação para download (se falhar, faça-o manualmente)" }, "menuCommand": { "settings": "configuración", "titleDateFormat": "Configuración de formato de hora:", "buttonClose": "cierre", "simplifyMode": "Modo simplificado", "turnOn": "Ativar", "turnOff": "Desativar" } }, "es": { "dateFormat": { "week": ["DOM", "LUN", "MAR", "MIER", "JUE", "VIE", "SÁB"] }, "download": { "download": "descargar", "completed": "Descarga completa", "tip": "Haga clic para descargar el vídeo", "preparing": "Preparándose para la descarga (si falla, hágalo manualmente)" }, "menuCommand": { "settings": "configuración", "titleDateFormat": "Configuración de formato de hora:", "buttonClose": "cierre", "simplifyMode": "Modo simplificado", "turnOn": "Activar", "turnOff": "Desactivar" } }, "th": { "dateFormat": { "week": ["วันอาทิตย์", "วันจันทร์", "วันอังคาร", "วันพุธ", " วันพฤหัสบดี", "วันศุกร์ ", "วันเสาร์ "] }, "download": { "download": "ดาวน์โหลด", "completed": "ดาวน์โหลดเสร็จสมบูรณ์", "tip": "คลิกเพื่อดาวน์โหลดวิดีโอ", "preparing": "กำลังเตรียมการดาวน์โหลด (หากล้มเหลว กรุณาดำเนินการด้วยตนเอง)" }, "menuCommand": { "settings": "ตั้งค่า", "titleDateFormat": "การตั้งค่ารูปแบบเวลา:", "buttonClose": "ปิด", "simplifyMode": "โหมดแบบย่อ", "turnOn": "เปิด", "turnOff": "ปิด" } }, "tr": { "dateFormat": { "week": ["Pazar", "Pazartesi", "Salı", "Çarşamba", "Perşembe", "Cuma", "Cumartesi"] }, "download": { "download": "indirmek", "completed": "İndirme tamamlandı", "tip": "Videoyu indirmek için tıklayın", "preparing": "İndirmeye hazırlanıyor (başarısız olursa lütfen manuel olarak yapın)" }, "menuCommand": { "settings": "kurmak", "titleDateFormat": "Saat formatı ayarları:", "buttonClose": "kapatma", "simplifyMode": "Basitleştirilmiş mod", "turnOn": "Aç", "turnOff": "Kapat" } }, "nl": { "dateFormat": { "week": ["zondag", "maandag", "dinsdag", "woensdag", "donderdag", "vrijdag", "zaterdag"] }, "download": { "download": "downloaden", "completed": "Downloaden voltooid", "tip": "Klik om video te downloaden", "preparing": "Voorbereiden voor downloaden (als dit mislukt, doe dit dan handmatig)" }, "menuCommand": { "settings": "opgezet", "titleDateFormat": "Instellingen tijdformaat:", "buttonClose": "sluiting", "simplifyMode": "Vereenvoudigde modus", "turnOn": "Inschakelen", "turnOff": "Uitschakelen" } } }; const lang = (navigator.language || navigator.userLanguage || "").slice(0, 2).toLowerCase() || "en"; const Commonlanguage = language[lang] ?? language["en"]; const FMT = 16; const XSettingsDialog = { number: Math.ceil(Math.random() * 1e8), formats: [ { format: "Do nothing", example: "N/A" }, { format: "ISO 8601 T", example: "2025-07-09T22:57:30" }, { format: "ISO 8601 (space + s)", example: "2025-07-09 22:57:30" }, { format: "ISO 8601 (space, no s)", example: "2025-07-09 22:57" }, { format: "US: MMM d, yyyy h:mm A", example: "Jul 9, 2025, 10:57 PM" }, { format: "US: EEE, MMM d, yyyy h:mm A", example: "Wed, Jul 9, 2025, 10:57 PM" }, { format: "US: MM/dd/yyyy h:mm A", example: "07/09/2025 10:57 PM" }, { format: "US: MM/dd/yyyy HH:mm", example: "07/09/2025 22:57" }, { format: "EU/UK: dd/MM/yyyy HH:mm", example: "09/07/2025 22:57" }, { format: "DE: dd.MM.yyyy, HH:mm", example: "09.07.2025, 22:57" }, { format: "EU long: d MMMM yyyy, HH:mm", example: "9 July 2025, 22:57" }, { format: "CN: yyyy年M月d日 HH:mm", example: "2025年7月9日 22:57" }, { format: "East Asia: yyyy/MM/dd HH:mm", example: "2025/07/09 22:57" }, { format: "UK short: EEE d MMM yyyy HH:mm", example: "Wed 9 Jul 2025 22:57" }, { format: "Unix ctime (en)", example: "Wed Jul 9 22:57:30 2025" }, { format: "US full: EEEE, MMMM d, yyyy h:mm:ss A", example: "Wednesday, July 9, 2025, 10:57:30 PM" }, { format: "Compact: hh.mm A·mmm d,yy", example: "10.57 PM·Jul 9,25" }, { format: "TW ROC: Myyy-MM-dd HH:mm", example: "M114-07-09 22:57" } ], make() { const dialog = document.createElement("div"); dialog.className = "dialog_u_" + this.number; dialog.style.all = "initial"; dialog.style.backgroundColor = "#fff"; dialog.style.border = "1px solid #e1e8ed"; dialog.style.borderRadius = "10px"; dialog.style.boxShadow = "0 16px 48px rgba(15, 20, 25, 0.14), 0 4px 16px rgba(15, 20, 25, 0.08)"; dialog.style.fontFamily = "monospace"; dialog.style.fontSize = "12px"; dialog.style.width = "640px"; dialog.style.maxWidth = "calc(100vw - 24px)"; dialog.style.boxSizing = "border-box"; dialog.style.paddingLeft = "8px"; dialog.style.paddingRight = "8px"; dialog.style.paddingTop = "8px"; dialog.style.paddingBottom = "8px"; dialog.style.position = "fixed"; dialog.style.right = "8px"; dialog.style.top = "8px"; dialog.style.zIndex = "2147483647"; dialog.style.overflow = "auto"; const escHtml = (s) => String(s).replace(/&/g, "&").replace(//g, ">"); let formatsHtml = ``; for (let i = 1; i <= this.formats.length; i++) { if (i % 2 !== 0) { formatsHtml += ``; } const item = this.formats[i - 1]; const exTitle = String(item.example).replace(/"/g, """); const fmtLine = escHtml(item.format); const exLine = escHtml(item.example); formatsHtml += ``; if (i % 2 === 0) { formatsHtml += ``; } } if (this.formats.length % 2 !== 0) { formatsHtml += ``; } formatsHtml += `
【${i}】${fmtLine}
${exLine}
`; const btnLabel = escHtml(Commonlanguage.menuCommand.buttonClose); const titleText = escHtml(Commonlanguage.menuCommand.titleDateFormat); dialog.innerHTML = `
` + titleText + `
` + formatsHtml + `
`; dialog.style.display = "none"; return dialog; }, addEvent(dialog) { dialog.querySelector("button[name='closex']").addEventListener( "click", () => { for (const e of dialog.querySelectorAll('input[name="fmt"]')) { if (e.checked) { fmt = e.value; break; } } GM_setValue("fmt", fmt); dialog.style.display = "none"; }, false ); }, init() { const dialog = this.make(); this.addEvent(dialog); document.body.appendChild(dialog); GM_registerMenuCommand(Commonlanguage.menuCommand.settings, () => { if (dialog.style.display !== "none") return; const input = dialog.querySelector( `input[name="fmt"][value="${String(fmt)}"]` ); if (input) input.checked = true; dialog.style.display = "block"; }); const isSimplifyMode = GM_getValue("x_simplify_mode", "") === "true"; GM_registerMenuCommand(Commonlanguage.menuCommand.simplifyMode + `(${isSimplifyMode ? Commonlanguage.menuCommand.turnOff : Commonlanguage.menuCommand.turnOn})`, () => { if (isSimplifyMode) { GM_deleteValue("x_simplify_mode"); } else { GM_setValue("x_simplify_mode", "true"); } location.reload(true); }); } }; let fmt = GM_getValue("fmt", FMT); (function syncFmtIndex() { const max = XSettingsDialog.formats.length - 1; const v = parseInt(String(fmt), 10); if (Number.isNaN(v) || v < 0 || v > max) { const next = Math.min(Math.max(parseInt(String(FMT), 10), 0), max); fmt = String(next); GM_setValue("fmt", fmt); } else { fmt = String(v); } })(); const XDateFormat = { df: function(date, f) { const WEEK_FULL = [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ]; const MONTH_SHORT = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ]; const MONTH_FULL = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ]; const pad = (num) => ("0" + num).slice(-2); const YE = date.getFullYear(); const YE2 = YE.toString().slice(-2); const YM = YE - 1911; const MO = pad(date.getMonth() + 1); const MO_IDX = date.getMonth(); const MO_NAME = MONTH_SHORT[MO_IDX]; const MO_NAME_FULL = MONTH_FULL[MO_IDX]; const DA = pad(date.getDate()); const dNum = parseInt(DA, 10); const weekAbbr = () => WEEK_FULL[date.getDay()].slice(0, 3); const HO = pad(date.getHours()); const MI = pad(date.getMinutes()); const SE = pad(date.getSeconds()); const h12 = date.getHours() % 12 || 12; const HO12 = pad(h12); const AMPM = date.getHours() >= 12 ? "PM" : "AM"; const F = [ `${YE}-${MO}-${DA}T${HO}:${MI}:${SE}`, `${YE}-${MO}-${DA} ${HO}:${MI}:${SE}`, `${YE}-${MO}-${DA} ${HO}:${MI}`, `${MO_NAME} ${dNum}, ${YE}, ${HO12}:${MI} ${AMPM}`, `${weekAbbr()}, ${MO_NAME} ${dNum}, ${YE}, ${HO12}:${MI} ${AMPM}`, `${MO}/${DA}/${YE} ${HO12}:${MI} ${AMPM}`, `${MO}/${DA}/${YE} ${HO}:${MI}`, `${DA}/${MO}/${YE} ${HO}:${MI}`, `${DA}.${MO}.${YE}, ${HO}:${MI}`, `${dNum} ${MO_NAME_FULL} ${YE}, ${HO}:${MI}`, `${YE}年${MO_IDX + 1}月${dNum}日 ${HO}:${MI}`, `${YE}/${MO}/${DA} ${HO}:${MI}`, `${weekAbbr()} ${dNum} ${MO_NAME} ${YE} ${HO}:${MI}`, `${weekAbbr()} ${MO_NAME} ${String(dNum).padStart(2, " ")} ${HO}:${MI}:${SE} ${YE}`, `${WEEK_FULL[date.getDay()]}, ${MO_NAME_FULL} ${dNum}, ${YE}, ${HO12}:${MI}:${SE} ${AMPM}`, `${HO12}.${MI} ${AMPM}·${MO_NAME} ${dNum},${YE2}`, `M${YM}-${MO}-${DA} ${HO}:${MI}` ]; return F[f] ?? F[0]; }, repldatetime: function() { const MYNAME = "peter_parker_x1190"; const SEL = 'main div[data-testid="primaryColumn"] section article time[datetime*=":"]'; const SEL_2 = 'div[aria-labelledby="modal-header"] div[data-testid^="User-Name"] time[datetime]'; const SEL_3 = 'div[aria-labelledby="modal-header"] div[aria-label] time[datetime]'; const SEL_4 = 'main section[aria-labelledby="detail-header"] article div[data-testid^="User-Name"] time[datetime]'; const SEL_5 = 'main section div[data-testid="conversation"] div[aria-label] time[datetime]'; document.querySelectorAll( SEL + ", " + SEL_2 + ", " + SEL_3 + ", " + SEL_4 + ", " + SEL_5 ).forEach((e) => { if (fmt != 0) { const SEL_ADD = "span.us-" + MYNAME; let d = e.getAttribute("datetime"); let df = this.df(new Date(d), fmt - 1); let pe = e.parentNode; let old = pe.querySelectorAll(SEL_ADD); if (!old.length) { let span = document.createElement("span"); span.className = "us-" + MYNAME; span.setAttribute("datetime", d); span.setAttribute("local-datetime", df); span.textContent = df; span.style = e.style; e.style.setProperty("display", "none"); pe.appendChild(span); } else if (old[0].getAttribute("local-datetime") != df) { old[0].setAttribute("local-datetime", df); old[0].textContent = df; old[0].style = e.style; } } }); } }; const XDownloadUI = { showSensitive: true, svg: ` `, isTweetdeck: () => window.location.host.includes("tweetdeck"), extractStatusId: (url) => url ? (url.match(/\/status\/(\d+)/) || [null, null])[1] : null, uniqueArray: (arr) => Array.from(new Set(arr)), getExtension: (url) => new URL(url).pathname.split(".").pop() || null, sanitizeFilename: (filename) => filename.replace(/[\/\\\?\%\*\:\|\\"<>\r\n]/g, "_"), setStatus: function(btn, classnames, title, style) { if (classnames) { btn.classList.remove("download", "completed", "loading", "failed"); btn.classList.add(...classnames); } if (title) btn.title = title; if (style) btn.style.cssText = style; }, clickDownloadEvent: function(btn, statusIds) { const filenameTemplate = "{name}"; const uniqueStatusIds = this.uniqueArray(statusIds); const handleDownload = (url, filename, defaultExt) => { return new Promise((resolve, reject) => { const finalFilename = filename + "." + (this.getExtension(url) ?? defaultExt); GM_download({ url, name: finalFilename, onload: () => { XDownloadUI.setStatus(btn, ["completed"], "下载完成"); resolve(); }, onerror: (error) => { reject(error); } }); }); }; this.setStatus(btn, ["loading"], "正在准备下载..."); const downloadPromises = uniqueStatusIds.map((statusId) => { const media = XDownload.mediaMap.get(statusId); if (!media) { return Promise.reject( `Media data not found for status ID: ${statusId}` ); } const { entityId, video, photo, text } = media; const filename = filenameTemplate.replace( "{name}", this.sanitizeFilename(text) || entityId ); if (video) { return handleDownload(video, filename, "mp4"); } else if (photo) { return handleDownload(photo, filename, "jpg"); } return Promise.reject("No media to download"); }); Promise.allSettled(downloadPromises).then((results) => { const anySuccess = results.some( (result) => result.status === "fulfilled" ); if (anySuccess) { this.setStatus(btn, ["completed"], "下载成功"); } else { this.setStatus(btn, ["failed"], "未找到媒体资源"); } }).catch((error) => { this.setStatus(btn, ["failed"], "下载失败"); }); }, addButtonTo: function(article) { if (article.dataset.detected) return; article.dataset.detected = "true"; const statusIds = Array.from( article.querySelectorAll('a[href*="/status/"]') ).map((el) => this.extractStatusId(el.href)).filter((id) => id); if (statusIds.length === 0) return; const mediaSelector = [ 'a[href*="/photo/1"]', 'div[role="progressbar"]', 'button[data-testid="playButton"]', 'div[data-testid="videoComponent"]', 'a[href="/settings/content_you_see"]', "div.media-image-container", "div.media-preview-container", 'div[aria-labelledby]>div:first-child>div[role="button"][tabindex="0"]' ]; const hasMedia = article.querySelector(mediaSelector.join(",")); if (hasMedia) { const btnGroup = article.querySelector( 'div[role="group"]:last-of-type, ul.tweet-actions, ul.tweet-detail-actions' ); if (btnGroup) { const btnShare = Array.from( btnGroup.querySelectorAll( ":scope>div>div, li.tweet-action-item>a, li.tweet-detail-action-item>a" ) ).pop().parentNode; const btnDownload = btnShare.cloneNode(true); btnDownload.style.marginLeft = "10px"; btnDownload.querySelector("button")?.removeAttribute("disabled"); const svgContainer = this.isTweetdeck() ? btnDownload.firstElementChild : btnDownload.querySelector("svg"); if (svgContainer) { if (this.isTweetdeck()) { svgContainer.innerHTML = `${this.svg}`; svgContainer.removeAttribute("rel"); btnDownload.classList.replace("pull-left", "pull-right"); } else { svgContainer.innerHTML = this.svg; } } this.setStatus(btnDownload, ["x-master-dl", "download"], "下载媒体"); btnGroup.insertBefore(btnDownload, btnShare.nextSibling); btnDownload.onclick = () => this.clickDownloadEvent(btnDownload, statusIds); if (this.showSensitive) { article.querySelector( 'div[aria-labelledby] div[role="button"][tabindex="0"]:not([data-testid]) > div[dir] > span > span' )?.click(); } } } const imgs = article.querySelectorAll('a[href*="/photo/"]'); if (imgs.length > 1) { imgs.forEach((img) => { const imgStatusId = this.extractStatusId(img.href); if (!imgStatusId || img.parentNode.querySelector(".x-master-dl.tmd-img")) return; const btnDownload = document.createElement("div"); btnDownload.style.cssText = "position: absolute; top: 0; right: 0; z-index: 10; margin: 5px;"; btnDownload.innerHTML = `
${this.svg}
`; this.setStatus( btnDownload, ["x-master-dl", "tmd-img", "download"], "下载图片" ); img.parentNode.appendChild(btnDownload); btnDownload.onclick = (e) => { e.preventDefault(); e.stopPropagation(); this.clickDownloadEvent(btnDownload, [imgStatusId]); }; }); } }, addButtonToMedia: function(listitems) { listitems.forEach((li) => { if (li.dataset.detected) return; li.dataset.detected = "true"; const statusElement = li.querySelector('a[href*="/status/"]'); const statusId = statusElement ? this.extractStatusId(statusElement.href) : null; if (!statusId) return; const btnDownload = document.createElement("div"); btnDownload.innerHTML = `
${this.svg}
`; this.setStatus( btnDownload, ["x-master-dl", "tmd-media", "download"], "下载媒体" ); li.appendChild(btnDownload); btnDownload.onclick = () => this.clickDownloadEvent(btnDownload, [statusId]); }); }, detect: function(node) { const article = node.tagName === "ARTICLE" && node || node.tagName === "DIV" && (node.querySelector("article") || node.closest("article")); if (article) { this.addButtonTo(article); } const listitems = node.tagName === "LI" && node.getAttribute("role") === "listitem" ? [node] : node.tagName === "DIV" && node.querySelectorAll('li[role="listitem"]'); if (listitems) { this.addButtonToMedia(listitems); } } }; const XDownload = { mediaMap: /* @__PURE__ */ new Map(), findParent: function(obj, targetKey) { let result = []; if (typeof obj === "object" && obj !== null) { for (let key in obj) { if (obj.hasOwnProperty(key)) { if (key === targetKey) { result.push(obj); } result = result.concat(this.findParent(obj[key], targetKey)); } } } else if (Array.isArray(obj)) { for (let item of obj) { result = result.concat(this.findParent(item, targetKey)); } } return result; }, extractMediaFromResponse: function(url, responseText) { try { const data = JSON.parse(responseText); const entities = this.findParent(data, "extended_entities"); if (entities.length === 0) { return; } for (let entity of entities) { if (!entity.extended_entities) continue; const entityId = entity.id_str || entity.conversation_id_str; (entity.extended_entities.media || []).filter((m) => ["video", "animated_gif", "photo"].includes(m.type)).forEach((m) => { const bestVideo = m.video_info?.variants?.filter((v) => v.content_type === "video/mp4").sort((a, b) => b.bitrate - a.bitrate)[0]; const text = (entity.full_text || "").split("https://t.co")[0]?.trim()?.slice(0, 50) || entityId; const mediaItem = { entityId, id: m.id_str, thumbnail: m.media_url_https?.split(".jpg")[0], video: bestVideo?.url, photo: m.media_url_https, text }; this.mediaMap.set(entityId, mediaItem); }); } } catch (e) { } }, hookXMLHttpRequest: function() { const self = this; const originalOpen = XMLHttpRequest.prototype.open; const originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url) { this._url = url; return originalOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function() { this.addEventListener("load", function() { if (this._url && this.response) { try { if (this.responseType === "" || this.responseType === "text") { self.extractMediaFromResponse(this._url, this.responseText); } } catch (e) { } } }); return originalSend.apply(this, arguments); }; }, init: function() { this.hookXMLHttpRequest(); GM_addStyle( css_248z + (this.showSensitive ? css_248z$1 : "") ); } }; const X = { start: function() { XDownload.init(); XSettingsDialog.init(); const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === 1) { XDownloadUI.detect(node); XDateFormat.repldatetime(); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); } }; (function() { const enabled = GM_getValue("x_simplify_mode", "") === "true"; if (!enabled) { return; } function update() { const width = Math.min(document.documentElement.offsetWidth || 800, 800); if (window.innerWidth === width && document.documentElement.clientWidth === width) { return; } window.__defineGetter__("innerWidth", () => width); document.documentElement.__defineGetter__("clientWidth", () => width); if (window.visualViewport) { window.visualViewport.__defineGetter__("width", () => width); } window.dispatchEvent(new Event("resize")); if (window.visualViewport) { window.visualViewport.dispatchEvent(new Event("resize")); } } window.addEventListener("load", update); window.addEventListener("resize", update); if (window.visualViewport) { window.visualViewport.addEventListener("resize", update); } document.addEventListener("visibilitychange", update); GM_addStyle(` #react-root main { -webkit-flex-grow: 1 !important; flex-grow: 1 !important; } @media (min-width: 800px) { [role='listbox'] { max-width: 500px !important; } } `); update(); })(); const Init = { async start() { X.start(); } }; Init.start(); }());