// ==UserScript== // @name Twitter / X — Media Copy & Download // @name:zh-TW Twitter / X — 媒體複製與下載 // @name:zh-CN Twitter / X — 媒体复制与下载 // @name:ja Twitter / X — メディアコピー & ダウンロード // @name:ko Twitter / X — 미디어 복사 & 다운로드 // @name:es Twitter / X — Copiar y Descargar Medios // @name:pt-BR Twitter / X — Copiar e Baixar Mídia // @name:fr Twitter / X — Copier & Télécharger les Médias // @name:ru Twitter / X — Копирование и загрузка медиа // @namespace https://greasyfork.org/en/users/1575945-star-tanuki07?locale_override=1 // @version 2.0.3 // @license MIT // @author Star_tanuki07 // @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.com // @match https://twitter.com/* // @match https://x.com/* // @grant GM_setClipboard // @grant unsafeWindow // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_xmlhttpRequest // @connect twitter.com // @connect x.com // @connect twimg.com // @run-at document-idle // @description Adds a media button and a link button to every tweet for one-click URL copying and media downloading. // @description:zh-TW 在每則推文新增媒體與連結按鈕,提供一鍵複製網址及下載媒體的功能。 // @description:zh-CN 在每条推文新增媒体与链接按钮,提供一键复制网址及下载媒体的功能。 // @description:ja 各ツイートにメディアボタンとリンクボタンを追加し、URLのコピーとメディアのダウンロードをワンクリックで行えます。 // @description:ko 모든 트윗에 미디어 및 링크 버튼을 추가하여 원클릭으로 URL 복사 및 미디어 다운로드를 제공합니다. // @description:es Agrega botones de medios y enlaces a cada tweet para copiar URLs y descargar medios con un solo clic. // @description:pt-BR Adiciona botões de mídia e links a cada tweet para copiar URLs e baixar mídia com um clique. // @description:fr Ajoute des boutons de médias et de liens à chaque tweet pour copier les URL et télécharger des médias en un clic. // @description:ru Добавляет кнопки медиа и ссылок к каждому твиту для копирования URL и скачивания медиа в один клик. // @downloadURL https://update.greasyfork.icu/scripts/569424/Twitter%20%20X%20%E2%80%94%20Media%20Copy%20%20Download.user.js // @updateURL https://update.greasyfork.icu/scripts/569424/Twitter%20%20X%20%E2%80%94%20Media%20Copy%20%20Download.meta.js // ==/UserScript== (function () { 'use strict'; const KEY_PREFIX_TEXT = 'discord_prefix_text'; const KEY_LANG = 'app_language'; const KEY_LINK_DOMAIN_CLICK = 'app_link_domain_click'; const KEY_CLICK_MODE_CUSTOM = 'app_link_click_mode_custom'; const KEY_DATE_FORMAT = 'app_date_format'; const KEY_CUSTOM_LANG = 'app_custom_lang_json'; const KEY_VIDEO_VOLUME = 'app_video_volume'; const KEY_ONBOARDING_DONE = 'app_onboarding_done'; const KEY_FEEDBACK_STYLE = 'app_feedback_style'; const KEY_SEEN_FEATURES = 'app_seen_features'; const KEY_HISTORY_RECORDS = 'app_history_records'; const KEY_HISTORY_PANEL_POS = 'app_history_panel_pos'; const KEY_HISTORY_VIEW_MODE = 'app_history_view_mode'; const KEY_DOCK_STYLE = 'app_dock_style'; const KEY_DOCK_HOVER_DELAY = 'app_dock_hover_delay'; const KEY_DOCK_TRIGGER_L = 'app_dock_trigger_l'; const KEY_DOCK_TRIGGER_R = 'app_dock_trigger_r'; const KEY_DOCK_PERSISTED = 'app_dock_persisted'; const KEY_GROUP_ON_DOWNLOAD = 'app_group_on_download'; const KEY_GROUPS = 'app_media_groups'; const KEY_GROUP_PANEL_CFG = 'app_group_panel_cfg'; const HISTORY_MAX_RECORDS = 300; const NEW_FEATURE_IDS = [ 'feedback_style', 'history_panel', 'dock_style', 'sp_feedback_picker', 'sp_date_picker', 'sp_dock_picker', 'sp_trigger_dist', 'sp_slider_controls', 'sp_dock_persist', 'sp_group_on_dl', 'sp_group_glow_color', 'sp_group_glow_size', 'sp_group_text_color', ]; const DOMAIN_LIST = [ "vxtwitter.com", "fixupx.com", "fxtwitter.com", "cunnyx.com", "fixvx.com", "twitter.com", "x.com" ]; const TR = { 'en': { langName: 'English', menu_domain_click: '🔗 Set "Single-Click" Behavior', menu_domain_long: '🔗 Set "Long-Press" Domain', menu_prefix: '⚙️ Set Custom Prefix (Discord)', menu_lang: '🌐 Change Language', menu_help: '📖 Help / Manual', prompt_prefix: 'Enter custom prefix (e.g., [text]):', prompt_lang: 'Select language (enter number):\n1. English\n2. 繁體中文\n3. 简体中文\n4. 日本語\n5. 한국어\n6. Español\n7. Português (BR)\n8. Français\n9. Русский\n10. ✏️ Custom Language', prompt_domain: 'Select domain (Enter Number):\n', status_default: 'Default (x.com)', status_custom: 'Custom', btn_tooltip: 'Left Click: Copy Media Links\nMiddle Click: Preview Video / Image Lightbox\nRight Click: Download Files', link_tooltip: 'Click: Copy ', link_tooltip_long: '\nLong Press: Copy prefix + ', msg_prefix_copied: 'Prefix Copied', msg_copied: 'Copied', msg_downloaded: 'Downloaded', msg_no_media: '❌ No Media', play_btn_tooltip: 'Click: Preview Video in Floating Player', msg_no_video: '❌ No Video', reload_msg: 'Settings Saved', toast_domain_click: '🔗 Single-Click Domain → ', toast_domain_long: '🔗 Long-Press Domain → ', toast_prefix: '⚙️ Discord Prefix → ', toast_date_fmt: '📅 Date Format → ', toast_lang_pending: '🌐 Language change will apply after reload.', confirm_lang_reload: 'Language changed to {lang}.\nReload page now to apply?', menu_date_format: '📅 Date Format', status_date_asian: 'Asian (YYYY.MM.DD)', status_date_western: 'Western (DD.MM.YYYY)', help_title: 'Twitter Media Copy Button - Manual', help_content: `

🖱️ Media Button (🎞️):
Left Click: Copy media links (images / video URLs).
Long Press (0.5s): Copy links with custom prefix (Markdown format, for Discord).
Middle Click: Preview — floating video player or image lightbox.
Right Click: Force download all media with structured filenames.
(Format: [twitter] Name(@ID)_Date_Text_ID.ext)


🔗 Link Button (🔗):
Click: Copy tweet link (default: x.com, or custom click domain).
Long Press: Copy with custom prefix + long-press embed domain (e.g. fixupx).


📋 Download History:
• Right-click downloads are automatically logged (up to 300 entries).
• Downloaded tweets show a 🟢 badge on the 🎞️ button.
• Click 📋 (top-right) to browse history: list / thumbnail view, search, export CSV / JSON.


⚙️ Settings Panel:
• Hover the top-right corner → 📋 history / ⚙️ gear button appears.
• Configure: click domain, long-press domain, Discord prefix, date format, language, feedback style.


⚠️ Disclaimer:
Embed domains (e.g. fixupx, vxtwitter) are third-party services unaffiliated with this script.

`, onboard_title: '⚙ Settings Panel', onboard_body: 'Hover the top-right corner to reveal the settings button. Click it to quickly manage domains, prefix, language and more — no script manager menu needed.', onboard_got_it: 'Got it!', menu_feedback_style: '🔔 Feedback Style', status_feedback_toast: 'Toast', status_feedback_icon: 'Icon Only', status_feedback_silent: 'Silent', toast_feedback_style: '🔔 Feedback Style → ', }, 'zh-TW': { langName: '繁體中文', menu_domain_click: '🔗 設定「單擊」行為模式', menu_domain_long: '🔗 設定「長按」網址域名', menu_prefix: '⚙️ 設定 Discord 前綴文字', menu_lang: '🌐 切換語言 (Change Language)', menu_help: '📖 使用說明書', prompt_prefix: '請輸入自定義前綴(例如 [text]):', prompt_lang: '請輸入數字選擇語言:\n1. English\n2. 繁體中文\n3. 简体中文\n4. 日本語\n5. 한국어\n6. Español\n7. Português (BR)\n8. Français\n9. Русский\n10. ✏️ Custom Language', prompt_domain: '請輸入數字選擇域名:\n', status_default: '預設 (x.com)', status_custom: '自定義', btn_tooltip: '左鍵:複製媒體連結\n中鍵:預覽影片 / 圖片燈箱\n右鍵:強制下載檔案', link_tooltip: '點擊:複製 ', link_tooltip_long: '\n長按:複製前綴 + ', msg_prefix_copied: '前綴已複製', msg_copied: '已複製', msg_downloaded: '已下載', msg_no_media: '❌ 無媒體', play_btn_tooltip: '點擊:在浮動播放器中預覽影片', msg_no_video: '❌ 無影片', reload_msg: '設定已儲存', toast_domain_click: '🔗 單擊域名 → ', toast_domain_long: '🔗 長按域名 → ', toast_prefix: '⚙️ Discord 前綴 → ', toast_date_fmt: '📅 日期格式 → ', toast_lang_pending: '🌐 語言已變更,重新載入後生效。', confirm_lang_reload: '語言已切換為 {lang}。\n立即重新載入頁面以套用?', menu_date_format: '📅 日期格式', status_date_asian: '亞洲慣用 (YYYY.MM.DD)', status_date_western: '歐美慣用 (DD.MM.YYYY)', help_title: '推特媒體腳本 — 說明書', help_content: `

🖱️ 媒體按鈕 (🎞️):
左鍵單擊: 複製推文中所有圖片/影片連結。
長按 (0.5秒): 複製含自定義前綴的連結,例如 [text](url)(方便 Discord 嵌入)。
中鍵點擊: 開啟浮動影片播放器或圖片燈箱。
右鍵點擊: 下載全部媒體,自動生成結構化檔名。
(格式:[twitter] 暱稱(@ID)_日期_內文_ID.副檔名)


🔗 連結按鈕 (🔗):
單擊: 複製推文網址(x.com 或自定義單擊域名)。
長按: 複製前綴 + 長按域名網址(如 fixupx.com)。


📋 下載履歷:
• 右鍵下載後自動記錄(最多 300 筆)。
• 滑鼠移至右上角 → 點擊 🕐 開啟履歷面板。
• 支援列表/縮圖切換、搜尋、Shift 區間選取、批次刪除、CSV/JSON 匯出。


⚙️ 設定面板:
• 將滑鼠移至右上角,顯示齒輪 ⚙️ 與履歷 🕐 按鈕。
• 點擊 ⚙️ 可設定:單擊域名、長按域名、Discord 前綴、提示風格、日期格式、語言。


⚠️ 免責聲明:
fixupx / vxtwitter 等域名皆為第三方服務,與本腳本無關,請僅使用您信任的域名。

`, onboard_title: '⚙ 設定面板', onboard_body: '將滑鼠移至右上角即可叫出設定按鈕,點擊後可快速管理域名、前綴、語言等設定,無需開啟腳本管理器選單。', onboard_got_it: '知道了!', menu_feedback_style: '🔔 提示風格', status_feedback_toast: 'Toast 提示', status_feedback_icon: '僅圖示', status_feedback_silent: '靜默(僅圖示)', toast_feedback_style: '🔔 提示風格 → ', }, 'zh-CN': { langName: '简体中文', menu_domain_click: '🔗 设置“单击”行为模式', menu_domain_long: '🔗 设置“长按”网址域名', menu_prefix: '⚙️ 设置 Discord 前缀文字', menu_lang: '🌐 切换语言 (Change Language)', menu_help: '📖 使用说明书', prompt_prefix: '请输入自定义前缀(例如 [text]):', prompt_lang: '请输入数字选择语言:\n1. English\n2. 繁體中文\n3. 简体中文\n4. 日本語\n5. 한국어\n6. Español\n7. Português (BR)\n8. Français\n9. Русский\n10. ✏️ Custom Language', prompt_domain: '请输入数字选择域名:\n', status_default: '默认 (x.com)', status_custom: '自定义', btn_tooltip: '左键:复制媒体链接\n中键:预览视频 / 图片灯箱\n右键:强制下载文件', link_tooltip: '点击:复制 ', link_tooltip_long: '\n长按:复制前缀 + ', msg_prefix_copied: '前缀已复制', msg_copied: '已复制', msg_downloaded: '已下载', msg_no_media: '❌ 无媒体', play_btn_tooltip: '点击:在浮动播放器中预览视频', msg_no_video: '❌ 无视频', reload_msg: '设置已保存', toast_domain_click: '🔗 单击域名 → ', toast_domain_long: '🔗 长按域名 → ', toast_prefix: '⚙️ Discord 前缀 → ', toast_date_fmt: '📅 日期格式 → ', toast_lang_pending: '🌐 语言已变更,重新载入后生效。', confirm_lang_reload: '语言已切换为 {lang}。\n立即重新载入页面以应用?', menu_date_format: '📅 日期格式', status_date_asian: '亚洲惯用 (YYYY.MM.DD)', status_date_western: '欧美惯用 (DD.MM.YYYY)', help_title: '推特媒体脚本 — 说明书', help_content: `

🖱️ 媒体按钮 (🎞️):
左键单击: 复制推文中所有图片/视频链接。
长按 (0.5秒): 复制含自定义前缀的链接,例如 [text](url)(方便 Discord 嵌入)。
中键单击: 打开浮动视频播放器或图片灯箱。
右键单击: 下载全部媒体,自动生成结构化文件名。
(格式:[twitter] 昵称(@ID)_日期_内文_ID.扩展名)


🔗 链接按钮 (🔗):
单击: 复制推文链接(x.com 或自定义单击域名)。
长按: 复制前缀 + 长按域名链接(如 fixupx.com)。


📋 下载历史:
• 右键下载后自动记录(最多 300 条)。
• 鼠标移至右上角 → 点击 🕐 打开历史面板。
• 支持列表/缩略图切换、搜索、Shift 区间选择、批量删除、CSV/JSON 导出。


⚙️ 设置面板:
• 将鼠标移至右上角,显示齿轮 ⚙️ 与历史 🕐 按钮。
• 点击 ⚙️ 可设置:单击域名、长按域名、Discord 前缀、提示风格、日期格式、语言。


⚠️ 免责声明:
fixupx / vxtwitter 等域名均为第三方服务,与本脚本无关,请仅使用您信任的域名。

`, onboard_title: '⚙ 设置面板', onboard_body: '将鼠标移到右上角即可呼出设置按钮,点击后可快速管理域名、前缀、语言等设置,无需打开脚本管理器菜单。', onboard_got_it: '知道了!', menu_feedback_style: '🔔 提示风格', status_feedback_toast: 'Toast 提示', status_feedback_icon: '仅图标', status_feedback_silent: '静默(仅图标)', toast_feedback_style: '🔔 提示风格 → ', }, 'ja': { langName: '日本語', menu_domain_click: '🔗 クリック動作設定', menu_domain_long: '🔗 長押しURLドメイン設定', menu_prefix: '⚙️ プレフィックス設定 (Discord)', menu_lang: '🌐 言語変更 (Change Language)', menu_help: '📖 ヘルプ / 説明書', prompt_prefix: 'カスタムプレフィックスを入力(例: [text]):', prompt_lang: '番号を入力して言語を選択:\n1. English\n2. 繁體中文\n3. 简体中文\n4. 日本語\n5. 한국어\n6. Español\n7. Português (BR)\n8. Français\n9. Русский\n10. ✏️ Custom Language', prompt_domain: 'ドメインの番号を入力:\n', status_default: 'デフォルト (x.com)', status_custom: 'カスタム', btn_tooltip: '左:メディアリンクをコピー\n中:動画プレビュー / 画像ライトボックス\n右:ファイルをダウンロード', link_tooltip: 'クリック:コピー ', link_tooltip_long: '\n長押し:プレフィックス付きコピー ', msg_prefix_copied: 'プレフィックス付', msg_copied: 'コピー完了', msg_downloaded: 'ダウンロード完了', msg_no_media: '❌ メディアなし', play_btn_tooltip: 'クリック:フローティングプレーヤーで動画を再生', msg_no_video: '❌ 動画なし', reload_msg: '設定が保存されました', toast_domain_click: '🔗 クリックドメイン → ', toast_domain_long: '🔗 長押しドメイン → ', toast_prefix: '⚙️ Discordプレフィックス → ', toast_date_fmt: '📅 日付フォーマット → ', toast_lang_pending: '🌐 言語を変更しました。再読み込み後に反映されます。', confirm_lang_reload: '言語を {lang} に変更しました。\n今すぐページを再読み込みしますか?', menu_date_format: '📅 日付フォーマット', status_date_asian: 'アジア式 (YYYY.MM.DD)', status_date_western: '欧米式 (DD.MM.YYYY)', help_title: 'Twitter メディアスクリプト — マニュアル', help_content: `

🖱️ メディアボタン (🎞️):
左クリック: ツイート内の画像/動画URLをすべてコピー。
長押し (0.5秒): カスタムプレフィックス付きでコピー(例:[text](url)、Discord向け)。
中クリック: フローティング動画プレーヤーまたは画像ライトボックスを開く。
右クリック: 全メディアをダウンロード(構造化ファイル名)。
(形式:[twitter] 名前(@ID)_日付_本文_ID.拡張子)


🔗 リンクボタン (🔗):
クリック: ツイートURLをコピー(x.com またはカスタムドメイン)。
長押し: プレフィックス + 長押しドメインURLをコピー(例:fixupx.com)。


📋 ダウンロード履歴:
• 右クリックダウンロードは自動記録(最大300件)。
• 右上にカーソルを合わせ → 🕐 をクリックして履歴パネルを開く。
• リスト/サムネイル表示、検索、Shift選択一括削除、CSV/JSONエクスポート対応。


⚙️ 設定パネル:
• 右上隅にカーソルを合わせると ⚙️ と 🕐 ボタンが表示される。
• ⚙️ をクリックして設定:クリックドメイン、長押しドメイン、プレフィックス、通知スタイル、日付形式、言語。


⚠️ 免責事項:
fixupx / vxtwitter 等のドメインは第三者サービスであり、このスクリプトとは無関係です。信頼できるドメインのみご使用ください。

`, onboard_title: '⚙ 設定パネル', onboard_body: '右上隅にカーソルを合わせると設定ボタンが現れます。クリックすればスクリプト管理器を開かずにドメイン・プレフィックス・言語などをすばやく管理できます。', onboard_got_it: 'わかった!', menu_feedback_style: '🔔 フィードバックスタイル', status_feedback_toast: 'トースト通知', status_feedback_icon: 'アイコンのみ', status_feedback_silent: 'サイレント(アイコンのみ)', toast_feedback_style: '🔔 フィードバックスタイル → ', }, 'ko': { langName: '한국어', menu_domain_click: '🔗 클릭 동작 설정', menu_domain_long: '🔗 길게 누르기 도메인 설정', menu_prefix: '⚙️ 접두사 설정 (Discord)', menu_lang: '🌐 언어 변경 (Change Language)', menu_help: '📖 도움말 / 설명서', prompt_prefix: '사용자 지정 접두사 입력 (예: [text]):', prompt_lang: '숫자를 입력하여 언어 선택:\n1. English\n2. 繁體中文\n3. 简体中文\n4. 日本語\n5. 한국어\n6. Español\n7. Português (BR)\n8. Français\n9. Русский\n10. ✏️ Custom Language', prompt_domain: '도메인 번호를 선택하세요:\n', status_default: '기본 (x.com)', status_custom: '사용자 지정', btn_tooltip: '왼쪽: 미디어 링크 복사\n가운데: 동영상 미리보기 / 이미지 라이트박스\n오른쪽: 파일 다운로드', link_tooltip: '클릭: 복사 ', link_tooltip_long: '\n길게 누르기: 접두사 포함 복사 ', msg_prefix_copied: '접두사 복사됨', msg_copied: '복사 완료', msg_downloaded: '다운로드 완료', msg_no_media: '❌ 미디어 없음', play_btn_tooltip: '클릭: 플로팅 플레이어에서 동영상 재생', msg_no_video: '❌ 동영상 없음', reload_msg: '설정이 저장되었습니다', toast_domain_click: '🔗 클릭 도메인 → ', toast_domain_long: '🔗 길게 누르기 도메인 → ', toast_prefix: '⚙️ Discord 접두사 → ', toast_date_fmt: '📅 날짜 형식 → ', toast_lang_pending: '🌐 언어가 변경되었습니다. 새로고침 후 적용됩니다.', confirm_lang_reload: '언어가 {lang}(으)로 변경되었습니다.\n지금 페이지를 새로고침하시겠습니까?', menu_date_format: '📅 날짜 형식', status_date_asian: '아시아식 (YYYY.MM.DD)', status_date_western: '서양식 (DD.MM.YYYY)', help_title: '트위터 미디어 스크립트 — 설명서', help_content: `

🖱️ 미디어 버튼 (🎞️):
좌클릭: 트윗 내 모든 이미지/동영상 URL 복사。
길게 누르기 (0.5초): 커스텀 접두사 포함 복사(예:[text](url), Discord용)。
중간 클릭: 동영상 플레이어 또는 이미지 라이트박스 열기。
우클릭: 모든 미디어 다운로드(구조화된 파일명 자동 생성)。
(형식:[twitter] 이름(@ID)_날짜_본문_ID.확장자)


🔗 링크 버튼 (🔗):
클릭: 트윗 URL 복사(x.com 또는 커스텀 도메인)。
길게 누르기: 접두사 + 길게 누르기 도메인 URL 복사(예:fixupx.com)。


📋 다운로드 기록:
• 우클릭 다운로드 후 자동 기록(최대 300건)。
• 오른쪽 상단에 마우스를 올려 → 🕐 클릭으로 기록 패널 열기。
• 목록/썸네일 보기, 검색, Shift 범위 선택, 일괄 삭제, CSV/JSON 내보내기 지원。


⚙️ 설정 패널:
• 오른쪽 상단에 마우스를 올리면 ⚙️ 와 🕐 버튼이 나타납니다。
• ⚙️ 클릭으로 설정:클릭 도메인, 길게 누르기 도메인, 접두사, 알림 스타일, 날짜 형식, 언어。


⚠️ 면책 조항:
fixupx / vxtwitter 등은 본 스크립트와 무관한 제3자 서비스입니다. 신뢰할 수 있는 도메인만 사용하세요。

`, onboard_title: '⚙ 설정 패널', onboard_body: '오른쪽 상단 모서리에 마우스를 올리면 설정 버튼이 나타납니다. 클릭하면 스크립트 관리자 없이 도메인, 접두사, 언어 등을 빠르게 관리할 수 있습니다.', onboard_got_it: '알겠어요!', menu_feedback_style: '🔔 피드백 스타일', status_feedback_toast: '토스트', status_feedback_icon: '아이콘만', status_feedback_silent: '조용히 (아이콘만)', toast_feedback_style: '🔔 피드백 스타일 → ', }, 'es': { langName: 'Español', menu_domain_click: '🔗 Configurar comportamiento de "clic"', menu_domain_long: '🔗 Configurar dominio de "pulsación larga"', menu_prefix: '⚙️ Configurar prefijo personalizado (Discord)', menu_lang: '🌐 Cambiar idioma (Change Language)', menu_help: '📖 Ayuda / Manual', prompt_prefix: 'Ingrese el prefijo personalizado (ej. [texto]):', prompt_lang: 'Ingrese un número para seleccionar el idioma:\n1. English\n2. 繁體中文\n3. 简体中文\n4. 日本語\n5. 한국어\n6. Español\n7. Português (BR)\n8. Français\n9. Русский\n10. ✏️ Custom Language', prompt_domain: 'Seleccione el número del dominio:\n', status_default: 'Predeterminado (x.com)', status_custom: 'Personalizado', btn_tooltip: 'Clic izq: Copiar enlaces de medios\nClic central: Ver video / Galería de imágenes\nClic der: Descargar archivos', link_tooltip: 'Clic: Copiar ', link_tooltip_long: '\nPulsación larga: Copiar prefijo + ', msg_prefix_copied: 'Prefijo copiado', msg_copied: 'Copiado', msg_downloaded: 'Descargado', msg_no_media: '❌ Sin medios', play_btn_tooltip: 'Clic: Ver video en reproductor flotante', msg_no_video: '❌ Sin video', reload_msg: 'Configuración guardada', toast_domain_click: '🔗 Dominio de clic → ', toast_domain_long: '🔗 Dominio de pulsación larga → ', toast_prefix: '⚙️ Prefijo de Discord → ', toast_date_fmt: '📅 Formato de fecha → ', toast_lang_pending: '🌐 Idioma cambiado. Se aplicará al recargar.', confirm_lang_reload: 'Idioma cambiado a {lang}.\n¿Recargar la página ahora?', menu_date_format: '📅 Formato de fecha', status_date_asian: 'Asiático (YYYY.MM.DD)', status_date_western: 'Occidental (DD.MM.YYYY)', help_title: 'Botón de copia de medios de Twitter - Manual', help_content: `

🖱️ Botón de medios (🎞️):
Clic izquierdo: Copiar enlaces de imágenes/videos.
Pulsación larga (0.5s): Copiar con prefijo personalizado (formato Markdown, para Discord).
Clic central: Vista previa——reproductor de video flotante o galería de imágenes.
Clic derecho: Descargar todos los medios con nombres de archivo estructurados.
(Formato: [twitter] Nombre(@ID)_Fecha_Texto_ID.ext)


🔗 Botón de enlace (🔗):
Clic: Copiar enlace del tweet (predeterminado x.com, o dominio personalizado de clic).
Pulsación larga: Copiar con prefijo + dominio de pulsación larga (ej. fixupx).


📋 Historial de descargas:
• Se registra automáticamente tras descargar con clic derecho (máx. 300 entradas).
• Los tweets descargados muestran un 🟢 badge en el botón 🎞️.
• Clic en 📋 (esquina superior derecha): vista lista/miniaturas, búsqueda, exportar CSV/JSON.


⚙️ Panel de configuración:
• Pasa el ratón por la esquina superior derecha → aparecen 📋 y ⚙️.
• Configura: dominio de clic, dominio de pulsación larga, prefijo Discord, formato de fecha, idioma, estilo de notificación.


⚠️ Aviso legal:
Los dominios de conversión (ej. fixupx, vxtwitter) son servicios de terceros sin relación con este script.

`, onboard_title: '⚙ Panel de Configuración', onboard_body: 'Mueve el cursor a la esquina superior derecha para revelar el botón de configuración. Haz clic para gestionar dominios, prefijo, idioma y más sin abrir el administrador de scripts.', onboard_got_it: '¡Entendido!', menu_feedback_style: '🔔 Estilo de Aviso', status_feedback_toast: 'Toast', status_feedback_icon: 'Solo icono', status_feedback_silent: 'Silencioso (solo icono)', toast_feedback_style: '🔔 Estilo de Aviso → ', }, 'pt-BR': { langName: 'Português (BR)', menu_domain_click: '🔗 Configurar comportamento de "clique"', menu_domain_long: '🔗 Configurar domínio de "pressão longa"', menu_prefix: '⚙️ Configurar prefixo personalizado (Discord)', menu_lang: '🌐 Mudar idioma (Change Language)', menu_help: '📖 Ajuda / Manual', prompt_prefix: 'Digite o prefixo personalizado (ex. [texto]):', prompt_lang: 'Digite um número para selecionar o idioma:\n1. English\n2. 繁體中文\n3. 简体中文\n4. 日本語\n5. 한국어\n6. Español\n7. Português (BR)\n8. Français\n9. Русский\n10. ✏️ Custom Language', prompt_domain: 'Selecione o número do domínio:\n', status_default: 'Padrão (x.com)', status_custom: 'Personalizado', btn_tooltip: 'Clique esq: Copiar links de mídia\nClique do meio: Visualizar vídeo / Galeria de imagens\nClique dir: Baixar arquivos', link_tooltip: 'Clique: Copiar ', link_tooltip_long: '\nPressão longa: Copiar prefixo + ', msg_prefix_copied: 'Prefixo copiado', msg_copied: 'Copiado', msg_downloaded: 'Baixado', msg_no_media: '❌ Sem mídia', play_btn_tooltip: 'Clique: Reproduzir vídeo no player flutuante', msg_no_video: '❌ Sem vídeo', reload_msg: 'Configurações salvas', toast_domain_click: '🔗 Domínio de clique → ', toast_domain_long: '🔗 Domínio de pressão longa → ', toast_prefix: '⚙️ Prefixo do Discord → ', toast_date_fmt: '📅 Formato de data → ', toast_lang_pending: '🌐 Idioma alterado. Será aplicado ao recarregar.', confirm_lang_reload: 'Idioma alterado para {lang}.\nRecarregar a página agora?', menu_date_format: '📅 Formato de data', status_date_asian: 'Asiático (YYYY.MM.DD)', status_date_western: 'Ocidental (DD.MM.YYYY)', help_title: 'Botão de cópia de mídia do Twitter - Manual', help_content: `

🖱️ Botão de mídia (🎞️):
Clique esquerdo: Copiar links de imagens/vídeos.
Pressão longa (0.5s): Copiar com prefixo personalizado (formato Markdown, para Discord).
Clique do meio: Visualizar——player de vídeo flutuante ou galeria de imagens.
Clique direito: Baixar todas as mídias com nomes de arquivo estruturados.
(Formato: [twitter] Nome(@ID)_Data_Texto_ID.ext)


🔗 Botão de link (🔗):
Clique: Copiar link do tweet (padrão x.com, ou domínio de clique personalizado).
Pressão longa: Copiar com prefixo + domínio de pressão longa (ex. fixupx).


📋 Histórico de downloads:
• Registrado automaticamente após download com clique direito (máx. 300 entradas).
• Tweets baixados mostram 🟢 badge no botão 🎞️.
• Clique em 📋 (canto superior direito): lista/miniaturas, pesquisa, exportar CSV/JSON.


⚙️ Painel de configurações:
• Passe o mouse pelo canto superior direito → 📋 e ⚙️ aparecem.
• Configure: domínio de clique, domínio de pressão longa, prefixo Discord, formato de data, idioma, estilo de aviso.


⚠️ Aviso legal:
Os domínios de conversão (ex. fixupx, vxtwitter) são serviços de terceiros sem relação com este script.

`, onboard_title: '⚙ Painel de Configurações', onboard_body: 'Passe o mouse no canto superior direito para revelar o botão de configurações. Clique para gerenciar domínios, prefixo, idioma e mais sem abrir o gerenciador de scripts.', onboard_got_it: 'Entendi!', menu_feedback_style: '🔔 Estilo de Aviso', status_feedback_toast: 'Toast', status_feedback_icon: 'Só ícone', status_feedback_silent: 'Silencioso (só ícone)', toast_feedback_style: '🔔 Estilo de Aviso → ', }, 'fr': { langName: 'Français', menu_domain_click: '🔗 Configurer le comportement "clic"', menu_domain_long: '🔗 Configurer le domaine "appui long"', menu_prefix: '⚙️ Configurer le préfixe personnalisé (Discord)', menu_lang: '🌐 Changer de langue (Change Language)', menu_help: '📖 Aide / Manuel', prompt_prefix: 'Entrez le préfixe personnalisé (ex. [texte]) :', prompt_lang: 'Entrez un numéro pour sélectionner la langue :\n1. English\n2. 繁體中文\n3. 简体中文\n4. 日本語\n5. 한국어\n6. Español\n7. Português (BR)\n8. Français\n9. Русский\n10. ✏️ Custom Language', prompt_domain: 'Sélectionnez le numéro du domaine :\n', status_default: 'Par défaut (x.com)', status_custom: 'Personnalisé', btn_tooltip: 'Clic gauche : Copier les liens médias\nClic milieu : Aperçu vidéo / Galerie images\nClic droit : Télécharger les fichiers', link_tooltip: 'Clic : Copier ', link_tooltip_long: '\nAppui long : Copier préfixe + ', msg_prefix_copied: 'Préfixe copié', msg_copied: 'Copié', msg_downloaded: 'Téléchargé', msg_no_media: '❌ Aucun média', play_btn_tooltip: 'Clic : Lire la vidéo dans le lecteur flottant', msg_no_video: '❌ Aucune vidéo', reload_msg: 'Paramètres enregistrés', toast_domain_click: '🔗 Domaine clic → ', toast_domain_long: '🔗 Domaine appui long → ', toast_prefix: '⚙️ Préfixe Discord → ', toast_date_fmt: '📅 Format de date → ', toast_lang_pending: '🌐 Langue modifiée. Sera appliqué au rechargement.', confirm_lang_reload: 'Langue changée en {lang}.\nRecharger la page maintenant ?', menu_date_format: '📅 Format de date', status_date_asian: 'Asiatique (YYYY.MM.DD)', status_date_western: 'Occidental (DD.MM.YYYY)', help_title: 'Bouton de copie de médias Twitter - Manuel', help_content: `

🖱️ Bouton média (🎞️) :
Clic gauche : Copier les liens des images/vidéos.
Appui long (0.5s) : Copier avec préfixe personnalisé (format Markdown, pour Discord).
Clic central : Aperçu——lecteur vidéo flottant ou galerie d'images.
Clic droit : Télécharger tous les médias avec noms de fichiers structurés.
(Format : [twitter] Nom(@ID)_Date_Texte_ID.ext)


🔗 Bouton lien (🔗) :
Clic : Copier le lien du tweet (x.com par défaut, ou domaine de clic personnalisé).
Appui long : Copier avec préfixe + domaine appui long (ex. fixupx).


