// ==UserScript== // @name SkyFetch - BlueSky 最高分辨率图片下载 // @namespace https://github.com/CookSleep // @version 1.1 // @name:en SkyFetch - High-Resolution Image Downloader for BlueSky // @name:en-uk SkyFetch - High-Resolution Image Downloader for BlueSky // @name:ja SkyFetch - BlueSky用高解像度画像ダウンローダー // @name:ko SkyFetch - BlueSky용 고해상도 이미지 다운로더 // @name:ru SkyFetch - Загрузчик Изображений Высокого Разрешения для BlueSky // @name:zh-CN SkyFetch - BlueSky 最高分辨率图片下载 // @name:zh-TW SkyFetch - BlueSky 最高解析度圖片下載 // @name:yue SkyFetch - BlueSky 最高解析度圖片下載 // @description 在 BlueSky 含有图片的帖子右下角添加下载按钮,自动下载最高分辨率图片,可自定义命名规则。 // @description:en Add a download button at the bottom right of BlueSky posts containing images to automatically download the highest resolution images with customizable naming rules. // @description:en-uk Add a download button at the bottom right of BlueSky posts containing images to automatically download the highest resolution images with customizable naming rules. // @description:ja 画像を含むBlueSkyの投稿の右下隅にダウンロードボタンを追加し、最高解像度の画像を自動的にダウンロードし、カスタマイズ可能な命名規則を適用します。 // @description:ko 이미지가 포함된 BlueSky 게시물의 오른쪽 하단에 다운로드 버튼을 추가하여 최고 해상도 이미지를 자동으로 다운로드하고 사용자 정의 가능한 명명 규칙을 적용합니다. // @description:ru Добавляет кнопку загрузки в правый нижний угол постов BlueSky, содержащих изображения, для автоматической загрузки изображений наивысшего разрешения с настраиваемыми правилами именования. // @description:zh-CN 在 BlueSky 含有图片的帖子右下角添加下载按钮,自动下载最高分辨率图片,可自定义命名规则。 // @description:zh-TW 在 BlueSky 含有圖片的帖子右下角添加下載按鈕,自動下載最高解析度圖片,可自定義命名規則。 // @description:yue 在 BlueSky 含有圖片的帖子右下角添加下載按鈕,自動下載最高解析度圖片,可自定義命名規則。 // @author Cook Sleep // @match https://bsky.app/* // @grant GM_download // @grant GM_setClipboard // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @run-at document-end // @license GPLv3 // @downloadURL https://update.greasyfork.icu/scripts/524331/SkyFetch%20-%20BlueSky%20%E6%9C%80%E9%AB%98%E5%88%86%E8%BE%A8%E7%8E%87%E5%9B%BE%E7%89%87%E4%B8%8B%E8%BD%BD.user.js // @updateURL https://update.greasyfork.icu/scripts/524331/SkyFetch%20-%20BlueSky%20%E6%9C%80%E9%AB%98%E5%88%86%E8%BE%A8%E7%8E%87%E5%9B%BE%E7%89%87%E4%B8%8B%E8%BD%BD.meta.js // ==/UserScript== /* * 本脚本使用了 [Twitter Media Downloader](https://greasyfork.org/es/scripts/423001-twitter-media-downloader) 项目的下载按钮 SVG, * 该项目基于 MIT 许可证发布。 * MIT 许可证: https://opensource.org/licenses/MIT * * This script uses the download button SVG from the [Twitter Media Downloader](https://greasyfork.org/es/scripts/423001-twitter-media-downloader) project, * which is licensed under the MIT License. * MIT License: https://opensource.org/licenses/MIT */ (function() { 'use strict'; // ===================== // CSS 样式管理 // ===================== const styles = ` /* 主题样式 */ :root { --background: #ffffff; --background-hover: #f7fafc; --background-translucent: rgba(255, 255, 255, 0.95); --text: #1a202c; --border: #e2e8f0; --overlay: rgba(0, 0, 0, 0.5); --input-background: #f7fafc; --accent: #1083FE; --accent-hover: #0168D5; --accent-translucent: rgba(16, 131, 254, 0.1); --button-background: #ffffff; --button-background-hover: #F1F3F5; --button-text: #1a202c; --button-border: #e2e8f0; } @media (prefers-color-scheme: dark) { :root { --background: #161E27; --background-hover: #2d3748; --background-translucent: rgba(22, 30, 39, 0.95); --text: #fff; --border: #2d3748; --overlay: rgba(0, 0, 0, 0.75); --input-background: #2d3748; --accent: #208BFE; --accent-hover: #4CA2FE; --accent-translucent: rgba(32, 139, 254, 0.2); --button-background: #1E2936; --button-background-hover: #2d3748; --button-text: #fff; --button-border: #4a5568; } } /* 按钮样式 */ .tmd-down { align-items: center; } .tmd-down button { padding: 5px; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; border-radius: 999px; cursor: pointer; background: transparent; border: none; } .tmd-down button:hover { background-color: var(--hover-color, rgba(120, 142, 165, 0.1)); } /* 深色模式图标颜色 */ @media (prefers-color-scheme: dark) { .tmd-down svg { color: hsl(211, 20%, 56%); } .tmd-down { --hover-color: rgba(120, 142, 165, 0.1); } } /* 浅色模式图标颜色 */ @media (prefers-color-scheme: light) { .tmd-down svg { color: hsl(211, 20%, 53%); } .tmd-down { --hover-color: rgba(120, 142, 165, 0.1); } } .tmd-down.loading svg { animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .tmd-down.failed svg { color: rgb(255, 51, 51); } /* 设置界面样式 */ .skyfetch-settings-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: var(--overlay); display: flex; justify-content: center; align-items: center; z-index: 1001; padding: 16px; box-sizing: border-box; } .skyfetch-settings-content { background-color: var(--background); color: var(--text); padding: 24px; border-radius: 12px; width: min(480px, 90vw); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); border: 1px solid var(--border); position: relative; } .skyfetch-settings-content h2 { margin: 0 0 16px 0; font-size: 20px; font-weight: 600; padding-right: 32px; } .skyfetch-settings-content label { display: block; margin: 0 0 8px 0; font-weight: 500; } .skyfetch-settings-content textarea { width: 100%; padding: 12px; margin: 0 0 20px 0; border: 1px solid var(--border); border-radius: 6px; background-color: var(--input-background); color: var(--text); resize: vertical; min-height: 80px; font-family: monospace; font-size: 14px; line-height: 1.4; box-sizing: border-box; } .skyfetch-settings-content textarea:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-translucent); } .skyfetch-settings-content .button-group { display: flex; gap: 12px; justify-content: flex-end; } .skyfetch-settings-content button { padding: 8px 16px; background-color: var(--button-background); color: var(--button-text); border: 1px solid var(--button-border); border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; } .skyfetch-settings-content button:hover { background-color: var(--button-background-hover); } .skyfetch-settings-content button.primary { background-color: var(--accent); border-color: var(--accent); color: white; } .skyfetch-settings-content button.primary:hover { background-color: var(--accent-hover); border-color: var(--accent-hover); } /* 不支持语言提示弹窗样式 */ .skyfetch-notice-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: var(--overlay); display: flex; justify-content: center; align-items: center; z-index: 1001; padding: 16px; box-sizing: border-box; backdrop-filter: blur(4px); } .skyfetch-notice-content { max-width: 480px; width: 90vw; background-color: var(--background); color: var(--text); padding: 28px; border-radius: 16px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); border: 1px solid var(--border); position: relative; } .skyfetch-notice-content h2 { font-size: 22px; margin: 0 0 20px 0; padding-right: 32px; font-weight: 600; color: var(--text); } .skyfetch-notice-content p { font-size: 15px; line-height: 1.6; margin: 0 0 16px 0; color: var(--text); opacity: 0.9; } .skyfetch-notice-content button { width: 100%; padding: 12px; background-color: var(--accent); color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 15px; font-weight: 500; transition: all 0.2s ease; } .skyfetch-notice-content button:hover { background-color: var(--accent-hover); transform: translateY(-1px); } /* 支持语言列表样式 */ .skyfetch-supported-languages { background: var(--input-background); border-radius: 12px; padding: 20px; margin: 20px 0; } .supported-languages-title { font-weight: 600; font-size: 15px; margin-bottom: 16px; color: var(--text); } .supported-languages-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; } .languages-column { display: flex; flex-direction: column; gap: 8px; } .language-item { font-size: 14px; color: var(--text); opacity: 0.9; display: flex; align-items: center; gap: 8px; } /* 关闭按钮样式 */ .skyfetch-close-btn { position: absolute; top: 24px; right: 24px; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; border-radius: 8px; background: var(--input-background); color: var(--text); transition: all 0.2s ease; } .skyfetch-close-btn:hover { background: var(--button-background-hover); transform: rotate(90deg); } .skyfetch-close-btn svg { width: 20px; height: 20px; } `; // 将CSS添加到文档头部 const styleSheet = document.createElement('style'); styleSheet.textContent = styles; document.head.appendChild(styleSheet); // ===================== // 初始设置 // ===================== const defaultFilename = 'BlueSky_{userName}_{userId}_{date}'; let filenamePattern = GM_getValue('filenamePattern', defaultFilename); let currentLang = 'en'; // ===================== // 多语言翻译 // ===================== const translations = { 'en': { settingsTitle: 'SkyFetch Settings', filenameLabel: 'Filename Pattern:', resetButton: 'Reset', saveButton: 'Save', downloadButtonLabel: 'Download Image', downloadFailed: 'Download failed', downloadCompleted: 'Download completed', downloading: 'Downloading...', unsupportedLanguageTitle: 'Language Not Supported', unsupportedLanguageMessage1: 'The current interface language ({lang}) is not supported', unsupportedLanguageMessage2: 'Post dates will be marked as "unknown_date" in downloaded image filenames', understood: 'Understood', supportedLanguages: 'Supported Languages' }, 'en-uk': { settingsTitle: 'SkyFetch Settings', filenameLabel: 'Filename Pattern:', resetButton: 'Reset', saveButton: 'Save', downloadButtonLabel: 'Download Image', downloadFailed: 'Download failed', downloadCompleted: 'Download completed', downloading: 'Downloading...', unsupportedLanguageTitle: 'Language Not Supported', unsupportedLanguageMessage1: 'The current interface language ({lang}) is not supported', unsupportedLanguageMessage2: 'Post dates will be marked as "unknown_date" in downloaded image filenames', understood: 'Understood', supportedLanguages: 'Supported Languages' }, 'ja': { settingsTitle: 'SkyFetch 設定', filenameLabel: 'ファイル名のパターン:', resetButton: 'リセット', saveButton: '保存', downloadButtonLabel: '画像をダウンロード', downloadFailed: 'ダウンロードに失敗しました', downloadCompleted: 'ダウンロード完了', downloading: 'ダウンロード中...', unsupportedLanguageTitle: '未対応の言語', unsupportedLanguageMessage1: '現在のインターフェース言語({lang})はサポートされていません', unsupportedLanguageMessage2: '投稿日は「unknown_date」としてダウンロードされます', understood: '了解', supportedLanguages: '対応言語' }, 'ko': { settingsTitle: 'SkyFetch 설정', filenameLabel: '파일명 패턴:', resetButton: '초기화', saveButton: '저장', downloadButtonLabel: '이미지 다운로드', downloadFailed: '다운로드 실패', downloadCompleted: '다운로드 완료', downloading: '다운로드 중...', unsupportedLanguageTitle: '지원되지 않는 언어', unsupportedLanguageMessage1: '현재 인터페이스 언어({lang})는 지원되지 않습니다', unsupportedLanguageMessage2: '게시물 날짜는 "unknown_date"로 표시됩니다', understood: '확인', supportedLanguages: '지원되는 언어' }, 'ru': { settingsTitle: 'Настройки SkyFetch', filenameLabel: 'Шаблон имени файла:', resetButton: 'Сбросить', saveButton: 'Сохранить', downloadButtonLabel: 'Скачать изображение', downloadFailed: 'Скачивание не удалось', downloadCompleted: 'Скачивание завершено', downloading: 'Скачивание...', unsupportedLanguageTitle: 'Язык не поддерживается', unsupportedLanguageMessage1: 'Текущий язык интерфейса ({lang}) не поддерживается', unsupportedLanguageMessage2: 'Даты публикаций будут отмечены как "unknown_date"', understood: 'Понятно', supportedLanguages: 'Поддерживаемые языки' }, 'zh-CN': { settingsTitle: 'SkyFetch 设置', filenameLabel: '文件名模式:', resetButton: '重置', saveButton: '保存', downloadButtonLabel: '下载图片', downloadFailed: '下载失败', downloadCompleted: '下载完成', downloading: '下载中...', unsupportedLanguageTitle: '不支持的语言', unsupportedLanguageMessage1: '当前界面语言({lang})不受支持', unsupportedLanguageMessage2: '下载的图片文件名中的发布日期将标记为"unknown_date"', understood: '知道了', supportedLanguages: '支持的语言' }, 'zh-TW': { settingsTitle: 'SkyFetch 設定', filenameLabel: '文件名模式:', resetButton: '重置', saveButton: '保存', downloadButtonLabel: '下載圖片', downloadFailed: '下載失敗', downloadCompleted: '下載完成', downloading: '下載中...', unsupportedLanguageTitle: '不支援的語言', unsupportedLanguageMessage1: '目前介面語言({lang})不受支援', unsupportedLanguageMessage2: '下載嘅圖片檔名入面嘅發布日期會標記做"unknown_date"', understood: '知道了', supportedLanguages: '支援的語言' }, 'yue': { settingsTitle: 'SkyFetch 設定', filenameLabel: '文件名模式:', resetButton: '重設', saveButton: '儲存', downloadButtonLabel: '下載圖片', downloadFailed: '下載失敗', downloadCompleted: '下載完成', downloading: '下載中...', unsupportedLanguageTitle: '唔支援嘅語言', unsupportedLanguageMessage1: '而家嘅界面語言({lang})係唔支援嘅', unsupportedLanguageMessage2: '下載嘅圖片檔名入面嘅發布日期會標記做"unknown_date"', understood: '知道喇', supportedLanguages: '支援嘅語言' } }; // ===================== // 语言相关设置 // ===================== const languageMapping = { 'en': 'en', 'en-GB': 'en-uk', 'ja': 'ja', 'ko': 'ko', 'ru': 'ru', 'zh-Hans-CN': 'zh-CN', 'zh-Hant-TW': 'zh-TW', 'zh-Hant-HK': 'yue' }; /** * 检查语言是否受支持 * @param {string} lang - 语言代码 * @returns {boolean} 是否支持 */ function isLanguageSupported(lang) { return lang in translations; } /** * 获取网站当前语言并设置翻译 * @returns {object} 包含映射前后语言代码的对象 */ function getSiteLanguage() { const langSelector = document.querySelector('select[data-testid="web_picker"]'); let siteLanguage = langSelector ? langSelector.value : document.documentElement.lang || 'en'; // 映射语言代码 let mappedLanguage = languageMapping[siteLanguage] || 'en'; // 如果映射后的语言不在支持列表中,使用英语 if (!isLanguageSupported(mappedLanguage)) { mappedLanguage = 'en'; } currentLang = mappedLanguage; return { mappedLanguage, siteLanguage }; } /** * 语言提示状态管理 */ const LanguageNoticeManager = { // 存储键名 STORAGE_KEY: 'skyfetch_language_notice', /** * 获取上次提示的语言 * @returns {string|null} 上次提示的语言代码 */ getLastNotifiedLang() { return GM_getValue(this.STORAGE_KEY, null); }, /** * 记录语言提示状态 * @param {string} lang - 语言代码 */ markAsNotified(lang) { GM_setValue(this.STORAGE_KEY, lang); }, /** * 重置提示状态 */ reset() { GM_setValue(this.STORAGE_KEY, null); }, /** * 检查是否需要显示提示 * @param {string} currentLang - 当前语言代码 * @returns {boolean} 是否需要显示提示 */ shouldShowNotice(currentLang) { const lastNotifiedLang = this.getLastNotifiedLang(); // 如果从未提示过,或者切换到了新的不支持的语言 return !lastNotifiedLang || lastNotifiedLang !== currentLang; } }; /** * 获取浏览器语言并映射到支持的语言代码 * @returns {string} 映射后的语言代码 */ function getBrowserLanguage() { const browserLang = navigator.language || navigator.userLanguage; // 尝试映射浏览器语言 let mappedLanguage = languageMapping[browserLang] || languageMapping[browserLang.split('-')[0]] || 'en'; // 如果映射后的语言不在支持列表中,使用英语 if (!isLanguageSupported(mappedLanguage)) { mappedLanguage = 'en'; } return mappedLanguage; } // 添加支持的语言列表 const supportedLanguages = { 'en': 'English', 'en-uk': 'English (UK)', 'ja': '日本語', 'ko': '한국어', 'ru': 'Русский', 'zh-CN': '简体中文', 'zh-TW': '繁體中文', 'yue': '粵文' }; /** * 显示语言不支持提示 * @param {string} lang - 不支持的语言代码 * @returns {Promise} - 用户确认后resolve的Promise */ function showUnsupportedLanguageNotice(lang) { return new Promise((resolve) => { const browserLang = getBrowserLanguage(); const t = translations[browserLang] || translations['en']; const modal = document.createElement('div'); modal.className = 'skyfetch-notice-modal'; const content = document.createElement('div'); content.className = 'skyfetch-notice-content'; const closeBtn = document.createElement('div'); closeBtn.className = 'skyfetch-close-btn'; closeBtn.innerHTML = ` `; const sadFaceIcon = document.createElement('div'); sadFaceIcon.className = 'skyfetch-sad-face'; sadFaceIcon.innerHTML = ` `; const title = document.createElement('h2'); title.innerText = t.unsupportedLanguageTitle; // 第一行消息:当前界面语言(xxx)不受支持 const message1 = document.createElement('p'); message1.innerText = t.unsupportedLanguageMessage1.replace('{lang}', lang || 'unknown'); // 第二行消息:下载的图片文件名中的发布日期将标记为"unknown_date" const message2 = document.createElement('p'); message2.innerText = t.unsupportedLanguageMessage2; const button = document.createElement('button'); button.className = 'primary'; button.innerText = t.understood; button.onclick = () => { modal.remove(); LanguageNoticeManager.markAsNotified(lang); resolve(); }; closeBtn.onclick = button.onclick; content.appendChild(closeBtn); content.appendChild(sadFaceIcon); content.appendChild(title); content.appendChild(message1); content.appendChild(message2); content.appendChild(button); // 优化后的支持语言列表 const supportedLangList = document.createElement('div'); supportedLangList.className = 'skyfetch-supported-languages'; supportedLangList.innerHTML = `