// ==UserScript== // @name 朝阳的工具 // @namespace https://bbs.tampermonkey.net.cn/ // @version 1.3 // @description 修复了清除Token和清除所有数据的功能,优化了错误提示,并使用Tampermonkey存储。增加了“入库”按钮拖动并记忆位置的功能,Steam商店和SteamDB页面位置独立记忆。现在支持按钮联动移动,拖动不再弹出浮窗,且增加了主页入口。 // @author zhaoyang // @match https://store.steampowered.com/app/* // @match https://steamdb.info/app/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_deleteValue // @grant GM_notification // @connect api.github.com // @connect raw.githubusercontent.com // @connect github.com // ==/UserScript== (function() { 'use strict'; // --- 配置信息 --- // 硬编码的清单网站列表 const HARDCODED_GITHUB_MANIFEST_REPOS = [ // type: 'branches' 表示每个App ID是一个独立的分支 // 经核实,你提供的所有清单网站均为App ID作为分支名。 { name: '清单网站1', url: 'Auiowu/ManifestAutoUpdate', type: 'branches' }, { name: '清单网站2', url: 'SteamAutoCracks/ManifestHub', type: 'branches' }, { name: '清单网站3', url: 'hansaes/ManifestAutoUpdate', type: 'branches' }, { name: '清单网站4', url: 'ikun0014/ManifestHub', type: 'branches' }, { name: '清单网站5', url: 'tymolu233/ManifestAutoUpdate', type: 'branches' }, { name: '清单网站6', url: 'Fairyvmos/BlankTMing', type: 'branches' }, { name: '清单网站7', url: 'sean-who/ManifestAutoUpdate', type: 'branches' }, { name: '清单网站8', url: 'Scropiouos/ManifestAutoUpdate_PrivateBackUp', type: 'branches' } ]; const CACHE_LIFETIME_MS = 1 * 60 * 60 * 1000; // 缓存有效期设为1小时 const BACKGROUND_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // 后台定时检查间隔6小时 const RETRY_LIMIT = 3; // 每个失败请求的重试次数 const RETRY_DELAY_MS = 2000; // 重试间隔时间 (毫秒) const DRAG_THRESHOLD = 5; // 拖动阈值,鼠标移动超过此距离才算拖动,否则算点击 const APP_ID_REGEX_STEAM = /\/app\/(\d+)/; const APP_ID_REGEX_STEAMDB = /\/app\/(\d+)/; // 明确使用 GM_ 前缀表示 Tampermonkey 存储 const GM_KEY_PREFIX = 'chaoyang_tool_'; const GM_GITHUB_TOKEN = GM_KEY_PREFIX + 'github_token'; const GM_DOWNLOAD_RECORDS = GM_KEY_PREFIX + 'download_records'; const GM_LAST_CHECK_TIME = GM_KEY_PREFIX + 'last_check_time'; const GM_GITHUB_MANIFESTS_CACHE = GM_KEY_PREFIX + 'github_manifests_cache'; const GM_USER_MANIFESTS = GM_KEY_PREFIX + 'user_manifests'; // 按钮位置存储键 const GM_BUTTON_POS_STEAM = GM_KEY_PREFIX + 'button_pos_steam'; const GM_BUTTON_POS_STEAMDB = GM_KEY_PREFIX + 'button_pos_steamdb'; let currentAppId = null; let githubToken = ''; // 从Tampermonkey存储加载 let githubManifestsCache = {}; // 存储所有仓库的App ID及其最新更新时间 let downloadRecords = {}; // 存储用户已下载的App ID及状态 { 'appId': { downloadedTime: timestamp, manifestSource: '自动/清单网站X' } } let userDefinedManifests = []; // 存储用户自定义的清单网站 let allManifestRepos = []; // 硬编码 + 用户自定义的清单网站 let storedButtonPosition = null; // 用于存储从GM_getValue加载的按钮位置 // --- 调试工具 --- /** * 清除所有脚本相关的Tampermonkey存储数据 */ async function clearAllTampermonkeyStorage() { if (!confirm('朝阳的工具: 确定要清除所有脚本本地数据吗?这将清除所有清单缓存、下载记录、自定义清单、GitHub Token和按钮位置。此操作不可逆!')) { return; } console.warn('朝阳的工具: 正在清除所有脚本Tampermonkey存储数据...'); try { // 明确删除所有定义的存储键 await GM_deleteValue(GM_DOWNLOAD_RECORDS); await GM_deleteValue(GM_LAST_CHECK_TIME); await GM_deleteValue(GM_GITHUB_MANIFESTS_CACHE); await GM_deleteValue(GM_USER_MANIFESTS); await GM_deleteValue(GM_GITHUB_TOKEN); await GM_deleteValue(GM_BUTTON_POS_STEAM); // 清除Steam按钮位置 await GM_deleteValue(GM_BUTTON_POS_STEAMDB); // 清除SteamDB按钮位置 // 重置内存中的变量 downloadRecords = {}; githubManifestsCache = {}; userDefinedManifests = []; githubToken = ''; storedButtonPosition = null; // 重置按钮位置 allManifestRepos = [...HARDCODED_GITHUB_MANIFEST_REPOS]; // 重置为硬编码 console.warn('朝阳的工具: 所有Tampermonkey存储数据已清除成功。'); alert('朝阳的工具: 所有Tampermonkey存储数据已清除。请刷新页面。'); // 可以选择刷新页面让更改立即生效 window.location.reload(); // 强制刷新页面以应用更改 } catch (e) { console.error('朝阳的工具: 清除Tampermonkey存储时出错:', e); GM_notification({ title: '朝阳的工具', text: `清除Tampermonkey存储时出错,请手动检查Tampermonkey存储。错误:${e.message || e}`, image: 'https://www.tampermonkey.net/_favicon.ico', timeout: 5000 }); alert('朝阳的工具: 清除Tampermonkey存储时出错,请手动检查Tampermonkey存储。'); } } // --- 工具函数 (保持不变或微调日志) --- /** * 从当前URL获取App ID * @returns {string|null} */ function getAppIdFromUrl() { const url = window.location.href; let match; if (url.startsWith('https://store.steampowered.com/app/')) { match = url.match(APP_ID_REGEX_STEAM); } else if (url.startsWith('https://steamdb.info/app/')) { match = url.match(APP_ID_REGEX_STEAMDB); } const appId = match ? String(match[1]) : null; return appId; } /** * 暂停指定毫秒数 * @param {number} ms - 毫秒数 * @returns {Promise} */ function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * 执行一个GitHub API请求,并带重试逻辑 * @param {string} url - 请求URL * @param {string} repoName - 仓库名称,用于日志 * @param {string} method - HTTP 方法 (GET, HEAD) * @param {number} retries - 当前重试次数 * @returns {Promise<{data: Object|null, headers: string, status: number}>} - data为null时通常是HEAD请求或404 */ async function makeGitHubApiRequestWithRetry(url, repoName, method = 'GET', retries = 0) { const headers = { 'Accept': 'application/vnd.github.v3+json', 'X-GitHub-Api-Version': '2022-11-28' }; if (githubToken) { headers['Authorization'] = `token ${githubToken}`; } return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: method, url: url, headers: headers, timeout: 10000, // 10秒超时 onload: async function(res) { const remaining = res.responseHeaders.match(/x-ratelimit-remaining:\s*(\d+)/i); const reset = res.responseHeaders.match(/x-ratelimit-reset:\s*(\d+)/i); if (remaining && reset) { const resetTime = new Date(parseInt(reset[1]) * 1000); if (parseInt(remaining[1]) < 100) { console.warn(`朝阳的工具: [${repoName}] 速率限制:剩余 ${remaining[1]} 次请求,重置时间 ${resetTime.toLocaleTimeString()}`); if (parseInt(remaining[1]) === 0) { // 如果归零了,提示用户 GM_notification({ title: '朝阳的工具: 速率限制警告', text: `您的GitHub API请求已达到速率限制,将在 ${resetTime.toLocaleTimeString()} 后重置。请检查您的Token设置。`, image: 'https://www.tampermonkey.net/_favicon.ico', timeout: 8000 }); } } } if (res.status === 200) { resolve({ data: (method === 'HEAD' ? null : JSON.parse(res.responseText)), headers: res.responseHeaders, status: res.status }); } else if (res.status === 403 || res.status === 429) { if (retries < RETRY_LIMIT) { console.warn(`朝阳的工具: [${repoName}] 请求失败,状态码 ${res.status} (重试 ${retries + 1}/${RETRY_LIMIT})。URL: ${url}`); await sleep(RETRY_DELAY_MS * (retries + 1)); makeGitHubApiRequestWithRetry(url, repoName, method, retries + 1).then(resolve).catch(reject); } else { console.error(`朝阳的工具: [${repoName}] 请求失败,状态码 ${res.status} (已达最大重试次数)。URL: ${url}`); console.error('朝阳的工具: 请检查您的GitHub Token是否有效,或等待速率限制解除。'); GM_notification({ title: '朝阳的工具: 请求失败', text: `[${repoName}] GitHub API请求失败,状态码 ${res.status} (已达最大重试次数)。请检查网络或Token。`, image: 'https://www.tampermonkey.net/_favicon.ico', timeout: 5000 }); reject(new Error(`Failed to fetch: ${res.status} (Max retries exceeded)`)); } } else if (res.status === 404) { resolve({ data: null, headers: res.responseHeaders, status: res.status }); } else if (res.status >= 500) { if (retries < RETRY_LIMIT) { console.warn(`朝阳的工具: [${repoName}] 服务器错误 ${res.status} (重试 ${retries + 1}/${RETRY_LIMIT})。URL: ${url}`); await sleep(RETRY_DELAY_MS * (retries + 1)); makeGitHubApiRequestWithRetry(url, repoName, method, retries + 1).then(resolve).catch(reject); } else { console.error(`朝阳的工具: [${repoName}] 服务器错误 ${res.status} (已达最大重试次数)。URL: ${url}`); GM_notification({ title: '朝阳的工具: 请求失败', text: `[${repoName}] 服务器错误 ${res.status} (已达最大重试次数)。`, image: 'https://www.tampermonkey.net/_favicon.ico', timeout: 5000 }); reject(new Error(`Server error: ${res.status} (Max retries exceeded)`)); } } else { console.error(`朝阳的工具: [${repoName}] 请求失败,状态码: ${res.status}, URL: ${url}`); console.error('朝阳的工具: [${repoName}] API 响应全文:', res.responseText); GM_notification({ title: '朝阳的工具: 请求失败', text: `[${repoName}] GitHub API请求失败,状态码: ${res.status}。`, image: 'https://www.tampermonkey.net/_favicon.ico', timeout: 5000 }); reject(new Error(`Failed to fetch: ${res.status}`)); } }, onerror: async function(err) { if (retries < RETRY_LIMIT) { console.warn(`朝阳的工具: [${repoName}] 请求出错 (网络/CORS/其他, 重试 ${retries + 1}/${RETRY_LIMIT})。URL: ${url}`, err); await sleep(RETRY_DELAY_MS * (retries + 1)); makeGitHubApiRequestWithRetry(url, repoName, method, retries + 1).then(resolve).catch(reject); } else { console.error(`朝阳的工具: [${repoName}] 请求出错 (已达最大重试次数)。URL: ${url}`, err); GM_notification({ title: '朝阳的工具: 网络错误', text: `[${repoName}] GitHub API请求发生网络错误。`, image: 'https://www.tampermonkey.net/_favicon.ico', timeout: 5000 }); reject(new Error(`Request error (Max retries exceeded): ${err.message}`)); } }, ontimeout: async function() { if (retries < RETRY_LIMIT) { console.warn(`朝阳的工具: [${repoName}] 请求超时 (重试 ${retries + 1}/${RETRY_LIMIT})。URL: ${url}`); await sleep(RETRY_DELAY_MS * (retries + 1)); makeGitHubApiRequestWithRetry(url, repoName, method, retries + 1).then(resolve).catch(reject); } else { console.error(`朝阳的工具: [${repoName}] 请求超时 (已达最大重试次数)。URL: ${url}`); GM_notification({ title: '朝阳的工具: 请求超时', text: `[${repoName}] GitHub API请求超时。`, image: 'https://www.tampermonkey.net/_favicon.ico', timeout: 5000 }); reject(new Error('Request Timeout (Max retries exceeded)')); } } }); }); } /** * 获取指定App ID在所有清单网站的最新更新时间 * @param {string} appId - 游戏App ID (应为字符串) * @returns {Promise<{manifests: Object, latestTime: number, latestSource: string}>} * manifests: { '清单网站名称': timestamp | 0 | -1 } // 0: Not Found, 1: Found, -1: Unknown/Error/Loading * latestTime: 所有清单网站中该App ID的最新更新时间 (目前只标记存在为1) * latestSource: 对应最新更新时间的清单网站名称 */ async function getAppIdManifestInfo(appId) { const targetAppId = String(appId); const results = {}; let latestTime = 0; let latestSource = ''; const promises = allManifestRepos.map(async (repoInfo) => { const ownerRepoParts = repoInfo.url.split('/'); const owner = ownerRepoParts[0]; const repo = ownerRepoParts[1]; const repoName = repoInfo.name; const cacheKey = `${owner}/${repo}`; let cachedRepoData = githubManifestsCache[cacheKey]; if (!cachedRepoData) { cachedRepoData = { data: {}, lastFetched: 0 }; githubManifestsCache[cacheKey] = cachedRepoData; } let foundInThisRepo = false; let repoStatus = -1; if (cachedRepoData.data && Object.keys(cachedRepoData.data).length > 0) { if (cachedRepoData.data[targetAppId] === 1) { foundInThisRepo = true; repoStatus = 1; } else if (cachedRepoData.data[targetAppId] === 0) { repoStatus = 0; } } const isCacheStale = (Date.now() - cachedRepoData.lastFetched) > CACHE_LIFETIME_MS; // 当缓存中没有该App ID的明确状态 (1或0) 且缓存过期时,才进行实时查询 // 否则,如果缓存有效且有明确状态,或者缓存过期但App ID已经在缓存中标记为不存在 (0),则不需要额外查询 if ((cachedRepoData.data[targetAppId] === undefined) || isCacheStale) { try { const branchCheckUrl = `https://api.github.com/repos/${owner}/${repo}/branches/${targetAppId}`; const response = await makeGitHubApiRequestWithRetry(branchCheckUrl, repoName, 'HEAD'); if (response.status === 200) { foundInThisRepo = true; repoStatus = 1; cachedRepoData.data[targetAppId] = 1; cachedRepoData.lastFetched = Date.now(); await GM_setValue(GM_GITHUB_MANIFESTS_CACHE, githubManifestsCache); // 使用 await console.log(`朝阳的工具: [AppIdInfo] 实时查询发现 App ID ${targetAppId} 在 ${repoName} 存在,已更新缓存。`); } else if (response.status === 404) { repoStatus = 0; cachedRepoData.data[targetAppId] = 0; cachedRepoData.lastFetched = Date.now(); await GM_setValue(GM_GITHUB_MANIFESTS_CACHE, githubManifestsCache); // 使用 await } else { console.warn(`朝阳的工具: [AppIdInfo] 实时查询 App ID ${targetAppId} 在 ${repoName} 返回非200/404状态: ${response.status}`); repoStatus = -1; } } catch (error) { console.error(`朝阳的工具: [AppIdInfo] 实时查询 App ID ${targetAppId} 在 ${repoName} 失败:`, error); repoStatus = -1; } } return { repoName, status: repoStatus, found: foundInThisRepo }; }); const allResults = await Promise.all(promises); for (const res of allResults) { results[res.repoName] = res.status; if (res.found) { if (!latestSource) { // If multiple sources, just pick the first one found as "latest" for simplicity latestTime = 1; // Mark as found latestSource = res.repoName; } } } return { manifests: results, latestTime: latestTime, latestSource: latestSource }; } /** * 下载App ID对应的分支文件 * @param {string} appId - 游戏App ID * @param {string} sourceOption - 下载来源 ('自动', '清单网站1', etc.) */ async function downloadManifest(appId, sourceOption) { let downloadUrl = ''; let targetRepoName = ''; let repoToDownload = null; if (sourceOption === '自动') { console.log(`朝阳的工具: 下载模式为 '自动',正在查找 App ID ${appId} 的最新来源...`); const { latestSource } = await getAppIdManifestInfo(appId); if (latestSource) { repoToDownload = allManifestRepos.find(r => r.name === latestSource); targetRepoName = latestSource; console.log(`朝阳的工具: '自动' 模式找到最新来源: ${latestSource}`); } else { console.error(`朝阳的工具: '自动' 模式未能找到 App ID ${appId} 的任何有效清单来源。`); alert(`朝阳的工具: 无法找到 App ID ${appId} 的最新清单来源,请尝试手动选择清单网站。`); return; } } else { repoToDownload = allManifestRepos.find(r => r.name === sourceOption); targetRepoName = sourceOption; console.log(`朝阳的工具: 下载模式为 '${sourceOption}'。`); } if (repoToDownload) { const ownerRepo = repoToDownload.url.split('/'); downloadUrl = `https://github.com/${ownerRepo[0]}/${ownerRepo[1]}/archive/refs/heads/${String(appId)}.zip`; console.log(`朝阳的工具: 准备下载 App ID ${appId} from ${targetRepoName}. 下载URL: ${downloadUrl}`); GM_xmlhttpRequest({ method: 'GET', url: downloadUrl, responseType: 'blob', onload: async function(response) { // 添加 async if (response.status === 200) { const blob = new Blob([response.response], { type: response.response.type }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `${appId}_${targetRepoName}.zip`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(a.href); downloadRecords[appId] = { downloadedTime: Date.now(), manifestSource: targetRepoName }; await GM_setValue(GM_DOWNLOAD_RECORDS, downloadRecords); // 使用 await console.log(`朝阳的工具: App ID ${appId} 下载记录已保存。`); updateFloatingWindowContent(); alert(`朝阳的工具: App ID ${appId} 下载成功!`); } else if (response.status === 404) { console.error(`朝阳的工具: 下载 App ID ${appId} 失败,清单分支不存在 (404 Not Found)。URL: ${downloadUrl}`); alert(`朝阳的工具: 下载 App ID ${appId} 失败,指定清单中不存在该App ID的下载。`); } else { console.error(`朝阳的工具: 下载 App ID ${appId} 失败,状态码: ${response.status}. URL: ${downloadUrl}`); alert(`朝阳的工具: 下载 App ID ${appId} 失败,请检查网络或清单是否存在 (HTTP ${response.status})。`); } }, onerror: function(error) { console.error(`朝阳的工具: 下载 App ID ${appId} 时发生错误:`, error); alert(`朝阳的工具: 下载 App ID ${appId} 时发生网络错误。`); } }); } else { alert(`朝阳的工具: 无法找到 App ID ${appId} 的下载链接或指定来源: ${sourceOption}。`); console.error(`朝阳的工具: 无法找到 App ID ${appId} 的下载链接或指定来源: ${sourceOption}`); } } /** * 检查并更新所有清单网站的App ID及其更新时间缓存 (后台定时任务) */ async function checkAndUpdateAllManifests() { console.log('朝阳的工具: 正在执行后台清单更新检查...'); const lastCheckTime = await GM_getValue(GM_LAST_CHECK_TIME, 0); const currentTime = Date.now(); // 仅在到达预设的后台检查间隔时,才更新全局的“上次检查时间戳” // 且不再主动发起对所有清单网站的“所有分支”的API请求 if (currentTime - lastCheckTime >= BACKGROUND_CHECK_INTERVAL_MS) { await GM_setValue(GM_LAST_CHECK_TIME, currentTime); // 更新时间戳 console.log('朝阳的工具: 后台检查周期已到,更新检查时间戳。'); } else { console.log('朝阳的工具: 尚未到达下次后台检查时间。跳过全局时间戳更新。'); } // 无论是否到达后台检查周期,只要App ID存在或浮窗打开,都尝试刷新UI // 这些UI刷新操作会按需(基于缓存状态和时效性)触发对当前App ID的精确查询 if (currentAppId) { console.log('朝阳的工具: 后台检查后,正在刷新主页状态框...'); await updateStatusBox(); // 这将触发对 currentAppId 在各清单的按需检查 } const overlay = document.getElementById('chaoyang-tool-overlay'); if (overlay && overlay.style.display === 'flex') { console.log('朝阳的工具: 后台检查后,正在刷新浮窗内容...'); await updateFloatingWindowContent(); // 这也将触发对 currentAppId 在各清单的按需检查 } console.log('朝阳的工具: 后台清单检查逻辑完成。'); } /** * 更新“入库”按钮旁边的状态小框 */ async function updateStatusBox() { const statusBox = document.getElementById('chaoyang-tool-status-box'); if (!statusBox) return; if (!currentAppId) { statusBox.textContent = 'N/A'; statusBox.style.backgroundColor = '#6c757d'; return; } statusBox.textContent = '检查中'; statusBox.style.backgroundColor = '#ffc107'; const { latestTime } = await getAppIdManifestInfo(currentAppId); if (latestTime > 0) { statusBox.textContent = '存在'; statusBox.style.backgroundColor = '#4CAF50'; } else { statusBox.textContent = '没有'; statusBox.style.backgroundColor = '#f44336'; } } /** * 更新浮窗中各个清单网站的App ID存在状态 * @param {Object} manifestInfo - getAppIdManifestInfo 返回的对象 */ function updateManifestStatusDisplay(manifestInfo) { if (!currentAppId) { return; } allManifestRepos.forEach(repo => { const statusBtn = document.getElementById(`repo-status-${repo.name.replace(/\s+/g, '-')}`); if (statusBtn) { const status = manifestInfo.manifests[repo.name]; const ownerRepoParts = repo.url.split('/'); const cacheKey = `${ownerRepoParts[0]}/${ownerRepoParts[1]}`; const cachedData = githubManifestsCache[cacheKey]; let baseColor = repo.isCustom ? '#3366cc' : '#6c757d'; let foundColor = repo.isCustom ? '#008000' : '#28a745'; let notFoundColor = repo.isCustom ? '#cc3333' : '#dc3545'; let checkingColor = repo.isCustom ? '#ff8c00' : '#ffc107'; let errorColor = '#ff5555'; if (status === 1) { statusBtn.textContent = `${repo.name}: Found`; statusBtn.style.backgroundColor = foundColor; } else if (status === 0) { // Check if there's valid cached data explicitly marking it as not found const hasValidCachedStatus = cachedData && (cachedData.data[currentAppId] === 0 || cachedData.data[currentAppId] === 1); if (hasValidCachedStatus) { statusBtn.textContent = `${repo.name}: Not Found`; statusBtn.style.backgroundColor = notFoundColor; } else { // If no specific status is known in cache for this App ID, or cache is stale statusBtn.textContent = `${repo.name}: Checking...`; statusBtn.style.backgroundColor = checkingColor; } } else if (status === -1) { statusBtn.textContent = `${repo.name}: Error!`; statusBtn.style.backgroundColor = errorColor; } else { // This else block handles cases where 'status' is not 0, 1, or -1 (e.g., initial state) if (!cachedData || Object.keys(cachedData.data).length === 0) { statusBtn.textContent = `${repo.name}: Fetching...`; // First load or empty cache, needs check statusBtn.style.backgroundColor = baseColor; } else if (Date.now() - cachedData.lastFetched >= CACHE_LIFETIME_MS) { statusBtn.textContent = `${repo.name}: Stale Cache`; // Cache expired, needs refresh statusBtn.style.backgroundColor = checkingColor; } else { statusBtn.textContent = `${repo.name}: Ready`; // Cache valid, but App ID status not explicitly 0/1 statusBtn.style.backgroundColor = foundColor; // Or another color indicating general readiness } } statusBtn.style.color = 'white'; } }); const autoOption = document.getElementById('option-自动'); if (autoOption && autoOption.nextElementSibling) { const autoLabel = autoOption.nextElementSibling; let existingHint = autoLabel.querySelector('.auto-hint'); if (existingHint) { autoLabel.removeChild(existingHint); } const hintSpan = document.createElement('span'); hintSpan.className = 'auto-hint'; hintSpan.style.marginLeft = '10px'; hintSpan.style.fontSize = '0.9em'; hintSpan.style.fontWeight = 'normal'; hintSpan.style.color = '#ccc'; if (manifestInfo.latestSource) { hintSpan.textContent = `(当前最新来源: ${manifestInfo.latestSource})`; hintSpan.style.color = '#8be9fd'; } else { hintSpan.textContent = `(当前无可用最新来源)`; hintSpan.style.color = '#ff5555'; } autoLabel.appendChild(hintSpan); } } /** * 更新浮窗内容,特别是第二行的下载记录和更新状态 */ async function updateFloatingWindowContent() { const recordList = document.getElementById('chaoyang-tool-record-list'); const mainDownloadBtn = document.getElementById('chaoyang-tool-main-download-btn'); const optionGroup = document.getElementById('chaoyang-tool-option-group'); const repoStatusGroup = document.getElementById('chaoyang-tool-repo-status-group'); const customManifestList = document.getElementById('chaoyang-tool-custom-manifest-list'); const githubTokenInput = document.getElementById('chaoyang-tool-github-token-input'); const currentTokenDisplay = document.getElementById('chaoyang-tool-current-token-display'); if (!recordList || !mainDownloadBtn || !optionGroup || !repoStatusGroup || !customManifestList || !githubTokenInput || !currentTokenDisplay) return; // 更新Token显示 githubTokenInput.value = githubToken; // 显示当前Token currentTokenDisplay.textContent = githubToken ? '已设置' : '未设置'; currentTokenDisplay.style.color = githubToken ? '#4CAF50' : '#f44336'; // 清空并重新生成下载源选项和状态显示 optionGroup.innerHTML = ''; repoStatusGroup.innerHTML = ''; customManifestList.innerHTML = ''; // 重新生成“自动”选项 const autoRadioId = `option-自动`; const autoLabel = document.createElement('label'); autoLabel.htmlFor = autoRadioId; const autoRadio = document.createElement('input'); autoRadio.type = 'radio'; autoRadio.name = 'downloadOption'; autoRadio.value = '自动'; autoRadio.id = autoRadioId; autoRadio.checked = true; // 默认选中“自动” autoLabel.appendChild(autoRadio); autoLabel.appendChild(document.createTextNode('自动')); optionGroup.appendChild(autoLabel); let selectedDownloadOption = '自动'; // 默认选中“自动” // 重新生成所有清单网站的选项和状态按钮 allManifestRepos.forEach(repoInfo => { const radioId = `option-${repoInfo.name.replace(/\s+/g, '-')}`; const label = document.createElement('label'); label.htmlFor = radioId; label.classList.add('chaoyang-tool-repo-label'); if (repoInfo.isCustom) { label.classList.add('chaoyang-tool-custom-label'); // 添加自定义样式 } const radio = document.createElement('input'); radio.type = 'radio'; radio.name = 'downloadOption'; radio.value = repoInfo.name; radio.id = radioId; label.appendChild(radio); label.appendChild(document.createTextNode(repoInfo.name)); optionGroup.appendChild(label); const statusBtn = document.createElement('button'); statusBtn.id = `repo-status-${repoInfo.name.replace(/\s+/g, '-')}`; statusBtn.className = 'chaoyang-tool-repo-status-btn'; if (repoInfo.isCustom) { statusBtn.classList.add('chaoyang-tool-custom-status-btn'); } statusBtn.textContent = `${repoInfo.name}: Checking...`; repoStatusGroup.appendChild(statusBtn); }); // 重新绑定下载按钮的事件 optionGroup.querySelectorAll('input[type="radio"]').forEach(radio => { radio.onchange = () => { selectedDownloadOption = radio.value; mainDownloadBtn.disabled = false; }; }); document.getElementById('option-自动').checked = true; mainDownloadBtn.disabled = false; mainDownloadBtn.onclick = () => { if (currentAppId && selectedDownloadOption) { downloadManifest(currentAppId, selectedDownloadOption); } else { alert('朝阳的工具: 请先选择一个下载选项。'); } }; // 填充自定义清单列表 userDefinedManifests.forEach(repo => { const li = document.createElement('li'); li.className = 'chaoyang-tool-custom-repo-item'; li.textContent = `${repo.name} (${repo.url})`; const removeBtn = document.createElement('button'); removeBtn.textContent = '移除'; removeBtn.className = 'chaoyang-tool-remove-custom-repo-btn'; removeBtn.onclick = () => removeUserManifest(repo.url); li.appendChild(removeBtn); customManifestList.appendChild(li); }); recordList.innerHTML = ''; if (!currentAppId) { const li = document.createElement('div'); li.style.gridColumn = '1 / span 3'; li.style.textAlign = 'center'; li.style.padding = '10px 0'; li.style.borderBottom = 'none'; li.textContent = '请打开一个Steam/SteamDB游戏页面。'; recordList.appendChild(li); // 无论是否有App ID,都更新浮窗中所有清单网站的加载状态 allManifestRepos.forEach(repo => { const statusBtn = document.getElementById(`repo-status-${repo.name.replace(/\s+/g, '-')}`); if (statusBtn) { const ownerRepoParts = repo.url.split('/'); const cacheKey = `${ownerRepoParts[0]}/${ownerRepoParts[1]}`; const cachedData = githubManifestsCache[cacheKey]; let baseColor = repo.isCustom ? '#3366cc' : '#6c757d'; let readyColor = repo.isCustom ? '#008000' : '#28a745'; let staleColor = repo.isCustom ? '#ff8c00' : '#ffc107'; if (cachedData && Object.keys(cachedData.data).length > 0 && (Date.now() - cachedData.lastFetched < CACHE_LIFETIME_MS)) { statusBtn.textContent = `${repo.name}: Ready`; statusBtn.style.backgroundColor = readyColor; } else if (cachedData && Object.keys(cachedData.data).length > 0) { statusBtn.textContent = `${repo.name}: Stale Cache`; statusBtn.style.backgroundColor = staleColor; } else { statusBtn.textContent = `${repo.name}: Fetching...`; statusBtn.style.backgroundColor = baseColor; } statusBtn.style.color = 'white'; } }); return; } const appInfoForCurrent = await getAppIdManifestInfo(currentAppId); updateManifestStatusDisplay(appInfoForCurrent); if (Object.keys(downloadRecords).length === 0) { const li = document.createElement('div'); li.style.gridColumn = '1 / span 3'; li.style.textAlign = 'center'; li.style.padding = '10px 0'; li.style.borderBottom = 'none'; li.textContent = '暂无下载记录。'; recordList.appendChild(li); return; } const recordPromises = Object.keys(downloadRecords).map(async (appId) => { const appInfo = await getAppIdManifestInfo(appId); const latestFound = appInfo.latestTime > 0; return { appId, latestFound, latestSource: appInfo.latestSource }; }); const recordsWithStatus = await Promise.all(recordPromises); for (const record of recordsWithStatus) { const appId = record.appId; const latestFound = record.latestFound; const latestSource = record.latestSource; let statusText = '没有'; let statusColor = '#f44336'; let buttonEnabled = false; if (latestFound) { statusText = '最新'; statusColor = '#4CAF50'; buttonEnabled = true; } else { statusText = '没有'; statusColor = '#f44336'; buttonEnabled = false; } const idCol = document.createElement('div'); idCol.textContent = appId; idCol.style.padding = '5px'; idCol.style.borderBottom = '1px solid #555'; recordList.appendChild(idCol); const statusCol = document.createElement('div'); statusCol.textContent = statusText; statusCol.style.color = statusColor; statusCol.style.padding = '5px'; statusCol.style.borderBottom = '1px solid #555'; recordList.appendChild(statusCol); const updateBtnContainer = document.createElement('div'); const updateBtn = document.createElement('button'); updateBtn.textContent = '入库'; updateBtn.style.cssText = `background-color: ${buttonEnabled ? '#007bff' : '#6c757d'}; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: ${buttonEnabled ? 'pointer' : 'not-allowed'}; opacity: ${buttonEnabled ? '1' : '0.6'}; transition: background-color 0.2s; width: 80px; box-shadow: 0 2px 4px rgba(0,0,0,0.2); `; updateBtn.disabled = !buttonEnabled; updateBtn.onclick = () => { if (buttonEnabled) { if (latestSource) { downloadManifest(appId, latestSource); } else { alert(`朝阳的工具: 无法找到 App ID ${appId} 最新版本的来源。`); } } }; updateBtnContainer.style.padding = '5px'; updateBtnContainer.style.textAlign = 'center'; updateBtnContainer.style.borderBottom = '1px solid #555'; updateBtnContainer.appendChild(updateBtn); recordList.appendChild(updateBtnContainer); } } /** * 添加用户自定义清单网站 * @param {string} name - 显示名称 (可选) * @param {string} githubUrl - GitHub仓库的任意URL (例如: owner/repo, 或包含/tree/分支名 的完整URL) */ async function addUserManifest(name, githubUrl) { // 添加 async if (!githubUrl) { alert('朝阳的工具: GitHub仓库URL不能为空!'); return; } const githubRepoRegex = /(?:github\.com\/)?([^/]+)\/([^/]+)(?:$|\/.*)/; const match = githubUrl.match(githubRepoRegex); if (!match || match.length < 3) { alert('朝阳的工具: GitHub仓库URL格式不正确,请确保是有效的GitHub仓库链接 (例如: https://github.com/owner/repo 或 owner/repo)。'); return; } const owner = match[1]; const repo = match[2]; const repoUrl = `${owner}/${repo}`; const displayName = name.trim() || repoUrl; const existing = allManifestRepos.find(r => r.url.toLowerCase() === repoUrl.toLowerCase()); if (existing) { alert(`朝阳的工具: 清单网站 "${displayName}" (URL: ${repoUrl}) 已存在!`); return; } const newRepo = { name: displayName, url: repoUrl, type: 'branches', isCustom: true }; userDefinedManifests.push(newRepo); await GM_setValue(GM_USER_MANIFESTS, userDefinedManifests); // 使用 await allManifestRepos = [...HARDCODED_GITHUB_MANIFEST_REPOS, ...userDefinedManifests]; console.log(`朝阳的工具: 已添加自定义清单: ${displayName} (${repoUrl})`); updateFloatingWindowContent(); } /** * 移除用户自定义清单网站 * @param {string} url - GitHub仓库URL (owner/repo) */ async function removeUserManifest(url) { // 添加 async if (confirm(`朝阳的工具: 确定要移除清单网站 ${url} 吗?`)) { userDefinedManifests = userDefinedManifests.filter(repo => repo.url !== url); await GM_setValue(GM_USER_MANIFESTS, userDefinedManifests); // 使用 await allManifestRepos = [...HARDCODED_GITHUB_MANIFEST_REPOS, ...userDefinedManifests]; console.log(`朝阳的工具: 已移除自定义清单: ${url}`); const ownerRepoParts = url.split('/'); const cacheKey = `${ownerRepoParts[0]}/${ownerRepoParts[1]}`; if (githubManifestsCache[cacheKey]) { delete githubManifestsCache[cacheKey]; await GM_setValue(GM_GITHUB_MANIFESTS_CACHE, githubManifestsCache); // 使用 await console.log(`朝阳的工具: 已清除 ${url} 的缓存。`); } updateFloatingWindowContent(); } } /** * 保存GitHub Token * @param {string} tokenValue */ async function saveGitHubToken(tokenValue) { // 添加 async githubToken = tokenValue.trim(); try { await GM_setValue(GM_GITHUB_TOKEN, githubToken); // 使用 await alert('朝阳的工具: GitHub Token已保存。'); updateFloatingWindowContent(); } catch (e) { console.error('朝阳的工具: 保存GitHub Token时出错:', e); GM_notification({ title: '朝阳的工具', text: `保存GitHub Token时出错,请手动检查Tampermonkey存储。错误:${e.message || e}`, image: 'https://www.tampermonkey.net/_favicon.ico', timeout: 5000 }); alert('朝阳的工具: 保存GitHub Token时出错。'); } } /** * 清除GitHub Token */ async function clearGitHubToken() { // 添加 async if (confirm('朝阳的工具: 确定要清除您的GitHub Token吗?这将可能导致API速率限制。')) { try { await GM_deleteValue(GM_GITHUB_TOKEN); // 使用 await githubToken = ''; // 清空内存变量 alert('朝阳的工具: GitHub Token已清除。'); updateFloatingWindowContent(); // 刷新UI } catch (e) { console.error('朝阳的工具: 清除GitHub Token时出错:', e); GM_notification({ title: '朝阳的工具', text: `清除GitHub Token时出错,请手动检查Tampermonkey存储。错误:${e.message || e}`, image: 'https://www.tampermonkey.net/_favicon.ico', timeout: 5000 }); alert('朝阳的工具: 清除GitHub Token时出错。'); } } } // --- UI 渲染 (保持不变,或微调样式) --- function createUI() { const isSteamDB = window.location.hostname === 'steamdb.info'; GM_addStyle( `.chaoyang-tool-btn { position: fixed; width: 60px; height: 60px; border-radius: 50%; background-color: rgba(0, 0, 0, 0.7); color: white; font-size: 16px; font-weight: bold; border: none; cursor: grab; /* 提示用户按钮可拖动 */ z-index: 9999; display: flex; justify-content: center; align-items: center; box-shadow: 0 4px 8px rgba(0,0,0,0.3); transition: background-color 0.3s, top 0.1s, left 0.1s; /* 增加top/left过渡 */ } .chaoyang-tool-btn:hover { background-color: rgba(0, 0, 0, 0.9); } .chaoyang-tool-status-box { position: fixed; width: 70px; height: 30px; border-radius: 5px; background-color: #ccc; color: white; font-size: 14px; display: flex; justify-content: center; align-items: center; z-index: 9999; box-shadow: 0 2px 4px rgba(0,0,0,0.2); transition: background-color 0.3s, top 0.1s, left 0.1s; /* 增加top/left过渡 */ } .chaoyang-tool-refresh-btn { position: fixed; width: 70px; height: 25px; border-radius: 5px; background-color: #007bff; color: white; font-size: 12px; border: none; cursor: pointer; z-index: 9999; box-shadow: 0 2px 4px rgba(0,0,0,0.2); transition: background-color 0.3s, top 0.1s, left 0.1s; /* 增加top/left过渡 */ } .chaoyang-tool-refresh-btn:hover { background-color: #0056b3; } .chaoyang-tool-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); z-index: 10000; display: flex; justify-content: center; align-items: center; } .chaoyang-tool-float-window { background-color: #2a2a2a; border-radius: 8px; box-shadow: 0 8px 16px rgba(0, 0, 0, 0.5); padding: 25px; width: 600px; max-width: 90%; max-height: 90%; overflow-y: auto; color: #e0e0e0; font-family: 'Segoe UI', Arial, sans-serif; box-sizing: border-box; position: relative; } .chaoyang-tool-float-window::-webkit-scrollbar { width: 8px; } .chaoyang-tool-float-window::-webkit-scrollbar-track { background: #444; border-radius: 10px; } .chaoyang-tool-float-window::-webkit-scrollbar-thumb { background: #888; border-radius: 10px; } .chaoyang-tool-float-window::-webkit-scrollbar-thumb:hover { background: #555; } .chaoyang-tool-section { margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #444; } .chaoyang-tool-section:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; } .chaoyang-tool-section h3 { margin-top: 0; margin-bottom: 15px; color: #f0f0f0; font-size: 18px; } .chaoyang-tool-option-group { display: flex; gap: 15px; flex-wrap: wrap; margin-bottom: 15px; } .chaoyang-tool-option-group label { background-color: #3a3a3a; padding: 8px 15px; border-radius: 5px; cursor: pointer; transition: background-color 0.2s; font-size: 15px; } .chaoyang-tool-option-group label:hover { background-color: #4a4a4a; } .chaoyang-tool-option-group input[type="radio"] { margin-right: 8px; } .chaoyang-tool-custom-label { background-color: #3366cc !important; } .chaoyang-tool-custom-label:hover { background-color: #2a52a3 !important; } .chaoyang-tool-custom-status-btn { background-color: #3366cc !important; } .chaoyang-tool-repo-status-group { display: flex; flex-wrap: wrap; gap: 10px; justify-content: flex-start; margin-bottom: 15px; } .chaoyang-tool-repo-status-btn { background-color: #6c757d; color: white; border: none; padding: 8px 12px; border-radius: 5px; font-size: 14px; cursor: default; transition:background-color 0.3s; min-width: 120px; text-align: center; } .chaoyang-tool-record-header, .chaoyang-tool-record-list div { display:grid; grid-template-columns: 1fr 0.8fr 0.8fr; gap: 10px; align-items: center; padding: 8px 0; } .chaoyang-tool-record-header { font-weight: bold; color: #cccccc; border-bottom: 2px solid #555; padding-bottom: 10px; margin-bottom: 5px; } .chaoyang-tool-record-list { max-height: 200px; overflow-y: auto; background-color: #333; padding: 10px; border-radius: 5px; } .chaoyang-tool-record-list::-webkit-scrollbar { width: 8px; } .chaoyang-tool-record-list::-webkit-scrollbar-track { background: #444; border-radius: 10px; } .chaoyang-tool-record-list::-webkit-scrollbar-thumb { background: #888; border-radius: 10px; } .chaoyang-tool-record-list::-webkit-scrollbar-thumb:hover { background: #555; } .chaoyang-tool-main-download-btn { background-color: #007bff; color: white; border: none; padding: 12px 25px; border-radius: 5px; cursor: pointer; font-size: 16px; width: 100%; box-shadow: 0 4px 8px rgba(0,0,0,0.3); transition: background-color 0.3s, opacity 0.3s; margin-top: 20px; box-sizing: border-box; } .chaoyang-tool-main-download-btn:disabled { background-color: #6c757d; cursor: not-allowed; opacity: 0.7; } .chaoyang-tool-close-btn { position: absolute; top: 10px; right: 10px; background: none; border: none; font-size: 24px; color: #ccc; cursor: pointer; } .chaoyang-tool-close-btn:hover { color: white; } .chaoyang-tool-clear-cache-btn { position: fixed; bottom: 20px; right: 20px; background-color: #dc3545; color: white; padding: 10px 15px; border-radius: 5px; border: none; cursor: pointer; z-index: 9999; box-shadow: 0 2px 4px rgba(0,0,0,0.2); font-size: 14px; } .chaoyang-tool-clear-cache-btn:hover { background-color: #c82333; } .chaoyang-tool-custom-input-group { display: flex; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; } .chaoyang-tool-custom-input-group input[type="text"] { flex: 1; padding: 8px; border-radius: 4px; border: 1px solid #555; background-color: #333; color: #e0e0e0; min-width: 150px; } .chaoyang-tool-custom-input-group button { padding: 8px 15px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; transition: background-color 0.2s; } .chaoyang-tool-custom-input-group button:hover { background-color: #0056b3; } .chaoyang-tool-custom-manifest-list { list-style: none; padding: 0; max-height: 150px; overflow-y: auto; background-color: #333; border-radius: 5px; } .chaoyang-tool-custom-manifest-list::-webkit-scrollbar { width: 8px; } .chaoyang-tool-custom-manifest-list::-webkit-scrollbar-track { background: #444; border-radius: 10px; } .chaoyang-tool-custom-manifest-list::-webkit-scrollbar-thumb { background: #888; border-radius: 10px; } .chaoyang-tool-custom-manifest-list::-webkit-scrollbar-thumb:hover { background: #555; } .chaoyang-tool-custom-manifest-list li { display: flex; justify-content: space-between; align-items: center; padding: 8px 10px; border-bottom: 1px solid #444; } .chaoyang-tool-custom-manifest-list li:last-child { border-bottom: none; } .chaoyang-tool-remove-custom-repo-btn { background-color: #dc3545; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; font-size: 12px; transition: background-color 0.2s; } .chaoyang-tool-remove-custom-repo-btn:hover { background-color: #c82333; } .chaoyang-tool-token-section { margin-top: 20px; padding-top: 15px; border-top: 1px solid #444; } .chaoyang-tool-token-input-group { display: flex; gap: 10px; margin-bottom: 10px; align-items: center; flex-wrap: wrap; } .chaoyang-tool-token-input-group input[type="password"] { flex: 1; padding: 8px; border-radius: 4px; border: 1px solid #555; background-color: #333; color: #e0e0e0; min-width: 200px; } .chaoyang-tool-token-input-group button { padding: 8px 15px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; transition: background-color 0.2s; } .chaoyang-tool-token-input-group button:hover { background-color: #0056b3; } .chaoyang-tool-token-input-group button.clear { background-color: #dc3545; } .chaoyang-tool-token-input-group button.clear:hover { background-color: #c82333; } .chaoyang-tool-token-status { margin-top: 5px; font-size: 0.9em; color: #ccc; } #chaoyang-tool-homepage-box { position: absolute; top: 15px; /* 调整位置避免与关闭按钮冲突 */ left: 20px; background-color: #555; color: white; padding: 5px 10px; border-radius: 4px; font-size: 14px; cursor: pointer; /* 修改为可点击指针 */ z-index: 10001; } #chaoyang-tool-homepage-box:hover { background-color: #666; /* 悬停效果 */ } `); const storageBtn = document.createElement('button'); storageBtn.id = 'chaoyang-tool-storage-btn'; storageBtn.className = 'chaoyang-tool-btn'; storageBtn.textContent = '入库'; document.body.appendChild(storageBtn); const statusBox = document.createElement('div'); statusBox.id = 'chaoyang-tool-status-box'; statusBox.className = 'chaoyang-tool-status-box'; statusBox.textContent = '加载中'; document.body.appendChild(statusBox); const refreshStatusBtn = document.createElement('button'); refreshStatusBtn.id = 'chaoyang-tool-refresh-btn'; refreshStatusBtn.className = 'chaoyang-tool-refresh-btn'; refreshStatusBtn.textContent = '刷新状态'; document.body.appendChild(refreshStatusBtn); /** * 更新伴随按钮(状态框和刷新按钮)的位置,使其跟随主“入库”按钮。 */ function updateCompanionButtonPositions() { const btnRect = storageBtn.getBoundingClientRect(); const spacing = 10; // 按钮与伴随元素的间距 // 状态框位置 (在主按钮左侧) statusBox.style.left = `${btnRect.left - statusBox.offsetWidth - spacing}px`; statusBox.style.top = `${btnRect.top}px`; statusBox.style.right = 'auto'; // 确保使用left定位 // 刷新按钮位置 (在状态框下方,与主按钮左侧对齐) refreshStatusBtn.style.left = `${btnRect.left - refreshStatusBtn.offsetWidth - spacing}px`; refreshStatusBtn.style.top = `${btnRect.top + statusBox.offsetHeight + 5}px`; refreshStatusBtn.style.right = 'auto'; // 确保使用left定位 } // --- 拖动逻辑及点击检测 --- let isDragging = false; let offsetX, offsetY; // 鼠标点击位置相对于按钮左上角的偏移 let startX, startY; // 鼠标按下时的页面坐标 let hasMoved = false; // 标记是否发生了拖动行为(鼠标移动超过阈值) // 应用初始位置:如果已加载则使用加载的位置,否则使用默认位置 if (storedButtonPosition && typeof storedButtonPosition.top === 'number' && typeof storedButtonPosition.left === 'number') { storageBtn.style.top = `${storedButtonPosition.top}px`; storageBtn.style.left = `${storedButtonPosition.left}px`; storageBtn.style.right = 'auto'; // 如果设置了left,确保right自动调整 } else { // 默认位置:页面右侧20px,顶部根据Steam/SteamDB区分 const defaultButtonWidth = 60; // 根据CSS .chaoyang-tool-btn width const defaultRightMargin = 20; const defaultButtonLeft = window.innerWidth - defaultButtonWidth - defaultRightMargin; const defaultButtonTop = isSteamDB ? 120 : 20; storageBtn.style.top = `${defaultButtonTop}px`; storageBtn.style.left = `${defaultButtonLeft}px`; storageBtn.style.right = 'auto'; } // 初始更新伴随按钮位置 updateCompanionButtonPositions(); storageBtn.addEventListener('mousedown', (e) => { if (e.button !== 0) return; // 只响应鼠标左键 isDragging = true; hasMoved = false; startX = e.clientX; startY = e.clientY; // 计算鼠标点击位置与按钮左上角的偏移量 offsetX = e.clientX - storageBtn.getBoundingClientRect().left; offsetY = e.clientY - storageBtn.getBoundingClientRect().top; storageBtn.style.cursor = 'grabbing'; // 改变光标以示拖动中 // 拖动时禁用CSS过渡,保证流畅 storageBtn.style.transition = 'none'; statusBox.style.transition = 'none'; refreshStatusBtn.style.transition = 'none'; // 在document上添加事件监听器,即使鼠标移出按钮也能继续拖动 document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }); const onMouseMove = (e) => { if (!isDragging) return; // 计算按钮的新位置 const newLeft = e.clientX - offsetX; const newTop = e.clientY - offsetY; storageBtn.style.left = `${newLeft}px`; storageBtn.style.top = `${newTop}px`; storageBtn.style.right = 'auto'; // 拖动时总是设置left/top,所以right需要自动 // 检查鼠标是否移动超过阈值 if (Math.abs(e.clientX - startX) > DRAG_THRESHOLD || Math.abs(e.clientY - startY) > DRAG_THRESHOLD) { hasMoved = true; } updateCompanionButtonPositions(); // 拖动时实时更新伴随按钮位置 e.preventDefault(); // 阻止默认行为(如文本选择) }; const onMouseUp = async () => { isDragging = false; storageBtn.style.cursor = 'grab'; // 恢复光标 // 恢复CSS过渡 storageBtn.style.transition = ''; statusBox.style.transition = ''; refreshStatusBtn.style.transition = ''; // 移除document上的事件监听器 document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); // 如果鼠标没有发生明显的拖动,则认为是点击 if (!hasMoved) { overlay.style.display = 'flex'; updateFloatingWindowContent(); } // 保存按钮的最终位置 const currentPos = { top: storageBtn.getBoundingClientRect().top, left: storageBtn.getBoundingClientRect().left }; const positionKey = isSteamDB ? GM_BUTTON_POS_STEAMDB : GM_BUTTON_POS_STEAM; await GM_setValue(positionKey, currentPos); console.log(`朝阳的工具: 按钮位置已保存到 ${positionKey}:`, currentPos); hasMoved = false; // 重置hasMoved状态 }; // --- 拖动逻辑结束 --- refreshStatusBtn.onclick = () => { console.log('朝阳的工具: 用户点击了刷新状态按钮。'); updateStatusBox(); }; const overlay = document.createElement('div'); overlay.id = 'chaoyang-tool-overlay'; overlay.className = 'chaoyang-tool-overlay'; overlay.style.display = 'none'; document.body.appendChild(overlay); const floatWindow = document.createElement('div'); floatWindow.className = 'chaoyang-tool-float-window'; overlay.appendChild(floatWindow); // --- 新增: 主页小框 --- const homepageBox = document.createElement('div'); homepageBox.id = 'chaoyang-tool-homepage-box'; homepageBox.textContent = '更新'; // 添加点击事件和指针样式 homepageBox.onclick = () => { window.open('https://tool.steam.al/', '_blank'); }; floatWindow.appendChild(homepageBox); // 暂时放在父容器直接子元素,通过绝对定位来调整 const closeBtn = document.createElement('button'); closeBtn.className = 'chaoyang-tool-close-btn'; closeBtn.innerHTML = '×'; closeBtn.onclick = () => overlay.style.display = 'none'; floatWindow.appendChild(closeBtn); // --- Section 1: GitHub Token管理 --- const tokenSection = document.createElement('div'); tokenSection.className = 'chaoyang-tool-section chaoyang-tool-token-section'; floatWindow.appendChild(tokenSection); const h3_token = document.createElement('h3'); h3_token.textContent = 'GitHub Token 管理:'; tokenSection.appendChild(h3_token); const tokenInputGroup = document.createElement('div'); tokenInputGroup.className = 'chaoyang-tool-token-input-group'; tokenSection.appendChild(tokenInputGroup); const githubTokenInput = document.createElement('input'); githubTokenInput.type = 'password'; githubTokenInput.id = 'chaoyang-tool-github-token-input'; githubTokenInput.placeholder = '在此输入您的GitHub Personal Access Token'; tokenInputGroup.appendChild(githubTokenInput); const saveTokenBtn = document.createElement('button'); saveTokenBtn.textContent = '保存 Token'; saveTokenBtn.onclick = () => saveGitHubToken(githubTokenInput.value); tokenInputGroup.appendChild(saveTokenBtn); const clearTokenBtn = document.createElement('button'); clearTokenBtn.textContent = '清除 Token'; clearTokenBtn.className = 'clear'; clearTokenBtn.onclick = clearGitHubToken; tokenInputGroup.appendChild(clearTokenBtn); const tokenStatus = document.createElement('div'); tokenStatus.id = 'chaoyang-tool-current-token-display'; tokenStatus.className = 'chaoyang-tool-token-status'; tokenStatus.textContent = 'Token状态: 未设置'; tokenSection.appendChild(tokenStatus); // --- Section 2: 下载源选择 --- const section1 = document.createElement('div'); section1.className = 'chaoyang-tool-section'; floatWindow.appendChild(section1); const h3_1 = document.createElement('h3'); h3_1.textContent = '选择下载源:'; section1.appendChild(h3_1); const optionGroup = document.createElement('div'); optionGroup.id = 'chaoyang-tool-option-group'; optionGroup.className = 'chaoyang-tool-option-group'; section1.appendChild(optionGroup); // --- Section 3: 当前App ID在各清单网站状态 --- const sectionStatus = document.createElement('div'); sectionStatus.className = 'chaoyang-tool-section'; floatWindow.appendChild(sectionStatus); const h3_status = document.createElement('h3'); h3_status.textContent = '当前App ID在各清单网站状态:'; sectionStatus.appendChild(h3_status); const repoStatusGroup = document.createElement('div'); repoStatusGroup.id = 'chaoyang-tool-repo-status-group'; repoStatusGroup.className = 'chaoyang-tool-repo-status-group'; sectionStatus.appendChild(repoStatusGroup); // --- Section 4: 我的入库记录 --- const section2 = document.createElement('div'); section2.className = 'chaoyang-tool-section'; floatWindow.appendChild(section2); const h3_2 = document.createElement('h3'); h3_2.textContent = '我的入库记录:'; section2.appendChild(h3_2); const recordHeader = document.createElement('div'); recordHeader.className = 'chaoyang-tool-record-header'; recordHeader.innerHTML = '
App ID
更新状态
操作
'; section2.appendChild(recordHeader); const recordList = document.createElement('div'); recordList.id = 'chaoyang-tool-record-list'; recordList.className = 'chaoyang-tool-record-list'; section2.appendChild(recordList); // --- Section 5: 自定义清单管理 --- const sectionCustom = document.createElement('div'); sectionCustom.className = 'chaoyang-tool-section'; floatWindow.appendChild(sectionCustom); const h3_custom = document.createElement('h3'); h3_custom.textContent = '自定义清单管理:'; sectionCustom.appendChild(h3_custom); const customInputGroup = document.createElement('div'); customInputGroup.className = 'chaoyang-tool-custom-input-group'; sectionCustom.appendChild(customInputGroup); const customNameInput = document.createElement('input'); customNameInput.type = 'text'; customNameInput.placeholder = '清单名称 (可选,例如: 我的私人清单)'; customInputGroup.appendChild(customNameInput); const customUrlInput = document.createElement('input'); customUrlInput.type = 'text'; customUrlInput.placeholder = 'GitHub仓库URL (例如: https://github.com/owner/repo 或 owner/repo)'; customInputGroup.appendChild(customUrlInput); const addCustomBtn = document.createElement('button'); addCustomBtn.textContent = '添加自定义清单'; addCustomBtn.onclick = () => { addUserManifest(customNameInput.value.trim(), customUrlInput.value.trim()); customNameInput.value = ''; customUrlInput.value = ''; }; customInputGroup.appendChild(addCustomBtn); const customManifestList = document.createElement('ul'); customManifestList.id = 'chaoyang-tool-custom-manifest-list'; customManifestList.className = 'chaoyang-tool-custom-manifest-list'; sectionCustom.appendChild(customManifestList); // --- Section 6: 主下载按钮 --- const section3 = document.createElement('div'); section3.className = 'chaoyang-tool-section'; floatWindow.appendChild(section3); const mainDownloadBtn = document.createElement('button'); mainDownloadBtn.id = 'chaoyang-tool-main-download-btn'; mainDownloadBtn.className = 'chaoyang-tool-main-download-btn'; mainDownloadBtn.textContent = '入库'; mainDownloadBtn.disabled = true; section3.appendChild(mainDownloadBtn); const clearCacheBtn = document.createElement('button'); clearCacheBtn.className = 'chaoyang-tool-clear-cache-btn'; clearCacheBtn.textContent = '清除所有数据'; clearCacheBtn.onclick = clearAllTampermonkeyStorage; // 调用新的清除函数 document.body.appendChild(clearCacheBtn); } // --- 初始化和运行逻辑 --- async function init() { console.log('朝阳的工具: 脚本初始化开始。'); currentAppId = getAppIdFromUrl(); // 使用 await 确保从 Tampermonkey 存储中加载数据完成后再继续 githubToken = await GM_getValue(GM_GITHUB_TOKEN, ''); downloadRecords = await GM_getValue(GM_DOWNLOAD_RECORDS, {}); githubManifestsCache = await GM_getValue(GM_GITHUB_MANIFESTS_CACHE, {}); userDefinedManifests = await GM_getValue(GM_USER_MANIFESTS, []); // 加载按钮位置,区分Steam商店和SteamDB const isSteamDB = window.location.hostname === 'steamdb.info'; const positionKey = isSteamDB ? GM_BUTTON_POS_STEAMDB : GM_BUTTON_POS_STEAM; storedButtonPosition = await GM_getValue(positionKey, null); // 加载按钮位置 allManifestRepos = [...HARDCODED_GITHUB_MANIFEST_REPOS, ...userDefinedManifests]; console.log('朝阳的工具: 已加载Tampermonkey下载记录:', Object.keys(downloadRecords).length, '条'); console.log('朝阳的工具: 已加载Tampermonkey GitHub缓存:', Object.keys(githubManifestsCache).length, '个仓库'); console.log('朝阳的工具: 已加载用户自定义清单:', userDefinedManifests.length, '个'); console.log('朝阳的工具: GitHub Token状态:', githubToken ? '已设置' : '未设置'); console.log('朝阳的工具: 按钮上次保存位置:', storedButtonPosition); createUI(); // 现在 createUI 可以使用 storedButtonPosition if (!currentAppId) { console.warn('朝阳的工具: 未检测到当前App ID,脚本部分功能受限。'); updateStatusBox(); // 即使没有App ID,也启动后台检查和UI更新,确保浮窗正常显示 checkAndUpdateAllManifests(); setInterval(checkAndUpdateAllManifests, BACKGROUND_CHECK_INTERVAL_MS); console.log('朝阳的工具: 脚本初始化完成,后台定时检查已启动。'); return; } console.log(`朝阳的工具: 检测到当前 App ID: ${currentAppId}`); await updateStatusBox(); checkAndUpdateAllManifests(); // 不等待,在后台执行 setInterval(checkAndUpdateAllManifests, BACKGROUND_CHECK_INTERVAL_MS); console.log('朝阳的工具: 脚本初始化完成,后台定时检查已启动。'); } window.addEventListener('load', init); })();