📋 Historique des téléchargements :
• Enregistré automatiquement après téléchargement par clic droit (max. 300 entrées).
• Les tweets téléchargés affichent un 🟢 badge sur le bouton 🎞️.
• Cliquez sur 📋 (coin supérieur droit) : liste/miniatures, recherche, export CSV/JSON.


⚙️ Panneau de paramètres :
• Survolez le coin supérieur droit → 📋 et ⚙️ apparaissent.
• Configurez : domaine de clic, domaine d'appui long, préfixe Discord, format de date, langue, style de retour.


⚠️ Avertissement :
Les domaines de conversion (ex. fixupx, vxtwitter) sont des services tiers sans lien avec ce script.

`, onboard_title: '⚙ Panneau de Paramètres', onboard_body: 'Survolez le coin supérieur droit pour afficher le bouton de paramètres. Cliquez pour gérer les domaines, le préfixe, la langue et plus en utilisant le panneau de paramètres intégré.', onboard_got_it: "Compris !", menu_feedback_style: '🔔 Style de Retour', status_feedback_toast: 'Toast', status_feedback_icon: 'Icône seul', status_feedback_silent: 'Silencieux (icône seul)', toast_feedback_style: '🔔 Style de Retour → ', }, 'ru': { langName: 'Русский', menu_domain_click: '🔗 Настроить поведение «клика»', menu_domain_long: '🔗 Настроить домен «долгого нажатия»', menu_prefix: '⚙️ Настроить префикс (Discord)', menu_lang: '🌐 Изменить язык (Change Language)', menu_help: '📖 Справка / Руководство', prompt_prefix: 'Введите префикс (например [текст]):', prompt_lang: 'Введите номер для выбора языка:\n1. English\n2. 繁體中文\n3. 简体中文\n4. 日本語\n5. 한국어\n6. Español\n7. Português (BR)\n8. Français\n9. Русский\n10. ✏️ Custom Language', prompt_domain: 'Выберите номер домена:\n', status_default: 'По умолчанию (x.com)', status_custom: 'Пользовательский', btn_tooltip: 'Левый клик: Копировать медиа-ссылки\nСредний клик: Просмотр видео / Галерея изображений\nПравый клик: Скачать файлы', link_tooltip: 'Клик: Копировать ', link_tooltip_long: '\nДолгое нажатие: Копировать с префиксом ', msg_prefix_copied: 'Префикс скопирован', msg_copied: 'Скопировано', msg_downloaded: 'Загружено', msg_no_media: '❌ Нет медиа', play_btn_tooltip: 'Клик: Воспроизвести видео во всплывающем плеере', msg_no_video: '❌ Нет видео', reload_msg: 'Настройки сохранены', toast_domain_click: '🔗 Домен клика → ', toast_domain_long: '🔗 Домен долгого нажатия → ', toast_prefix: '⚙️ Префикс Discord → ', toast_date_fmt: '📅 Формат даты → ', toast_lang_pending: '🌐 Язык изменён. Применится после перезагрузки.', confirm_lang_reload: 'Язык изменён на {lang}.\nПерезагрузить страницу сейчас?', menu_date_format: '📅 Формат даты', status_date_asian: 'Азиатский (YYYY.MM.DD)', status_date_western: 'Западный (DD.MM.YYYY)', help_title: 'Кнопка копирования медиа Twitter - Руководство', help_content: `

🖱️ Кнопка медиа (🎞️):
Левый клик: Копировать ссылки на изображения/видео.
Долгое нажатие (0.5с): Копировать с префиксом (формат Markdown, для Discord).
Средний клик: Предпросмотр——всплывающий видеоплеер или галерея изображений.
Правый клик: Принудительно скачать все медиафайлы со структурированными именами.
(Формат: [twitter] Имя(@ID)_Дата_Текст_ID.ext)


🔗 Кнопка ссылки (🔗):
Клик: Копировать ссылку на твит (по умолчанию x.com, или пользовательский домен).
Долгое нажатие: Копировать с префиксом + домен долгого нажатия (напр. fixupx).


📋 История загрузок:
• Автоматически записывается после правого клика (макс. 300 записей).
• Скачанные твиты показывают 🟢 значок на кнопке 🎞️.
• Нажмите 📋 (верхний правый угол): список/миниатюры, поиск, экспорт CSV/JSON.


⚙️ Панель настроек:
• Наведите курсор в правый верхний угол → появятся 📋 и ⚙️.
• Настройте: домен клика, домен долгого нажатия, префикс Discord, формат даты, язык, стиль уведомлений.


⚠️ Отказ от ответственности:
Домены конвертации (fixupx, vxtwitter и др.) — сторонние сервисы, не связанные с этим скриптом. Используйте только те домены, которым доверяете.

`, onboard_title: '⚙ Панель настроек', onboard_body: 'Наведите курсор в правый верхний угол, чтобы показать кнопку настроек. Нажмите для быстрого управления доменами, префиксом, языком и другими параметрами.', onboard_got_it: 'Понятно!', menu_feedback_style: '🔔 Стиль уведомлений', status_feedback_toast: 'Тост', status_feedback_icon: 'Только иконка', status_feedback_silent: 'Тихий (только иконка)', toast_feedback_style: '🔔 Стиль уведомлений → ', } }; function getAppLanguage() { let lang = GM_getValue(KEY_LANG, null); if (!lang) { const navLang = navigator.language || 'en'; if (navLang.toLowerCase().startsWith('zh-cn')) lang = 'zh-CN'; else if (navLang.toLowerCase().startsWith('zh')) lang = 'zh-TW'; else if (navLang.toLowerCase().startsWith('ja')) lang = 'ja'; else if (navLang.toLowerCase().startsWith('ko')) lang = 'ko'; else if (navLang.toLowerCase().startsWith('pt')) lang = 'pt-BR'; else if (navLang.toLowerCase().startsWith('fr')) lang = 'fr'; else if (navLang.toLowerCase().startsWith('ru')) lang = 'ru'; else if (navLang.toLowerCase().startsWith('es')) lang = 'es'; else lang = 'en'; GM_setValue(KEY_LANG, lang); } if (lang === 'custom') { try { const raw = GM_getValue(KEY_CUSTOM_LANG, null); if (raw) { const parsed = JSON.parse(raw); if (parsed && parsed.langName) { TR['custom'] = parsed; return 'custom'; } } } catch(e) {} return 'en'; } return TR[lang] ? lang : 'en'; } const CURRENT_LANG = getAppLanguage(); const T = TR[CURRENT_LANG]; let _cachedDateFormat = GM_getValue(KEY_DATE_FORMAT, 'asian'); function _refreshDateFormatCache() { _cachedDateFormat = GM_getValue(KEY_DATE_FORMAT, 'asian'); } function _readSettings() { return { clickCustom: GM_getValue(KEY_CLICK_MODE_CUSTOM, false), clickDomain: GM_getValue(KEY_LINK_DOMAIN_CLICK, 'x.com'), prefix: GM_getValue(KEY_PREFIX_TEXT, '[text]'), fmt: GM_getValue(KEY_DATE_FORMAT, 'asian'), fbStyle: GM_getValue(KEY_FEEDBACK_STYLE, 'toast'), dockStyle: GM_getValue(KEY_DOCK_STYLE, 'ruler'), dockHoverDelay: parseInt(GM_getValue(KEY_DOCK_HOVER_DELAY, '500'), 10) || 500, dockTriggerL: parseInt(GM_getValue(KEY_DOCK_TRIGGER_L, '80'), 10) || 80, dockTriggerR: parseInt(GM_getValue(KEY_DOCK_TRIGGER_R, '80'), 10) || 80, }; } function showToast(message) { const existing = document.getElementById('tm-reload-toast'); if (existing) existing.remove(); const toast = document.createElement('div'); toast.id = 'tm-reload-toast'; toast.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: #1d9bf0; color: white; padding: 10px 20px; border-radius: 9999px; box-shadow: 0 8px 16px rgba(0,0,0,0.2); font-family: system-ui, -apple-system, sans-serif; font-size: 14px; font-weight: bold; z-index: 999999; display: flex; align-items: center; gap: 8px; transition: opacity 0.3s ease-in-out; opacity: 0; pointer-events: none; white-space: nowrap; max-width: 90vw; overflow: hidden; text-overflow: ellipsis; `; const toastSpan = document.createElement('span'); toastSpan.textContent = message; toast.appendChild(toastSpan); document.body.appendChild(toast); requestAnimationFrame(() => { toast.style.opacity = '1'; }); setTimeout(() => { if (toast) { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 300); } }, 2500); } function sanitizeHelpHtml(htmlString, container) { const ALLOWED_TAGS = new Set(['P','B','BR','HR','CODE','UL','LI','A','SPAN']); const parser = new DOMParser(); const doc = parser.parseFromString(htmlString, 'text/html'); function walk(srcNode, destParent) { srcNode.childNodes.forEach(child => { if (child.nodeType === Node.TEXT_NODE) { destParent.appendChild(document.createTextNode(child.textContent)); return; } if (child.nodeType !== Node.ELEMENT_NODE) return; const tag = child.tagName.toUpperCase(); if (!ALLOWED_TAGS.has(tag)) { destParent.appendChild(document.createTextNode(child.textContent)); return; } const el = document.createElement(tag === 'A' ? 'a' : tag); if (tag === 'A') { const href = child.getAttribute('href'); if (href && /^https?:\/\//i.test(href)) { el.href = href; el.rel = 'noopener noreferrer'; el.target = '_blank'; } } if ((tag === 'P' || tag === 'SPAN') && child.hasAttribute('style')) { const raw = child.getAttribute('style'); const safe = raw.split(';') .filter(rule => /^\s*(color|font-size)\s*:/i.test(rule)) .join(';'); if (safe) el.setAttribute('style', safe); } walk(child, el); destParent.appendChild(el); }); } walk(doc.body, container); } function showHelpModal() { const old = document.getElementById('tm-copy-help-modal'); if (old) old.remove(); const curLang = GM_getValue(KEY_LANG, 'en'); const curT = TR[curLang] || TR['en']; const dark = window.matchMedia('(prefers-color-scheme: dark)').matches; const C = dark ? { overlay: 'rgba(0,0,0,0.82)', panel: '#16202b', text: '#e7e9ea', border: '#2f3336', sub: '#8b98a5', } : { overlay: 'rgba(0,0,0,0.7)', panel: '#ffffff', text: '#333333', border: '#eeeeee', sub: '#536471', }; const modal = document.createElement('div'); modal.id = 'tm-copy-help-modal'; modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: ${C.overlay}; z-index: 99999; display: flex; align-items: center; justify-content: center; `; const content = document.createElement('div'); content.style.cssText = ` background: ${C.panel}; color: ${C.text}; padding: 25px; border-radius: 12px; width: 90%; max-width: 500px; box-shadow: 0 4px 15px rgba(0,0,0,0.3); font-family: sans-serif; line-height: 1.6; position: relative; max-height: 90vh; overflow-y: auto; `; const title = document.createElement('h2'); title.textContent = curT.help_title; title.style.cssText = `margin-top: 0; border-bottom: 2px solid ${C.border}; padding-bottom: 10px; font-size: 1.2rem; color: ${C.text};`; const body = document.createElement('div'); body.style.cssText = `font-size: 14px; color: ${C.text};`; sanitizeHelpHtml(curT.help_content, body); if (dark) { const style = document.createElement('style'); style.textContent = `#tm-copy-help-modal code { background: #1e2732; border-radius: 4px; padding: 1px 5px; color: #8b98a5; }`; content.appendChild(style); } const closeBtn = document.createElement('button'); closeBtn.innerHTML = ``; closeBtn.style.cssText = ` position: absolute; top: 10px; right: 12px; border: none; background: none; width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; cursor: pointer; color: ${C.sub}; border-radius: 5px; `; closeBtn.onclick = () => modal.remove(); modal.onclick = (e) => { if (e.target === modal) modal.remove(); }; content.appendChild(closeBtn); content.appendChild(title); content.appendChild(body); modal.appendChild(content); document.body.appendChild(modal); } function showLangPickerModal() { const old = document.getElementById('tm-lang-picker-modal'); if (old) old.remove(); const dark = window.matchMedia('(prefers-color-scheme: dark)').matches; const C = dark ? { overlay: 'rgba(0,0,0,0.82)', panel: '#16202b', text: '#e7e9ea', sub: '#8b98a5', border: '#2f3336', rowBg: '#1e2732', rowHover: '#2d3741', activeBg: '#1e3a4f', activeBorder:'#1d9bf0', activeText: '#1d9bf0', customBg: '#2b2510', customBorder:'#7a5c00', customText: '#f4c430', customHover: '#332c10', } : { overlay: 'rgba(0,0,0,0.72)', panel: '#ffffff', text: '#0f1419', sub: '#536471', border: '#eff3f4', rowBg: '#ffffff', rowHover: '#f7f9f9', activeBg: '#e8f5fe', activeBorder:'#1d9bf0', activeText: '#1d9bf0', customBg: '#fffbea', customBorder:'#e0a800', customText: '#7a5700', customHover: '#fff8d6', }; const LANG_MAP = [ { code: 'en', label: 'English' }, { code: 'zh-TW', label: '繁體中文' }, { code: 'zh-CN', label: '简体中文' }, { code: 'ja', label: '日本語' }, { code: 'ko', label: '한국어' }, { code: 'es', label: 'Español' }, { code: 'pt-BR', label: 'Português (BR)' }, { code: 'fr', label: 'Français' }, { code: 'ru', label: 'Русский' }, ]; const currentCode = GM_getValue(KEY_LANG, 'en'); const modal = document.createElement('div'); modal.id = 'tm-lang-picker-modal'; modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: ${C.overlay}; z-index: 999999; display: flex; align-items: center; justify-content: center; font-family: system-ui, -apple-system, sans-serif; `; const panel = document.createElement('div'); panel.style.cssText = ` background: ${C.panel}; color: ${C.text}; padding: 24px 20px 20px; border-radius: 16px; width: 92%; max-width: 420px; box-shadow: 0 8px 32px rgba(0,0,0,0.35); position: relative; `; const closeBtn = document.createElement('button'); closeBtn.innerHTML = ``; closeBtn.style.cssText = ` position: absolute; top: 12px; right: 14px; border: none; background: none; width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; cursor: pointer; color: ${C.sub}; border-radius: 5px; `; closeBtn.onclick = () => modal.remove(); modal.onclick = e => { if (e.target === modal) modal.remove(); }; const title = document.createElement('h3'); title.textContent = '🌐 ' + T.menu_lang.replace(/^🌐\s*/, ''); title.style.cssText = `margin: 0 0 16px; font-size: 1rem; color: ${C.text}; padding-right: 24px;`; const list = document.createElement('div'); list.style.cssText = 'display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px;'; const applyLang = (code) => { modal.remove(); GM_setValue(KEY_LANG, code); GM_deleteValue(KEY_ONBOARDING_DONE); const newT = TR[code] || TR['en']; const confirmMsg = T.confirm_lang_reload.replace('{lang}', newT.langName); if (confirm(confirmMsg)) { location.reload(); } else { showToast(newT.toast_lang_pending); } }; LANG_MAP.forEach(({ code, label }) => { const btn = document.createElement('button'); const isActive = (code === currentCode); btn.textContent = (isActive ? '★ ' : '') + label; btn.style.cssText = ` width: 100%; padding: 9px 14px; border-radius: 9999px; text-align: left; border: 2px solid ${isActive ? C.activeBorder : C.border}; background: ${isActive ? C.activeBg : C.rowBg}; color: ${isActive ? C.activeText : C.text}; font-size: 14px; font-weight: ${isActive ? '700' : '400'}; cursor: pointer; transition: border-color 0.15s, background 0.15s; `; btn.onmouseenter = () => { if (!isActive) { btn.style.borderColor = C.sub; btn.style.background = C.rowHover; } }; btn.onmouseleave = () => { if (!isActive) { btn.style.borderColor = C.border; btn.style.background = C.rowBg; } }; btn.onclick = () => applyLang(code); list.appendChild(btn); }); const hr = document.createElement('hr'); hr.style.cssText = `border: none; border-top: 1px solid ${C.border}; margin: 4px 0 10px;`; const customBtn = document.createElement('button'); const hasCustom = !!GM_getValue(KEY_CUSTOM_LANG, null); const isCustomActive = (currentCode === 'custom'); customBtn.textContent = (isCustomActive ? '★ ' : '') + '✏️ Custom Language' + (hasCustom && !isCustomActive ? ' (loaded)' : ''); customBtn.style.cssText = ` width: 100%; padding: 9px 14px; border-radius: 9999px; text-align: left; border: 2px solid ${isCustomActive ? C.activeBorder : C.customBorder}; background: ${isCustomActive ? C.activeBg : C.customBg}; color: ${isCustomActive ? C.activeText : C.customText}; font-size: 14px; font-weight: ${isCustomActive ? '700' : '500'}; cursor: pointer; transition: border-color 0.15s, background 0.15s; `; customBtn.onmouseenter = () => { if (!isCustomActive) customBtn.style.background = C.customHover; }; customBtn.onmouseleave = () => { if (!isCustomActive) customBtn.style.background = C.customBg; }; customBtn.onclick = () => { modal.remove(); showCustomLangPanel(); }; panel.appendChild(closeBtn); panel.appendChild(title); panel.appendChild(list); panel.appendChild(hr); panel.appendChild(customBtn); modal.appendChild(panel); document.body.appendChild(modal); } const CUSTOM_LANG_HOW_TO = [ "English: Export → translate the values → Import", "Deutsch: Exportieren → Werte übersetzen → Importieren", "Français: Exporter → traduire les valeurs → Importer", "Español: Exportar → traducir los valores → Importar", "Italiano: Esporta → traduci i valori → Importa", "Português: Exportar → traduzir os valores → Importar", "Русский: Экспорт → перевести значения → Импорт", "Українська: Експорт → перекласти значення → Імпорт", "ภาษาไทย: ส่งออก → แปลค่า → นำเข้า", "Türkçe: Dışa aktar → değerleri çevir → İçe aktar", "Polski: Eksportuj → przetłumacz wartości → Importuj", "Čeština: Exportovat → přeložit hodnoty → Importovat", "Română: Exportați → traduceți valorile → Importați", "Magyar: Exportálás → értékek fordítása → Importálás", "Ελληνικά: Εξαγωγή → μετάφραση τιμών → Εισαγωγή", "العربية: تصدير ← ترجمة القيم ← استيراد", "עברית: ייצוא ← תרגום הערכים ← ייבוא", "فارسی: صادر کردن ← ترجمه مقادیر ← وارد کردن", "हिन्दी: निर्यात → मान अनुवाद करें → आयात", "বাংলা: রপ্তানি → মান অনুবাদ করুন → আমদানি", "Indonesia: Ekspor → terjemahkan nilai → Impor", "Bahasa Melayu: Eksport → terjemah nilai → Import", "Filipino: I-export → isalin ang mga halaga → I-import", "Tiếng Việt: Xuất → dịch các giá trị → Nhập", "Nederlands: Exporteren → waarden vertalen → Importeren", "Svenska: Exportera → översätt värdena → Importera", "Kiswahili: Hamisha → tafsiri maadili → Ingiza", "한국어: 내보내기 → 값 번역 → 가져오기", "日本語: エクスポート → 値を翻訳 → インポート", "繁體中文: 匯出 → 翻譯內容 → 匯入", "简体中文: 导出 → 翻译内容 → 导入", ]; function buildExportTemplate() { const base = Object.assign({}, TR['en']); const template = { _note: "Translate the VALUES only. Do NOT change the KEYS. Keep {placeholders} like {lang} untouched. Preserve HTML tags and emoji as-is. Set \"langName\" to your language's native name.", langName: "My Custom Language", menu_domain_click: base.menu_domain_click, menu_domain_long: base.menu_domain_long, menu_prefix: base.menu_prefix, menu_lang: base.menu_lang, menu_help: base.menu_help, prompt_prefix: base.prompt_prefix, prompt_domain: base.prompt_domain, status_default: base.status_default, status_custom: base.status_custom, btn_tooltip: base.btn_tooltip, link_tooltip: base.link_tooltip, link_tooltip_long: base.link_tooltip_long, msg_prefix_copied: base.msg_prefix_copied, msg_copied: base.msg_copied, msg_downloaded: base.msg_downloaded, msg_no_media: base.msg_no_media, play_btn_tooltip: base.play_btn_tooltip, msg_no_video: base.msg_no_video, reload_msg: base.reload_msg, toast_domain_click: base.toast_domain_click, toast_domain_long: base.toast_domain_long, toast_prefix: base.toast_prefix, toast_date_fmt: base.toast_date_fmt, toast_lang_pending: base.toast_lang_pending, confirm_lang_reload: base.confirm_lang_reload, menu_date_format: base.menu_date_format, status_date_asian: base.status_date_asian, status_date_western: base.status_date_western, help_title: base.help_title, help_content: base.help_content.trim(), onboard_title: base.onboard_title, onboard_body: base.onboard_body, onboard_got_it: base.onboard_got_it, }; return template; } function showCustomLangPanel() { const old = document.getElementById('tm-custom-lang-modal'); if (old) old.remove(); const dark = window.matchMedia('(prefers-color-scheme: dark)').matches; const C = dark ? { overlay: 'rgba(0,0,0,0.82)', panel: '#16202b', text: '#e7e9ea', sub: '#8b98a5', border: '#2f3336', guideBg: '#1e2732', guideText: '#8b98a5', exportBorder: '#1d9bf0', exportText: '#1d9bf0', exportBg: '#16202b', exportHover: '#1e2f3f', importBg: '#1d9bf0', importHover: '#1a8cd8', clearBorder: '#e0245e', clearText: '#e0245e', clearBg: '#16202b', clearHover: '#2a1520', } : { overlay: 'rgba(0,0,0,0.75)', panel: '#ffffff', text: '#0f1419', sub: '#536471', border: '#eff3f4', guideBg: '#f7f9f9', guideText: '#536471', exportBorder: '#1d9bf0', exportText: '#1d9bf0', exportBg: '#ffffff', exportHover: '#e8f5fe', importBg: '#1d9bf0', importHover: '#1a8cd8', clearBorder: '#e0245e', clearText: '#e0245e', clearBg: '#ffffff', clearHover: '#fdf0f2', }; const existingJson = GM_getValue(KEY_CUSTOM_LANG, null); const hasCustom = !!existingJson; const modal = document.createElement('div'); modal.id = 'tm-custom-lang-modal'; modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: ${C.overlay}; z-index: 999999; display: flex; align-items: center; justify-content: center; font-family: system-ui, -apple-system, sans-serif; `; const panel = document.createElement('div'); panel.style.cssText = ` background: ${C.panel}; color: ${C.text}; padding: 28px 28px 24px; border-radius: 16px; width: 95%; max-width: 640px; box-shadow: 0 8px 32px rgba(0,0,0,0.35); position: relative; max-height: 90vh; overflow-y: auto; `; const closeBtn = document.createElement('button'); closeBtn.innerHTML = ``; closeBtn.style.cssText = ` position: absolute; top: 12px; right: 14px; border: none; background: none; width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; cursor: pointer; color: ${C.sub}; border-radius: 5px; `; closeBtn.onclick = () => modal.remove(); modal.onclick = e => { if (e.target === modal) modal.remove(); }; const title = document.createElement('h3'); title.textContent = '✏️ Custom Language'; title.style.cssText = `margin: 0 0 6px; font-size: 1.1rem; color: ${C.text};`; const statusLine = document.createElement('p'); statusLine.style.cssText = `margin: 0 0 16px; font-size: 13px; color: ${C.sub};`; if (hasCustom) { try { const parsed = JSON.parse(existingJson); statusLine.textContent = ''; const starText = document.createTextNode('★ Active: '); const boldEl = document.createElement('b'); boldEl.textContent = parsed.langName || 'Custom'; statusLine.appendChild(starText); statusLine.appendChild(boldEl); statusLine.style.color = '#1d9bf0'; } catch(e) { statusLine.textContent = '⚠️ Saved data is corrupted.'; statusLine.style.color = '#e0245e'; } } else { statusLine.textContent = 'No custom language loaded.'; } const hr = document.createElement('hr'); hr.style.cssText = `border: none; border-top: 1px solid ${C.border}; margin: 0 0 12px;`; const guideBox = document.createElement('div'); guideBox.style.cssText = ` background: ${C.guideBg}; border: 1px solid ${C.border}; border-radius: 8px; padding: 10px 14px; margin-bottom: 16px; font-size: 12px; font-family: monospace; line-height: 1.8; color: ${C.guideText}; white-space: pre; `; guideBox.textContent = CUSTOM_LANG_HOW_TO.join('\n'); const btnRow = document.createElement('div'); btnRow.style.cssText = 'display: flex; gap: 10px; flex-wrap: wrap;'; const exportBtn = document.createElement('button'); exportBtn.textContent = '📤 Export Template'; exportBtn.style.cssText = ` flex: 1; min-width: 140px; padding: 10px 16px; border-radius: 9999px; border: 2px solid ${C.exportBorder}; background: ${C.exportBg}; color: ${C.exportText}; font-size: 14px; font-weight: 700; cursor: pointer; transition: background 0.15s; `; exportBtn.onmouseenter = () => { exportBtn.style.background = C.exportHover; }; exportBtn.onmouseleave = () => { exportBtn.style.background = C.exportBg; }; exportBtn.onclick = () => { const template = buildExportTemplate(); const jsonStr = JSON.stringify(template, null, 2); const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'twitter-media-copy-custom-lang.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 5000); showToast('📤 Template exported!'); }; const importBtn = document.createElement('button'); importBtn.textContent = '📥 Import Translation'; importBtn.style.cssText = ` flex: 1; min-width: 140px; padding: 10px 16px; border-radius: 9999px; border: none; background: ${C.importBg}; color: #fff; font-size: 14px; font-weight: 700; cursor: pointer; transition: background 0.15s; `; importBtn.onmouseenter = () => { importBtn.style.background = C.importHover; }; importBtn.onmouseleave = () => { importBtn.style.background = C.importBg; }; importBtn.onclick = () => { const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.json,application/json'; fileInput.onchange = e => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = ev => { try { const parsed = JSON.parse(ev.target.result); if (!parsed.langName) throw new Error('Missing "langName" field.'); const merged = Object.assign({}, TR['en'], parsed); delete merged._note; GM_setValue(KEY_CUSTOM_LANG, JSON.stringify(merged)); GM_setValue(KEY_LANG, 'custom'); GM_deleteValue(KEY_ONBOARDING_DONE); TR['custom'] = merged; modal.remove(); showToast(`✅ Loaded: ${merged.langName}`); if (confirm(`Custom language "${merged.langName}" loaded.\nReload page now to apply?`)) { location.reload(); } } catch(err) { alert(`❌ Import failed: ${err.message}\n\nMake sure the file is valid JSON and contains a "langName" field.`); } }; reader.readAsText(file, 'UTF-8'); }; document.body.appendChild(fileInput); fileInput.click(); document.body.removeChild(fileInput); }; btnRow.appendChild(exportBtn); btnRow.appendChild(importBtn); panel.appendChild(closeBtn); panel.appendChild(title); panel.appendChild(statusLine); panel.appendChild(hr); panel.appendChild(guideBox); panel.appendChild(btnRow); if (hasCustom) { const clearBtn = document.createElement('button'); clearBtn.textContent = '🗑️ Clear Custom'; clearBtn.style.cssText = ` width: 100%; margin-top: 10px; padding: 9px 16px; border-radius: 9999px; border: 2px solid ${C.clearBorder}; background: ${C.clearBg}; color: ${C.clearText}; font-size: 13px; font-weight: 600; cursor: pointer; transition: background 0.15s; `; clearBtn.onmouseenter = () => { clearBtn.style.background = C.clearHover; }; clearBtn.onmouseleave = () => { clearBtn.style.background = C.clearBg; }; clearBtn.onclick = () => { if (!confirm('Remove custom language and revert to English?')) return; GM_deleteValue(KEY_CUSTOM_LANG); GM_setValue(KEY_LANG, 'en'); delete TR['custom']; modal.remove(); showToast('🗑️ Custom language cleared.'); if (confirm('Reverted to English.\nReload page now?')) location.reload(); }; panel.appendChild(clearBtn); } modal.appendChild(panel); document.body.appendChild(modal); } function selectDomain(key) { let msg = T.prompt_domain; DOMAIN_LIST.forEach((d, index) => { msg += `${index + 1}. ${d}\n`; }); const input = prompt(msg, ""); if (input !== null) { const index = parseInt(input.trim()) - 1; if (!isNaN(index) && DOMAIN_LIST[index]) { GM_setValue(key, DOMAIN_LIST[index]); return true; } else if (input.trim() !== "") { const cleanDomain = input.trim(); if (/^[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}(\.[a-zA-Z0-9\-]{1,63})*\.[a-zA-Z]{2,}$/.test(cleanDomain)) { GM_setValue(key, cleanDomain); return true; } else { alert(`Invalid domain: "${cleanDomain}"\nPlease enter a plain domain (e.g. fixupx.com), without http:// or paths.`); } } } return false; } let menuIds = []; function registerMenus() { menuIds.forEach(id => GM_unregisterMenuCommand(id)); menuIds = []; const currentPrefix = GM_getValue(KEY_PREFIX_TEXT, '[text]'); const clickCustom = GM_getValue(KEY_CLICK_MODE_CUSTOM, false); const clickDomain = GM_getValue(KEY_LINK_DOMAIN_CLICK, 'x.com'); const clickStatusText = clickCustom ? `${T.status_custom} (${clickDomain})` : T.status_default; menuIds.push(GM_registerMenuCommand(T.menu_domain_click + ` [${clickStatusText}]`, () => { if (!clickCustom) { if(selectDomain(KEY_LINK_DOMAIN_CLICK)) { GM_setValue(KEY_CLICK_MODE_CUSTOM, true); const newDomain = GM_getValue(KEY_LINK_DOMAIN_CLICK, 'x.com'); showToast(T.toast_domain_click + newDomain); registerMenus(); } } else { GM_setValue(KEY_CLICK_MODE_CUSTOM, false); showToast(T.toast_domain_click + 'x.com'); registerMenus(); } })); menuIds.push(GM_registerMenuCommand(T.menu_prefix + ` (${currentPrefix})`, () => { const newPrefix = prompt(T.prompt_prefix, currentPrefix); if (newPrefix !== null) { GM_setValue(KEY_PREFIX_TEXT, newPrefix); showToast(T.toast_prefix + (newPrefix || '(empty)')); registerMenus(); } })); menuIds.push(GM_registerMenuCommand(T.menu_lang + ` [${T.langName}]`, () => { showLangPickerModal(); })); const currentFmt = GM_getValue(KEY_DATE_FORMAT, 'asian'); const fmtStatusText = currentFmt === 'western' ? T.status_date_western : T.status_date_asian; menuIds.push(GM_registerMenuCommand(T.menu_date_format + ` [${fmtStatusText}]`, () => { const newFmt = currentFmt === 'western' ? 'asian' : 'western'; GM_setValue(KEY_DATE_FORMAT, newFmt); _refreshDateFormatCache(); const newLabel = newFmt === 'western' ? T.status_date_western : T.status_date_asian; showToast(T.toast_date_fmt + newLabel); registerMenus(); })); menuIds.push(GM_registerMenuCommand(T.menu_help, showHelpModal)); } registerMenus(); function _initSettingsPanel() { if (document.body) { createSettingsPanel(); _initStarPip(); } else { document.addEventListener('DOMContentLoaded', () => { createSettingsPanel(); _initStarPip(); }, { once: true }); } } function _initStarPip() { if (document.getElementById('tm-star-pip')) return; const pip = document.createElement('button'); pip.id = 'tm-star-pip'; pip.className = 'tm-star-pip'; pip.title = 'Group this download'; pip.textContent = '⭐'; pip.addEventListener('mouseenter', () => { if (!_fanOpen && getGroups().length) openGroupFan(); }); pip.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); if (_fanOpen) closeGroupFan(); else openGroupFan(); }); document.body.appendChild(pip); } function showOnboardingOverlay() { if (GM_getValue(KEY_ONBOARDING_DONE, false)) return; const gearEl = document.getElementById('tm-settings-gear-btn'); if (!gearEl) { setTimeout(showOnboardingOverlay, 400); return; } const rect = gearEl.getBoundingClientRect(); const cx = rect.left + rect.width / 2; const cy = rect.top + rect.height / 2; const r1 = 26; const r2 = 42; const wrapperEl = document.getElementById('tm-settings-wrapper'); if (wrapperEl) { wrapperEl.style.setProperty('opacity', '1', 'important'); wrapperEl.style.setProperty('transition', 'none', 'important'); } gearEl.style.setProperty('opacity', '1', 'important'); gearEl.style.setProperty('transition', 'none', 'important'); const obStyle = document.createElement('style'); obStyle.id = 'tm-ob-style'; obStyle.textContent = ` @keyframes tm-ob-pulse-ring { 0% { transform:translate(-50%,-50%) scale(1); opacity:0.9; } 70% { transform:translate(-50%,-50%) scale(1.55); opacity:0; } 100% { transform:translate(-50%,-50%) scale(1.55); opacity:0; } } @keyframes tm-ob-fadein { from { opacity:0; } to { opacity:1; } } @keyframes tm-ob-card-in { from { opacity:0; transform:translateY(10px) scale(0.96); } to { opacity:1; transform:translateY(0) scale(1); } } #tm-ob-overlay { position:fixed; inset:0; z-index:999985; pointer-events:all; animation: tm-ob-fadein 0.45s ease forwards; } #tm-ob-ring { position:fixed; border-radius:50%; pointer-events:none; border: 2px solid rgba(29,155,240,0.85); animation: tm-ob-pulse-ring 1.7s cubic-bezier(0.215,0.61,0.355,1) infinite; z-index:999988; } #tm-ob-card { position:fixed; z-index:999988; animation: tm-ob-card-in 0.4s 0.2s cubic-bezier(0.34,1.56,0.64,1) both; pointer-events:all; } #tm-ob-got-it { width:100%; padding:9px; border-radius:9999px; border:none; background:#1d9bf0; color:#fff; font-size:14px; font-weight:700; cursor:pointer; text-align:center; display:block; transition:background 0.15s; } #tm-ob-got-it:hover { background:#1a8cd8; } `; document.head.appendChild(obStyle); const overlay = document.createElement('div'); overlay.id = 'tm-ob-overlay'; overlay.style.background = `radial-gradient(circle at ${cx}px ${cy}px, transparent ${r1}px, rgba(0,0,0,0.80) ${r2}px)`; const ring = document.createElement('div'); ring.id = 'tm-ob-ring'; ring.style.cssText = `width:${r1 * 2}px; height:${r1 * 2}px; left:${cx}px; top:${cy}px; transform:translate(-50%,-50%);`; const dark = window.matchMedia('(prefers-color-scheme: dark)').matches; const cardBg = dark ? '#16202b' : '#ffffff'; const cardText = dark ? '#e7e9ea' : '#0f1419'; const cardSub = dark ? '#8b98a5' : '#536471'; const arrowClr = dark ? '#16202b' : '#ffffff'; const cardW = 270; const cardLeft = Math.max(8, Math.min(cx - cardW / 2, window.innerWidth - cardW - 8)); const cardTop = cy + r2 + 10; const card = document.createElement('div'); card.id = 'tm-ob-card'; card.style.cssText = ` width:${cardW}px; left:${cardLeft}px; top:${cardTop}px; background:${cardBg}; border-radius:14px; box-shadow:0 8px 32px rgba(0,0,0,0.32); padding:18px 18px 14px; z-index:999989; `; const arrow = document.createElement('div'); arrow.style.cssText = ` position:absolute; top:-10px; right:16px; width:0; height:0; border-left:10px solid transparent; border-right:10px solid transparent; border-bottom:10px solid ${arrowClr}; `; const titleEl = document.createElement('div'); titleEl.style.cssText = `font-size:15px;font-weight:700;color:${cardText};margin-bottom:8px;`; titleEl.textContent = T.onboard_title || '⚙ Settings Panel'; const bodyEl = document.createElement('div'); bodyEl.style.cssText = `font-size:13px;color:${cardSub};line-height:1.55;margin-bottom:14px;`; bodyEl.textContent = T.onboard_body || 'Hover the top-right corner to reveal the settings button.'; const gotItBtn = document.createElement('button'); gotItBtn.id = 'tm-ob-got-it'; gotItBtn.textContent = T.onboard_got_it || 'Got it!'; const dismiss = () => { GM_setValue(KEY_ONBOARDING_DONE, true); [overlay, card, ring].forEach(el => { el.style.transition = 'opacity 0.3s ease'; el.style.opacity = '0'; }); setTimeout(() => { [overlay, card, ring, obStyle].forEach(el => el.remove()); gearEl.style.removeProperty('opacity'); gearEl.style.removeProperty('transition'); if (wrapperEl) { wrapperEl.style.removeProperty('opacity'); wrapperEl.style.removeProperty('transition'); } }, 320); }; gotItBtn.addEventListener('click', e => { e.stopPropagation(); dismiss(); }); overlay.addEventListener('click', dismiss); card.appendChild(arrow); card.appendChild(titleEl); card.appendChild(bodyEl); card.appendChild(gotItBtn); document.body.appendChild(overlay); document.body.appendChild(ring); document.body.appendChild(card); } setTimeout(showOnboardingOverlay, 1200); _initSettingsPanel(); function showDomainPickerModal(key, onSuccess) { const old = document.getElementById('tm-domain-picker-modal'); if (old) old.remove(); const dark = window.matchMedia('(prefers-color-scheme: dark)').matches; const C = dark ? { overlay: 'rgba(0,0,0,0.82)', panel: '#16202b', text: '#e7e9ea', sub: '#8b98a5', border: '#2f3336', rowBg: '#1e2732', rowHover: '#2d3741', activeBg: '#1e3a4f', activeBorder: '#1d9bf0', activeText: '#1d9bf0', } : { overlay: 'rgba(0,0,0,0.72)', panel: '#ffffff', text: '#0f1419', sub: '#536471', border: '#eff3f4', rowBg: '#ffffff', rowHover: '#f7f9f9', activeBg: '#e8f5fe', activeBorder: '#1d9bf0', activeText: '#1d9bf0', }; const currentVal = GM_getValue(key, 'x.com'); const modal = document.createElement('div'); modal.id = 'tm-domain-picker-modal'; modal.style.cssText = ` position:fixed;top:0;left:0;width:100%;height:100%; background:${C.overlay};z-index:9999999; display:flex;align-items:center;justify-content:center; font-family:system-ui,-apple-system,sans-serif; `; const panel = document.createElement('div'); panel.style.cssText = ` background:${C.panel};color:${C.text};padding:22px 18px 18px; border-radius:16px;width:92%;max-width:360px; box-shadow:0 8px 32px rgba(0,0,0,0.35);position:relative; `; const closeBtn = document.createElement('button'); closeBtn.innerHTML = ``; closeBtn.style.cssText = `position:absolute;top:12px;right:14px;border:none; background:none;width:26px;height:26px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:${C.sub};border-radius:5px;`; closeBtn.onclick = () => modal.remove(); modal.onclick = e => { if (e.target === modal) modal.remove(); }; const title = document.createElement('h3'); title.textContent = T.menu_domain_click.replace(/^🔗\s*/, ''); title.style.cssText = `margin:0 0 14px;font-size:0.95rem;color:${C.text};padding-right:24px;`; const list = document.createElement('div'); list.style.cssText = 'display:flex;flex-direction:column;gap:6px;'; DOMAIN_LIST.forEach(domain => { const isActive = domain === currentVal; const btn = document.createElement('button'); btn.textContent = (isActive ? '★ ' : '') + domain; btn.style.cssText = ` width:100%;padding:8px 14px;border-radius:9999px;text-align:left; border:2px solid ${isActive ? C.activeBorder : C.border}; background:${isActive ? C.activeBg : C.rowBg}; color:${isActive ? C.activeText : C.text}; font-size:13px;font-weight:${isActive ? '700' : '400'}; cursor:pointer;transition:border-color 0.15s,background 0.15s; `; btn.onmouseenter = () => { if (!isActive) { btn.style.borderColor = C.sub; btn.style.background = C.rowHover; } }; btn.onmouseleave = () => { if (!isActive) { btn.style.borderColor = C.border; btn.style.background = C.rowBg; } }; btn.onclick = () => { GM_setValue(key, domain); modal.remove(); if (onSuccess) onSuccess(domain); }; list.appendChild(btn); }); panel.appendChild(closeBtn); panel.appendChild(title); panel.appendChild(list); modal.appendChild(panel); document.body.appendChild(modal); } function isFeatureNew(id) { if (!NEW_FEATURE_IDS.includes(id)) return false; try { const seen = JSON.parse(GM_getValue(KEY_SEEN_FEATURES, '[]')); return !seen.includes(id); } catch(_) { return true; } } function markFeatureSeen(id) { try { const seen = JSON.parse(GM_getValue(KEY_SEEN_FEATURES, '[]')); if (!seen.includes(id)) { seen.push(id); GM_setValue(KEY_SEEN_FEATURES, JSON.stringify(seen)); } } catch(_) {} } function createSettingsPanel() { const existingWrapper = document.getElementById('tm-settings-wrapper'); if (existingWrapper) existingWrapper.remove(); const existingStyle = document.getElementById('tm-settings-panel-style'); if (existingStyle) existingStyle.remove(); const dark = window.matchMedia('(prefers-color-scheme: dark)').matches; const C = dark ? { panel: '#16202b', header: '#1e2732', text: '#e7e9ea', sub: '#8b98a5', border: '#2f3336', rowHover: '#1e2732', badge: '#1d9bf0', gearFg: '#e7e9ea', gearBg: 'rgba(255,255,255,0.08)', } : { panel: '#ffffff', header: '#f7f9f9', text: '#0f1419', sub: '#536471', border: '#eff3f4', rowHover: '#f7f9f9', badge: '#1d9bf0', gearFg: '#536471', gearBg: 'rgba(0,0,0,0.06)', }; const panelStyle = document.createElement('style'); panelStyle.id = 'tm-settings-panel-style'; panelStyle.textContent = ` #tm-settings-wrapper { position: fixed; top: 12px; right: 12px; z-index: 999990; width: 90px; height: 50px; opacity: 0; transition: opacity 0.3s ease; } #tm-settings-wrapper:hover, #tm-settings-wrapper[data-open="true"] { opacity: 1; } #tm-history-btn { position: absolute; right: 28px; top: 2px; width: 44px; height: 44px; border-radius: 50%; border: none; background: transparent; cursor: pointer; padding: 0; display: flex; align-items: center; justify-content: center; color: ${C.gearFg}; z-index: 3; opacity: 1; transform: scale(1) translateX(0); transition: all 0.35s cubic-bezier(0.34, 1.56, 0.64, 1); } #tm-history-btn svg { width: 24px; height: 24px; display: block; } #tm-settings-gear-btn { position: absolute; right: 4px; top: 6px; width: 36px; height: 36px; border-radius: 50%; border: none; background: transparent; cursor: pointer; padding: 0; display: flex; align-items: center; justify-content: center; color: ${C.gearFg}; z-index: 1; opacity: 0.5; transform: scale(0.9) translateX(0); transition: all 0.35s cubic-bezier(0.34, 1.56, 0.64, 1); } #tm-settings-gear-btn svg { width: 20px; height: 20px; display: block; transition: transform 0.3s ease; } #tm-settings-wrapper[data-focus="hist"] #tm-history-btn { transform: scale(1.15); background: ${C.gearBg}; } #tm-settings-wrapper[data-focus="gear"] #tm-settings-gear-btn, #tm-settings-wrapper[data-open="true"] #tm-settings-gear-btn { z-index: 4; opacity: 1; transform: scale(1.2) translateX(-22px); background: ${C.gearBg}; } #tm-settings-wrapper[data-focus="gear"] #tm-history-btn, #tm-settings-wrapper[data-open="true"] #tm-history-btn { z-index: 1; opacity: 0.35; transform: scale(0.75) translateX(26px); background: transparent; } #tm-settings-wrapper[data-open="true"] #tm-settings-gear-btn svg { transform: rotate(90deg); } #tm-settings-panel { position: absolute; top: calc(100% + 4px); right: 4px; width: 300px; background: ${C.panel}; border-radius: 14px; box-shadow: 0 8px 32px rgba(0,0,0,0.18), 0 2px 8px rgba(0,0,0,0.10); border: 1px solid ${C.border}; font-family: system-ui, -apple-system, sans-serif; overflow: hidden; transform-origin: top right; transform: scale(0.88) translateY(-8px); opacity: 0; transition: transform 0.22s cubic-bezier(0.34,1.56,0.64,1), opacity 0.18s ease; pointer-events: none; } #tm-settings-wrapper[data-open="true"] #tm-settings-panel { transform: scale(1) translateY(0); opacity: 1; pointer-events: all; } .tm-sp-header { display: flex; align-items: center; padding: 11px 14px 10px; background: ${C.header}; border-bottom: 1px solid ${C.border}; font-size: 12px; font-weight: 700; color: ${C.sub}; letter-spacing: 0.04em; text-transform: uppercase; } .tm-sp-group-header { padding: 8px 14px 5px; font-size: 10px; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; color: ${C.sub}; background: ${C.header}; border-bottom: 1px solid ${C.border}; border-top: 1px solid ${C.border}; user-select: none; opacity: 0.7; } .tm-sp-group-header:first-of-type { border-top: none; } .tm-sp-row { display: flex; align-items: center; justify-content: space-between; padding: 9px 14px; gap: 8px; border-bottom: 1px solid ${C.border}; cursor: pointer; transition: background 0.1s; } .tm-sp-row:last-child { border-bottom: none; } .tm-sp-row:hover { background: ${C.rowHover}; } .tm-sp-row-left { display:flex; flex-direction:column; gap:1px; min-width:0; } .tm-sp-row-label { font-size: 12px; color: ${C.sub}; white-space: nowrap; } .tm-sp-row-value { font-size: 13px; color: ${C.text}; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .tm-sp-arrow { font-size: 11px; color: ${C.sub}; flex-shrink: 0; margin-left: 4px; opacity: 0.5; } .tm-sp-picker { display: none; flex-direction: column; background: ${C.panel}; border-top: 1px solid ${C.border}; padding: 6px 10px 8px; gap: 4px; } .tm-sp-picker.open { display: flex; } .tm-sp-picker-opt { display: flex; align-items: center; gap: 8px; padding: 6px 10px; border-radius: 8px; border: none; background: transparent; cursor: pointer; text-align: left; font-size: 12px; color: ${C.text}; transition: background 0.1s; width: 100%; } .tm-sp-picker-opt:hover { background: ${C.rowHover}; } .tm-sp-picker-opt.active { background: rgba(29,155,240,0.10); color: #1d9bf0; font-weight: 700; } .tm-sp-picker-opt .tm-sp-opt-check { width: 14px; flex-shrink: 0; font-size: 11px; color: #1d9bf0; } .tm-sp-group-header { display: flex; align-items: center; gap: 5px; cursor: pointer; transition: opacity 0.15s; } .tm-sp-group-header:hover { opacity: 1 !important; } .tm-sp-group-chevron { margin-left: auto; flex-shrink: 0; opacity: 0.5; transition: transform 0.2s ease; display: inline-flex; align-items: center; } .tm-sp-group-header.collapsed .tm-sp-group-chevron { transform: rotate(-90deg); } .tm-sp-group-body { overflow: hidden; max-height: 600px; transition: max-height 0.28s cubic-bezier(0.4,0,0.2,1), opacity 0.2s ease; opacity: 1; } .tm-sp-group-body.collapsed { max-height: 0; opacity: 0; pointer-events: none; } .tm-sp-help-badge { display: inline-flex; align-items: center; justify-content: center; width: 13px; height: 13px; flex-shrink: 0; opacity: 0.45; cursor: help; transition: opacity 0.15s; } .tm-sp-help-badge:hover { opacity: 0.9; } .tm-sp-help-badge svg { width: 13px; height: 13px; } .tm-sp-slider-row { padding: 7px 14px 8px; border-bottom: 1px solid ${C.border}; display: flex; flex-direction: column; gap: 4px; } .tm-sp-slider-header { display: flex; align-items: center; justify-content: space-between; } .tm-sp-slider-label { font-size: 12px; color: ${C.sub}; } .tm-sp-slider-value { font-size: 12px; font-weight: 600; color: #1d9bf0; min-width: 40px; text-align: right; } .tm-sp-slider { -webkit-appearance: none; appearance: none; width: 100%; height: 3px; border-radius: 2px; background: ${C.border}; outline: none; cursor: pointer; accent-color: #1d9bf0; } .tm-sp-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 14px; height: 14px; border-radius: 50%; background: #1d9bf0; cursor: pointer; box-shadow: 0 1px 4px rgba(29,155,240,0.4); transition: box-shadow 0.15s; } .tm-sp-slider::-webkit-slider-thumb:hover { box-shadow: 0 0 0 4px rgba(29,155,240,0.18); } .tm-sp-slider::-moz-range-thumb { width: 14px; height: 14px; border-radius: 50%; border: none; background: #1d9bf0; cursor: pointer; } .tm-sp-disabled-child { opacity: 0.32; pointer-events: none; cursor: default; user-select: none; transition: opacity 0.18s ease; } .tm-sp-disabled-child .tm-sp-slider { cursor: default; accent-color: rgba(255,255,255,.2); } #tm-dock-spotlight { position: fixed; inset: 0; z-index: 9999990; pointer-events: all; animation: tm-spotlight-fadein 0.22s ease; } @keyframes tm-spotlight-fadein { from { opacity: 0; } to { opacity: 1; } } #tm-dock-spotlight-canvas { position: absolute; inset: 0; pointer-events: none; } .tm-dock-spotlight-step { position: absolute; background: rgba(29,155,240,0.12); border: 2px solid rgba(29,155,240,0.75); border-radius: 8px; pointer-events: none; animation: tm-spotlight-pulse 1.8s ease-in-out infinite; } @keyframes tm-spotlight-pulse { 0%,100% { box-shadow: 0 0 0 0 rgba(29,155,240,0.4); } 50% { box-shadow: 0 0 0 6px rgba(29,155,240,0); } } .tm-dock-spotlight-label { position: absolute; background: rgba(15,20,25,0.92); color: #e7e9ea; font-size: 11px; font-weight: 600; padding: 5px 10px; border-radius: 6px; white-space: nowrap; pointer-events: none; border: 1px solid rgba(29,155,240,0.35); } #tm-dock-spotlight-dismiss { position: absolute; bottom: 32px; left: 50%; transform: translateX(-50%); padding: 8px 22px; border-radius: 99px; border: none; background: #1d9bf0; color: #fff; font-size: 13px; font-weight: 700; cursor: pointer; box-shadow: 0 4px 16px rgba(29,155,240,0.4); transition: background 0.15s; } #tm-dock-spotlight-dismiss:hover { background: #1a8cd8; } @keyframes tm-sp-new-pulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(29,155,240,0.55); } 50% { box-shadow: 0 0 0 4px rgba(29,155,240,0); } } .tm-sp-new-badge { font-size: 9px; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; padding: 2px 6px; border-radius: 9999px; flex-shrink: 0; background: #1d9bf0; color: #fff; animation: tm-sp-new-pulse 1.8s ease-in-out infinite; margin-right: 2px; } .tm-fb-demo { flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; width: 52px; height: 22px; pointer-events: none; overflow: visible; } .tm-fb-demo-toast-bubble { display: inline-flex; align-items: center; gap: 3px; padding: 2px 6px; border-radius: 99px; background: rgba(29,155,240,0.13); color: #1d9bf0; font-size: 9px; font-weight: 700; white-space: nowrap; border: 1px solid rgba(29,155,240,0.28); opacity: 0; transform: translateY(6px) scale(0.85); } .tm-fb-playing.tm-fb-demo-toast .tm-fb-demo-toast-bubble { animation: tm-fb-toast-in 0.55s cubic-bezier(0.34,1.56,0.64,1) forwards; } @keyframes tm-fb-toast-in { 0% { opacity: 0; transform: translateY(6px) scale(0.85); } 55% { opacity: 1; transform: translateY(0) scale(1); } 80% { opacity: 1; transform: translateY(0) scale(1); } 100% { opacity: 0; transform: translateY(-3px) scale(0.9); } } .tm-fb-demo-icon-wrap { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 50%; background: rgba(29,155,240,0.10); border: 1.5px solid rgba(29,155,240,0.25); color: #1d9bf0; opacity: 0; transform: scale(0.4); } .tm-fb-playing.tm-fb-demo-icon .tm-fb-demo-icon-wrap { animation: tm-fb-icon-pop 0.6s cubic-bezier(0.34,1.56,0.64,1) forwards; } .tm-fb-demo-icon-svg { stroke: #1d9bf0; } @keyframes tm-fb-icon-pop { 0% { opacity: 0; transform: scale(0.3) rotate(-15deg); } 55% { opacity: 1; transform: scale(1.15) rotate(3deg); } 75% { opacity: 1; transform: scale(0.95) rotate(0deg); } 85% { opacity: 1; transform: scale(1) rotate(0deg); } 100% { opacity: 0; transform: scale(1) rotate(0deg); } } .tm-fb-demo-silent-text { font-size: 9px; font-weight: 700; color: ${C.sub}; white-space: nowrap; opacity: 0; } .tm-fb-playing.tm-fb-demo-silent .tm-fb-demo-silent-text { animation: tm-fb-silent-fade 1.1s ease forwards; } @keyframes tm-fb-silent-fade { 0% { opacity: 0.9; } 30% { opacity: 0.9; } 100% { opacity: 0; } } @keyframes tm-float-new-pulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(249, 24, 128, 0.6); } 50% { box-shadow: 0 0 0 4px rgba(249, 24, 128, 0); } } .tm-float-new-badge { position: absolute; top: -2px; right: -4px; font-size: 8px; font-weight: 800; letter-spacing: 0.04em; text-transform: uppercase; padding: 2px 4px; border-radius: 4px; background: #f91880; color: #fff; animation: tm-float-new-pulse 1.8s ease-in-out infinite; pointer-events: none; z-index: 5; } .tm-gear-notify-dot { position: absolute; top: 1px; right: 1px; width: 7px; height: 7px; border-radius: 50%; background: #ff6b35; border: 1.5px solid var(--tm-gear-dot-border, #16202b); pointer-events: none; z-index: 6; animation: tm-dot-notify-pulse 2.2s ease-in-out infinite; } @keyframes tm-dot-notify-pulse { 0%,100% { box-shadow: 0 0 0 0 rgba(255,107,53,0.55); } 50% { box-shadow: 0 0 0 4px rgba(255,107,53,0); } } #tm-settings-wrapper[data-absorb="true"] { opacity: 1 !important; } #tm-settings-wrapper[data-absorb="true"] #tm-history-btn { z-index: 3 !important; opacity: 1 !important; } #tm-settings-wrapper[data-absorb="true"] #tm-settings-gear-btn { opacity: 0.35 !important; transform: scale(0.88) translateX(0) !important; z-index: 1 !important; } @keyframes tm-hist-absorb-bounce { 0% { transform: scale(1) translateX(0); filter: none; } 18% { transform: scale(1.42) translateX(0); filter: drop-shadow(0 0 10px rgba(29,155,240,0.95)); } 38% { transform: scale(0.88) translateX(0); filter: drop-shadow(0 0 5px rgba(29,155,240,0.5)); } 58% { transform: scale(1.18) translateX(0); filter: drop-shadow(0 0 7px rgba(29,155,240,0.7)); } 75% { transform: scale(0.95) translateX(0); filter: none; } 100% { transform: scale(1) translateX(0); filter: none; } } #tm-history-btn.tm-absorbing { animation: tm-hist-absorb-bounce 0.75s cubic-bezier(0.36,0.07,0.19,0.97) forwards; transition: none !important; } .tm-star-pip { position: fixed; width: 18px; height: 18px; border-radius: 50%; border: none; background: transparent; display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 99996; box-shadow: none; outline: none; pointer-events: none; transform: scale(0); opacity: 0; transition: transform .38s cubic-bezier(.34,1.56,.64,1), opacity .25s ease; font-size: 13px; line-height: 1; filter: drop-shadow(0 1px 2px rgba(0,0,0,.5)); } .tm-star-pip.tm-popped { transform: scale(1); opacity: 1; pointer-events: all; } .tm-star-pip:hover { transform: scale(1.35) !important; } .tm-star-pip.tm-escaping, .tm-star-pip.tm-escaping:hover { transform: scale(0.7) translate(-6px, 2px) !important; pointer-events: none !important; } .tm-fan-node { position: fixed; width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 99994; pointer-events: none; } .tm-fan-node.tm-spawned { pointer-events: all; } .tm-fan-node-inner { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; position: relative; background: transparent; transition: transform .18s cubic-bezier(.34,1.56,.64,1); } .tm-fan-node:hover .tm-fan-node-inner { transform: scale(1.15); } .tm-fan-glow { position: absolute; inset: -6px; border-radius: 50%; opacity: 0; transition: opacity .3s; pointer-events: none; } .tm-fan-node.tm-spawned .tm-fan-glow { opacity: 1; } .tm-fan-icon { font-size: 15px; line-height: 1; position: relative; z-index: 2; } .tm-fan-count { display: none; } .tm-fan-label { position: absolute; bottom: -17px; left: 50%; transform: translateX(-50%); font-size: 9px; color: rgba(255,255,255,.7); white-space: nowrap; pointer-events: none; opacity: 0; transition: opacity .25s .12s; text-shadow: 0 1px 4px rgba(0,0,0,.9); } .tm-fan-node.tm-spawned .tm-fan-label { opacity: 1; } .tm-ripple-ring { position: fixed; border-radius: 50%; pointer-events: none; border: 1px solid rgba(255,210,50,.45); animation: tm-ring-out .52s cubic-bezier(.2,.6,.4,1) forwards; z-index: 99993; } @keyframes tm-ring-out { 0% { transform: scale(1) translate(-50%,-50%); opacity: .85; } 100% { transform: scale(3.8) translate(-50%,-50%); opacity: 0; } } @keyframes tm-float-a { 0%,100%{transform:translate(0,0)} 33%{transform:translate(2.2px,-2.8px)} 66%{transform:translate(-1.8px,2.1px)} } @keyframes tm-float-b { 0%,100%{transform:translate(0,0)} 28%{transform:translate(-2.5px,-2.0px)} 68%{transform:translate(2.8px,1.8px)} } @keyframes tm-float-c { 0%,100%{transform:translate(0,0)} 42%{transform:translate(1.9px,2.6px)} 74%{transform:translate(-2.2px,-1.8px)} } @keyframes tm-float-d { 0%,100%{transform:translate(0,0)} 22%{transform:translate(2.6px,1.5px)} 62%{transform:translate(-1.5px,-2.8px)} } @keyframes tm-float-e { 0%,100%{transform:translate(0,0)} 36%{transform:translate(-2.8px,1.2px)} 71%{transform:translate(1.6px,-2.4px)} } .tm-float-a { animation: tm-float-a 3.4s ease-in-out infinite; } .tm-float-b { animation: tm-float-b 4.1s ease-in-out infinite .6s; } .tm-float-c { animation: tm-float-c 3.8s ease-in-out infinite 1.1s; } .tm-float-d { animation: tm-float-d 4.5s ease-in-out infinite .3s; } .tm-float-e { animation: tm-float-e 3.6s ease-in-out infinite 1.8s; } .tm-group-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.52); backdrop-filter: blur(4px); z-index: 99998; display: flex; align-items: center; justify-content: center; opacity: 0; pointer-events: none; transition: opacity .2s; } .tm-group-modal-overlay.tm-show { opacity: 1; pointer-events: all; } .tm-group-modal-box { background: #1a1f2e; border: 0.5px solid rgba(255,255,255,.13); border-radius: 14px; padding: 18px 20px; width: 230px; transform: scale(.88) translateY(10px); transition: transform .26s cubic-bezier(.34,1.56,.64,1); } .tm-group-modal-overlay.tm-show .tm-group-modal-box { transform: scale(1) translateY(0); } .tm-group-modal-title { font-size: 14px; color: rgba(255,255,255,.92); margin-bottom: 14px; font-weight: 600; letter-spacing: .01em; } .tm-group-modal-input { width: 100%; background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.18); border-radius: 8px; padding: 8px 11px; font-size: 13px; color: #e7e9ea; outline: none; transition: border-color .15s; box-sizing: border-box; font-family: inherit; } .tm-group-modal-input:focus { border-color: rgba(29,155,240,.7); } .tm-group-emoji-row { display: flex; gap: 5px; margin-top: 10px; flex-wrap: wrap; } .tm-group-emoji-btn { font-size: 17px; cursor: pointer; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; border-radius: 6px; transition: background .12s; border: none; background: transparent; padding: 0; font-family: inherit; } .tm-group-emoji-btn:hover { background: rgba(255,255,255,.1); } .tm-group-emoji-btn.tm-sel { background: rgba(29,155,240,.28); outline: 1.5px solid rgba(29,155,240,.5); } .tm-group-modal-btns { display: flex; gap: 8px; margin-top: 16px; } .tm-group-modal-btn { flex: 1; padding: 8px 0; border-radius: 8px; border: none; font-size: 13px; font-weight: 500; cursor: pointer; transition: background .15s, opacity .15s; font-family: inherit; letter-spacing: .01em; } .tm-group-modal-btn.tm-confirm { background: #1d9bf0; color: #fff; } .tm-group-modal-btn.tm-confirm:hover { background: #1a8cd8; } .tm-group-modal-btn.tm-skip { background: rgba(255,255,255,.09); color: rgba(255,255,255,.65); border: 1px solid rgba(255,255,255,.12); } .tm-group-modal-btn.tm-skip:hover { background: rgba(255,255,255,.14); } `; document.head.appendChild(panelStyle); const wrapper = document.createElement('div'); wrapper.id = 'tm-settings-wrapper'; wrapper.setAttribute('data-focus', 'hist'); wrapper.setAttribute('data-open', 'false'); let focusTimer = null; let currentFocus = 'hist'; wrapper.addEventListener('mousemove', (e) => { if (wrapper.getAttribute('data-open') === 'true') return; const rect = wrapper.getBoundingClientRect(); const x = e.clientX - rect.left; const targetFocus = x > 45 ? 'gear' : 'hist'; if (targetFocus !== currentFocus) { if (!focusTimer) { focusTimer = setTimeout(() => { currentFocus = targetFocus; wrapper.setAttribute('data-focus', currentFocus); focusTimer = null; }, 150); } } else { if (focusTimer) { clearTimeout(focusTimer); focusTimer = null; } } }); wrapper.addEventListener('mouseleave', () => { if (wrapper.getAttribute('data-open') === 'true') return; if (focusTimer) { clearTimeout(focusTimer); focusTimer = null; } currentFocus = 'hist'; wrapper.setAttribute('data-focus', 'hist'); }); const SVG_GEAR = ``; const gearBtn = document.createElement('button'); gearBtn.id = 'tm-settings-gear-btn'; gearBtn.innerHTML = SVG_GEAR; gearBtn.title = '⚙️ Twitter Media Script Settings'; const _hasUnseenFeature = NEW_FEATURE_IDS.some(id => isFeatureNew(id)); if (_hasUnseenFeature) { const dot = document.createElement('span'); dot.className = 'tm-gear-notify-dot'; dot.style.setProperty('--tm-gear-dot-border', dark ? '#16202b' : '#f7f9f9'); gearBtn.appendChild(dot); } const SVG_HISTORY = ``; const histBtn = document.createElement('button'); histBtn.id = 'tm-history-btn'; histBtn.innerHTML = SVG_HISTORY; histBtn.title = '📋 Download History'; if (isFeatureNew('history_panel')) { const floatBadge = document.createElement('span'); floatBadge.className = 'tm-float-new-badge'; floatBadge.textContent = 'NEW'; histBtn.appendChild(floatBadge); } histBtn.addEventListener('click', e => { e.stopPropagation(); if (isFeatureNew('history_panel')) { markFeatureSeen('history_panel'); const badge = histBtn.querySelector('.tm-float-new-badge'); if (badge) badge.remove(); } wrapper.setAttribute('data-open', 'false'); showHistoryPanel(); }); const panel = document.createElement('div'); panel.id = 'tm-settings-panel'; function buildContent() { panel.innerHTML = ''; const s = _readSettings(); const { clickCustom, clickDomain, prefix, fmt, fbStyle: _fbStyle, dockStyle, dockHoverDelay, dockTriggerL, dockTriggerR } = s; const header = document.createElement('div'); header.className = 'tm-sp-header'; header.textContent = T.onboard_title || '⚙ Media Script Settings'; panel.appendChild(header); const makeRow = (label, value, onClick, featureId = null) => { const row = document.createElement('div'); row.className = 'tm-sp-row'; const left = document.createElement('div'); left.className = 'tm-sp-row-left'; const lbl = document.createElement('span'); lbl.className = 'tm-sp-row-label'; lbl.textContent = label; const val = document.createElement('span'); val.className = 'tm-sp-row-value'; const getVal = typeof value === 'function' ? value : () => value; val.textContent = getVal(); left.appendChild(lbl); left.appendChild(val); row.appendChild(left); if (featureId && isFeatureNew(featureId)) { const badge = document.createElement('span'); badge.className = 'tm-sp-new-badge'; badge.textContent = 'NEW'; row.appendChild(badge); } const arrow = document.createElement('span'); arrow.className = 'tm-sp-arrow'; arrow.textContent = '›'; row.appendChild(arrow); row.addEventListener('click', () => { if (featureId) markFeatureSeen(featureId); onClick(); val.textContent = getVal(); }); return row; }; const makeGroup = (label, defaultOpen = true, tooltip = null, onOpen = null) => { const SVG_CHEVRON = ``; const SVG_HELP = ``; const g = document.createElement('div'); g.className = 'tm-sp-group-header' + (defaultOpen ? '' : ' collapsed'); const labelSpan = document.createElement('span'); labelSpan.textContent = label; g.appendChild(labelSpan); if (tooltip) { const helpBadge = document.createElement('span'); helpBadge.className = 'tm-sp-help-badge'; helpBadge.innerHTML = SVG_HELP; helpBadge.title = tooltip; g.appendChild(helpBadge); } const chevron = document.createElement('span'); chevron.className = 'tm-sp-group-chevron'; chevron.innerHTML = SVG_CHEVRON; if (defaultOpen) chevron.style.transform = 'rotate(0deg)'; else chevron.style.transform = 'rotate(-90deg)'; g.appendChild(chevron); const body = document.createElement('div'); body.className = 'tm-sp-group-body' + (defaultOpen ? '' : ' collapsed'); g.addEventListener('click', (e) => { if (e.target.closest('.tm-sp-help-badge')) return; const isCollapsed = body.classList.toggle('collapsed'); g.classList.toggle('collapsed', isCollapsed); chevron.style.transform = isCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)'; if (!isCollapsed && onOpen) onOpen(); }); panel.appendChild(g); panel.appendChild(body); return { body, append: (el) => body.appendChild(el), }; }; const makePickerRow = (label, options, currentVal, onSelect, featureId = null) => { const wrap = document.createElement('div'); wrap.style.cssText = 'border-bottom: 1px solid ${C.border};'; const row = document.createElement('div'); row.className = 'tm-sp-row'; row.style.borderBottom = 'none'; const left = document.createElement('div'); left.className = 'tm-sp-row-left'; const lbl = document.createElement('span'); lbl.className = 'tm-sp-row-label'; lbl.textContent = label; const val = document.createElement('span'); val.className = 'tm-sp-row-value'; val.textContent = (options.find(o => o.value === currentVal) || options[0]).label; left.appendChild(lbl); left.appendChild(val); row.appendChild(left); if (featureId && isFeatureNew(featureId)) { const badge = document.createElement('span'); badge.className = 'tm-sp-new-badge'; badge.textContent = 'NEW'; row.appendChild(badge); } const arrow = document.createElement('span'); arrow.className = 'tm-sp-arrow'; arrow.textContent = '›'; arrow.style.cssText = 'transition: transform 0.18s ease;'; row.appendChild(arrow); const picker = document.createElement('div'); picker.className = 'tm-sp-picker'; options.forEach(opt => { const btn = document.createElement('button'); btn.className = 'tm-sp-picker-opt' + (opt.value === currentVal ? ' active' : ''); const check = document.createElement('span'); check.className = 'tm-sp-opt-check'; check.textContent = opt.value === currentVal ? '✓' : ''; const txt = document.createElement('span'); txt.textContent = opt.label; btn.appendChild(check); btn.appendChild(txt); btn.addEventListener('click', (e) => { e.stopPropagation(); if (featureId) markFeatureSeen(featureId); onSelect(opt.value); val.textContent = opt.label; picker.querySelectorAll('.tm-sp-picker-opt').forEach(b => { b.classList.remove('active'); b.querySelector('.tm-sp-opt-check').textContent = ''; }); btn.classList.add('active'); check.textContent = '✓'; picker.classList.remove('open'); arrow.style.transform = ''; }); picker.appendChild(btn); }); row.addEventListener('click', (e) => { e.stopPropagation(); const isOpen = picker.classList.toggle('open'); arrow.style.transform = isOpen ? 'rotate(90deg)' : ''; if (featureId && isOpen) markFeatureSeen(featureId); }); wrap.addEventListener('click', e => e.stopPropagation()); wrap.appendChild(row); wrap.appendChild(picker); return wrap; }; const makeFeedbackPickerRow = (label, options, currentVal, onSelect, featureId = null) => { const wrap = document.createElement('div'); wrap.style.cssText = 'border-bottom: 1px solid ${C.border};'; const row = document.createElement('div'); row.className = 'tm-sp-row'; row.style.borderBottom = 'none'; const left = document.createElement('div'); left.className = 'tm-sp-row-left'; const lbl = document.createElement('span'); lbl.className = 'tm-sp-row-label'; lbl.textContent = label; const val = document.createElement('span'); val.className = 'tm-sp-row-value'; val.textContent = (options.find(o => o.value === currentVal) || options[0]).label; left.appendChild(lbl); left.appendChild(val); row.appendChild(left); if (featureId && isFeatureNew(featureId)) { const badge = document.createElement('span'); badge.className = 'tm-sp-new-badge'; badge.textContent = 'NEW'; row.appendChild(badge); } const arrow = document.createElement('span'); arrow.className = 'tm-sp-arrow'; arrow.textContent = '›'; arrow.style.cssText = 'transition: transform 0.18s ease;'; row.appendChild(arrow); const picker = document.createElement('div'); picker.className = 'tm-sp-picker'; const makeDemoEl = (value) => { const demo = document.createElement('span'); demo.className = 'tm-fb-demo tm-fb-demo-' + value; demo.setAttribute('aria-hidden', 'true'); if (value === 'toast') { demo.innerHTML = ` OK `; } else if (value === 'icon') { demo.innerHTML = ` `; } else if (value === 'silent') { demo.innerHTML = `Copied`; } return demo; }; options.forEach(opt => { const btn = document.createElement('button'); btn.className = 'tm-sp-picker-opt' + (opt.value === currentVal ? ' active' : ''); const check = document.createElement('span'); check.className = 'tm-sp-opt-check'; check.textContent = opt.value === currentVal ? '✓' : ''; const txt = document.createElement('span'); txt.style.cssText = 'flex: 1;'; txt.textContent = opt.label; const demoEl = makeDemoEl(opt.value); btn.appendChild(check); btn.appendChild(txt); btn.appendChild(demoEl); btn.addEventListener('click', (e) => { e.stopPropagation(); if (featureId) markFeatureSeen(featureId); onSelect(opt.value); val.textContent = opt.label; picker.querySelectorAll('.tm-sp-picker-opt').forEach(b => { b.classList.remove('active'); b.querySelector('.tm-sp-opt-check').textContent = ''; }); btn.classList.add('active'); check.textContent = '✓'; picker.classList.remove('open'); arrow.style.transform = ''; }); btn.addEventListener('mouseenter', () => { const d = btn.querySelector('.tm-fb-demo'); if (!d) return; d.classList.remove('tm-fb-playing'); void d.offsetWidth; d.classList.add('tm-fb-playing'); }); picker.appendChild(btn); }); row.addEventListener('click', (e) => { e.stopPropagation(); const isOpen = picker.classList.toggle('open'); arrow.style.transform = isOpen ? 'rotate(90deg)' : ''; if (featureId && isOpen) markFeatureSeen(featureId); if (isOpen) { picker.querySelectorAll('.tm-fb-demo').forEach(d => { d.classList.remove('tm-fb-playing'); void d.offsetWidth; d.classList.add('tm-fb-playing'); }); } }); wrap.addEventListener('click', e => e.stopPropagation()); wrap.appendChild(row); wrap.appendChild(picker); return wrap; }; const makeSliderRow = (label, value, min, max, step, unit, onChange, onCommit, featureId = null) => { const wrap = document.createElement('div'); wrap.className = 'tm-sp-slider-row'; const hdr = document.createElement('div'); hdr.className = 'tm-sp-slider-header'; const lbl = document.createElement('span'); lbl.className = 'tm-sp-slider-label'; lbl.textContent = label; const valDisplay = document.createElement('span'); valDisplay.className = 'tm-sp-slider-value'; valDisplay.textContent = value + ' ' + unit; if (featureId && isFeatureNew(featureId)) { const badge = document.createElement('span'); badge.className = 'tm-sp-new-badge'; badge.textContent = 'NEW'; hdr.appendChild(lbl); hdr.appendChild(badge); } else { hdr.appendChild(lbl); } hdr.appendChild(valDisplay); const slider = document.createElement('input'); slider.type = 'range'; slider.className = 'tm-sp-slider'; slider.min = String(min); slider.max = String(max); slider.step = String(step); slider.value = String(value); slider.addEventListener('input', () => { const n = parseInt(slider.value, 10); valDisplay.textContent = n + ' ' + unit; if (onChange) onChange(n); }); slider.addEventListener('change', () => { const n = parseInt(slider.value, 10); if (onCommit) onCommit(n); if (featureId) markFeatureSeen(featureId); }); slider.addEventListener('click', e => e.stopPropagation()); wrap.addEventListener('click', e => e.stopPropagation()); wrap.appendChild(hdr); wrap.appendChild(slider); return wrap; }; const showDockSpotlight = () => { if (GM_getValue('app_dock_spotlight_done', false)) return; GM_setValue('app_dock_spotlight_done', true); if (document.getElementById('tm-dock-spotlight')) return; const histPanel = document.getElementById('tm-hist-panel'); if (!histPanel) return; const trigL = histPanel.querySelector('.tm-dock-trigger.left'); const trigR = histPanel.querySelector('.tm-dock-trigger.right'); if (!trigL && !trigR) return; const overlay = document.createElement('div'); overlay.id = 'tm-dock-spotlight'; const canvas = document.createElement('canvas'); canvas.id = 'tm-dock-spotlight-canvas'; canvas.width = window.innerWidth; canvas.height = window.innerHeight; const ctx = canvas.getContext('2d'); ctx.fillStyle = 'rgba(0,0,0,0.62)'; ctx.fillRect(0, 0, canvas.width, canvas.height); const pr = histPanel.getBoundingClientRect(); ctx.clearRect(pr.left - 4, pr.top - 4, pr.width + 8, pr.height + 8); overlay.appendChild(canvas); const addHighlight = (el, labelText, labelSide) => { if (!el) return; const r = el.getBoundingClientRect(); const pad = 6; const box = document.createElement('div'); box.className = 'tm-dock-spotlight-step'; box.style.cssText = [ 'left:' + (r.left - pad) + 'px', 'top:' + (r.top - pad) + 'px', 'width:' + (r.width + pad * 2) + 'px', 'height:' + (r.height + pad * 2) + 'px', ].join(';'); overlay.appendChild(box); const lbl = document.createElement('div'); lbl.className = 'tm-dock-spotlight-label'; lbl.textContent = labelText; lbl.style.cssText = labelSide === 'left' ? 'left:' + (r.right + 10) + 'px; top:' + (r.top + r.height/2 - 12) + 'px;' : 'right:' + (window.innerWidth - r.left + 10) + 'px; top:' + (r.top + r.height/2 - 12) + 'px;'; overlay.appendChild(lbl); }; addHighlight(trigL, '① Click to dock left →', 'left'); addHighlight(trigR, '← Click to dock right ②', 'right'); const dismissBtn = document.createElement('button'); dismissBtn.id = 'tm-dock-spotlight-dismiss'; dismissBtn.textContent = 'Got it!'; dismissBtn.addEventListener('click', (e) => { e.stopPropagation(); overlay.remove(); }); overlay.appendChild(dismissBtn); overlay.addEventListener('click', (e) => { e.stopPropagation(); if (e.target === overlay || e.target === canvas) overlay.remove(); }); document.body.appendChild(overlay); }; const grpLink = makeGroup('🔗 Link', true); const clickVal = clickCustom ? clickDomain : 'x.com (default)'; const clickLabel = T.menu_domain_click ? T.menu_domain_click.replace(/^🔗\s*/, '') : 'Single-Click Domain'; grpLink.append(makeRow(clickLabel, clickVal, () => { if (!clickCustom) { showDomainPickerModal(KEY_LINK_DOMAIN_CLICK, dom => { GM_setValue(KEY_CLICK_MODE_CUSTOM, true); showToast(T.toast_domain_click + dom); registerMenus(); buildContent(); }); } else { GM_setValue(KEY_CLICK_MODE_CUSTOM, false); showToast(T.toast_domain_click + 'x.com'); registerMenus(); buildContent(); } })); const prefixLabel = T.menu_prefix ? T.menu_prefix.replace(/^⚙️\s*/, '') : 'Discord Prefix'; grpLink.append(makeRow(prefixLabel, prefix || '(empty)', () => { const newPrefix = prompt(T.prompt_prefix, prefix); if (newPrefix !== null) { GM_setValue(KEY_PREFIX_TEXT, newPrefix); showToast(T.toast_prefix + (newPrefix || '(empty)')); registerMenus(); buildContent(); } })); const grpMedia = makeGroup('🎞 Media', true); const fbOpts = [ { value: 'toast', label: T.status_feedback_toast || 'Toast' }, { value: 'icon', label: T.status_feedback_icon || 'Icon Only' }, ]; const fbLabel = T.menu_feedback_style ? T.menu_feedback_style.replace(/^🔔\s*/, '') : 'Feedback Style'; grpMedia.append(makeFeedbackPickerRow(fbLabel, fbOpts, _fbStyle, (newFb) => { GM_setValue(KEY_FEEDBACK_STYLE, newFb); const chosen = fbOpts.find(o => o.value === newFb); showToast((T.toast_feedback_style || '🔔 Feedback Style → ') + (chosen ? chosen.label : newFb)); buildContent(); }, 'sp_feedback_picker')); const fmtOpts = [ { value: 'asian', label: (T.status_date_asian || 'Asian (YYYY.MM.DD)') }, { value: 'western', label: (T.status_date_western || 'Western (DD.MM.YYYY)') }, ]; const fmtLabel = T.menu_date_format ? T.menu_date_format.replace(/^📅\s*/, '') : 'Date Format'; grpMedia.append(makePickerRow(fmtLabel, fmtOpts, fmt, (newFmt) => { GM_setValue(KEY_DATE_FORMAT, newFmt); _refreshDateFormatCache(); const chosen = fmtOpts.find(o => o.value === newFmt); showToast(T.toast_date_fmt + (chosen ? chosen.label : newFmt)); registerMenus(); buildContent(); }, 'sp_date_picker')); const langLabel = T.menu_lang.replace(/^🌐\s*/, '').replace(/\s*\(Change Language\)/i, '').trim(); grpMedia.append(makeRow('🌐 ' + langLabel, T.langName, () => { showLangPickerModal(); })); const grpGroups = makeGroup('⭐ Groups', true); const groupOnDlRow = makeRow( 'Group on Download', () => GM_getValue(KEY_GROUP_ON_DOWNLOAD, false) ? (T.status_on || 'On') : (T.status_off || 'Off'), () => { const next = !GM_getValue(KEY_GROUP_ON_DOWNLOAD, false); GM_setValue(KEY_GROUP_ON_DOWNLOAD, next); showToast('Group on Download → ' + (next ? (T.status_on || 'On') : (T.status_off || 'Off'))); _syncGroupChildrenDisabled(); }, 'sp_group_on_dl' ); grpGroups.append(groupOnDlRow); const _grpCfgRaw = (() => { try { return JSON.parse(GM_getValue(KEY_GROUP_PANEL_CFG, '{}')); } catch(_) { return {}; } })(); const _grpGlowClr = _grpCfgRaw.glowColor || 'multi'; const _grpGlowSz = Number(_grpCfgRaw.glowSize ?? 12); const _grpTxtClr = _grpCfgRaw.textColor || 'white'; const glowColorRow = (() => { const wrap = document.createElement('div'); wrap.style.cssText = 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,.06)'; wrap.addEventListener('click', e => e.stopPropagation()); const labelRow = document.createElement('div'); labelRow.style.cssText = 'display:flex;align-items:center;justify-content:space-between;margin-bottom:7px'; const lbl = document.createElement('span'); lbl.style.cssText = 'font-size:12px;color:rgba(255,255,255,.7)'; lbl.textContent = 'Glow Color'; const multiToggle = document.createElement('button'); multiToggle.type = 'button'; const isMulti = _grpGlowClr === 'multi'; multiToggle.style.cssText = ` padding:2px 8px;border-radius:99px;border:none;font-size:10px; cursor:pointer;font-family:inherit;line-height:1.4; background:${isMulti ? 'rgba(29,155,240,.7)' : 'rgba(255,255,255,.1)'}; color:${isMulti ? '#fff' : 'rgba(255,255,255,.5)'}; transition:background .12s,color .12s; `; multiToggle.textContent = 'Multi'; multiToggle.title = 'Use individual color per group'; multiToggle.addEventListener('click', () => { const cfg = (() => { try { return JSON.parse(GM_getValue(KEY_GROUP_PANEL_CFG, '{}')); } catch(_) { return {}; } })(); const next = cfg.glowColor !== 'multi' ? 'multi' : '#1d9bf0'; cfg.glowColor = next; GM_setValue(KEY_GROUP_PANEL_CFG, JSON.stringify(cfg)); multiToggle.style.background = next === 'multi' ? 'rgba(29,155,240,.7)' : 'rgba(255,255,255,.1)'; multiToggle.style.color = next === 'multi' ? '#fff' : 'rgba(255,255,255,.5)'; showToast('Glow Color → ' + (next === 'multi' ? 'Multi' : next)); }); labelRow.appendChild(lbl); labelRow.appendChild(multiToggle); const colorRow = document.createElement('div'); colorRow.style.cssText = 'display:flex;align-items:center;gap:5px;flex-wrap:wrap'; const presets = ['#1d9bf0','#ffd700','#ff6b6b','#7bed9f','#a29bfe','#ff9f43','#ffffff']; const curHex = (_grpGlowClr !== 'multi' && _grpGlowClr) ? _grpGlowClr : '#1d9bf0'; const colorInput = document.createElement('input'); colorInput.type = 'color'; colorInput.value = curHex; colorInput.style.cssText = ` width:24px;height:24px;border:none;border-radius:50%; padding:0;cursor:pointer;background:transparent; flex-shrink:0;outline:none; `; colorInput.title = 'Custom color'; const saveColor = (hex) => { const cfg = (() => { try { return JSON.parse(GM_getValue(KEY_GROUP_PANEL_CFG, '{}')); } catch(_) { return {}; } })(); cfg.glowColor = hex; GM_setValue(KEY_GROUP_PANEL_CFG, JSON.stringify(cfg)); multiToggle.style.background = 'rgba(255,255,255,.1)'; multiToggle.style.color = 'rgba(255,255,255,.5)'; }; colorInput.addEventListener('input', () => saveColor(colorInput.value)); colorInput.addEventListener('change', () => saveColor(colorInput.value)); presets.forEach(hex => { const swatch = document.createElement('button'); swatch.type = 'button'; swatch.style.cssText = ` width:18px;height:18px;border-radius:50%;border:2px solid ${hex === curHex ? 'rgba(255,255,255,.8)' : 'transparent'}; background:${hex};cursor:pointer;flex-shrink:0;padding:0; transition:border-color .1s,transform .1s; `; swatch.addEventListener('mouseover', () => swatch.style.transform = 'scale(1.2)'); swatch.addEventListener('mouseout', () => swatch.style.transform = ''); swatch.addEventListener('click', () => { colorInput.value = hex; saveColor(hex); colorRow.querySelectorAll('button[data-swatch]').forEach(s => s.style.borderColor = 'transparent'); swatch.style.borderColor = 'rgba(255,255,255,.8)'; }); swatch.dataset.swatch = hex; colorRow.appendChild(swatch); }); colorRow.appendChild(colorInput); wrap.appendChild(labelRow); wrap.appendChild(colorRow); return wrap; })(); grpGroups.append(glowColorRow); grpGroups.append(makeSliderRow( 'Glow Size', _grpGlowSz, 4, 60, 2, 'px', null, (n) => { const cfg = (() => { try { return JSON.parse(GM_getValue(KEY_GROUP_PANEL_CFG, '{}')); } catch(_) { return {}; } })(); cfg.glowSize = n; GM_setValue(KEY_GROUP_PANEL_CFG, JSON.stringify(cfg)); }, 'sp_group_glow_size' )); const labelColorRow = (() => { const wrap = document.createElement('div'); wrap.style.cssText = 'padding:8px 12px;border-bottom:1px solid rgba(255,255,255,.06)'; wrap.addEventListener('click', e => e.stopPropagation()); const topRow = document.createElement('div'); topRow.style.cssText = 'display:flex;align-items:center;justify-content:space-between;margin-bottom:7px'; const lbl = document.createElement('span'); lbl.style.cssText = 'font-size:12px;color:rgba(255,255,255,.7)'; lbl.textContent = 'Label Color'; topRow.appendChild(lbl); const colorRow = document.createElement('div'); colorRow.style.cssText = 'display:flex;align-items:center;gap:5px;flex-wrap:wrap'; const txtPresets = ['#ffffff','#ffd700','#1d9bf0','#aaaaaa','#ff9f43','#7bed9f','#f368e0']; const curTxt = (_grpTxtClr && !['white','yellow','blue','gray'].includes(_grpTxtClr)) ? _grpTxtClr : { white:'#ffffff', yellow:'#ffd700', blue:'#1d9bf0', gray:'#aaaaaa' }[_grpTxtClr] || '#ffffff'; const txtInput = document.createElement('input'); txtInput.type = 'color'; txtInput.value = curTxt; txtInput.style.cssText = 'width:24px;height:24px;border:none;border-radius:50%;padding:0;cursor:pointer;background:transparent;flex-shrink:0;outline:none'; txtInput.title = 'Custom label color'; const saveTxt = (hex) => { const cfg = (() => { try { return JSON.parse(GM_getValue(KEY_GROUP_PANEL_CFG, '{}')); } catch(_) { return {}; } })(); cfg.textColor = hex; GM_setValue(KEY_GROUP_PANEL_CFG, JSON.stringify(cfg)); }; txtInput.addEventListener('input', () => saveTxt(txtInput.value)); txtInput.addEventListener('change', () => saveTxt(txtInput.value)); txtPresets.forEach(hex => { const sw = document.createElement('button'); sw.type = 'button'; sw.style.cssText = ` width:18px;height:18px;border-radius:50%; border:2px solid ${hex === curTxt ? 'rgba(255,255,255,.8)' : 'transparent'}; background:${hex};cursor:pointer;flex-shrink:0;padding:0; transition:border-color .1s,transform .1s; `; sw.addEventListener('mouseover', () => sw.style.transform = 'scale(1.2)'); sw.addEventListener('mouseout', () => sw.style.transform = ''); sw.addEventListener('click', () => { txtInput.value = hex; saveTxt(hex); colorRow.querySelectorAll('button[data-swatch]').forEach(s => s.style.borderColor = 'transparent'); sw.style.borderColor = 'rgba(255,255,255,.8)'; }); sw.dataset.swatch = hex; colorRow.appendChild(sw); }); colorRow.appendChild(txtInput); wrap.appendChild(topRow); wrap.appendChild(colorRow); return wrap; })(); grpGroups.append(labelColorRow); const grpBtnRow = document.createElement('div'); grpBtnRow.style.cssText = 'padding:6px 12px 10px'; grpBtnRow.addEventListener('click', e => e.stopPropagation()); const manageBtn = document.createElement('button'); manageBtn.type = 'button'; manageBtn.textContent = 'Manage Groups'; manageBtn.style.cssText = ` width:100%;padding:8px 0;border-radius:8px; border:1px solid rgba(255,255,255,.15); background:rgba(255,255,255,.06); color:rgba(255,255,255,.75); font-size:12px;font-weight:500;cursor:pointer; font-family:inherit;text-align:center;line-height:1; transition:background .12s,border-color .12s; `; manageBtn.addEventListener('mouseover', () => { manageBtn.style.background='rgba(255,255,255,.12)'; manageBtn.style.borderColor='rgba(255,255,255,.3)'; }); manageBtn.addEventListener('mouseout', () => { manageBtn.style.background='rgba(255,255,255,.06)'; manageBtn.style.borderColor='rgba(255,255,255,.15)'; }); manageBtn.addEventListener('click', (e) => { e.stopPropagation(); showGroupManagerModal(); }); grpBtnRow.appendChild(manageBtn); grpGroups.append(grpBtnRow); const _groupChildren = [glowColorRow, labelColorRow, grpBtnRow]; const _getGroupSliders = () => Array.from(grpGroups.body.querySelectorAll('.tm-sp-slider-row')); const _syncGroupChildrenDisabled = () => { const isOn = GM_getValue(KEY_GROUP_ON_DOWNLOAD, false); [..._groupChildren, ..._getGroupSliders()].forEach(el => { el.classList.toggle('tm-sp-disabled-child', !isOn); }); }; _syncGroupChildrenDisabled(); const HIST_TOOLTIP = 'Hidden feature: The history panel has invisible dock triggers on its left & right edges. Click them to auto-hide the panel to the screen edge!'; const grpHist = makeGroup('🗂 History Panel', false, HIST_TOOLTIP, showDockSpotlight); const dockStyleOpts = [ { value: 'ruler', label: '— Ruler 📏' }, { value: 'ghost', label: '· Ghost' }, { value: 'notch', label: '| Notch' }, ]; grpHist.append(makePickerRow('Dock Style', dockStyleOpts, dockStyle, (next) => { GM_setValue(KEY_DOCK_STYLE, next); showToast('🗂 Dock Style → ' + (dockStyleOpts.find(o => o.value === next)?.label || next)); const curTab = document.getElementById('tm-hist-dock-tab'); if (curTab) { curTab.className = 'style-' + next; curTab.innerHTML = ''; } buildContent(); }, 'sp_dock_picker')); grpHist.append(makeSliderRow( 'Hover Delay', dockHoverDelay, 100, 3000, 50, 'ms', null, (n) => { GM_setValue(KEY_DOCK_HOVER_DELAY, String(n)); showToast('⏱ Hover Delay → ' + n + ' ms'); }, 'sp_slider_controls' )); grpHist.append(makeSliderRow( 'Trigger Distance ◀ Left', dockTriggerL, 20, 300, 5, 'px', null, (n) => { GM_setValue(KEY_DOCK_TRIGGER_L, String(n)); showToast('◀ Trigger → ' + n + ' px'); }, 'sp_slider_controls' )); grpHist.append(makeSliderRow( 'Trigger Distance ▶ Right', dockTriggerR, 20, 300, 5, 'px', null, (n) => { GM_setValue(KEY_DOCK_TRIGGER_R, String(n)); showToast('▶ Trigger → ' + n + ' px'); }, 'sp_slider_controls' )); const helpLabel = T.menu_help ? T.menu_help.replace(/^📖\s*/, '') : 'Help / Manual'; const helpRow = makeRow('📖 ' + helpLabel, '', () => { showHelpModal(); }); helpRow.style.borderTop = `1px solid ${C.border}`; panel.appendChild(helpRow); } buildContent(); gearBtn.addEventListener('click', e => { e.stopPropagation(); const isOpen = wrapper.getAttribute('data-open') === 'true'; wrapper.setAttribute('data-open', String(!isOpen)); if (!isOpen) { gearBtn.querySelector('.tm-gear-notify-dot')?.remove(); } }); document.addEventListener('click', e => { if (wrapper.contains(e.target)) return; if (e.target.closest('#tm-group-modal-overlay')) return; if (e.target.closest('#tm-group-mgr-overlay')) return; if (e.target.closest('.tm-sp-picker')) return; wrapper.setAttribute('data-open', 'false'); }); wrapper.appendChild(histBtn); wrapper.appendChild(gearBtn); wrapper.appendChild(panel); document.body.appendChild(wrapper); } const _downloadedIds = (() => { try { const arr = JSON.parse(GM_getValue(KEY_HISTORY_RECORDS, '[]')); return new Set(arr.map(r => r.tweetId)); } catch (_) { return new Set(); } })(); let _historyUndoBuffer = null; let _historyUndoTimer = null; let _dockSideGlobal = null; let _dockTabElGlobal = null; let _dockHoverTimerGlobal = null; let _dockPeekedGlobal = false; let _dockRetractTimerGlobal = null; let _dockSnapshotGlobal = null; (function _restorePersistedDock() { const persisted = GM_getValue(KEY_DOCK_PERSISTED, ''); if (persisted === 'left' || persisted === 'right') { _dockSideGlobal = persisted; requestAnimationFrame(() => { showHistoryPanel(); }); } })(); function _getTweetIdFromArticle(article) { for (const lk of article.querySelectorAll('a[href*="/status/"]')) { const m = lk.getAttribute('href')?.match(/\/status\/(\d+)/); if (m) return m[1]; } return null; } function _applyHistoryBadge(btn) { if (!btn || btn.querySelector('.tm-hist-badge')) return; const badge = document.createElement('span'); badge.className = 'tm-hist-badge'; badge.style.cssText = ` position: absolute; top: 6px; right: 6px; width: 8px; height: 8px; border-radius: 50%; background: #00ba7c; pointer-events: none; box-shadow: 0 0 0 2px rgba(0,0,0,0.65); animation: tm-pop-bounce 0.35s cubic-bezier(0.175,0.885,0.32,1.275) both; z-index: 10; `; btn.appendChild(badge); } let _pendingGroupRecordId = null; let _pendingStarPipEl = null; window.addEventListener('scroll', () => { if (!_fanOpen) hideStarPip(); }, { passive: true, capture: true }); document.addEventListener('visibilitychange', () => { if (document.hidden) hideStarPip(); }); (() => { const _wrap = fn => function(...args) { const r = fn.apply(this, args); hideStarPip?.(); return r; }; history.pushState = _wrap(history.pushState); history.replaceState = _wrap(history.replaceState); })(); function getGroups() { try { return JSON.parse(GM_getValue(KEY_GROUPS, '[]')); } catch (_) { return []; } } function saveGroups(arr) { GM_setValue(KEY_GROUPS, JSON.stringify(arr)); } function createGroup(name, icon = '📁', glow = 'rgba(80,160,240,.5)') { const groups = getGroups(); const group = { id: 'g_' + Date.now(), name: name.slice(0, 24), icon, glow, createdAt: Date.now(), }; groups.push(group); saveGroups(groups); return group; } function deleteGroup(groupId) { const groups = getGroups().filter(g => g.id !== groupId); saveGroups(groups); try { const records = JSON.parse(GM_getValue(KEY_HISTORY_RECORDS, '[]')); records.forEach(r => { if (r.groupId === groupId) delete r.groupId; }); GM_setValue(KEY_HISTORY_RECORDS, JSON.stringify(records)); } catch (_) {} } function assignGroup(recordId, groupId) { try { const records = JSON.parse(GM_getValue(KEY_HISTORY_RECORDS, '[]')); const rec = records.find(r => r.id === recordId); if (!rec) return; if (groupId === null) { delete rec.groupId; } else { rec.groupId = groupId; } GM_setValue(KEY_HISTORY_RECORDS, JSON.stringify(records)); if (groupId !== null) { GM_setValue('app_group_unread_' + groupId, true); } const existPanel = document.getElementById('tm-hist-panel'); if (existPanel) existPanel.dispatchEvent(new CustomEvent('tm-hist-refresh')); } catch (e) { console.error('[TMGroup] assignGroup error:', e); } } const STAR_AUTO_HIDE_SEC = 5; const STAR_FLOAT_CLS = ['tm-float-a','tm-float-b','tm-float-c','tm-float-d','tm-float-e']; const STAR_GLOW_COLORS = [ 'rgba(80,200,180,.28)', 'rgba(160,100,240,.28)', 'rgba(240,160,80,.28)', 'rgba(240,100,100,.28)', 'rgba(80,160,240,.28)', 'rgba(200,200,80,.28)', 'rgba(180,80,200,.28)', 'rgba(100,200,120,.28)', 'rgba(240,120,160,.28)', 'rgba(220,160,60,.28)', ]; let _starAutoHideTimer = null; let _fanNodes = []; let _fanOpen = false; function popStarPip(mediaBtnEl) { const pip = document.getElementById('tm-star-pip'); if (!pip) return; if (mediaBtnEl) { _pendingStarPipEl = pip; const r = mediaBtnEl.getBoundingClientRect(); pip.style.left = (r.right + 2) + 'px'; pip.style.top = (r.top - 9) + 'px'; } pip.classList.add('tm-popped'); clearTimeout(_starAutoHideTimer); _starAutoHideTimer = setTimeout(() => { if (!_fanOpen) hideStarPip(); }, STAR_AUTO_HIDE_SEC * 1000); } function hideStarPip() { const pip = document.getElementById('tm-star-pip'); if (!pip) return; pip.classList.remove('tm-popped'); clearTimeout(_starAutoHideTimer); _pendingStarPipEl = null; } function onStarPipClick() { if (_fanOpen) closeGroupFan(); else openGroupFan(); } function _layerCfg(n) { if (n <= 5) return [{c:n, r:48}]; if (n <= 12) return [{c:Math.min(5,n), r:46}, {c:n-Math.min(5,n), r:88}]; const l1=5, l2=Math.min(7,n-5); return [{c:l1,r:46},{c:l2,r:88},{c:n-l1-l2,r:128}]; } function _fanPositions(n, cx, cy) { const pos = []; _layerCfg(n).forEach(({c, r}) => { const startDeg = -55, endDeg = 55; const range = endDeg - startDeg; for (let i = 0; i < c; i++) { const deg = c === 1 ? 0 : startDeg + range * i / (c - 1); const rad = deg * Math.PI / 180; pos.push({ x: cx + r * Math.cos(rad) - 16, y: cy + r * Math.sin(rad) - 16 }); } }); return pos; } function _spawnRipple(cx, cy) { const r = document.createElement('div'); r.className = 'tm-ripple-ring'; const sz = 18; r.style.cssText = `width:${sz}px;height:${sz}px;left:${cx}px;top:${cy}px`; document.body.appendChild(r); setTimeout(() => r.remove(), 560); } function _getStarPipPos() { const pip = document.getElementById('tm-star-pip'); if (!pip) return { cx: 0, cy: 0 }; const r = pip.getBoundingClientRect(); return { cx: r.left + r.width / 2, cy: r.top + r.height / 2 }; } function _buildFanDom() { _fanNodes.forEach(n => n.remove()); _fanNodes = []; const _cfg = (() => { try { return JSON.parse(GM_getValue(KEY_GROUP_PANEL_CFG, '{}')); } catch(_) { return {}; } })(); const _glowClr = _cfg.glowColor || 'multi'; const _glowSz = Number(_cfg.glowSize ?? 12); const _txtClrMap = { white: 'rgba(255,255,255,.7)', yellow: 'rgba(255,220,60,.85)', blue: 'rgba(29,155,240,.9)', gray: 'rgba(180,180,180,.65)' }; const _txtClr = _cfg.textColor ? (_txtClrMap[_cfg.textColor] || _cfg.textColor) : _txtClrMap.white; const _glowPx = Math.max(4, Math.min(60, _glowSz)); const groups = getGroups(); groups.forEach((g, i) => { const baseGlow = _glowClr === 'multi' ? (g.glow || STAR_GLOW_COLORS[i % STAR_GLOW_COLORS.length]) : _glowClr + '85'; const half = Math.round(_glowPx / 2); const ic = _GROUP_SVG_ICONS.find(x => x.id === g.icon) || null; const iconHtml = ic ? `
${ic.svg}
` : `${g.icon}`; const el = document.createElement('div'); el.className = 'tm-fan-node ' + STAR_FLOAT_CLS[i % STAR_FLOAT_CLS.length]; el.style.cssText = 'left:-300px;top:-300px;opacity:0'; el.innerHTML = `
${iconHtml}
${g.name}`; el.addEventListener('click', () => _onFanGroupClick(g.id, g.name)); document.body.appendChild(el); _fanNodes.push(el); }); } function _countGroupRecords(groupId) { try { const records = JSON.parse(GM_getValue(KEY_HISTORY_RECORDS, '[]')); return records.filter(r => r.groupId === groupId).length; } catch (_) { return 0; } } let _starEscaping = false; function _runStarEscapeAnim(callback) { const pip = document.getElementById('tm-star-pip'); if (!pip || _starEscaping) { if (!pip) callback?.(); return; } _starEscaping = true; clearTimeout(_starAutoHideTimer); pip.classList.add('tm-escaping'); const rect = pip.getBoundingClientRect(); const directions = [ { dx: 220, dy: -180, rot: 340 }, { dx: 180, dy: 200, rot: -320 }, { dx: 260, dy: -60, rot: 380 }, { dx: 200, dy: 140, rot: -360 }, ]; const dir = directions[Math.floor(Math.random() * directions.length)]; pip.style.transition = 'transform .12s cubic-bezier(.4,0,.2,1)'; pip.style.transform = 'scale(0.7) translate(-6px, 2px)'; setTimeout(() => { pip.style.transition = 'transform .42s cubic-bezier(.2,0,.8,1), opacity .38s ease .08s'; pip.style.transform = `translate(${dir.dx}px, ${dir.dy}px) rotate(${dir.rot}deg) scale(0.15)`; pip.style.opacity = '0'; }, 130); setTimeout(() => { pip.style.transition = 'none'; pip.style.transform = ''; pip.style.opacity = ''; pip.classList.remove('tm-popped', 'tm-escaping'); _pendingStarPipEl = null; _starEscaping = false; callback?.(); }, 600); } function openGroupFan() { if (!getGroups().length) { _runStarEscapeAnim(() => showGroupCreateModal()); return; } _fanOpen = true; _buildFanDom(); const { cx, cy } = _getStarPipPos(); const groups = getGroups(); const positions = _fanPositions(groups.length, cx, cy); const pip = document.getElementById('tm-star-pip'); if (pip) pip.classList.add('tm-lit'); _spawnRipple(cx, cy); setTimeout(() => _spawnRipple(cx, cy), 110); _fanNodes.forEach((node, i) => { const pos = positions[i]; node.style.cssText = `left:${cx-16}px;top:${cy-16}px;opacity:0;transition:none`; node.classList.remove('tm-spawned'); const delay = 20 + i * 46; setTimeout(() => { node.style.transition = `opacity .26s ease ${delay}ms, left .42s cubic-bezier(.34,1.56,.64,1) ${delay}ms, top .42s cubic-bezier(.34,1.56,.64,1) ${delay}ms`; node.style.left = pos.x + 'px'; node.style.top = pos.y + 'px'; node.style.opacity = '1'; setTimeout(() => node.classList.add('tm-spawned'), delay + 380); }, 10); }); } function closeGroupFan() { _fanOpen = false; const { cx, cy } = _getStarPipPos(); const pip = document.getElementById('tm-star-pip'); if (pip) pip.classList.remove('tm-lit'); _fanNodes.forEach(node => { node.classList.remove('tm-spawned'); node.style.transition = 'opacity .15s ease, left .22s cubic-bezier(.6,0,.2,1), top .22s cubic-bezier(.6,0,.2,1)'; node.style.left = (cx - 16) + 'px'; node.style.top = (cy - 16) + 'px'; node.style.opacity = '0'; }); setTimeout(() => { _fanNodes.forEach(n => n.remove()); _fanNodes = []; }, 300); } function _onFanGroupClick(groupId, groupName) { assignGroup(_pendingGroupRecordId, groupId); _pendingGroupRecordId = null; showToast(`⭐ → ${groupName}`); closeGroupFan(); const pip = document.getElementById('tm-star-pip'); if (pip) { } } document.addEventListener('click', e => { if (!_fanOpen) return; if (e.target.closest('.tm-fan-node') || e.target.closest('#tm-star-pip') || e.target.closest('.tm-group-modal-overlay')) return; closeGroupFan(); }, true); const _GROUP_SVG_ICONS = [ { id:'travel', label:'Travel', color:'#4dd0e1', svg:'' }, { id:'art', label:'Art', color:'#ce93d8', svg:'' }, { id:'photo', label:'Photo', color:'#ffb74d', svg:'' }, { id:'music', label:'Music', color:'#f48fb1', svg:'' }, { id:'food', label:'Food', color:'#a5d6a7', svg:'' }, { id:'game', label:'Game', color:'#80cbc4', svg:'' }, { id:'nature', label:'Nature', color:'#c5e1a5', svg:'' }, { id:'book', label:'Book', color:'#ffcc80', svg:'' }, { id:'star', label:'Fav', color:'#fff176', svg:'' }, { id:'work', label:'Work', color:'#b0bec5', svg:'' }, ]; let _selectedIconId = _GROUP_SVG_ICONS[0].id; function showGroupCreateModal() { const old = document.getElementById('tm-group-modal-overlay'); if (old) old.remove(); _selectedIconId = _GROUP_SVG_ICONS[0].id; const overlay = document.createElement('div'); overlay.id = 'tm-group-modal-overlay'; overlay.className = 'tm-group-modal-overlay'; const box = document.createElement('div'); box.className = 'tm-group-modal-box'; const title = document.createElement('div'); title.className = 'tm-group-modal-title'; title.textContent = 'New Group'; const input = document.createElement('input'); input.className = 'tm-group-modal-input'; input.placeholder = 'Group name…'; input.maxLength = 24; const iconLabel = document.createElement('div'); iconLabel.style.cssText = 'font-size:10px;color:rgba(255,255,255,.4);margin:10px 0 6px;letter-spacing:.06em;text-transform:uppercase'; iconLabel.textContent = 'Icon'; const iconGrid = document.createElement('div'); iconGrid.style.cssText = 'display:grid;grid-template-columns:repeat(5,1fr);gap:5px'; _GROUP_SVG_ICONS.forEach(ic => { const cell = document.createElement('button'); cell.type = 'button'; cell.dataset.iconId = ic.id; const isFirst = ic.id === _selectedIconId; cell.style.cssText = ` display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px; padding:6px 2px;border-radius:8px;border:1.5px solid ${isFirst ? ic.color : 'transparent'}; background:${isFirst ? 'rgba(255,255,255,.07)' : 'transparent'}; cursor:pointer;transition:all .12s;font-family:inherit; `; const iconWrap = document.createElement('div'); iconWrap.style.cssText = `width:20px;height:20px;color:${ic.color};flex-shrink:0`; iconWrap.innerHTML = ic.svg; const iconLbl = document.createElement('span'); iconLbl.style.cssText = 'font-size:8px;color:rgba(255,255,255,.45);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:40px'; iconLbl.textContent = ic.label; cell.appendChild(iconWrap); cell.appendChild(iconLbl); cell.addEventListener('click', () => { _selectedIconId = ic.id; iconGrid.querySelectorAll('button').forEach(b => { const bid = b.dataset.iconId; const bic = _GROUP_SVG_ICONS.find(x => x.id === bid); b.style.border = `1.5px solid ${bid === ic.id ? bic.color : 'transparent'}`; b.style.background = bid === ic.id ? 'rgba(255,255,255,.07)' : 'transparent'; }); }); iconGrid.appendChild(cell); }); const btns = document.createElement('div'); btns.style.cssText = 'display:flex;gap:8px;margin-top:16px'; const skipBtn = document.createElement('button'); skipBtn.type = 'button'; skipBtn.style.cssText = ` flex:1;padding:9px 0;border-radius:8px; border:1px solid rgba(255,255,255,.15); background:rgba(255,255,255,.06); color:rgba(255,255,255,.6); font-size:13px;font-weight:500;cursor:pointer; font-family:inherit;text-align:center;line-height:1; transition:background .12s; `; skipBtn.textContent = 'Skip'; skipBtn.addEventListener('mouseover', () => skipBtn.style.background = 'rgba(255,255,255,.12)'); skipBtn.addEventListener('mouseout', () => skipBtn.style.background = 'rgba(255,255,255,.06)'); skipBtn.addEventListener('click', () => _closeGroupModal(false, null)); const confirmBtn = document.createElement('button'); confirmBtn.type = 'button'; confirmBtn.style.cssText = ` flex:1;padding:9px 0;border-radius:8px; border:none;background:#1d9bf0; color:#fff;font-size:13px;font-weight:500;cursor:pointer; font-family:inherit;text-align:center;line-height:1; transition:background .12s; `; confirmBtn.textContent = 'Create'; confirmBtn.addEventListener('mouseover', () => confirmBtn.style.background = '#1a8cd8'); confirmBtn.addEventListener('mouseout', () => confirmBtn.style.background = '#1d9bf0'); confirmBtn.addEventListener('click', () => _closeGroupModal(true, input.value.trim())); btns.appendChild(skipBtn); btns.appendChild(confirmBtn); box.appendChild(title); box.appendChild(input); box.appendChild(iconLabel); box.appendChild(iconGrid); box.appendChild(btns); overlay.appendChild(box); document.body.appendChild(overlay); input.addEventListener('keydown', e => { if (e.key === 'Enter') _closeGroupModal(true, input.value.trim()); if (e.key === 'Escape') _closeGroupModal(false, null); }); overlay.addEventListener('click', e => { if (e.target === overlay) _closeGroupModal(false, null); }); requestAnimationFrame(() => { overlay.classList.add('tm-show'); setTimeout(() => input.focus(), 200); }); } function _closeGroupModal(confirm, name) { const overlay = document.getElementById('tm-group-modal-overlay'); if (!overlay) return; overlay.classList.remove('tm-show'); setTimeout(() => overlay.remove(), 220); if (!confirm || !name) { hideStarPip(); return; } const ic = _GROUP_SVG_ICONS.find(x => x.id === _selectedIconId) || _GROUP_SVG_ICONS[0]; const glowIdx = getGroups().length % STAR_GLOW_COLORS.length; const group = createGroup(name, ic.id, STAR_GLOW_COLORS[glowIdx]); if (_pendingGroupRecordId !== null) { assignGroup(_pendingGroupRecordId, group.id); _pendingGroupRecordId = null; } showToast(`⭐ Created「${ic.label} · ${name}」`); setTimeout(() => { if (document.getElementById('tm-star-pip')?.classList.contains('tm-popped')) { openGroupFan(); } }, 300); } function showGroupManagerModal() { const old = document.getElementById('tm-group-mgr-overlay'); if (old) old.remove(); const overlay = document.createElement('div'); overlay.id = 'tm-group-mgr-overlay'; overlay.className = 'tm-group-modal-overlay'; const box = document.createElement('div'); box.className = 'tm-group-modal-box'; box.style.cssText += ';width:280px;max-width:92vw'; const titleRow = document.createElement('div'); titleRow.style.cssText = 'display:flex;align-items:center;justify-content:space-between;margin-bottom:14px'; const title = document.createElement('div'); title.className = 'tm-group-modal-title'; title.style.margin = '0'; title.textContent = 'Manage Groups'; const newBtn = document.createElement('button'); newBtn.type = 'button'; newBtn.textContent = '+ New'; newBtn.style.cssText = ` padding:5px 10px;border-radius:7px;border:none; background:#1d9bf0;color:#fff; font-size:11px;font-weight:600;cursor:pointer; font-family:inherit;line-height:1;transition:background .12s; `; newBtn.addEventListener('mouseover', () => newBtn.style.background = '#1a8cd8'); newBtn.addEventListener('mouseout', () => newBtn.style.background = '#1d9bf0'); newBtn.addEventListener('click', () => { overlay.classList.remove('tm-show'); setTimeout(() => { overlay.remove(); showGroupCreateModal(); }, 180); }); titleRow.appendChild(title); titleRow.appendChild(newBtn); const list = document.createElement('div'); list.style.cssText = 'max-height:300px;overflow-y:auto;display:flex;flex-direction:column;gap:6px;margin-bottom:14px;scrollbar-width:thin'; let _openIconPicker = null; function buildIconPicker(g, iconWrap, onPick) { const picker = document.createElement('div'); picker.style.cssText = ` display:grid;grid-template-columns:repeat(5,1fr);gap:4px; padding:8px;background:rgba(20,25,40,.98); border-radius:8px;border:1px solid rgba(255,255,255,.1); margin-top:4px; `; _GROUP_SVG_ICONS.forEach(ic => { const cell = document.createElement('button'); cell.type = 'button'; cell.style.cssText = ` display:flex;flex-direction:column;align-items:center;gap:2px; padding:5px 2px;border-radius:6px;border:1.5px solid ${ic.id === g.icon ? ic.color : 'transparent'}; background:${ic.id === g.icon ? 'rgba(255,255,255,.07)' : 'transparent'}; cursor:pointer;transition:all .1s;font-family:inherit; `; const svg = document.createElement('div'); svg.style.cssText = `width:16px;height:16px;color:${ic.color}`; svg.innerHTML = ic.svg; const lbl = document.createElement('span'); lbl.style.cssText = 'font-size:7px;color:rgba(255,255,255,.4);white-space:nowrap;overflow:hidden;max-width:38px;text-overflow:ellipsis'; lbl.textContent = ic.label; cell.appendChild(svg); cell.appendChild(lbl); cell.addEventListener('click', () => { onPick(ic); picker.remove(); _openIconPicker = null; }); picker.appendChild(cell); }); return picker; } function rebuildList() { list.innerHTML = ''; _openIconPicker = null; const groups = getGroups(); if (!groups.length) { const empty = document.createElement('div'); empty.style.cssText = 'font-size:12px;color:rgba(255,255,255,.35);text-align:center;padding:16px 0'; empty.textContent = 'No groups yet — click + New to create one'; list.appendChild(empty); return; } groups.forEach(g => { const wrapper = document.createElement('div'); wrapper.style.cssText = 'display:flex;flex-direction:column;gap:0'; const row = document.createElement('div'); row.style.cssText = 'display:flex;align-items:center;gap:8px;background:rgba(255,255,255,.05);border-radius:8px;padding:7px 8px'; const iconWrap = document.createElement('button'); iconWrap.type = 'button'; iconWrap.title = 'Change icon'; iconWrap.style.cssText = ` width:22px;height:22px;flex-shrink:0;border:none; background:transparent;cursor:pointer;padding:0;border-radius:4px; transition:background .12s;display:flex;align-items:center;justify-content:center; `; const ic = _GROUP_SVG_ICONS.find(x => x.id === g.icon); iconWrap.style.color = ic?.color || 'rgba(255,255,255,.5)'; iconWrap.innerHTML = ic?.svg || `${g.icon||'📁'}`; iconWrap.addEventListener('mouseover', () => iconWrap.style.background = 'rgba(255,255,255,.1)'); iconWrap.addEventListener('mouseout', () => iconWrap.style.background = 'transparent'); iconWrap.addEventListener('click', () => { if (_openIconPicker) { _openIconPicker.remove(); _openIconPicker = null; } const picker = buildIconPicker(g, iconWrap, (newIc) => { const arr = getGroups(); const idx = arr.findIndex(x => x.id === g.id); if (idx > -1) { arr[idx].icon = newIc.id; saveGroups(arr); g.icon = newIc.id; } iconWrap.style.color = newIc.color; iconWrap.innerHTML = newIc.svg; showToast(`Icon → ${newIc.label}`); }); wrapper.appendChild(picker); _openIconPicker = picker; }); const nameInput = document.createElement('input'); nameInput.value = g.name; nameInput.maxLength = 24; nameInput.style.cssText = 'flex:1;background:transparent;border:none;border-bottom:1px solid rgba(255,255,255,.08);outline:none;color:rgba(255,255,255,.88);font-size:12px;font-family:inherit;padding:2px 0;transition:border-color .12s'; nameInput.addEventListener('focus', () => nameInput.style.borderColor = 'rgba(29,155,240,.6)'); nameInput.addEventListener('blur', () => nameInput.style.borderColor = 'rgba(255,255,255,.08)'); nameInput.addEventListener('change', () => { const n = nameInput.value.trim(); if (!n) { nameInput.value = g.name; return; } const arr = getGroups(); const idx = arr.findIndex(x => x.id === g.id); if (idx > -1) { arr[idx].name = n; saveGroups(arr); g.name = n; showToast(`Renamed → ${n}`); } }); const cnt = _countGroupRecords(g.id); const cntBadge = document.createElement('span'); cntBadge.style.cssText = 'font-size:10px;color:rgba(255,255,255,.3);flex-shrink:0;white-space:nowrap'; cntBadge.textContent = cnt ? `${cnt} items` : ''; const delBtn = document.createElement('button'); delBtn.type = 'button'; delBtn.title = 'Delete group'; delBtn.style.cssText = 'background:transparent;border:none;color:rgba(255,80,80,.55);font-size:14px;cursor:pointer;flex-shrink:0;padding:0 2px;line-height:1;transition:color .12s'; delBtn.innerHTML = ''; delBtn.addEventListener('mouseover', () => delBtn.style.color = 'rgba(255,80,80,.9)'); delBtn.addEventListener('mouseout', () => delBtn.style.color = 'rgba(255,80,80,.55)'); delBtn.addEventListener('click', () => { if (cnt > 0 && !confirm(`Delete「${g.name}」? This will ungroup ${cnt} item(s).`)) return; if (cnt === 0 && !confirm(`Delete「${g.name}」?`)) return; deleteGroup(g.id); rebuildList(); showToast(`Deleted「${g.name}」`); }); row.appendChild(iconWrap); row.appendChild(nameInput); row.appendChild(cntBadge); row.appendChild(delBtn); wrapper.appendChild(row); list.appendChild(wrapper); }); } rebuildList(); const doneBtn = document.createElement('button'); doneBtn.type = 'button'; doneBtn.style.cssText = ` width:100%;padding:9px 0;border-radius:8px;border:none; background:#1d9bf0;color:#fff; font-size:13px;font-weight:500;cursor:pointer; font-family:inherit;text-align:center;line-height:1; transition:background .12s; `; doneBtn.textContent = 'Done'; doneBtn.addEventListener('mouseover', () => doneBtn.style.background = '#1a8cd8'); doneBtn.addEventListener('mouseout', () => doneBtn.style.background = '#1d9bf0'); doneBtn.addEventListener('click', () => { overlay.classList.remove('tm-show'); setTimeout(() => overlay.remove(), 220); }); box.appendChild(titleRow); box.appendChild(list); box.appendChild(doneBtn); overlay.appendChild(box); document.body.appendChild(overlay); overlay.addEventListener('click', e => { if (e.target === overlay) { overlay.classList.remove('tm-show'); setTimeout(() => overlay.remove(), 220); } }); requestAnimationFrame(() => overlay.classList.add('tm-show')); } function recordHistory(info, urls, mediaBtn) { try { const thumbUrls = urls.filter(u => !u.includes('.mp4')); const hasVideo = urls.some(u => u.includes('.mp4')); if (thumbUrls.length === 0 && info.videoThumb) { thumbUrls.push(info.videoThumb); } const _now = new Date(); const _yy = _now.getFullYear(); const _mm = String(_now.getMonth() + 1).padStart(2, '0'); const yyyymm = `${_yy}.${_mm}`; const record = { id: Date.now(), ts: Date.now(), yyyymm, tweetId: info.id, tweetUrl: `https://x.com/${info.screenName}/status/${info.id}`, tweetDate: info.date, downloadDate: `${_yy}-${_mm}-${String(_now.getDate()).padStart(2,'0')}`, screenName: info.screenName, displayName: info.displayName, text: (info.text || '').slice(0, 80), thumbUrls, hasVideo, count: urls.length, }; let records = []; try { records = JSON.parse(GM_getValue(KEY_HISTORY_RECORDS, '[]')); } catch (_) {} const _oldRecord = records.find(r => r.tweetId === info.id); if (_oldRecord?.favorited) record.favorited = true; records = records.filter(r => r.tweetId !== info.id); records.unshift(record); if (records.length > HISTORY_MAX_RECORDS) { const _overflow = records.slice(HISTORY_MAX_RECORDS).filter(r => r.favorited); records = [...records.slice(0, HISTORY_MAX_RECORDS), ..._overflow]; } GM_setValue(KEY_HISTORY_RECORDS, JSON.stringify(records)); if (GM_getValue(KEY_GROUP_ON_DOWNLOAD, false)) { _pendingGroupRecordId = record.id; setTimeout(() => popStarPip(mediaBtn || null), 80); } _downloadedIds.add(info.id); document.querySelectorAll(`article a[href*="/status/${info.id}"]`).forEach(a => { const art = a.closest('article'); if (art) { const targetBtn = art.querySelector('.force-media-copy-btn'); if (targetBtn) _applyHistoryBadge(targetBtn); } }); const existPanel = document.getElementById('tm-hist-panel'); if (existPanel) existPanel.dispatchEvent(new CustomEvent('tm-hist-refresh')); } catch (e) { console.error('[TMHist] recordHistory error:', e); } } function showHistoryPanel() { const existing = document.getElementById('tm-hist-panel'); if (existing) { if (_dockSideGlobal) { existing.dispatchEvent(new CustomEvent('tm-hist-toggle-peek')); return; } if (typeof existing._tmCleanup === 'function') existing._tmCleanup(); existing.remove(); _cleanZoom(); return; } const dark = window.matchMedia('(prefers-color-scheme: dark)').matches; const C = dark ? { bg: '#16202b', header: '#1e2732', text: '#e7e9ea', sub: '#8b98a5', border: '#2f3336', rowHover: '#1e2732', inputBg: '#1e2732', groupHdr: '#2f3336', groupTxt: '#8b98a5', thumbBg: '#1e2732', danger: '#e0245e', dangerHover: '#c01e4e', badgeNew: '#1d9bf0', scrollbar: '#2f3336', } : { bg: '#ffffff', header: '#f7f9f9', text: '#0f1419', sub: '#536471', border: '#eff3f4', rowHover: '#f7f9f9', inputBg: '#f7f9f9', groupHdr: '#f7f9f9', groupTxt: '#536471', thumbBg: '#f7f9f9', danger: '#e0245e', dangerHover: '#c01e4e', badgeNew: '#1d9bf0', scrollbar: '#eff3f4', }; let pos = { x: Math.max(8, window.innerWidth - 408), y: 60, w: 390, h: 540, }; try { const saved = JSON.parse(GM_getValue(KEY_HISTORY_PANEL_POS, 'null')); if (saved && typeof saved.x === 'number') { pos = { x: Math.min(saved.x, window.innerWidth - 300), y: Math.min(saved.y, window.innerHeight - 200), w: Math.max(300, Math.min(saved.w || 390, 680)), h: Math.max(280, Math.min(saved.h || 540, window.innerHeight - 80)), }; } } catch (_) {} let viewMode = GM_getValue(KEY_HISTORY_VIEW_MODE, 'list'); let editMode = false; let query = ''; let activeGroupId = null; const selectedIds = new Set(); const collapsedGroups = new Set(); let anchorIdx = -1; let histStyleEl = document.getElementById('tm-hist-style'); if (!histStyleEl) { histStyleEl = document.createElement('style'); histStyleEl.id = 'tm-hist-style'; document.head.appendChild(histStyleEl); } histStyleEl.textContent = ` #tm-hist-panel { position: fixed; z-index: 999980; font-family: system-ui, -apple-system, sans-serif; display: flex; flex-direction: column; background: ${C.bg}; border: 1px solid ${C.border}; border-radius: 14px; box-shadow: 0 12px 40px rgba(0,0,0,0.22), 0 2px 8px rgba(0,0,0,0.10); overflow: hidden; min-width: 300px; min-height: 280px; } #tm-hist-titlebar { display: flex; align-items: center; gap: 6px; padding: 9px 12px; cursor: grab; background: ${C.header}; border-bottom: 1px solid ${C.border}; user-select: none; flex-shrink: 0; } #tm-hist-titlebar:active { cursor: grabbing; } .tm-hist-title { font-size: 13px; font-weight: 700; color: ${C.text}; flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .tm-hist-count-badge { font-size: 10px; padding: 2px 7px; border-radius: 99px; background: ${C.groupHdr}; color: ${C.sub}; white-space: nowrap; flex-shrink: 0; } .tm-hist-icon-btn { width: 26px; height: 26px; border-radius: 6px; border: none; background: transparent; cursor: pointer; display: flex; align-items: center; justify-content: center; color: ${C.sub}; transition: background 0.1s, color 0.1s; flex-shrink: 0; } .tm-hist-icon-btn:hover { background: ${C.rowHover}; color: ${C.text}; } .tm-hist-icon-btn svg { width: 14px; height: 14px; pointer-events: none; } .tm-hist-icon-btn.active { color: #1d9bf0; } .tm-hist-searchbar { padding: 7px 12px; border-bottom: 1px solid ${C.border}; flex-shrink: 0; } #tm-hist-search { width: 100%; padding: 5px 10px; border-radius: 99px; border: 1px solid ${C.border}; background: ${C.inputBg}; color: ${C.text}; font-size: 12px; outline: none; box-sizing: border-box; } #tm-hist-search::placeholder { color: ${C.sub}; } #tm-hist-body { flex: 1; overflow-y: auto; overflow-x: hidden; scrollbar-width: thin; scrollbar-color: ${C.scrollbar} transparent; } .tm-hist-group-header { position: sticky; top: 0; z-index: 2; padding: 5px 12px 4px; background: ${C.groupHdr}; border-bottom: 1px solid ${C.border}; font-size: 11px; font-weight: 700; color: ${C.groupTxt}; letter-spacing: 0.03em; cursor: pointer; user-select: none; display: flex; align-items: center; gap: 5px; } .tm-hist-group-header:hover { background: ${C.rowHover}; } .tm-hist-group-chevron { opacity: 0.55; flex-shrink: 0; display: inline-flex; align-items: center; transition: transform 0.18s ease; } .tm-hist-group-header.tm-collapsed .tm-hist-group-chevron { transform: rotate(-90deg); } .tm-hist-group-count { margin-left: auto; font-size: 10px; font-weight: 400; opacity: 0.5; padding-right: 2px; } .tm-hist-sel-all-btn { padding: 4px 10px; border-radius: 99px; border: 1px solid ${C.border}; background: transparent; color: ${C.sub}; font-size: 11px; cursor: pointer; transition: background 0.1s, color 0.1s; white-space: nowrap; flex-shrink: 0; } .tm-hist-sel-all-btn:hover { background: ${C.rowHover}; color: ${C.text}; } #tm-hist-gh-footer { flex-shrink: 0; box-sizing: border-box; height: 26px; border-top: 1px solid ${C.border}; display: flex; align-items: center; justify-content: center; background: ${C.header}; } #tm-hist-gh-footer a { font-size: 10px; color: ${C.sub}; text-decoration: none; opacity: 0.38; letter-spacing: 0.02em; font-family: system-ui, -apple-system, sans-serif; transition: opacity 0.18s; pointer-events: all; } #tm-hist-gh-footer a:hover { opacity: 0.85; text-decoration: underline; } .tm-hist-row { display: flex; align-items: flex-start; gap: 10px; padding: 8px 12px; border-bottom: 1px solid ${C.border}; transition: background 0.08s; position: relative; } .tm-hist-row:hover { background: ${C.rowHover}; } .tm-hist-row.selected { background: rgba(29,155,240,0.08); } .tm-hist-row.selected::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: #1d9bf0; border-radius: 0 2px 2px 0; } .tm-hist-cb { flex-shrink: 0; margin-top: 3px; cursor: pointer; } .tm-hist-thumb-wrap { width: 44px; height: 44px; border-radius: 6px; overflow: hidden; flex-shrink: 0; background: ${C.thumbBg}; border: 1px solid ${C.border}; display: flex; align-items: center; justify-content: center; position: relative; cursor: pointer; } .tm-hist-thumb-wrap img { width: 100%; height: 100%; object-fit: cover; display: block; } .tm-hist-thumb-wrap .tm-hist-video-icon { color: ${C.sub}; } .tm-hist-thumb-wrap .tm-hist-video-icon svg { width: 20px; height: 20px; } .tm-hist-info { flex: 1; min-width: 0; } .tm-hist-author { font-size: 12px; font-weight: 600; color: ${C.text}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .tm-hist-handle { font-size: 11px; color: ${C.sub}; margin-left: 4px; font-weight: 400; } .tm-hist-text { font-size: 11px; color: ${C.sub}; margin: 2px 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.4; } .tm-hist-url { font-size: 10px; color: #1d9bf0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; } .tm-hist-url:hover { text-decoration: underline; } .tm-hist-actions { display: flex; flex-direction: row; gap: 1px; flex-shrink: 0; align-items: center; } .tm-hist-act-btn { width: 24px; height: 24px; border-radius: 5px; border: none; background: transparent; cursor: pointer; display: flex; align-items: center; justify-content: center; color: ${C.sub}; transition: background 0.1s, color 0.1s; } .tm-hist-act-btn:hover { background: ${C.rowHover}; color: ${C.text}; } .tm-hist-act-btn.danger:hover { color: ${C.danger}; } .tm-hist-act-btn svg { width: 13px; height: 13px; pointer-events: none; } .tm-hist-act-btn.tm-fav-active { color: #e0245e; } .tm-hist-act-btn.tm-fav-btn:hover { color: #e0245e; } .tm-hist-act-btn.tm-fav-btn svg { width: 17px; height: 17px; } #tm-hist-thumb-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); gap: 3px; padding: 8px; } .tm-hist-grid-cell { aspect-ratio: 1; border-radius: 6px; overflow: hidden; position: relative; cursor: pointer; background: ${C.thumbBg}; } .tm-hist-grid-cell img { width: 100%; height: 100%; object-fit: cover; display: block; } .tm-hist-grid-cell .tm-hist-grid-overlay { position: absolute; inset: 0; background: linear-gradient(to top, rgba(0,0,0,0.72) 0%, rgba(0,0,0,0) 55%); opacity: 0; transition: opacity 0.18s; display: flex; flex-direction: column; justify-content: flex-end; padding: 6px; } .tm-hist-grid-cell:hover .tm-hist-grid-overlay { opacity: 1; } .tm-hist-grid-overlay .gov-author { font-size: 11px; font-weight: 700; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .tm-hist-grid-overlay .gov-text { font-size: 10px; color: rgba(255,255,255,0.82); overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; line-height: 1.3; margin-top: 2px; } .tm-hist-grid-cell .tm-hist-grid-nothumb { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; color: ${C.sub}; } .tm-hist-grid-cell .tm-hist-grid-nothumb svg { width: 28px; height: 28px; } #tm-thumb-lb-backdrop { position: fixed; inset: 0; z-index: 10000010; background: rgba(0,0,0,0); backdrop-filter: blur(0px); display: flex; align-items: center; justify-content: center; transition: background 0.22s ease, backdrop-filter 0.22s ease; cursor: zoom-out; } #tm-thumb-lb-backdrop.tm-lb-visible { background: rgba(0,0,0,0.88); backdrop-filter: blur(6px); } #tm-thumb-lb-img { max-width: 92vw; max-height: 88vh; border-radius: 10px; box-shadow: 0 24px 80px rgba(0,0,0,0.7); object-fit: contain; display: block; transform-origin: var(--lb-ox, 50%) var(--lb-oy, 50%); transform: scale(var(--lb-scale-from, 0.18)) translate(var(--lb-tx, 0px), var(--lb-ty, 0px)); opacity: 0; transition: transform 0.32s cubic-bezier(0.22,1,0.36,1), opacity 0.22s ease; cursor: default; } #tm-thumb-lb-img.tm-lb-visible { transform: scale(1) translate(0,0); opacity: 1; } #tm-thumb-lb-nav { position: fixed; bottom: 28px; left: 50%; transform: translateX(-50%); display: flex; align-items: center; gap: 12px; z-index: 10000011; pointer-events: none; opacity: 0; transition: opacity 0.2s; } #tm-thumb-lb-nav.tm-lb-visible { opacity: 1; pointer-events: auto; } .tm-lb-nav-btn { width: 36px; height: 36px; border-radius: 50%; border: none; background: rgba(255,255,255,0.15); backdrop-filter: blur(8px); color: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background 0.15s; font-size: 18px; line-height: 1; } .tm-lb-nav-btn:hover { background: rgba(255,255,255,0.28); } .tm-lb-nav-btn:disabled { opacity: 0.3; cursor: default; } #tm-lb-counter { font-size: 12px; color: rgba(255,255,255,0.7); font-family: system-ui, -apple-system, sans-serif; min-width: 40px; text-align: center; } #tm-thumb-lb-close { position: fixed; top: 18px; right: 22px; z-index: 10000012; width: 36px; height: 36px; border-radius: 50%; border: none; background: rgba(255,255,255,0.15); backdrop-filter: blur(8px); color: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.2s, background 0.15s; pointer-events: none; } #tm-thumb-lb-close.tm-lb-visible { opacity: 1; pointer-events: auto; } #tm-thumb-lb-close:hover { background: rgba(255,255,255,0.28); } .tm-hist-thumb-wrap.tm-has-thumb { cursor: zoom-in; } #tm-hist-footer { border-top: 1px solid ${C.border}; padding: 7px 12px; display: flex; align-items: center; gap: 8px; background: ${C.header}; flex-shrink: 0; } #tm-hist-footer.hidden { display: none; } .tm-hist-del-sel-btn { padding: 5px 12px; border-radius: 99px; border: none; background: ${C.danger}; color: #fff; font-size: 12px; font-weight: 600; cursor: pointer; transition: background 0.1s; } .tm-hist-del-sel-btn:hover { background: ${C.dangerHover}; } .tm-hist-cancel-edit { padding: 5px 12px; border-radius: 99px; border: 1px solid ${C.border}; background: transparent; color: ${C.text}; font-size: 12px; cursor: pointer; } #tm-hist-resize { position: absolute; bottom: 0; right: 0; width: 14px; height: 14px; cursor: se-resize; opacity: 0.4; } #tm-hist-resize:hover { opacity: 0.8; } .tm-dock-trigger { position: absolute; top: 50%; transform: translateY(-50%); width: 10px; height: 40px; background: transparent; border: none; padding: 0; cursor: pointer; display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.25s ease; z-index: 5; overflow: visible; } #tm-hist-panel:hover .tm-dock-trigger { opacity: 1; } .tm-dock-trigger.left { left: -1px; } .tm-dock-trigger.right { right: -1px; } .tm-dock-trigger svg { width: 6px; height: 28px; opacity: 0.18; transition: opacity 0.2s ease, filter 0.2s ease; pointer-events: none; } .tm-dock-trigger:hover svg { opacity: 0.55; filter: drop-shadow(0 0 2px rgba(29,155,240,0.7)); } #tm-hist-panel { transition: left 0.38s cubic-bezier(0.4,0,0.2,1), right 0.38s cubic-bezier(0.4,0,0.2,1), opacity 0.28s ease, box-shadow 0.28s ease; } #tm-hist-panel.tm-docked { opacity: 0.0; pointer-events: none; } #tm-hist-panel.tm-docked .tm-dock-trigger { opacity: 1 !important; pointer-events: all; } #tm-hist-dock-tab { position: fixed; z-index: 999979; width: 6px; border-radius: 3px; pointer-events: all; cursor: pointer; overflow: hidden; transition: width 0.22s ease, opacity 0.22s ease; } #tm-hist-dock-tab:hover { width: 9px; } #tm-hist-dock-tab.style-ruler { background: ${C.border}; } #tm-hist-dock-tab.style-ruler::before { content: ''; position: absolute; inset: 0; background: repeating-linear-gradient( to bottom, transparent 0px, transparent 7px, rgba(29,155,240,0.45) 7px, rgba(29,155,240,0.45) 8px ); } #tm-hist-dock-tab.style-ruler::after { content: ''; position: absolute; inset: 0; background: repeating-linear-gradient( to bottom, transparent 0px, transparent 23px, rgba(29,155,240,0.85) 23px, rgba(29,155,240,0.85) 25px ); box-shadow: 0 0 3px rgba(29,155,240,0.4); } #tm-hist-dock-tab.style-ghost { background: repeating-linear-gradient( to bottom, transparent 0px, transparent 3px, rgba(128,128,128,0.22) 3px, rgba(128,128,128,0.22) 4px ); border-radius: 3px; } #tm-hist-dock-tab.style-ghost:hover { background: repeating-linear-gradient( to bottom, transparent 0px, transparent 3px, rgba(29,155,240,0.38) 3px, rgba(29,155,240,0.38) 4px ); } #tm-hist-dock-tab.style-notch { background: transparent; display: flex; align-items: center; justify-content: center; } #tm-hist-dock-tab.style-notch::before { content: ''; width: 2px; height: 28px; border-radius: 1px; background: ${C.border}; box-shadow: inset 0 0 0 1px rgba(29,155,240,0.2); transition: background 0.2s, box-shadow 0.2s; } #tm-hist-dock-tab.style-notch:hover::before { background: rgba(29,155,240,0.45); box-shadow: 0 0 4px rgba(29,155,240,0.5); } .tm-hist-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px 20px; color: ${C.sub}; font-size: 13px; gap: 10px; text-align: center; } .tm-hist-empty svg { width: 36px; height: 36px; opacity: 0.4; } #tm-hist-zoom { position: fixed; z-index: 999999; width: 200px; height: 200px; border-radius: 8px; overflow: hidden; pointer-events: none; box-shadow: 0 8px 24px rgba(0,0,0,0.4); border: 2px solid rgba(255,255,255,0.2); } #tm-hist-zoom img { width: 100%; height: 100%; object-fit: cover; } #tm-group-tab-bar { position: relative; display: flex; align-items: center; padding: 0; gap: 0; border-bottom: 1px solid ${C.border}; flex-shrink: 0; overflow: hidden; background: ${C.header}; } #tm-group-tab-scroll { display: flex; align-items: center; gap: 4px; padding: 5px 8px; overflow-x: auto; overflow-y: hidden; scrollbar-width: none; flex: 1; min-width: 0; scroll-behavior: smooth; } #tm-group-tab-scroll::-webkit-scrollbar { display: none; } .tm-gtab-pill { display: inline-flex; align-items: center; gap: 4px; padding: 3px 9px 3px 7px; border-radius: 99px; border: 1px solid transparent; font-size: 11px; font-weight: 500; color: ${C.sub}; white-space: nowrap; cursor: pointer; flex-shrink: 0; background: transparent; font-family: inherit; line-height: 1.4; transition: background .12s, border-color .12s, color .12s; } .tm-gtab-pill:hover { background: ${C.rowHover}; color: ${C.text}; border-color: ${C.border}; } .tm-gtab-pill.active { background: rgba(29,155,240,.14); border-color: rgba(29,155,240,.45); color: #1d9bf0; } .tm-gtab-pill svg { width: 12px; height: 12px; flex-shrink: 0; pointer-events: none; } .tm-gtab-scroll-btn { flex-shrink: 0; width: 22px; height: 100%; border: none; background: transparent; cursor: pointer; display: none; align-items: center; justify-content: center; color: ${C.sub}; padding: 0; transition: background .1s, color .1s; z-index: 2; } .tm-gtab-scroll-btn:hover { background: ${C.rowHover}; color: ${C.text}; } .tm-gtab-scroll-btn.visible { display: flex; } .tm-gtab-scroll-btn svg { width: 10px; height: 10px; pointer-events: none; } .tm-gtab-scroll-btn.left { border-right: 1px solid ${C.border}; } .tm-gtab-scroll-btn.right { border-left: 1px solid ${C.border}; } .tm-gtab-dot { width: 6px; height: 6px; border-radius: 50%; background: #fbbf24; flex-shrink: 0; box-shadow: 0 0 4px rgba(251,191,36,0.7); animation: tm-dot-pop 0.25s cubic-bezier(0.34,1.56,0.64,1); } @keyframes tm-dot-pop { from { transform: scale(0); opacity: 0; } to { transform: scale(1); opacity: 1; } } `; const panel = document.createElement('div'); panel.id = 'tm-hist-panel'; panel.style.cssText = `left:${pos.x}px; top:${pos.y}px; width:${pos.w}px; height:${pos.h}px;`; if (_dockSideGlobal) { panel.style.opacity = '0'; panel.style.transition = 'none'; } const titlebar = document.createElement('div'); titlebar.id = 'tm-hist-titlebar'; const titleIcon = document.createElement('span'); titleIcon.style.cssText = 'display:inline-flex;align-items:center;flex-shrink:0;opacity:0.55;margin-right:2px;'; titleIcon.innerHTML = ``; const titleEl = document.createElement('span'); titleEl.className = 'tm-hist-title'; titleEl.textContent = 'Download History'; titleEl.title = 'Download History'; const countBadge = document.createElement('span'); countBadge.className = 'tm-hist-count-badge'; const SVG_LIST = ``; const SVG_GRID = ``; const SVG_EDIT = ``; const SVG_EXP = ``; const SVG_CLOSE = ``; const btnViewToggle = _mkIconBtn(viewMode === 'list' ? SVG_GRID : SVG_LIST, viewMode === 'list' ? 'Switch to Thumbnail' : 'Switch to List'); const btnEdit = _mkIconBtn(SVG_EDIT, 'Edit mode'); const btnExp = _mkIconBtn(SVG_EXP, 'Export'); const btnClose = _mkIconBtn(SVG_CLOSE, 'Close'); titlebar.appendChild(titleIcon); titlebar.appendChild(titleEl); titlebar.appendChild(countBadge); titlebar.appendChild(btnViewToggle); titlebar.appendChild(btnEdit); titlebar.appendChild(btnExp); titlebar.appendChild(btnClose); panel.appendChild(titlebar); const searchBar = document.createElement('div'); searchBar.className = 'tm-hist-searchbar'; const searchInput = document.createElement('input'); searchInput.id = 'tm-hist-search'; searchInput.type = 'search'; searchInput.placeholder = '🔍 Search author / content…'; searchBar.appendChild(searchInput); panel.appendChild(searchBar); const groupTabBar = document.createElement('div'); groupTabBar.id = 'tm-group-tab-bar'; function buildGroupTabs() { groupTabBar.innerHTML = ''; const groups = getGroups(); if (!groups.length) { groupTabBar.style.display = 'none'; return; } groupTabBar.style.display = 'flex'; const SVG_LEFT = ``; const SVG_RIGHT = ``; const btnLeft = document.createElement('button'); btnLeft.className = 'tm-gtab-scroll-btn left'; btnLeft.innerHTML = SVG_LEFT; btnLeft.title = 'Scroll left'; btnLeft.addEventListener('click', (e) => { e.stopPropagation(); scrollArea.scrollBy({ left: -120, behavior: 'smooth' }); }); const scrollArea = document.createElement('div'); scrollArea.id = 'tm-group-tab-scroll'; const btnRight = document.createElement('button'); btnRight.className = 'tm-gtab-scroll-btn right'; btnRight.innerHTML = SVG_RIGHT; btnRight.title = 'Scroll right'; btnRight.addEventListener('click', (e) => { e.stopPropagation(); scrollArea.scrollBy({ left: 120, behavior: 'smooth' }); }); const makePill = (iconHtml, label, value) => { const pill = document.createElement('button'); pill.type = 'button'; pill.className = 'tm-gtab-pill' + ( (value !== '__ungrouped__' && activeGroupId === value) || (value === '__ungrouped__' && activeGroupId === '__ungrouped__') ? ' active' : '' ); const iconSpan = document.createElement('span'); iconSpan.innerHTML = iconHtml; iconSpan.style.cssText = 'display:inline-flex;align-items:center;flex-shrink:0'; const txtSpan = document.createElement('span'); txtSpan.textContent = label; pill.appendChild(iconSpan); pill.appendChild(txtSpan); if (value !== '__ungrouped__' && GM_getValue('app_group_unread_' + value, false)) { const dot = document.createElement('span'); dot.className = 'tm-gtab-dot'; pill.appendChild(dot); } pill.addEventListener('click', (e) => { e.stopPropagation(); activeGroupId = value; if (value !== '__ungrouped__') { GM_deleteValue('app_group_unread_' + value); pill.querySelector('.tm-gtab-dot')?.remove(); } scrollArea.querySelectorAll('.tm-gtab-pill').forEach(p => p.classList.remove('active')); pill.classList.add('active'); pill.scrollIntoView({ block: 'nearest', inline: 'nearest' }); render(); }); return pill; }; groups.forEach(g => { const ic = _GROUP_SVG_ICONS?.find(x => x.id === g.icon); const iconHtml = ic ? `${ic.svg.match(/]*>([\s\S]*?)<\/svg>/)?.[1] || ''}` : `${g.icon || '●'}`; scrollArea.appendChild(makePill(iconHtml, g.name, g.id)); }); const SVG_DASH = ``; scrollArea.appendChild(makePill(SVG_DASH, 'Ungrouped', '__ungrouped__')); groupTabBar.appendChild(btnLeft); groupTabBar.appendChild(scrollArea); groupTabBar.appendChild(btnRight); const syncArrows = () => { const canLeft = scrollArea.scrollLeft > 2; const canRight = scrollArea.scrollLeft < scrollArea.scrollWidth - scrollArea.clientWidth - 2; btnLeft.classList.toggle('visible', canLeft); btnRight.classList.toggle('visible', canRight); }; scrollArea.addEventListener('scroll', syncArrows, { passive: true }); if (window.ResizeObserver) { const ro = new ResizeObserver(syncArrows); ro.observe(scrollArea); } requestAnimationFrame(syncArrows); } buildGroupTabs(); panel.appendChild(groupTabBar); const body = document.createElement('div'); body.id = 'tm-hist-body'; panel.appendChild(body); const footer = document.createElement('div'); footer.id = 'tm-hist-footer'; footer.className = 'hidden'; const selAllBtn = document.createElement('button'); selAllBtn.className = 'tm-hist-sel-all-btn'; selAllBtn.textContent = 'Select All'; const delSelBtn = document.createElement('button'); delSelBtn.className = 'tm-hist-del-sel-btn'; const cancelEditBtn = document.createElement('button'); cancelEditBtn.className = 'tm-hist-cancel-edit'; cancelEditBtn.textContent = 'Cancel'; footer.appendChild(selAllBtn); footer.appendChild(delSelBtn); footer.appendChild(cancelEditBtn); panel.appendChild(footer); const ghFooterEl = document.createElement('div'); ghFooterEl.id = 'tm-hist-gh-footer'; const ghLink = document.createElement('a'); ghLink.href = 'https://github.com/Startanuki07/Twitter-X-Media-Copy-Download'; ghLink.target = '_blank'; ghLink.rel = 'noopener noreferrer'; ghLink.textContent = '★ Star on GitHub'; ghFooterEl.appendChild(ghLink); panel.appendChild(ghFooterEl); const resizeHandle = document.createElement('div'); resizeHandle.id = 'tm-hist-resize'; resizeHandle.innerHTML = ``; panel.appendChild(resizeHandle); const SVG_NOTCH = ` `; const dockTriggerL = document.createElement('button'); dockTriggerL.className = 'tm-dock-trigger left'; dockTriggerL.title = 'Dock panel to left edge'; dockTriggerL.innerHTML = SVG_NOTCH; panel.appendChild(dockTriggerL); const dockTriggerR = document.createElement('button'); dockTriggerR.className = 'tm-dock-trigger right'; dockTriggerR.title = 'Dock panel to right edge'; dockTriggerR.innerHTML = SVG_NOTCH; panel.appendChild(dockTriggerR); document.body.appendChild(panel); function getRecords() { try { return JSON.parse(GM_getValue(KEY_HISTORY_RECORDS, '[]')); } catch (_) { return []; } } function getFiltered(records) { let result = records; if (activeGroupId === '__ungrouped__') { result = result.filter(r => !r.groupId); } else if (activeGroupId) { result = result.filter(r => r.groupId === activeGroupId); } if (query) { const q = query.toLowerCase(); result = result.filter(r => r.displayName?.toLowerCase().includes(q) || r.screenName?.toLowerCase().includes(q) || r.text?.toLowerCase().includes(q) ); } return result; } function _fmtGroupLabel(yyyymm) { const p = yyyymm.split('.'); if (p.length !== 2) return yyyymm; const y = p[0]; const m = parseInt(p[1], 10); if (_cachedDateFormat === 'western') { const names = ['','January','February','March','April','May','June', 'July','August','September','October','November','December']; return `${names[m] || p[1]} ${y}`; } return `${y}年 ${m}月`; } function render() { const records = getRecords(); const filtered = getFiltered(records); countBadge.textContent = `${records.length} / ${HISTORY_MAX_RECORDS}`; delSelBtn.textContent = `Delete selected (${selectedIds.size})`; const visibleIds = filtered .filter(r => !collapsedGroups.has(r.yyyymm)) .map(r => r.id); const allSelected = visibleIds.length > 0 && visibleIds.every(id => selectedIds.has(id)); selAllBtn.textContent = allSelected ? 'Deselect All' : 'Select All'; body.innerHTML = ''; buildGroupTabs(); if (viewMode === 'list') renderList(filtered); else renderThumb(filtered); btnViewToggle.innerHTML = viewMode === 'list' ? SVG_GRID : SVG_LIST; btnViewToggle.title = viewMode === 'list' ? 'Switch to Thumbnail' : 'Switch to List'; btnViewToggle.classList.toggle('active', true); } function renderList(records) { if (!records.length) { _renderEmpty(); return; } const groupCounts = {}; records.forEach(r => { groupCounts[r.yyyymm] = (groupCounts[r.yyyymm] || 0) + 1; }); let lastGroup = null; let _cbShiftDown = false; records.forEach((rec, idx) => { if (rec.yyyymm !== lastGroup) { lastGroup = rec.yyyymm; const isCollapsed = collapsedGroups.has(rec.yyyymm); const gh = document.createElement('div'); gh.className = 'tm-hist-group-header' + (isCollapsed ? ' tm-collapsed' : ''); gh.dataset.yyyymm = rec.yyyymm; const chevron = document.createElement('span'); chevron.className = 'tm-hist-group-chevron'; chevron.innerHTML = ``; const label = document.createElement('span'); label.textContent = _fmtGroupLabel(rec.yyyymm); const countEl = document.createElement('span'); countEl.className = 'tm-hist-group-count'; countEl.textContent = `${groupCounts[rec.yyyymm]}`; gh.appendChild(chevron); gh.appendChild(label); gh.appendChild(countEl); gh.addEventListener('click', () => { if (collapsedGroups.has(rec.yyyymm)) collapsedGroups.delete(rec.yyyymm); else collapsedGroups.add(rec.yyyymm); render(); }); body.appendChild(gh); } if (collapsedGroups.has(rec.yyyymm)) return; const row = document.createElement('div'); row.className = 'tm-hist-row' + (selectedIds.has(rec.id) ? ' selected' : ''); row.dataset.id = rec.id; row.dataset.idx = idx; if (editMode) { if (!rec.favorited) { const cb = document.createElement('input'); cb.type = 'checkbox'; cb.className = 'tm-hist-cb'; cb.checked = selectedIds.has(rec.id); cb.addEventListener('mousedown', e => { _cbShiftDown = e.shiftKey; }); cb.addEventListener('change', e => { e.stopPropagation(); _handleCheckbox(rec.id, idx, _cbShiftDown); _cbShiftDown = false; }); row.appendChild(cb); } else { const lock = document.createElement('span'); lock.className = 'tm-hist-cb'; lock.style.cssText = 'display:inline-flex;align-items:center;justify-content:center;opacity:0.35;font-size:10px;'; lock.textContent = '♥'; row.appendChild(lock); } } const thumbWrap = document.createElement('div'); thumbWrap.className = 'tm-hist-thumb-wrap'; if (rec.thumbUrls && rec.thumbUrls.length > 0) { const img = document.createElement('img'); img.src = _thumbUrl(rec.thumbUrls[0]); img.loading = 'lazy'; img.alt = ''; thumbWrap.appendChild(img); thumbWrap.classList.add('tm-has-thumb'); thumbWrap.addEventListener('mouseenter', (e) => _showZoom(rec.thumbUrls[0], e)); thumbWrap.addEventListener('mousemove', (e) => _moveZoom(e)); thumbWrap.addEventListener('mouseleave', _hideZoom); thumbWrap.addEventListener('click', (e) => { e.stopPropagation(); _hideZoom(); _openThumbLightbox(rec.thumbUrls, 0, thumbWrap.querySelector('img')); }); } else { const vi = document.createElement('div'); vi.className = 'tm-hist-video-icon'; vi.innerHTML = ``; thumbWrap.appendChild(vi); } row.appendChild(thumbWrap); const info = document.createElement('div'); info.className = 'tm-hist-info'; const author = document.createElement('div'); author.className = 'tm-hist-author'; author.textContent = rec.displayName || rec.screenName; const handle = document.createElement('span'); handle.className = 'tm-hist-handle'; handle.textContent = `@${rec.screenName}`; author.appendChild(handle); const textEl = document.createElement('div'); textEl.className = 'tm-hist-text'; textEl.textContent = rec.text || '(no caption)'; const urlEl = document.createElement('div'); urlEl.className = 'tm-hist-url'; urlEl.textContent = rec.tweetUrl; urlEl.title = rec.tweetUrl + '\n(點擊前往推文)'; urlEl.addEventListener('click', (e) => { e.stopPropagation(); const path = new URL(rec.tweetUrl).pathname; history.pushState({ tmNav: true }, '', path); window.dispatchEvent(new Event('popstate')); }); info.appendChild(author); info.appendChild(textEl); info.appendChild(urlEl); row.appendChild(info); const acts = document.createElement('div'); acts.className = 'tm-hist-actions'; const SVG_JUMP = ``; const SVG_DEL = ``; const SVG_HEART_EMPTY = ``; const SVG_HEART_FULL = ``; const jmpBtn = document.createElement('button'); jmpBtn.className = 'tm-hist-act-btn'; jmpBtn.innerHTML = SVG_JUMP; jmpBtn.title = 'Open tweet'; jmpBtn.addEventListener('click', (e) => { e.stopPropagation(); window.open(rec.tweetUrl, '_blank'); }); const favBtn = document.createElement('button'); const isFav = !!rec.favorited; favBtn.className = 'tm-hist-act-btn tm-fav-btn' + (isFav ? ' tm-fav-active' : ''); favBtn.innerHTML = isFav ? SVG_HEART_FULL : SVG_HEART_EMPTY; favBtn.title = isFav ? 'Unfavorite' : 'Favorite'; if (editMode) { favBtn.style.opacity = '0.3'; favBtn.style.pointerEvents = 'none'; } favBtn.addEventListener('click', (e) => { e.stopPropagation(); let records = getRecords(); const target = records.find(r => r.id === rec.id); if (!target) return; target.favorited = !target.favorited; GM_setValue(KEY_HISTORY_RECORDS, JSON.stringify(records)); const nowFav = target.favorited; favBtn.innerHTML = nowFav ? SVG_HEART_FULL : SVG_HEART_EMPTY; favBtn.title = nowFav ? 'Unfavorite' : 'Favorite'; favBtn.classList.toggle('tm-fav-active', nowFav); rec.favorited = nowFav; }); const delBtn = document.createElement('button'); delBtn.className = 'tm-hist-act-btn danger'; delBtn.innerHTML = SVG_DEL; delBtn.title = 'Delete'; delBtn.addEventListener('click', (e) => { e.stopPropagation(); _deleteOne(rec.id, idx); }); acts.appendChild(favBtn); acts.appendChild(jmpBtn); acts.appendChild(delBtn); row.appendChild(acts); if (editMode && !rec.favorited) { row.style.cursor = 'pointer'; row.addEventListener('click', (e) => { if (e.target.classList.contains('tm-hist-cb')) return; _handleCheckbox(rec.id, idx, e.shiftKey); }); } body.appendChild(row); }); } function renderThumb(records) { if (!records.length) { _renderEmpty(); return; } const grid = document.createElement('div'); grid.id = 'tm-hist-thumb-grid'; records.forEach(rec => { const cell = document.createElement('div'); cell.className = 'tm-hist-grid-cell'; cell.title = `${rec.displayName} @${rec.screenName}`; if (rec.thumbUrls && rec.thumbUrls.length > 0) { const img = document.createElement('img'); img.src = _thumbUrl(rec.thumbUrls[0]); img.loading = 'lazy'; img.alt = ''; cell.appendChild(img); } else { const ni = document.createElement('div'); ni.className = 'tm-hist-grid-nothumb'; ni.innerHTML = ``; cell.appendChild(ni); } const overlay = document.createElement('div'); overlay.className = 'tm-hist-grid-overlay'; const govAuthor = document.createElement('div'); govAuthor.className = 'gov-author'; govAuthor.textContent = rec.displayName || rec.screenName; const govText = document.createElement('div'); govText.className = 'gov-text'; govText.textContent = rec.text || ''; overlay.appendChild(govAuthor); overlay.appendChild(govText); cell.appendChild(overlay); cell.addEventListener('click', (e) => { e.stopPropagation(); const path = new URL(rec.tweetUrl).pathname; history.pushState({ tmNav: true }, '', path); window.dispatchEvent(new Event('popstate')); }); grid.appendChild(cell); }); body.appendChild(grid); } function _renderEmpty() { const em = document.createElement('div'); em.className = 'tm-hist-empty'; em.innerHTML = ``; const msg = document.createElement('div'); msg.textContent = query ? 'No results matching search.' : 'No download history yet.\nRight-click 🎞️ to download & record.'; msg.style.whiteSpace = 'pre-line'; em.appendChild(msg); body.appendChild(em); } function _handleCheckbox(id, idx, shiftKey) { const allRecords = getFiltered(getRecords()); const target = allRecords.find(r => r.id === id); if (target && target.favorited) return; if (shiftKey && anchorIdx >= 0) { const lo = Math.min(anchorIdx, idx); const hi = Math.max(anchorIdx, idx); for (let i = lo; i <= hi; i++) { if (allRecords[i] && !allRecords[i].favorited) selectedIds.add(allRecords[i].id); } } else { if (selectedIds.has(id)) selectedIds.delete(id); else { selectedIds.add(id); anchorIdx = idx; } } render(); } function _deleteOne(id, idx) { let records = getRecords(); const record = records.find(r => r.id === id); if (!record) return; if (record.favorited) return; records = records.filter(r => r.id !== id); GM_setValue(KEY_HISTORY_RECORDS, JSON.stringify(records)); _downloadedIds.delete(record.tweetId); if (_historyUndoTimer) clearTimeout(_historyUndoTimer); _historyUndoBuffer = { record, index: idx }; render(); const ut = document.createElement('div'); ut.style.cssText = ` position:fixed; bottom:24px; left:50%; transform:translateX(-50%); background:rgba(15,20,25,0.92); color:#fff; padding:8px 16px; border-radius:99px; font-size:12px; font-family:system-ui; display:flex; align-items:center; gap:10px; z-index:9999999; box-shadow:0 4px 16px rgba(0,0,0,0.3); animation:tm-toast-rise 5s forwards; `; ut.id = 'tm-hist-undo-toast'; const msg = document.createElement('span'); msg.textContent = 'Record deleted'; const undoBtn = document.createElement('button'); undoBtn.textContent = 'Undo'; undoBtn.style.cssText = `background:none;border:none;color:#1d9bf0;cursor:pointer;font-weight:700;font-size:12px;padding:0;`; undoBtn.addEventListener('click', () => { if (_historyUndoBuffer) { let recs = getRecords(); recs.splice(_historyUndoBuffer.index, 0, _historyUndoBuffer.record); GM_setValue(KEY_HISTORY_RECORDS, JSON.stringify(recs)); _downloadedIds.add(_historyUndoBuffer.record.tweetId); _historyUndoBuffer = null; ut.remove(); clearTimeout(_historyUndoTimer); render(); } }); ut.appendChild(msg); ut.appendChild(undoBtn); document.getElementById('tm-hist-undo-toast')?.remove(); document.body.appendChild(ut); _historyUndoTimer = setTimeout(() => { ut.remove(); _historyUndoBuffer = null; }, 5000); } function _thumbUrl(url) { try { if (url.includes('pbs.twimg.com') && url.includes('/media/')) { const u = new URL(url); u.searchParams.set('name', 'small'); return u.toString(); } } catch (_) {} return url; } function _showZoom(url, e) { let z = document.getElementById('tm-hist-zoom'); if (!z) { z = document.createElement('div'); z.id = 'tm-hist-zoom'; document.body.appendChild(z); } z.innerHTML = ''; const img = document.createElement('img'); img.src = _thumbUrl(url).replace('name=small', 'name=medium'); img.alt = ''; z.appendChild(img); _moveZoom(e); } function _moveZoom(e) { const z = document.getElementById('tm-hist-zoom'); if (!z) return; const w = 200, h = 200, margin = 14; let left = e.clientX - w - margin; let top = e.clientY - h / 2; if (left < 4) left = e.clientX + margin; if (top < 4) top = 4; if (top + h > window.innerHeight - 4) top = window.innerHeight - h - 4; z.style.cssText = `position:fixed;z-index:9999999;width:${w}px;height:${h}px;left:${left}px;top:${top}px;border-radius:8px;overflow:hidden;pointer-events:none;box-shadow:0 8px 24px rgba(0,0,0,0.4);border:2px solid rgba(255,255,255,0.2);`; } function _hideZoom() { document.getElementById('tm-hist-zoom')?.remove(); } function _openThumbLightbox(urls, startIdx, originEl) { document.getElementById('tm-thumb-lb-backdrop')?.remove(); const allUrls = (urls || []).map(u => { try { if (u.includes('pbs.twimg.com') && u.includes('/media/')) { const obj = new URL(u); obj.searchParams.set('name', 'large'); return obj.toString(); } } catch (_) {} return u; }); if (!allUrls.length) return; let idx = Math.max(0, Math.min(startIdx, allUrls.length - 1)); const originRect = originEl ? originEl.getBoundingClientRect() : null; const vpW = window.innerWidth, vpH = window.innerHeight; function _calcOrigin(rect) { if (!rect) return { ox: '50%', oy: '50%', tx: '0px', ty: '0px', scale: 0.18 }; const cx = rect.left + rect.width / 2; const cy = rect.top + rect.height / 2; const finalW = Math.min(vpW * 0.92, vpH * 0.88 * 1.5); const finalH = finalW / 1.5; const imgCx = vpW / 2, imgCy = vpH / 2; const scaleFrom = Math.max(rect.width / finalW, 0.06); const ox = ((cx - imgCx) / finalW * 100 + 50).toFixed(2) + '%'; const oy = ((cy - imgCy) / finalH * 100 + 50).toFixed(2) + '%'; return { ox, oy, tx: '0px', ty: '0px', scale: scaleFrom }; } const backdrop = document.createElement('div'); backdrop.id = 'tm-thumb-lb-backdrop'; const imgEl = document.createElement('img'); imgEl.id = 'tm-thumb-lb-img'; imgEl.alt = ''; const closeBtn = document.createElement('button'); closeBtn.id = 'tm-thumb-lb-close'; closeBtn.innerHTML = ``; closeBtn.title = 'Close'; const nav = document.createElement('div'); nav.id = 'tm-thumb-lb-nav'; const prevBtn = document.createElement('button'); prevBtn.className = 'tm-lb-nav-btn'; prevBtn.innerHTML = ``; prevBtn.title = 'Previous'; const counter = document.createElement('span'); counter.id = 'tm-lb-counter'; const nextBtn = document.createElement('button'); nextBtn.className = 'tm-lb-nav-btn'; nextBtn.innerHTML = ``; nextBtn.title = 'Next'; nav.appendChild(prevBtn); nav.appendChild(counter); nav.appendChild(nextBtn); backdrop.appendChild(imgEl); document.body.appendChild(backdrop); document.body.appendChild(closeBtn); document.body.appendChild(nav); function _showIdx(newIdx, fromEl) { idx = Math.max(0, Math.min(newIdx, allUrls.length - 1)); counter.textContent = allUrls.length > 1 ? `${idx + 1} / ${allUrls.length}` : ''; prevBtn.disabled = (idx === 0); nextBtn.disabled = (idx === allUrls.length - 1); nav.style.display = allUrls.length > 1 ? '' : 'none'; const o = _calcOrigin(fromEl ? fromEl.getBoundingClientRect() : null); imgEl.style.setProperty('--lb-ox', o.ox); imgEl.style.setProperty('--lb-oy', o.oy); imgEl.style.setProperty('--lb-scale-from', o.scale); imgEl.classList.remove('tm-lb-visible'); imgEl.src = allUrls[idx]; imgEl.onload = () => { requestAnimationFrame(() => { imgEl.classList.add('tm-lb-visible'); }); }; if (imgEl.complete && imgEl.naturalWidth) { requestAnimationFrame(() => imgEl.classList.add('tm-lb-visible')); } } function _close() { backdrop.classList.remove('tm-lb-visible'); closeBtn.classList.remove('tm-lb-visible'); nav.classList.remove('tm-lb-visible'); imgEl.classList.remove('tm-lb-visible'); setTimeout(() => { backdrop.remove(); closeBtn.remove(); nav.remove(); }, 320); document.removeEventListener('keydown', _onKey); } function _onKey(e) { if (e.key === 'Escape') _close(); if (e.key === 'ArrowLeft' && idx > 0) _showIdx(idx - 1, null); if (e.key === 'ArrowRight' && idx < allUrls.length - 1) _showIdx(idx + 1, null); } backdrop.addEventListener('click', (e) => { if (e.target === backdrop) _close(); }); closeBtn.addEventListener('click', _close); prevBtn.addEventListener('click', (e) => { e.stopPropagation(); _showIdx(idx - 1, null); }); nextBtn.addEventListener('click', (e) => { e.stopPropagation(); _showIdx(idx + 1, null); }); imgEl.addEventListener('click', (e) => e.stopPropagation()); document.addEventListener('keydown', _onKey); _showIdx(startIdx, originEl); requestAnimationFrame(() => { backdrop.classList.add('tm-lb-visible'); closeBtn.classList.add('tm-lb-visible'); if (allUrls.length > 1) nav.classList.add('tm-lb-visible'); }); } function _exportCSV() { const records = getRecords(); const header = 'tweetId,tweetUrl,date,screenName,displayName\n'; const rows = records.map(r => [r.tweetId, r.tweetUrl, r.tweetDate, r.screenName, `"${(r.displayName||'').replace(/"/g,'""')}"`].join(',') ).join('\n'); _download('history.csv', header + rows, 'text/csv'); } function _exportJSON() { const records = getRecords(); _download('history.json', JSON.stringify(records, null, 2), 'application/json'); } function _download(filename, content, type) { const blob = new Blob([content], { type }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 5000); } function _mkIconBtn(svg, title) { const b = document.createElement('button'); b.className = 'tm-hist-icon-btn'; b.innerHTML = svg; b.title = title; return b; } btnViewToggle.addEventListener('click', () => { viewMode = viewMode === 'list' ? 'thumb' : 'list'; GM_setValue(KEY_HISTORY_VIEW_MODE, viewMode); render(); }); btnClose.addEventListener('click', () => { if (_dockSideGlobal) { _retract(); return; } if (typeof panel._tmCleanup === 'function') panel._tmCleanup(); if (_historyUndoTimer) { clearTimeout(_historyUndoTimer); _historyUndoTimer = null; _historyUndoBuffer = null; } panel.remove(); _cleanZoom(); }); btnEdit.addEventListener('click', () => { editMode = !editMode; selectedIds.clear(); anchorIdx = -1; footer.classList.toggle('hidden', !editMode); btnEdit.classList.toggle('active', editMode); render(); }); selAllBtn.addEventListener('click', () => { const filtered = getFiltered(getRecords()); const visibleIds = filtered .filter(r => !collapsedGroups.has(r.yyyymm) && !r.favorited) .map(r => r.id); const allSelected = visibleIds.length > 0 && visibleIds.every(id => selectedIds.has(id)); if (allSelected) { visibleIds.forEach(id => selectedIds.delete(id)); } else { visibleIds.forEach(id => selectedIds.add(id)); const firstVisible = filtered.find(r => !collapsedGroups.has(r.yyyymm) && !r.favorited); if (firstVisible) { anchorIdx = filtered.indexOf(firstVisible); } } render(); }); delSelBtn.addEventListener('click', () => { if (!selectedIds.size) return; let records = getRecords(); records.filter(r => selectedIds.has(r.id) && !r.favorited).forEach(r => _downloadedIds.delete(r.tweetId)); records = records.filter(r => !selectedIds.has(r.id) || r.favorited); GM_setValue(KEY_HISTORY_RECORDS, JSON.stringify(records)); selectedIds.clear(); anchorIdx = -1; render(); }); cancelEditBtn.addEventListener('click', () => { editMode = false; selectedIds.clear(); anchorIdx = -1; footer.classList.add('hidden'); btnEdit.classList.remove('active'); render(); }); function _importJSON() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json,application/json'; input.style.display = 'none'; document.body.appendChild(input); input.addEventListener('change', () => { const file = input.files?.[0]; if (!file) { input.remove(); return; } const reader = new FileReader(); reader.onload = (e) => { try { const imported = JSON.parse(e.target.result); if (!Array.isArray(imported)) throw new Error('Not an array'); const existing = getRecords(); const existMap = new Map(existing.map(r => [r.tweetId, r])); let addCount = 0; imported.forEach(r => { if (r.tweetId) { if (!existMap.has(r.tweetId)) addCount++; existMap.set(r.tweetId, r); } }); const merged = [...existMap.values()].sort((a, b) => (b.id || 0) - (a.id || 0)); GM_setValue(KEY_HISTORY_RECORDS, JSON.stringify(merged)); merged.forEach(r => _downloadedIds.add(r.tweetId)); showToast(`✅ Imported ${addCount} new record(s)`); render(); } catch (err) { showToast(`❌ Import failed: ${err.message}`); } finally { input.remove(); } }; reader.readAsText(file); }); input.click(); } let _expMenu = null; function _showExpMenu() { const existing = document.getElementById('tm-exp-menu'); if (existing) { existing.remove(); _expMenu = null; return; } const menu = document.createElement('div'); menu.id = 'tm-exp-menu'; _expMenu = menu; const btnRect = btnExp.getBoundingClientRect(); const menuWidth = 152; const menuLeft = Math.max(4, btnRect.right - menuWidth); menu.style.cssText = ` position:fixed; left:${menuLeft}px; top:${btnRect.bottom + 6}px; background:rgba(22,32,43,.97); border:1px solid rgba(255,255,255,.12); border-radius:10px; padding:4px; z-index:999999; min-width:${menuWidth}px; box-shadow:0 6px 20px rgba(0,0,0,.5); animation:tm-exp-menu-in .15s cubic-bezier(.34,1.56,.64,1); `; if (!document.getElementById('tm-exp-menu-style')) { const s = document.createElement('style'); s.id = 'tm-exp-menu-style'; s.textContent = `@keyframes tm-exp-menu-in{from{opacity:0;transform:scale(.88) translateY(-4px)}to{opacity:1;transform:scale(1) translateY(0)}}`; document.head.appendChild(s); } const mkItem = (icon, label, onClick) => { const item = document.createElement('button'); item.type = 'button'; item.style.cssText = ` display:flex;align-items:center;gap:8px; width:100%;padding:7px 10px;border:none; background:transparent;border-radius:7px; color:rgba(255,255,255,.8);font-size:12px; cursor:pointer;font-family:inherit;text-align:left; transition:background .1s;white-space:nowrap; `; item.innerHTML = `${icon}${label}`; item.addEventListener('mouseover', () => item.style.background = 'rgba(255,255,255,.09)'); item.addEventListener('mouseout', () => item.style.background = 'transparent'); item.addEventListener('click', (e) => { e.stopPropagation(); menu.remove(); _expMenu = null; document.removeEventListener('click', closeMenu, true); onClick(); }); return item; }; const SVG_CSV = ``; const SVG_JSON = ``; const SVG_IMP = ``; const divider = document.createElement('div'); divider.style.cssText = 'height:1px;background:rgba(255,255,255,.08);margin:3px 4px'; menu.appendChild(mkItem(SVG_CSV, 'Export CSV', _exportCSV)); menu.appendChild(mkItem(SVG_JSON, 'Export JSON', _exportJSON)); menu.appendChild(divider); menu.appendChild(mkItem(SVG_IMP, 'Import JSON', _importJSON)); document.body.appendChild(menu); const closeMenu = (e) => { if (menu.contains(e.target) || e.target === btnExp) return; menu.remove(); _expMenu = null; document.removeEventListener('click', closeMenu, true); }; setTimeout(() => document.addEventListener('click', closeMenu, true), 80); } btnExp.addEventListener('click', (e) => { e.stopPropagation(); _showExpMenu(); }); btnExp.title = 'Export / Import'; searchInput.addEventListener('input', () => { query = searchInput.value.trim(); render(); }); panel.addEventListener('tm-hist-refresh', render); panel.addEventListener('tm-hist-toggle-peek', () => { if (_dockPeekedGlobal) _retract(); else _peekOut(); }); const _panelAC = new AbortController(); const _acSignal = { signal: _panelAC.signal }; panel._tmCleanup = () => { _panelAC.abort(); }; let _dragging = false, _dx = 0, _dy = 0; titlebar.addEventListener('mousedown', (e) => { if (e.button !== 0 || e.target.classList.contains('tm-hist-icon-btn')) return; _dragging = true; _dx = e.clientX - panel.getBoundingClientRect().left; _dy = e.clientY - panel.getBoundingClientRect().top; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!_dragging) return; let nx = e.clientX - _dx; let ny = e.clientY - _dy; nx = Math.max(0, Math.min(nx, window.innerWidth - panel.offsetWidth)); ny = Math.max(0, Math.min(ny, window.innerHeight - 60)); panel.style.left = nx + 'px'; panel.style.top = ny + 'px'; }, _acSignal); document.addEventListener('mouseup', () => { if (!_dragging) return; _dragging = false; _savePos(); }, _acSignal); let _resizing = false, _rsx = 0, _rsy = 0, _rsw = 0, _rsh = 0; resizeHandle.addEventListener('mousedown', (e) => { _resizing = true; _rsx = e.clientX; _rsy = e.clientY; _rsw = panel.offsetWidth; _rsh = panel.offsetHeight; e.preventDefault(); e.stopPropagation(); }); document.addEventListener('mousemove', (e) => { if (!_resizing) return; const nw = Math.max(300, Math.min(_rsw + (e.clientX - _rsx), 680)); const nh = Math.max(280, Math.min(_rsh + (e.clientY - _rsy), window.innerHeight - 80)); panel.style.width = nw + 'px'; panel.style.height = nh + 'px'; }, _acSignal); document.addEventListener('mouseup', () => { if (!_resizing) return; _resizing = false; _savePos(); }, _acSignal); function _savePos() { const r = panel.getBoundingClientRect(); GM_setValue(KEY_HISTORY_PANEL_POS, JSON.stringify({ x: r.left, y: r.top, w: r.width, h: r.height })); } panel.addEventListener('click', e => e.stopPropagation()); function _dockTabGeometry() { const snap = _dockSnapshotGlobal; if (!snap) { const r = panel.getBoundingClientRect(); const h = Math.min(r.height * 0.55, 200); const top = r.top + (r.height - h) / 2; return { h, top }; } const h = Math.min(snap.height * 0.55, 200); const top = snap.top + (snap.height - h) / 2; return { h, top }; } function _buildDockTab(side) { const style = GM_getValue(KEY_DOCK_STYLE, 'ruler'); const { h, top } = _dockTabGeometry(); const triggerKey = side === 'left' ? KEY_DOCK_TRIGGER_L : KEY_DOCK_TRIGGER_R; const triggerDist = Math.max(6, parseInt(GM_getValue(triggerKey, '80'), 10) || 80); const tab = document.createElement('div'); tab.id = 'tm-hist-dock-tab'; tab.className = 'style-' + style; tab.style.cssText = [ 'top:' + top + 'px', 'height:' + h + 'px', side === 'left' ? 'left:2px' : 'right:2px', ].join(';'); const hotzone = document.createElement('div'); hotzone.style.cssText = [ 'position:absolute', 'top:0', 'height:100%', 'width:' + triggerDist + 'px', side === 'left' ? 'left:0' : 'right:0', 'cursor:pointer', 'z-index:1', ].join(';'); const _onHotEnter = () => { clearTimeout(_dockHoverTimerGlobal); const delay = parseInt(GM_getValue(KEY_DOCK_HOVER_DELAY, '500'), 10) || 500; _dockHoverTimerGlobal = setTimeout(() => _peekOut(), delay); }; const _onHotLeave = () => { clearTimeout(_dockHoverTimerGlobal); _dockHoverTimerGlobal = null; }; hotzone.addEventListener('mouseenter', _onHotEnter); hotzone.addEventListener('mouseleave', _onHotLeave); tab.addEventListener('mouseenter', _onHotEnter); tab.addEventListener('mouseleave', _onHotLeave); let _tabHoldTimer = null; let _tabHoldRAF = null; let _tabHoldElapsed = 0; const TAB_HOLD_MS = 2000; const _tabTick = () => { _tabHoldElapsed += 16; const pct = Math.min(_tabHoldElapsed / TAB_HOLD_MS, 1); tab.style.opacity = (0.4 + pct * 0.6).toFixed(2); if (pct < 1) _tabHoldRAF = requestAnimationFrame(_tabTick); }; const _cancelTabHold = () => { if (_tabHoldTimer) { clearTimeout(_tabHoldTimer); _tabHoldTimer = null; } if (_tabHoldRAF) { cancelAnimationFrame(_tabHoldRAF); _tabHoldRAF = null; } _tabHoldElapsed = 0; tab.style.opacity = ''; }; tab.addEventListener('mousedown', (e) => { if (e.button !== 0) return; e.stopPropagation(); _tabHoldElapsed = 0; _tabHoldTimer = setTimeout(() => { cancelAnimationFrame(_tabHoldRAF); _tabHoldRAF = null; _tabHoldTimer = null; tab.style.opacity = ''; _forceResetDock(); }, TAB_HOLD_MS); _tabHoldRAF = requestAnimationFrame(_tabTick); }); tab.addEventListener('mouseup', _cancelTabHold); tab.addEventListener('mouseleave', _cancelTabHold); tab.appendChild(hotzone); return tab; } function _dock(side) { if (_dockSideGlobal) return; _dockPeekedGlobal = false; const r = panel.getBoundingClientRect(); const vpW = window.innerWidth; _dockSnapshotGlobal = { left: r.left, top: r.top, width: r.width, height: r.height }; _dockSideGlobal = side; GM_setValue(KEY_DOCK_PERSISTED, side); const PEEK = 6; panel.style.left = side === 'left' ? (-r.width + PEEK) + 'px' : (vpW - PEEK) + 'px'; panel.classList.add('tm-docked'); if (_dockTabElGlobal) _dockTabElGlobal.remove(); _dockTabElGlobal = _buildDockTab(side); document.body.appendChild(_dockTabElGlobal); } function _peekOut() { if (!_dockSideGlobal || _dockPeekedGlobal) return; _dockPeekedGlobal = true; clearTimeout(_dockRetractTimerGlobal); _dockRetractTimerGlobal = null; const snap = _dockSnapshotGlobal; const vpW = window.innerWidth; const vpH = window.innerHeight; if (snap) { const safeTop = Math.max(0, Math.min(snap.top, vpH - snap.height - 10)); const OFFSET_LEFT = 15; const OFFSET_RIGHT = -15; if (_dockSideGlobal === 'left') { panel.style.left = OFFSET_LEFT + 'px'; } else { panel.style.left = (vpW - snap.width + OFFSET_RIGHT) + 'px'; } panel.style.top = safeTop + 'px'; } panel.classList.remove('tm-docked'); panel.classList.remove('tm-docked'); if (_dockTabElGlobal) _dockTabElGlobal.style.pointerEvents = 'none'; let bridge = document.getElementById('tm-hover-bridge'); if (!bridge) { bridge = document.createElement('div'); bridge.id = 'tm-hover-bridge'; bridge.style.cssText = 'position:absolute; top:0; bottom:0; width:15px; background:transparent; z-index:-1;'; panel.appendChild(bridge); } if (_dockSideGlobal === 'left') { bridge.style.left = '-10px'; bridge.style.right = ''; } else { bridge.style.right = '-10px'; bridge.style.left = ''; } const body = document.getElementById('tm-hist-body'); if (body && !body.hasChildNodes()) render(); } function _retract() { if (!_dockSideGlobal || !_dockPeekedGlobal) return; _dockPeekedGlobal = false; const r = panel.getBoundingClientRect(); const vpW = window.innerWidth; const PEEK = 6; panel.style.left = _dockSideGlobal === 'left' ? (-r.width + PEEK) + 'px' : (vpW - PEEK) + 'px'; panel.classList.add('tm-docked'); if (_dockTabElGlobal) _dockTabElGlobal.style.pointerEvents = ''; } function _exitDockMode() { clearTimeout(_dockHoverTimerGlobal); clearTimeout(_dockRetractTimerGlobal); _dockHoverTimerGlobal = null; _dockRetractTimerGlobal = null; const snap = _dockSnapshotGlobal; if (snap) { const safeLeft = Math.max(0, Math.min(snap.left, window.innerWidth - snap.width)); const safeTop = Math.max(0, Math.min(snap.top, window.innerHeight - 60)); panel.style.left = safeLeft + 'px'; panel.style.top = safeTop + 'px'; } panel.classList.remove('tm-docked'); _dockSideGlobal = null; _dockPeekedGlobal = false; _dockSnapshotGlobal = null; GM_setValue(KEY_DOCK_PERSISTED, ''); if (_dockTabElGlobal) { _dockTabElGlobal.remove(); _dockTabElGlobal = null; } } function _forceResetDock() { clearTimeout(_dockHoverTimerGlobal); clearTimeout(_dockRetractTimerGlobal); _dockHoverTimerGlobal = null; _dockRetractTimerGlobal = null; _dockSideGlobal = null; _dockPeekedGlobal = false; _dockSnapshotGlobal = null; if (_dockTabElGlobal) { _dockTabElGlobal.remove(); _dockTabElGlobal = null; } GM_setValue(KEY_DOCK_PERSISTED, ''); panel.classList.remove('tm-docked'); const pw = panel.offsetWidth || 390; const ph = panel.offsetHeight || 540; panel.style.left = Math.round((window.innerWidth - pw) / 2) + 'px'; panel.style.top = Math.round((window.innerHeight - ph) / 4) + 'px'; render(); showToast('🔓 Dock reset — panel restored'); } dockTriggerL.addEventListener('click', (e) => { e.stopPropagation(); if (_dockSideGlobal === 'left') { _exitDockMode(); } else if (_dockSideGlobal === 'right') { showToast('⚠️ Undock right side first before docking left'); } else { _dock('left'); } }); dockTriggerR.addEventListener('click', (e) => { e.stopPropagation(); if (_dockSideGlobal === 'right') { _exitDockMode(); } else if (_dockSideGlobal === 'left') { showToast('⚠️ Undock left side first before docking right'); } else { _dock('right'); } }); panel.addEventListener('mouseleave', () => { if (!_dockSideGlobal || !_dockPeekedGlobal) return; clearTimeout(_dockRetractTimerGlobal); _dockRetractTimerGlobal = setTimeout(() => _retract(), 120); }); panel.addEventListener('mouseenter', () => { if (!_dockSideGlobal) return; clearTimeout(_dockRetractTimerGlobal); _dockRetractTimerGlobal = null; }); const _origCleanup = panel._tmCleanup; panel._tmCleanup = () => { _origCleanup(); clearTimeout(_dockHoverTimerGlobal); clearTimeout(_dockRetractTimerGlobal); _dockHoverTimerGlobal = null; _dockRetractTimerGlobal = null; if (_dockTabElGlobal) { _dockTabElGlobal.remove(); _dockTabElGlobal = null; } }; if (_dockSideGlobal) { requestAnimationFrame(() => { const side = _dockSideGlobal; _dockSideGlobal = null; _dock(side); requestAnimationFrame(() => { panel.style.transition = ''; panel.style.opacity = ''; }); }); } render(); } function _cleanZoom() { document.getElementById('tm-hist-zoom')?.remove(); } function fireMeteor(fromEl) { const histBtn = document.getElementById('tm-history-btn'); if (!histBtn || !fromEl) return; const fromRect = fromEl.getBoundingClientRect(); const toRect = histBtn.getBoundingClientRect(); const fromX = fromRect.left + fromRect.width / 2; const fromY = fromRect.top + fromRect.height / 2; const toX = toRect.left + toRect.width / 2; const toY = toRect.top + toRect.height / 2; const dx = toX - fromX; const dy = toY - fromY; const dist = Math.sqrt(dx * dx + dy * dy); const angle = Math.atan2(dy, dx) * 180 / Math.PI; const tailLen = Math.min(Math.max(dist * 0.24, 16), 40); const container = document.createElement('div'); container.style.cssText = ` position: fixed; left: ${fromX}px; top: ${fromY}px; width: 0; height: 0; pointer-events: none; z-index: 99999999; `; const tail = document.createElement('div'); tail.style.cssText = ` position: absolute; width: ${tailLen * 1.2}px; height: 6px; border-radius: 9999px; background: linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,220,100,0.15) 30%, rgba(255,200,80,0.3) 70%, rgba(255,240,150,0.25)100%); box-shadow: 0 0 4px 1px rgba(255,200,100,0.3); filter: drop-shadow(0 0 3px rgba(255,200,100,0.2)); transform: translateX(-100%) translateY(-50%) rotate(${angle}deg); transform-origin: 100% 50%; `; const tailGlow = document.createElement('div'); tailGlow.style.cssText = ` position: absolute; width: ${tailLen}px; height: 10px; border-radius: 9999px; background: linear-gradient(90deg, rgba(255,240,200,0) 0%, rgba(255,220,150,0.15) 50%, rgba(255,240,200,0) 100%); filter: blur(1.5px); transform: translateX(-100%) translateY(-50%) rotate(${angle}deg); transform-origin: 100% 50%; opacity: 0.4; `; const dot = document.createElement('div'); dot.style.cssText = ` position: absolute; width: 9px; height: 9px; border-radius: 50%; background: radial-gradient(circle at 40% 40%, #ffffdd, #ffeed4); box-shadow: 0 0 5px 2px rgba(255,220,150,0.4), 0 0 10px 4px rgba(255,200,100,0.15); transform: translate(-50%, -50%); `; const particles = []; for (let i = 0; i < 4; i++) { const particle = document.createElement('div'); const offsetX = (Math.random() - 0.5) * tailLen * 0.4; const offsetY = (Math.random() - 0.5) * 8; const size = 1.5 + Math.random() * 2; particle.style.cssText = ` position: absolute; width: ${size}px; height: ${size}px; border-radius: 50%; background: rgba(255, 240, 180, ${0.3 + Math.random() * 0.3}); left: ${offsetX}px; top: ${offsetY}px; filter: blur(0.5px); box-shadow: 0 0 ${size + 1}px rgba(255,220,150,0.3); `; particles.push(particle); } container.appendChild(tailGlow); container.appendChild(tail); particles.forEach(p => container.appendChild(p)); container.appendChild(dot); document.body.appendChild(container); const DURATION = 680; const anim = container.animate([ { transform: 'translate(0px, 0px)', opacity: 0, offset: 0 }, { transform: 'translate(0px, 0px)', opacity: 1, offset: 0.04 }, { transform: `translate(${dx}px, ${dy}px)`, opacity: 1, offset: 0.88 }, { transform: `translate(${dx}px, ${dy}px)`, opacity: 0, offset: 1 }, ], { duration: DURATION, easing: 'cubic-bezier(0.28, 0.0, 0.72, 1.0)', fill: 'forwards', }); anim.onfinish = () => { container.remove(); _triggerHistAbsorb(); }; } let _absorbTimer = null; function _triggerHistAbsorb() { const wrapper = document.getElementById('tm-settings-wrapper'); const histBtn = document.getElementById('tm-history-btn'); if (!wrapper || !histBtn) return; if (_absorbTimer) clearTimeout(_absorbTimer); wrapper.setAttribute('data-focus', 'hist'); wrapper.setAttribute('data-absorb', 'true'); histBtn.classList.remove('tm-absorbing'); void histBtn.offsetWidth; histBtn.classList.add('tm-absorbing'); _absorbTimer = setTimeout(() => { wrapper.removeAttribute('data-absorb'); histBtn.classList.remove('tm-absorbing'); _absorbTimer = null; }, 2500); } const BUTTON_CLASS = 'force-media-copy-btn'; const style = document.createElement('style'); style.textContent = ` .${BUTTON_CLASS}, .custom-copy-icon { display: inline-flex; align-items: center; justify-content: center; background: transparent; border: none; font-size: 11px; min-width: 36px; width: 36px; height: 36px; padding: 8px; box-sizing: border-box; opacity: 0.75; cursor: pointer; margin-left: 4px; transition: opacity 0.2s, filter 0.2s; color: #536471; flex-shrink: 0; } @media (prefers-color-scheme: dark) { .${BUTTON_CLASS}, .custom-copy-icon { color: #71767b; } } .${BUTTON_CLASS} svg, .custom-copy-icon svg { width: 20px; height: 20px; display: block; overflow: visible; flex-shrink: 0; } .${BUTTON_CLASS}:hover { opacity: 1.0; } .custom-copy-icon:hover { opacity: 1.0; filter: drop-shadow(0 0 4px currentColor); } `; document.head.appendChild(style); const _toastStyle = document.createElement('style'); _toastStyle.textContent = ` @keyframes tm-toast-rise { 0% { opacity: 0; transform: translate(-50%, -100%) scale(0.85); } 15% { opacity: 1; transform: translate(-50%, -118%) scale(1); } 70% { opacity: 1; transform: translate(-50%, -135%) scale(1); } 100% { opacity: 0; transform: translate(-50%, -152%) scale(0.95); } } .tm-action-toast { position: fixed; white-space: nowrap; pointer-events: none; transform: translate(-50%, -100%); background: rgba(29,155,240,0.92); color: #fff; font: 600 11px/1 system-ui,-apple-system,sans-serif; padding: 4px 9px; border-radius: 9999px; box-shadow: 0 2px 8px rgba(0,0,0,0.25); z-index: 9999999; animation: tm-toast-rise 1.6s cubic-bezier(0.22,1,0.36,1) forwards; } .tm-action-toast.warn { background: rgba(255,140,0,0.92); } .tm-action-toast.error { background: rgba(224,36,94,0.92); } @keyframes tm-spin { to { transform: rotate(360deg); } } .tm-dl-ring { display:inline-flex; align-items:center; justify-content:center; width:20px; height:20px; flex-shrink:0; } .tm-dl-ring svg { overflow:visible; } .tm-dl-ring .tm-bg { stroke: rgba(128,128,128,0.28); } .tm-dl-ring .tm-fg { stroke: currentColor; transition: stroke-dashoffset 0.15s linear; transform-origin: 10px 10px; } .tm-dl-ring.indeterminate .tm-fg { animation: tm-spin 0.85s linear infinite; } `; document.head.appendChild(_toastStyle); function showActionToast(anchorEl, message, type = 'ok') { if (GM_getValue(KEY_FEEDBACK_STYLE, 'toast') === 'silent') return; const rect = anchorEl.getBoundingClientRect(); const viewW = window.innerWidth; const cx = Math.max(48, Math.min(rect.left + rect.width / 2, viewW - 48)); const toast = document.createElement('span'); toast.className = 'tm-action-toast' + (type !== 'ok' ? ` ${type}` : ''); toast.textContent = message; toast.style.left = cx + 'px'; toast.style.top = rect.top + 'px'; document.body.appendChild(toast); toast.addEventListener('animationend', () => toast.remove(), { once: true }); } function createProgressRing() { const R = 8, C = +(2 * Math.PI * R).toFixed(4); const el = document.createElement('span'); el.className = 'tm-dl-ring indeterminate'; el.innerHTML = ` `; const fg = el.querySelector('.tm-fg'); const update = (pct) => { if (pct === null) { el.classList.add('indeterminate'); fg.style.strokeDashoffset = C; } else { el.classList.remove('indeterminate'); fg.style.strokeDashoffset = C * (1 - Math.max(0, Math.min(1, pct / 100))); } }; return { el, update, remove: () => el.remove() }; } async function forceDownloadBlob(url, filename, onProgress) { try { const resp = await unsafeWindow.fetch(url, { credentials: 'omit' }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const contentLength = parseInt(resp.headers.get('content-length') || '0', 10); const reader = resp.body.getReader(); const chunks = []; let received = 0; if (onProgress) onProgress(contentLength > 0 ? 0 : null); while (true) { const { value, done } = await reader.read(); if (done) break; chunks.push(value); received += value.length; if (onProgress && contentLength > 0) { onProgress(Math.round(received / contentLength * 100)); } } if (onProgress) onProgress(100); const blob = new Blob(chunks, { type: resp.headers.get('content-type') || 'application/octet-stream' }); const blobUrl = (window.URL || window.webkitURL).createObjectURL(blob); try { const tag = document.createElement('a'); tag.href = blobUrl; tag.download = filename; document.body.appendChild(tag); tag.click(); document.body.removeChild(tag); } finally { setTimeout(() => (window.URL || window.webkitURL).revokeObjectURL(blobUrl), 8000); } return; } catch (fetchErr) { console.warn('[MediaDL] fetch stream failed, falling back to GM_xmlhttpRequest:', fetchErr); } await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, responseType: "blob", onload: function(response) { if (response.status === 200) { const blob = response.response; const urlCreator = window.URL || window.webkitURL; const blobUrl = urlCreator.createObjectURL(blob); try { const tag = document.createElement('a'); tag.href = blobUrl; tag.download = filename; document.body.appendChild(tag); tag.click(); document.body.removeChild(tag); } finally { setTimeout(() => urlCreator.revokeObjectURL(blobUrl), 8000); } resolve(); } else { reject(new Error(`GM fallback HTTP ${response.status}`)); } }, onerror: function(err) { console.error('[MediaDL] GM fallback also failed:', err); const tag = document.createElement('a'); tag.href = url; tag.download = filename; tag.target = '_blank'; document.body.appendChild(tag); tag.click(); document.body.removeChild(tag); resolve(); } }); }); } function showFloatingVideoPlayer(videoUrls, startIndex = 0, imageUrls = null) { document.querySelectorAll('video, audio').forEach(media => { if (!media.paused) media.pause(); }); const oldModal = document.getElementById('tm-floating-video-modal'); if (oldModal) oldModal.remove(); const total = videoUrls.length; let currentIndex = Math.max(0, Math.min(startIndex, total - 1)); if (!document.getElementById('tm-vp-style')) { const s = document.createElement('style'); s.id = 'tm-vp-style'; s.textContent = ` #tm-floating-video-modal { opacity: 0; transition: opacity 0.22s ease; } #tm-floating-video-modal.tm-vp-visible { opacity: 1; } #tm-floating-video-modal video { opacity: 0; transform: scale(0.88) translateZ(0); transition: opacity 0.30s ease 0.10s, transform 0.38s cubic-bezier(0.22,1,0.36,1) 0.08s; will-change: transform, opacity; } #tm-floating-video-modal.tm-vp-visible video { opacity: 1; transform: scale(1) translateZ(0); } `; document.head.appendChild(s); } const modal = document.createElement('div'); modal.id = 'tm-floating-video-modal'; modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.95); z-index: 9999999; display: flex; align-items: center; justify-content: center; overscroll-behavior: contain; `; modal.addEventListener('wheel', e => e.preventDefault(), { passive: false }); modal.addEventListener('touchmove', e => e.preventDefault(), { passive: false }); const video = document.createElement('video'); video.controls = true; video.autoplay = true; video.volume = Math.max(0, Math.min(1, parseFloat(GM_getValue(KEY_VIDEO_VOLUME, '1')) || 1)); video.addEventListener('volumechange', () => { GM_setValue(KEY_VIDEO_VOLUME, String(video.volume)); }); video.style.cssText = ` max-width: 88%; max-height: 88%; border-radius: 8px; box-shadow: 0 10px 50px rgba(0,0,0,0.8); background: #000; outline: none; `; const counter = document.createElement('div'); counter.style.cssText = ` position: absolute; top: 20px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.55); backdrop-filter: blur(6px); color: rgba(255,255,255,0.85); padding: 4px 14px; border-radius: 9999px; font: 13px/1.5 system-ui, sans-serif; z-index: 3; pointer-events: none; white-space: nowrap; display: ${total > 1 ? 'block' : 'none'}; `; const NAV_BASE = ` position: absolute; top: 50%; transform: translateY(-50%); background: rgba(0,0,0,0.55); backdrop-filter: blur(4px); color: white; border: none; width: 46px; height: 46px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background 0.2s, opacity 0.2s; z-index: 3; `; const SVG_PREV = ``; const SVG_NEXT = ``; const prevBtn = document.createElement('button'); prevBtn.innerHTML = SVG_PREV; prevBtn.style.cssText = NAV_BASE + 'left: 20px;'; prevBtn.onmouseenter = () => prevBtn.style.background = 'rgba(255,255,255,0.25)'; prevBtn.onmouseleave = () => prevBtn.style.background = 'rgba(0,0,0,0.55)'; const nextBtn = document.createElement('button'); nextBtn.innerHTML = SVG_NEXT; nextBtn.style.cssText = NAV_BASE + 'right: 20px;'; nextBtn.onmouseenter = () => nextBtn.style.background = 'rgba(255,255,255,0.25)'; nextBtn.onmouseleave = () => nextBtn.style.background = 'rgba(0,0,0,0.55)'; const closeBtn = document.createElement('button'); closeBtn.innerHTML = ``; closeBtn.style.cssText = ` position: absolute; top: 20px; right: 25px; background: rgba(0,0,0,0.6); color: white; border: none; width: 40px; height: 40px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background 0.2s; z-index: 4; `; closeBtn.onmouseenter = () => closeBtn.style.background = 'rgba(255,255,255,0.3)'; closeBtn.onmouseleave = () => closeBtn.style.background = 'rgba(0,0,0,0.6)'; const viewImgBtn = imageUrls && imageUrls.length ? document.createElement('button') : null; if (viewImgBtn) { viewImgBtn.innerHTML = '🖼️ Images'; viewImgBtn.style.cssText = ` position: absolute; top: 20px; right: 75px; background: rgba(29,155,240,0.8); color: white; border: none; padding: 6px 16px; border-radius: 9999px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background 0.2s; z-index: 4; font: 13px/1.5 system-ui, sans-serif; `; viewImgBtn.onmouseenter = () => viewImgBtn.style.background = 'rgba(29,155,240,1)'; viewImgBtn.onmouseleave = () => viewImgBtn.style.background = 'rgba(29,155,240,0.8)'; viewImgBtn.onclick = (e) => { e.stopPropagation(); closeModal(); showImageLightbox(imageUrls, videoUrls); }; } function updatePlayer() { video.src = videoUrls[currentIndex]; video.play().catch(() => {}); if (total > 1) { counter.textContent = `${currentIndex + 1} / ${total}`; prevBtn.style.opacity = currentIndex === 0 ? '0.3' : '1'; nextBtn.style.opacity = currentIndex === total - 1 ? '0.3' : '1'; prevBtn.style.pointerEvents = currentIndex === 0 ? 'none' : 'auto'; nextBtn.style.pointerEvents = currentIndex === total - 1 ? 'none' : 'auto'; } } prevBtn.onclick = e => { e.stopPropagation(); if (currentIndex > 0) { currentIndex--; updatePlayer(); } }; nextBtn.onclick = e => { e.stopPropagation(); if (currentIndex < total - 1) { currentIndex++; updatePlayer(); } }; const closeModal = () => { modal.classList.remove('tm-vp-visible'); video.pause(); setTimeout(() => { modal.remove(); }, 220); document.removeEventListener('keydown', keyHandler); }; closeBtn.onclick = closeModal; modal.onclick = (e) => { if (e.target === modal) closeModal(); }; const keyHandler = (e) => { if (e.key === 'Escape') { closeModal(); return; } if ((e.key === 'ArrowRight' || e.key === 'ArrowDown') && currentIndex < total - 1) { currentIndex++; updatePlayer(); } if ((e.key === 'ArrowLeft' || e.key === 'ArrowUp') && currentIndex > 0) { currentIndex--; updatePlayer(); } }; document.addEventListener('keydown', keyHandler); modal.appendChild(video); modal.appendChild(closeBtn); if (viewImgBtn) modal.appendChild(viewImgBtn); modal.appendChild(counter); if (total > 1) { modal.appendChild(prevBtn); modal.appendChild(nextBtn); } document.body.appendChild(modal); updatePlayer(); requestAnimationFrame(() => requestAnimationFrame(() => { modal.classList.add('tm-vp-visible'); })); } function showImageLightbox(urls, videoUrls = null) { if (!urls.length) return; const old = document.getElementById('tm-image-lightbox'); if (old) old.remove(); if (!document.getElementById('tm-lb-style')) { const s = document.createElement('style'); s.id = 'tm-lb-style'; s.textContent = ` #tm-image-lightbox { opacity: 0; transition: opacity 0.22s ease; } #tm-image-lightbox.tm-lb-in { opacity: 1; } #tm-image-lightbox .tm-lb-single-img { opacity: 0; transform: scale(0.88) translateZ(0); transition: opacity 0.32s ease 0.08s, transform 0.40s cubic-bezier(0.22,1,0.36,1) 0.06s; will-change: transform, opacity; } #tm-image-lightbox.tm-lb-in .tm-lb-single-img { opacity: 1; transform: scale(1) translateZ(0); } #tm-image-lightbox .tm-lb-card.tm-lb-animated { transition: transform 0.38s cubic-bezier(0.34,1.18,0.64,1), opacity 0.26s ease; will-change: transform, opacity; } #tm-image-lightbox .tm-lb-card { box-shadow: 0 12px 36px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.07); } #tm-image-lightbox .tm-lb-card.tm-lb-focused { box-shadow: 0 28px 80px rgba(0,0,0,0.9), 0 0 0 1px rgba(255,255,255,0.18); } `; document.head.appendChild(s); } const total = urls.length; const isSingleImage = total === 1; let focused = 0; let _rafId = null; const VW = window.innerWidth; const VH = window.innerHeight; const CARD_W = Math.min(VW * 0.50, 580); const CARD_H = Math.min(VH * 0.88, 1000); const SPREAD = Math.min(CARD_W * 0.40, 200); function calcTransform(pos) { const abs = Math.abs(pos); return { dx: pos * SPREAD, rot: pos * 9, scale: Math.max(0.68, 1 - abs * 0.12), zIndex: 20 - abs * 2, opacity: abs >= 3 ? 0.5 : 1, focused: pos === 0, }; } const modal = document.createElement('div'); modal.id = 'tm-image-lightbox'; modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.95); z-index: 9999999; display: flex; align-items: center; justify-content: center; overflow: hidden; overscroll-behavior: contain; `; modal.addEventListener('wheel', e => e.preventDefault(), { passive: false }); modal.addEventListener('touchmove', e => e.preventDefault(), { passive: false }); function closeLightbox() { modal.classList.remove('tm-lb-in'); setTimeout(() => { modal.remove(); }, 220); document.removeEventListener('keydown', keyHandler); } if (isSingleImage) { const container = document.createElement('div'); container.style.cssText = ` position: relative; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; `; const img = document.createElement('img'); img.src = urls[0]; img.draggable = false; img.className = 'tm-lb-single-img'; img.style.cssText = ` max-width: 95vw; max-height: 95vh; object-fit: contain; display: block; background: transparent; pointer-events: none; user-select: none; -webkit-user-drag: none; `; container.appendChild(img); modal.appendChild(container); const closeBtn = document.createElement('button'); closeBtn.innerHTML = ``; closeBtn.style.cssText = ` position: absolute; top: 20px; right: 25px; background: rgba(0,0,0,0.6); color: white; border: none; width: 40px; height: 40px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background 0.2s; z-index: 30; `; closeBtn.onmouseenter = () => closeBtn.style.background = 'rgba(255,255,255,0.25)'; closeBtn.onmouseleave = () => closeBtn.style.background = 'rgba(0,0,0,0.6)'; closeBtn.onclick = closeLightbox; modal.onclick = e => { if (e.target === modal || e.target === container) closeLightbox(); }; modal.appendChild(closeBtn); if (videoUrls && videoUrls.length) { const viewVidBtn = document.createElement('button'); viewVidBtn.innerHTML = '▶️ Videos'; viewVidBtn.style.cssText = ` position: absolute; top: 20px; right: 75px; background: rgba(29,155,240,0.8); color: white; border: none; padding: 6px 16px; border-radius: 9999px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background 0.2s; z-index: 30; font: 13px/1.5 system-ui, sans-serif; `; viewVidBtn.onmouseenter = () => viewVidBtn.style.background = 'rgba(29,155,240,1)'; viewVidBtn.onmouseleave = () => viewVidBtn.style.background = 'rgba(29,155,240,0.8)'; viewVidBtn.onclick = e => { e.stopPropagation(); closeLightbox(); showFloatingVideoPlayer(videoUrls, 0, urls); }; modal.appendChild(viewVidBtn); } const keyHandler = e => { if (e.key === 'Escape') closeLightbox(); }; document.addEventListener('keydown', keyHandler); document.body.appendChild(modal); requestAnimationFrame(() => requestAnimationFrame(() => { modal.classList.add('tm-lb-in'); })); return; } const stage = document.createElement('div'); stage.style.cssText = ` position: relative; width: ${CARD_W}px; height: ${CARD_H}px; flex-shrink: 0; overflow: visible; `; const cards = urls.map((url, i) => { const card = document.createElement('div'); card.className = 'tm-lb-card'; card.style.cssText = ` position: absolute; left: 0; top: 0; width: ${CARD_W}px; height: ${CARD_H}px; border-radius: 14px; overflow: hidden; background: radial-gradient(ellipse at 50% 38%, #1e1e1e 0%, #0a0a0a 100%); cursor: pointer; transform: translateY(${VH * 0.6}px) scale(0.72) translateZ(0); opacity: 0; z-index: ${i}; `; const img = document.createElement('img'); img.src = url; img.loading = i === 0 ? 'eager' : 'lazy'; img.draggable = false; img.style.cssText = ` width: 100%; height: 100%; object-fit: contain; display: block; background: transparent; pointer-events: none; user-select: none; -webkit-user-drag: none; `; card.appendChild(img); card.addEventListener('click', e => { e.stopPropagation(); if (i === focused) return; focused = i; scheduleUpdate(); }); stage.appendChild(card); return card; }); const dotsWrap = document.createElement('div'); dotsWrap.style.cssText = ` position: absolute; bottom: 22px; left: 50%; transform: translateX(-50%); display: flex; gap: 8px; z-index: 30; `; const dots = urls.map((_, i) => { const dot = document.createElement('div'); dot.style.cssText = ` width: 7px; height: 7px; border-radius: 50%; background: rgba(255,255,255,0.35); cursor: pointer; transition: background 0.22s, transform 0.22s; `; dot.addEventListener('click', e => { e.stopPropagation(); focused = i; scheduleUpdate(); }); dotsWrap.appendChild(dot); return dot; }); const counter = document.createElement('div'); counter.style.cssText = ` position: absolute; top: 20px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.55); backdrop-filter: blur(6px); color: rgba(255,255,255,0.85); padding: 4px 14px; border-radius: 9999px; font: 13px/1.5 system-ui, sans-serif; z-index: 30; pointer-events: none; white-space: nowrap; `; const closeBtn = document.createElement('button'); closeBtn.innerHTML = ``; closeBtn.style.cssText = ` position: absolute; top: 20px; right: 25px; background: rgba(0,0,0,0.6); color: white; border: none; width: 40px; height: 40px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background 0.2s; z-index: 30; `; closeBtn.onmouseenter = () => closeBtn.style.background = 'rgba(255,255,255,0.25)'; closeBtn.onmouseleave = () => closeBtn.style.background = 'rgba(0,0,0,0.6)'; closeBtn.onclick = closeLightbox; modal.onclick = e => { if (e.target === modal) closeLightbox(); }; const viewVidBtn = videoUrls && videoUrls.length ? document.createElement('button') : null; if (viewVidBtn) { viewVidBtn.innerHTML = '▶️ Videos'; viewVidBtn.style.cssText = ` position: absolute; top: 20px; right: 75px; background: rgba(29,155,240,0.8); color: white; border: none; padding: 6px 16px; border-radius: 9999px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background 0.2s; z-index: 30; font: 13px/1.5 system-ui, sans-serif; `; viewVidBtn.onmouseenter = () => viewVidBtn.style.background = 'rgba(29,155,240,1)'; viewVidBtn.onmouseleave = () => viewVidBtn.style.background = 'rgba(29,155,240,0.8)'; viewVidBtn.onclick = e => { e.stopPropagation(); closeLightbox(); showFloatingVideoPlayer(videoUrls, 0, urls); }; } const keyHandler = e => { if (e.key === 'Escape') { closeLightbox(); return; } if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { focused = Math.min(focused + 1, total - 1); scheduleUpdate(); } if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { focused = Math.max(focused - 1, 0); scheduleUpdate(); } }; document.addEventListener('keydown', keyHandler); function scheduleUpdate() { if (_rafId) return; _rafId = requestAnimationFrame(() => { _rafId = null; _flushUpdate(); }); } function _flushUpdate() { cards.forEach((card, i) => { const { dx, rot, scale, zIndex, opacity, focused: isFocused } = calcTransform(i - focused); card.style.transform = `translateX(${dx}px) rotate(${rot}deg) scale(${scale}) translateZ(0)`; card.style.zIndex = zIndex; card.style.opacity = opacity; card.style.cursor = isFocused ? 'default' : 'pointer'; card.classList.toggle('tm-lb-focused', isFocused); }); dots.forEach((dot, i) => { dot.style.background = i === focused ? 'rgba(255,255,255,0.95)' : 'rgba(255,255,255,0.35)'; dot.style.transform = i === focused ? 'scale(1.4)' : 'scale(1)'; }); counter.textContent = `${focused + 1} / ${total}`; } modal.appendChild(stage); modal.appendChild(closeBtn); if (viewVidBtn) modal.appendChild(viewVidBtn); if (total > 1) { modal.appendChild(dotsWrap); modal.appendChild(counter); } document.body.appendChild(modal); requestAnimationFrame(() => requestAnimationFrame(() => { modal.classList.add('tm-lb-in'); cards.forEach((card, i) => { const delay = i * 55; setTimeout(() => { card.style.transition = ` transform 0.42s cubic-bezier(0.22,1,0.36,1) ${delay}ms, opacity 0.28s ease ${delay}ms `; card.style.transform = `translateX(0) rotate(0deg) scale(1) translateZ(0)`; card.style.opacity = '1'; card.style.zIndex = 20 - Math.abs(i); }, 0); }); const totalStagger = (total - 1) * 55 + 420; setTimeout(() => { cards.forEach(card => card.classList.add('tm-lb-animated')); _flushUpdate(); }, totalStagger); })); } function formatDate(dateInput) { try { if (!dateInput) return '0000.00.00'; const date = new Date(dateInput); if (isNaN(date.getTime())) return '0000.00.00'; const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, '0'); const d = String(date.getDate()).padStart(2, '0'); return _cachedDateFormat === 'western' ? `${d}.${m}.${y}` : `${y}.${m}.${d}`; } catch (e) { return '0000.00.00'; } } function sanitizeForFilename(text, maxLength = 50) { if (!text) return ""; let clean = text.replace(/[\r\n]+/g, ' '); clean = clean.replace(/[\\/:*?"<>|#%&]/g, ''); clean = clean.replace(/[\u0000-\u001f\u007f\uff0f\uff3c\uff1a\uff0a\uff1f\uff02\uff1c\uff1e\uff5c\u200b\u200c\u200d\uFEFF]/g, ''); clean = clean.replace(/\.+$/, '').replace(/^\.+/, ''); clean = clean.trim(); if (clean.length > maxLength) clean = clean.substring(0, maxLength).trimEnd().replace(/\.+$/, ''); if (!clean) return '_'; if (/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i.test(clean)) clean = '_' + clean; return clean; } function getTweetInfo(article) { let date = '0000.00.00'; let id = '0000'; let screenName = 'unknown'; let displayName = 'User'; let tweetText = ''; const timeEl = article.querySelector('time'); if (timeEl) date = formatDate(timeEl.getAttribute('datetime')); else date = formatDate(new Date()); const allLinks = article.querySelectorAll('a[href*="/status/"]'); for (const link of allLinks) { const href = link.getAttribute('href'); const match = href.match(/\/([a-zA-Z0-9_]+)\/status\/(\d+)/); if (match) { screenName = match[1]; id = match[2]; break; } } const textNode = article.querySelector('[data-testid="tweetText"]'); if (textNode) tweetText = textNode.innerText || ""; try { const userBlock = article.querySelector('[data-testid="User-Name"]'); if (userBlock) { const lines = userBlock.innerText.split('\n'); if (lines.length >= 1) displayName = lines[0].trim(); if (screenName === 'unknown' && lines.length >= 2) { const handle = lines.find(l => l.startsWith('@')); if (handle) screenName = handle.replace('@', ''); } } } catch(e) { console.warn('[MediaDL] Failed to extract displayName:', e); } if (id === '0000' && screenName === 'unknown') { id = "Ad_" + Date.now().toString().slice(-6); screenName = 'Promoted'; } let videoThumb = null; const posterVid = article.querySelector('video[poster]'); if (posterVid) { videoThumb = posterVid.getAttribute('poster'); } else { const thumbImg = article.querySelector('img[src*="video_thumb"], img[src*="ext_tw_video_thumb"], img[src*="amplify_video_thumb"]'); if (thumbImg) videoThumb = thumbImg.src; } return { screenName: screenName, displayName: displayName, id: id, date: date, text: sanitizeForFilename(tweetText), videoThumb: videoThumb }; } function extractFiberNode(node) { const key = Object.keys(node).find(k => k.startsWith("__reactFiber")); return key ? node[key] : null; } const _apiVideoCache = new Map(); const _API_CACHE_TTL = 300_000; function _parseVideosFromTweetResult(tweetResult) { try { const core = tweetResult?.result ?? tweetResult; const tweetId = core?.legacy?.id_str ?? core?.tweet?.legacy?.id_str; if (!tweetId) return null; let mp4Urls = []; const searchVariants = (obj, depth = 0) => { if (depth > 12 || !obj || typeof obj !== 'object') return; if (Array.isArray(obj.variants)) { const mp4s = obj.variants.filter(v => v.content_type === 'video/mp4'); if (mp4s.length > 0) { const best = mp4s.sort((a, b) => (b.bitrate || 0) - (a.bitrate || 0))[0]; if (best?.url) mp4Urls.push(best.url.split('?')[0]); } } if (typeof obj.value === 'string' && obj.value.startsWith('{') && obj.value.includes('"video/mp4"')) { try { const parsedCard = JSON.parse(obj.value); searchVariants(parsedCard, depth + 1); } catch(e) {} } for (const key in obj) { if (obj[key] && typeof obj[key] === 'object') { searchVariants(obj[key], depth + 1); } } }; searchVariants(core); if (mp4Urls.length > 0) { return { id: tweetId, urls: [...new Set(mp4Urls)] }; } } catch (_) {} return null; } function _processApiPayload(text) { try { const json = JSON.parse(text); const ts = Date.now(); const walk = (obj, depth) => { if (!obj || typeof obj !== 'object' || depth > 40) return; if (obj.tweet_results) { const parsed = _parseVideosFromTweetResult(obj.tweet_results); if (parsed) _apiVideoCache.set(parsed.id, { urls: parsed.urls, ts }); } if (obj.tweetResult) { const parsed = _parseVideosFromTweetResult(obj.tweetResult); if (parsed) _apiVideoCache.set(parsed.id, { urls: parsed.urls, ts }); } if (obj.retweeted_status_result) { const parsed = _parseVideosFromTweetResult(obj.retweeted_status_result); if (parsed) _apiVideoCache.set(parsed.id, { urls: parsed.urls, ts }); } if (obj.quoted_status_result) { const parsed = _parseVideosFromTweetResult(obj.quoted_status_result); if (parsed) _apiVideoCache.set(parsed.id, { urls: parsed.urls, ts }); } for (const v of Object.values(obj)) { if (v && typeof v === 'object') walk(v, depth + 1); } }; walk(json, 0); if (_apiVideoCache.size > 200) { const now = Date.now(); for (const [id, entry] of _apiVideoCache) { if (now - entry.ts >= _API_CACHE_TTL) _apiVideoCache.delete(id); } } } catch (_) {} } (function _interceptFetch() { const _origFetch = unsafeWindow.fetch; unsafeWindow.fetch = function(...args) { const url = typeof args[0] === 'string' ? args[0] : args[0]?.url ?? ''; const isGraphQL = url.includes('/graphql/') && ( url.includes('HomeTimeline') || url.includes('TweetDetail') || url.includes('UserTweets') || url.includes('SearchTimeline') || url.includes('ListTimeline') || url.includes('TweetResultByRestId') ); const promise = _origFetch.apply(this, args); if (!isGraphQL) return promise; return promise.then(resp => { const clone = resp.clone(); clone.text().then(_processApiPayload).catch(() => {}); return resp; }); }; })(); const _fiberVideoCache = new WeakMap(); const _FIBER_CACHE_TTL = 60_000; const _fiberImageCache = new WeakMap(); function _collectCandidateIds(article) { const ids = new Set(); article.querySelectorAll('a[href*="/status/"]').forEach(a => { const m = a.getAttribute('href')?.match(/\/status\/(\d+)/); if (m) ids.add(m[1]); }); article.querySelectorAll('video[poster]').forEach(v => { const m = v.getAttribute('poster')?.match(/(?:amplify_video_thumb|ext_tw_video_thumb|tweet_video_thumb)\/(\d+)/); if (m) ids.add(m[1]); }); article.querySelectorAll('img[src*="video_thumb"]').forEach(img => { const m = img.getAttribute('src')?.match(/(?:amplify_video_thumb|ext_tw_video_thumb|tweet_video_thumb)\/(\d+)/); if (m) ids.add(m[1]); }); return ids; } function _lookupApiCache(ids) { for (const id of ids) { const entry = _apiVideoCache.get(id); if (entry && (Date.now() - entry.ts < _API_CACHE_TTL)) return entry.urls; } return null; } async function fetchTweetMediaFromAPI(statusId) { try { let cookies = {}; document.cookie.split(';').forEach(c => { let [k, v] = c.split('='); if (k && v) cookies[k.trim()] = v.trim(); }); const AUTH_TOKEN = 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA'; const variables = {"tweetId":statusId,"with_rux_injections":false,"includePromotedContent":true,"withCommunity":true,"withQuickPromoteEligibilityTweetFields":true,"withBirdwatchNotes":true,"withVoice":true,"withV2Timeline":true}; const features = {"articles_preview_enabled":true,"c9s_tweet_anatomy_moderator_badge_enabled":true,"freedom_of_speech_not_reach_fetch_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"longform_notetweets_inline_media_enabled":true,"responsive_web_twitter_article_tweet_consumption_enabled":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":true,"view_counts_everywhere_api_enabled":true}; let url = `https://${location.hostname}/i/api/graphql/2ICDjqPd81tulZcYrtpTuQ/TweetResultByRestId?variables=${encodeURIComponent(JSON.stringify(variables))}&features=${encodeURIComponent(JSON.stringify(features))}`; let headers = { 'authorization': AUTH_TOKEN, 'x-twitter-active-user': 'yes' }; if (cookies.ct0) headers['x-csrf-token'] = cookies.ct0; if (cookies.gt) headers['x-guest-token'] = cookies.gt; let res = await fetch(url, { headers }); if (!res.ok) return null; let json = await res.json(); let core = json.data?.tweetResult?.result?.tweet || json.data?.tweetResult?.result; if (!core) return null; let result = { videos: [], images: [] }; const walk = (obj) => { if (!obj || typeof obj !== 'object') return; if (obj.extended_entities?.media) { obj.extended_entities.media.forEach(m => { if (m.type === 'photo') { result.images.push(m.media_url_https + '?name=orig'); } else if (m.type === 'video' || m.type === 'animated_gif') { let mp4s = m.video_info?.variants?.filter(v => v.content_type === 'video/mp4') || []; if (mp4s.length) { let best = mp4s.sort((a,b)=>(b.bitrate||0)-(a.bitrate||0))[0]; result.videos.push(best.url.split('?')[0]); } } }); } ['tweet', 'quoted_status_result', 'retweeted_status_result', 'result', 'legacy'].forEach(k => { if (obj[k]) walk(obj[k]); }); }; walk(core); return result; } catch(e) { return null; } } async function extractVideoUrl(article) { const cached = _fiberVideoCache.get(article); if (cached && (Date.now() - cached.ts < _FIBER_CACHE_TTL)) return cached.urls; let statusId = null; const links = article.querySelectorAll('a[href*="/status/"]'); for(let a of links) { const match = a.href.match(/\/status\/(\d+)/); if(match) { statusId = match[1]; break; } } if (statusId) { const apiData = await fetchTweetMediaFromAPI(statusId); if (apiData && apiData.videos.length > 0) { _fiberVideoCache.set(article, { urls: apiData.videos, ts: Date.now() }); return apiData.videos; } } let result = Array.from(article.querySelectorAll('video')) .map(v => v.src || v.querySelector('source')?.src) .filter(src => src && src.includes('mp4') && !src.startsWith('blob:')) .map(src => src.split('?')[0]); return result; } async function extractMediaUrls(article) { const uniqueMedias = new Map(); function addImageUrl(src) { if (!src) return; if (src.includes('/card_img/')) { uniqueMedias.set(src, src); return; } const idMatch = src.match(/\/(?:media|ext_tw_video_thumb|amplify_video_thumb|tweet_video_thumb)\/([A-Za-z0-9_-]+)/); const mediaId = idMatch ? idMatch[1] : src.split('?')[0]; if (uniqueMedias.has(mediaId)) return; try { const url = new URL(src); url.searchParams.set('name', 'orig'); uniqueMedias.set(mediaId, url.toString()); } catch (e) { uniqueMedias.set(mediaId, src); } } let statusId = null; const links = article.querySelectorAll('a[href*="/status/"]'); for(let a of links) { const match = a.href.match(/\/status\/(\d+)/); if(match) { statusId = match[1]; break; } } let apiSuccess = false; if (statusId) { const apiData = await fetchTweetMediaFromAPI(statusId); if (apiData && (apiData.videos.length > 0 || apiData.images.length > 0)) { apiData.videos.forEach(v => uniqueMedias.set(v, v)); apiData.images.forEach(addImageUrl); apiSuccess = true; } } if (!apiSuccess) { Array.from(article.querySelectorAll('img[src*="twimg.com"]')) .map(img => img.src) .filter(src => src.includes('pbs.twimg.com') && !src.includes('profile_images') && !src.includes('/emoji/') && !src.includes('twemoji')) .forEach(addImageUrl); const videos = await extractVideoUrl(article); videos.forEach(v => uniqueMedias.set(v, v)); article.querySelectorAll('[data-testid="tweet"] img[src*="pbs.twimg.com"]').forEach(img => { if (!img.src.includes('profile_images') && !img.src.includes('/emoji/')) addImageUrl(img.src); }); } return Array.from(uniqueMedias.values()); } function extractTweetUrl(article, baseUrl) { const timeLink = article.querySelector('a[href*="/status/"] > time')?.parentElement; if (timeLink) return baseUrl + timeLink.getAttribute('href'); const link = article.querySelector('a[href*="/status/"]'); if(link) return baseUrl + link.getAttribute('href'); return null; } function insertCopyButton(article) { if (article.querySelector(`.${BUTTON_CLASS}`)) return; const actions = Array.from(article.querySelectorAll('[role="group"]')).pop(); if (!actions) return; if (!document.getElementById('tm-icon-anim-style')) { const s = document.createElement('style'); s.id = 'tm-icon-anim-style'; s.textContent = ` @keyframes tm-pop-bounce { 0% { transform: scale(0.5); opacity: 0; } 60% { transform: scale(1.15); opacity: 1; } 100% { transform: scale(1); opacity: 1; } } @keyframes tm-pop-bounce-text { 0% { transform: translateY(-50%) scale(0.5); opacity: 0; } 60% { transform: translateY(-50%) scale(1.1); opacity: 1; } 100% { transform: translateY(-50%) scale(1); opacity: 1; } } .tm-anim-pop { animation: tm-pop-bounce 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275) both; } .tm-anim-pop-text { position: absolute; right: 4px; top: 50%; transform: translateY(-50%); transform-origin: right center; white-space: nowrap; font-weight: 700; font-size: 13px; font-family: system-ui, -apple-system, sans-serif; background: rgba(128, 128, 128, 0.2); backdrop-filter: blur(4px); padding: 5px 12px; border-radius: 9999px; color: currentColor; z-index: 9999; pointer-events: none; animation: tm-pop-bounce-text 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275) both; } `; document.head.appendChild(s); } const SVG_FILM = ``; const SVG_CHECK_SM = ``; const SVG_PREFIX_COPY = ``; const SVG_DL = ``; const btn = document.createElement('button'); btn.className = BUTTON_CLASS; btn.title = T.btn_tooltip; btn.style.position = 'relative'; const setMediaIcon = (state, extra, silentText, actionType = 'copy') => { const fbStyle = GM_getValue(KEY_FEEDBACK_STYLE, 'toast'); btn.classList.remove('tm-anim-pop'); const setTextMode = (text, customColor) => { const safeColor = (customColor && /^#[0-9a-fA-F]{3,8}$/.test(customColor)) ? customColor : null; const span = document.createElement('span'); span.className = 'tm-anim-pop-text'; span.textContent = text; if (safeColor) span.style.cssText = `color: ${safeColor} !important;`; btn.innerHTML = ''; btn.appendChild(span); }; const setIconMode = (svg, customColor) => { const safeColor = (customColor && /^#[0-9a-fA-F]{3,8}$/.test(customColor)) ? customColor : null; btn.innerHTML = svg; btn.classList.add('tm-anim-pop'); if (safeColor) { const svgEl = btn.querySelector('svg'); if (svgEl) { svgEl.style.color = safeColor; svgEl.style.filter = `drop-shadow(0 0 4px ${safeColor}66)`; } } }; const getSilentText = () => silentText || extra; if (state === 'default') { btn.innerHTML = SVG_FILM; } else if (state === 'dl') { btn.innerHTML = SVG_DL; } else if (state === 'ok') { if (fbStyle === 'silent') { setTextMode(getSilentText() || 'Copied'); } else if (fbStyle === 'icon') { if (actionType === 'prefix') setIconMode(SVG_PREFIX_COPY); else if (actionType === 'download') setIconMode(SVG_CHECK_SM); else setIconMode(SVG_CHECK_SM); } else { btn.innerHTML = SVG_CHECK_SM; btn.classList.add('tm-anim-pop'); showActionToast(btn, extra || T.msg_copied, 'ok'); } } else if (state === 'warn') { if (fbStyle === 'silent') { setTextMode(getSilentText(), '#ff8c00'); } else if (fbStyle === 'icon') { setIconMode(SVG_FILM, '#ff8c00'); } else { btn.innerHTML = SVG_FILM; showActionToast(btn, extra, 'warn'); } } else { if (fbStyle === 'silent' && getSilentText()) { setTextMode(getSilentText(), state === 'error' ? '#e0245e' : null); } else if (fbStyle === 'icon') { setIconMode(SVG_FILM, state === 'error' ? '#e0245e' : null); } else { btn.innerHTML = SVG_FILM; if (extra) showActionToast(btn, extra, state === 'error' ? 'error' : 'ok'); } } const tweetId = _getTweetIdFromArticle(article); if (tweetId && _downloadedIds.has(tweetId)) { _applyHistoryBadge(btn); } }; setMediaIcon('default'); let timer = null; btn.addEventListener('mousedown', async (e) => { e.preventDefault(); e.stopPropagation(); if (e.button === 0) { timer = setTimeout(async () => { const urls = await extractMediaUrls(article); if (!urls.length) return; const prefix = GM_getValue(KEY_PREFIX_TEXT, '[text]'); const txt = urls.map(u => `${prefix}(${u})`).join('\n'); GM_setClipboard(txt); setMediaIcon('ok', T.msg_prefix_copied, 'Prefix Copied', 'prefix'); setTimeout(() => setMediaIcon('default'), 1500); timer = null; }, 500); } else if (e.button === 1) { const videos = await extractVideoUrl(article); const allUrls = await extractMediaUrls(article); const imgUrls = allUrls.filter(u => !u.includes('.mp4')); if (videos.length && imgUrls.length) { showFloatingVideoPlayer(videos, 0, imgUrls); } else if (videos.length) { showFloatingVideoPlayer(videos); } else if (imgUrls.length) { showImageLightbox(imgUrls); } else { setMediaIcon('msg', T.msg_no_media, 'No Media'); setTimeout(() => setMediaIcon('default'), 1500); } } }); btn.addEventListener('mouseup', async (e) => { if (e.button !== 0) return; if (timer) { clearTimeout(timer); timer = null; const urls = await extractMediaUrls(article); if (!urls.length) { setMediaIcon('msg', T.msg_no_media, 'No Media'); setTimeout(() => setMediaIcon('default'), 1500); return; } GM_setClipboard(urls.join('\n')); setMediaIcon('ok', T.msg_copied, 'Copied', 'copy'); setTimeout(() => setMediaIcon('default'), 1500); } }); btn.addEventListener('mouseleave', () => { if (timer) { clearTimeout(timer); timer = null; } }); btn.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); }); btn.addEventListener('contextmenu', async (e) => { e.preventDefault(); e.stopPropagation(); const urls = await extractMediaUrls(article); if (urls.length === 0) return; const info = getTweetInfo(article); setMediaIcon('dl'); const ring = createProgressRing(); ring.el.style.cssText = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);pointer-events:none;'; btn.appendChild(ring.el); let index = 1; let failCount = 0; const total = urls.length; for (const url of urls) { let ext = '.jpg'; if (url.includes('.mp4')) ext = '.mp4'; else if (url.includes('format=png')) ext = '.png'; else { const parts = url.split('/').pop().split('?')[0].split('.'); if (parts.length > 1) ext = '.' + parts.pop(); } const textPart = info.text ? `_${info.text}` : ""; const safeDisplay = sanitizeForFilename(info.displayName); const safeScreen = sanitizeForFilename(info.screenName); const filename = `[twitter] ${safeDisplay}(@${safeScreen})_${info.date}${textPart}_${info.id}_${index}${ext}`; const fileOffset = (index - 1) / total; const fileShare = 1 / total; try { await forceDownloadBlob(url, filename, (pct) => { if (pct === null) { ring.update(null); } else { ring.update(Math.round((fileOffset + fileShare * pct / 100) * 100)); } }); } catch(_) { failCount++; } await new Promise(r => setTimeout(r, 250)); index++; } ring.remove(); const successCount = total - failCount; if (failCount > 0) { setMediaIcon('warn', `⚠️ ${successCount}/${total}`); } else { setMediaIcon('ok', T.msg_downloaded, 'Downloaded', 'download'); recordHistory(info, urls, btn); fireMeteor(btn); } setTimeout(() => setMediaIcon('default'), 2000); }); const LINK_BTN_CLASS = 'custom-copy-icon'; if (!article.querySelector(`.${LINK_BTN_CLASS}`)) { const icon = document.createElement('div'); icon.className = LINK_BTN_CLASS; icon.style.position = 'relative'; const SVG_LINK = ``; const SVG_CHECK = ``; const setLinkIcon = (state, extra, silentText, actionType = 'copy') => { const fbStyle = GM_getValue(KEY_FEEDBACK_STYLE, 'toast'); icon.classList.remove('tm-anim-pop'); const setTextMode = (text) => { const span = document.createElement('span'); span.className = 'tm-anim-pop-text'; span.textContent = text; icon.innerHTML = ''; icon.appendChild(span); }; const setIconMode = (svg) => { icon.innerHTML = svg; icon.classList.add('tm-anim-pop'); }; if (state === 'ok') { if (fbStyle === 'silent') { setTextMode(silentText || extra || 'Copied'); } else if (fbStyle === 'icon') { const useSvg = actionType === 'prefix' ? SVG_PREFIX_COPY : SVG_CHECK; setIconMode(useSvg); } else { icon.innerHTML = SVG_CHECK; icon.classList.add('tm-anim-pop'); showActionToast(icon, extra || T.msg_copied, 'ok'); } } else { icon.innerHTML = SVG_LINK; } }; setLinkIcon('default'); icon.addEventListener('mouseenter', () => { const custom = GM_getValue(KEY_CLICK_MODE_CUSTOM, false); const click = custom ? GM_getValue(KEY_LINK_DOMAIN_CLICK, 'x.com') : 'x.com'; icon.title = T.link_tooltip + click + T.link_tooltip_long + click; }); let lTimer = null; icon.addEventListener('mousedown', e => { if (e.button !== 0) return; e.preventDefault(); e.stopPropagation(); lTimer = setTimeout(() => { const useCustom = GM_getValue(KEY_CLICK_MODE_CUSTOM, false); const targetDomain = useCustom ? GM_getValue(KEY_LINK_DOMAIN_CLICK, 'x.com') : 'x.com'; const url = extractTweetUrl(article, 'https://' + targetDomain); if (url) { const prefix = GM_getValue(KEY_PREFIX_TEXT, '[text]'); GM_setClipboard(`${prefix}(${url})`); setLinkIcon('ok', T.msg_prefix_copied, 'Prefix Copied', 'prefix'); setTimeout(() => setLinkIcon('default'), 1500); } lTimer = null; }, 500); }); icon.addEventListener('mouseup', () => { if(lTimer) { clearTimeout(lTimer); lTimer = null; const useCustom = GM_getValue(KEY_CLICK_MODE_CUSTOM, false); const targetDomain = useCustom ? GM_getValue(KEY_LINK_DOMAIN_CLICK, 'x.com') : 'x.com'; const url = extractTweetUrl(article, 'https://' + targetDomain); if(url) { GM_setClipboard(url); setLinkIcon('ok', T.msg_copied, 'Copied', 'copy'); setTimeout(() => setLinkIcon('default'), 1500); } } }); icon.addEventListener('mouseleave', () => { if(lTimer) { clearTimeout(lTimer); lTimer = null; } }); icon.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); }); actions.appendChild(btn); actions.insertBefore(icon, btn); if (_downloadedIds.size > 0) { requestAnimationFrame(() => { const tweetId = _getTweetIdFromArticle(article); if (tweetId && _downloadedIds.has(tweetId)) _applyHistoryBadge(btn); }); } } } let _tmdDebounceTimer = null; const _processedArticles = new WeakSet(); function scanAndInsert() { document.querySelectorAll('article').forEach(article => { if (_processedArticles.has(article) && article.querySelector(`.${BUTTON_CLASS}`)) return; insertCopyButton(article); if (article.querySelector(`.${BUTTON_CLASS}`)) _processedArticles.add(article); }); } const observer = new MutationObserver(mutations => { let shouldCheck = false; for (let m of mutations) { if (m.addedNodes.length > 0 || m.removedNodes.length > 0) { shouldCheck = true; break; } } if (shouldCheck) { clearTimeout(_tmdDebounceTimer); _tmdDebounceTimer = setTimeout(scanAndInsert, 250); } }); observer.observe(document.body, { childList: true, subtree: true, attributes: false, characterData: false }); setInterval(scanAndInsert, 1500); setTimeout(scanAndInsert, 1000); })();