// ==UserScript== // @name 朝阳的工具 // @namespace https://bbs.tampermonkey.net.cn/ // @version 1.6.0.1 // @description 第n次更新 // @author zhaoyang // @match https://store.steampowered.com/app/* // @match https://steamdb.info/app/* // @match https://steamui.com/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_deleteValue // @grant GM_notification // @grant GM_setClipboard // @connect api.github.com // @connect raw.githubusercontent.com // @connect github.com // ==/UserScript== (function() { 'use strict'; // --- 脚本配置 --- // 定义核心清单网站,仅保留并重命名为清单网站1和2 const CORE_GITHUB_MANIFEST_REPOS = [ // type: 'branches' 表示每个App ID是一个独立的分支 { name: '清单网站1', url: 'SteamAutoCracks/ManifestHub', type: 'branches' }, { name: '清单网站2', url: 'hansaes/ManifestAutoUpdate', type: 'branches' } ]; const RETRY_LIMIT = 3; // 每个失败请求的重试次数 const RETRY_DELAY_MS = 2000; // 重试间隔时间 (毫秒) const DRAG_THRESHOLD = 5; // 拖动阈值,鼠标移动超过此距离才算拖动,否则算点击 // 用于从URL中提取App ID的正则表达式 const APP_ID_REGEX_STEAM = /\/app\/(\d+)/; const APP_ID_REGEX_STEAMDB = /\/app\/(\d+)/; const APP_ID_REGEX_CLIPBOARD = /^\d{1,10}$/; // 匹配1到10位数字作为App ID // Tampermonkey存储键的前缀 const GM_KEY_PREFIX = 'chaoyang_tool_v2_'; const GM_GITHUB_TOKEN = GM_KEY_PREFIX + 'github_token'; const GM_USER_MANIFESTS = GM_KEY_PREFIX + 'user_manifests'; // 用于存储用户自定义清单(UI展示用,核心下载不使用) const GM_BUTTON_POS_STEAM = GM_KEY_PREFIX + 'button_pos_steam'; const GM_BUTTON_POS_STEAMDB = GM_KEY_PREFIX + 'button_pos_steamdb'; const GM_SETTINGS_FIX_MANIFEST_VERSION = GM_KEY_PREFIX + 'setting_fix_manifest_version'; // 是否固定清单版本(即注释掉setManifestid) const GM_SETTINGS_DOWNLOAD_LUA_ONLY = GM_KEY_PREFIX + 'setting_download_lua_only'; // 是否仅下载lua文件,否则下载zip const GM_SETTINGS_SELECTED_MANIFEST_REPO = GM_KEY_PREFIX + 'setting_selected_manifest_repo'; // 选中的清单仓库 let currentAppId = null; // 当前页面的游戏App ID (从URL获取) let clipboardAppId = null; // 从剪贴板获取的App ID let activeAppId = null; // 当前正在操作的App ID (可能是currentAppId或clipboardAppId) let githubToken = ''; // 从Tampermonkey存储加载的GitHub Personal Access Token let userDefinedManifests = []; // 存储用户自定义的清单网站 (UI上保留,但核心下载不使用) let allManifestRepos = []; // 包含核心和用户自定义的清单网站(UI展示用) let storedButtonPosition = null; // 用于存储从GM_getValue加载的按钮位置 // 设置项变量 let setting_fixManifestVersion = true; // 默认:固定清单版本 (注释掉setManifestid) let setting_downloadLuaOnly = true; // 默认:仅下载lua文件 let setting_selectedManifestRepo = CORE_GITHUB_MANIFEST_REPOS[0].name; // 默认选中第一个核心清单网站 // --- 工具函数 --- /** * 清除所有脚本相关的Tampermonkey存储数据 */ async function clearAllTampermonkeyStorage() { if (!confirm('朝阳的工具: 确定要清除所有脚本本地数据吗?这将清除所有自定义清单、GitHub Token、按钮位置和所有设置。此操作不可逆!')) { return; } console.warn('朝阳的工具: 正在清除所有脚本Tampermonkey存储数据...'); try { await GM_deleteValue(GM_USER_MANIFESTS); await GM_deleteValue(GM_GITHUB_TOKEN); await GM_deleteValue(GM_BUTTON_POS_STEAM); await GM_deleteValue(GM_BUTTON_POS_STEAMDB); await GM_deleteValue(GM_SETTINGS_FIX_MANIFEST_VERSION); await GM_deleteValue(GM_SETTINGS_DOWNLOAD_LUA_ONLY); await GM_deleteValue(GM_SETTINGS_SELECTED_MANIFEST_REPO); // Clear selected manifest repo // 重置内存中的变量 userDefinedManifests = []; githubToken = ''; storedButtonPosition = null; setting_fixManifestVersion = true; setting_downloadLuaOnly = true; setting_selectedManifestRepo = CORE_GITHUB_MANIFEST_REPOS[0].name; // Reset to default allManifestRepos = [...CORE_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} 返回App ID字符串,如果未找到则返回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); } return match ? String(match[1]) : null; } /** * 更新 activeAppId 并刷新相关UI * @param {string|null} newAppId - 新的App ID * @param {string} source - App ID的来源 ('url', 'clipboard') * @returns {Promise} */ async function updateActiveAppId(newAppId, source) { // 如果新App ID与当前App ID相同,且来源不是URL(避免URL每次加载都触发),则不进行不必要的刷新 if (activeAppId === newAppId && source !== 'url') { console.log(`朝阳的工具: 尝试更新App ID为 ${newAppId} (来源: ${source}),但与当前App ID相同,跳过。`); return; } activeAppId = newAppId; console.log(`朝阳的工具: 当前活动App ID更新为 ${activeAppId || 'N/A'} (来源: ${source})`); const mainDownloadBtn = document.getElementById('chaoyang-tool-main-download-btn'); if (mainDownloadBtn) { mainDownloadBtn.textContent = activeAppId ? `下载 ${activeAppId} 清单` : '请选择App ID'; mainDownloadBtn.disabled = !activeAppId; } await updateStatusBox(); // 刷新主按钮旁边的状态框 // 如果浮窗是打开的,也刷新浮窗内的清单状态 if (document.getElementById('chaoyang-tool-overlay') && document.getElementById('chaoyang-tool-overlay').style.display === 'flex') { const appInfo = activeAppId ? await getAppIdManifestInfo(activeAppId) : { manifests: {}, latestSource: null }; updateManifestStatusDisplay(appInfo); } } /** * 暂停指定毫秒数 * @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请求 */ 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 : res.responseText), headers: res.responseHeaders, status: res.status }); } else if (res.status === 403 || res.status === 429 || res.status >= 500) { // 403/429/5xx错误进行重试 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}] 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 }); // 404视为不存在,非错误 } else { console.error(`朝阳的工具: [${repoName}] 请求失败,状态码: ${res.status}, URL: ${url}`); 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在核心清单网站的最新更新时间及存在状态 * 此函数现在会直接进行实时API查询,不使用缓存。 * 作用:仅用于浮窗内的App ID状态显示。 * @param {string} appId - 游戏App ID (应为字符串) * @returns {Promise<{manifests: Object, latestSource: string|null}>} * manifests: { '清单网站名称': status } // status: 1: Found, 0: Not Found, -1: Error * latestSource: 找到App ID的第一个清单网站名称,用于“自动”模式 */ async function getAppIdManifestInfo(appId) { const targetAppId = String(appId); const results = {}; let latestSource = null; // 仍保留此字段,尽管下载逻辑不直接使用,但仍可用于展示“自动”模式的依据 const promises = CORE_GITHUB_MANIFEST_REPOS.map(async (repoInfo) => { const ownerRepoParts = repoInfo.url.split('/'); const owner = ownerRepoParts[0]; const repo = ownerRepoParts[1]; const repoName = repoInfo.name; let repoStatus = -1; // 默认错误状态 try { // 检查分支是否存在 (HEAD请求更快) const branchCheckUrl = `https://api.github.com/repos/${owner}/${repo}/branches/${targetAppId}`; const response = await makeGitHubApiRequestWithRetry(branchCheckUrl, repoName, 'HEAD'); if (response.status === 200) { repoStatus = 1; // 存在 if (!latestSource) { // 如果是第一个找到的,记录为最新来源 latestSource = repoName; } console.log(`朝阳的工具: App ID ${targetAppId} 在 ${repoName} 存在。`); } else if (response.status === 404) { repoStatus = 0; // 不存在 console.log(`朝阳的工具: App ID ${targetAppId} 在 ${repoName} 不存在 (404)。`); } else { console.warn(`朝阳的工具: App ID ${targetAppId} 在 ${repoName} 检查返回非预期状态: ${response.status}`); repoStatus = -1; } } catch (error) { console.error(`朝阳的工具: 检查 App ID ${targetAppId} 在 ${repoName} 时失败:`, error); repoStatus = -1; } return { repoName, status: repoStatus }; }); const allResults = await Promise.all(promises); for (const res of allResults) { results[res.repoName] = res.status; } return { manifests: results, latestSource: latestSource }; } /** * 下载App ID对应的清单文件 (Lua或ZIP) * @param {string} appId - 游戏App ID * @param {string} sourceOption - 下载来源 ('清单网站1', '清单网站2', etc.) */ async function downloadManifest(appId, sourceOption) { if (!appId) { alert('朝阳的工具: 未指定App ID。'); return; } let repoToDownload = CORE_GITHUB_MANIFEST_REPOS.find(r => r.name === sourceOption); let targetRepoName = sourceOption; if (!repoToDownload) { alert(`朝阳的工具: 无法找到 App ID ${appId} 的下载链接或指定来源: ${sourceOption}。`); console.error(`朝阳的工具: 无法找到 App ID ${appId} 的下载链接或指定来源: ${sourceOption}`); return; } const ownerRepo = repoToDownload.url.split('/'); let downloadUrl = ''; let fileName = ''; let responseType = ''; let processContent = null; if (setting_downloadLuaOnly) { downloadUrl = `https://raw.githubusercontent.com/${ownerRepo[0]}/${ownerRepo[1]}/${String(appId)}/${String(appId)}.lua`; fileName = `${appId}.lua`; responseType = 'text'; processContent = (content) => { // 根据用户的设置(“否”才注释)来处理 setManifestid if (!setting_fixManifestVersion) { // 这里是关键修改:当设置值为 false (UI上选择“否”) 时才注释 console.log(`朝阳的工具: 根据设置,正在注释掉 setManifestid。`); return content.replace(/setManifestid/g, '--setManifestid'); } console.log(`朝阳的工具: 根据设置,不注释 setManifestid。`); return content; }; } else { // 下载整个分支的ZIP文件 downloadUrl = `https://github.com/${ownerRepo[0]}/${ownerRepo[1]}/archive/refs/heads/${String(appId)}.zip`; fileName = `${appId}.zip`; responseType = 'arraybuffer'; // 下载二进制文件 processContent = (content) => content; // ZIP文件不需要修改内容 } console.log(`朝阳的工具: 准备下载 App ID ${appId} 的文件 from ${targetRepoName}. URL: ${downloadUrl}`); GM_xmlhttpRequest({ method: 'GET', url: downloadUrl, responseType: responseType, onload: function(response) { if (response.status === 200) { let fileContent = response.response; if (setting_downloadLuaOnly && responseType === 'text') { fileContent = processContent(fileContent); } console.log(`朝阳的工具: 文件内容已获取,准备下载。`); const blob = new Blob([fileContent], { type: setting_downloadLuaOnly ? 'text/plain;charset=utf-8' : 'application/zip' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = fileName; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(a.href); alert(`朝阳的工具: App ID ${appId} 的${setting_downloadLuaOnly ? 'Lua文件' : 'ZIP文件'}下载成功!`); } else if (response.status === 404) { console.error(`朝阳的工具: 下载 App ID ${appId} 的文件失败,文件不存在 (404 Not Found)。URL: ${downloadUrl}`); alert(`朝阳的工具: 下载 App ID ${appId} 的文件失败,指定清单中不存在该App ID的${setting_downloadLuaOnly ? 'Lua文件' : 'ZIP分支'}。`); } 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} 的文件时发生网络错误。`); } }); } /** * 更新“入库”按钮旁边的状态小框 */ async function updateStatusBox() { const statusBox = document.getElementById('chaoyang-tool-status-box'); if (!statusBox) return; if (!activeAppId) { statusBox.textContent = 'N/A'; statusBox.style.backgroundColor = '#6c757d'; return; } statusBox.textContent = '检查中'; statusBox.style.backgroundColor = '#ffc107'; const { latestSource } = await getAppIdManifestInfo(activeAppId); if (latestSource) { statusBox.textContent = '存在'; statusBox.style.backgroundColor = '#4CAF50'; } else { statusBox.textContent = '没有'; statusBox.style.backgroundColor = '#f44336'; } } /** * 更新浮窗中各个清单网站的App ID存在状态 * @param {Object} manifestInfo - getAppIdManifestInfo 返回的对象 */ function updateManifestStatusDisplay(manifestInfo) { if (!activeAppId) { // 如果没有App ID,将所有状态按钮设为待检查/N/A allManifestRepos.forEach(repo => { const statusBtn = document.getElementById(`repo-status-${repo.name.replace(/\s+/g, '-')}`); if (statusBtn) { statusBtn.textContent = `${repo.name}: N/A`; statusBtn.style.backgroundColor = '#6c757d'; statusBtn.style.color = 'white'; } }); return; } CORE_GITHUB_MANIFEST_REPOS.forEach(repo => { // 只更新核心清单的状态 const statusBtn = document.getElementById(`repo-status-${repo.name.replace(/\s+/g, '-')}`); if (statusBtn) { const status = manifestInfo.manifests[repo.name]; let foundColor = '#28a745'; // 绿色 let notFoundColor = '#dc3545'; // 红色 let errorColor = '#ff5555'; // 错误更深的红 let checkingColor = '#ffc107'; // 检查中黄色 if (status === 1) { statusBtn.textContent = `${repo.name}: Found`; statusBtn.style.backgroundColor = foundColor; } else if (status === 0) { statusBtn.textContent = `${repo.name}: Not Found`; statusBtn.style.backgroundColor = notFoundColor; } else if (status === -1) { statusBtn.textContent = `${repo.name}: Error!`; statusBtn.style.backgroundColor = errorColor; } else { statusBtn.textContent = `${repo.name}: Checking...`; statusBtn.style.backgroundColor = checkingColor; } statusBtn.style.color = 'white'; } }); // 处理自定义清单的状态显示(由于核心逻辑不使用自定义清单进行查找,这里显示为N/A) userDefinedManifests.forEach(repo => { const statusBtn = document.getElementById(`repo-status-${repo.name.replace(/\s+/g, '-')}`); if (statusBtn) { statusBtn.textContent = `${repo.name}: N/A (自定义)`; statusBtn.style.backgroundColor = '#3366cc'; // 自定义清单的专属颜色 statusBtn.style.color = 'white'; } }); } /** * 更新浮窗内容到主下载界面 */ async function showMainDownloadPanel() { const floatWindow = document.querySelector('.chaoyang-tool-float-window'); if (!floatWindow) return; floatWindow.innerHTML = ''; // 清空内容 // --- 浮窗左上角功能区 --- const topLeftControlsContainer = document.createElement('div'); topLeftControlsContainer.id = 'chaoyang-tool-top-left-controls'; floatWindow.appendChild(topLeftControlsContainer); // 新增“获取DLC”按钮 const getDlcBtn = document.createElement('button'); getDlcBtn.textContent = '获取DLC'; getDlcBtn.onclick = fetchAndDownloadDlcIds; topLeftControlsContainer.appendChild(getDlcBtn); // 新增“设置”按钮 const settingsBtn = document.createElement('button'); settingsBtn.textContent = '设置'; settingsBtn.onclick = showSettingsPanel; // 切换到设置面板 topLeftControlsContainer.appendChild(settingsBtn); // 关闭浮窗按钮 const closeBtn = document.createElement('button'); closeBtn.className = 'chaoyang-tool-close-btn'; closeBtn.innerHTML = '×'; closeBtn.onclick = () => document.getElementById('chaoyang-tool-overlay').style.display = 'none'; floatWindow.appendChild(closeBtn); // --- Section 1: GitHub Token管理 --- const tokenSection = document.createElement('div'); tokenSection.className = 'chaoyang-tool-section'; // Use general section class tokenSection.id = 'chaoyang-tool-token-section-wrapper'; // New wrapper for border control 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'; 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: 自定义清单管理 (UI保留,核心下载不使用) --- 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 5: 主下载按钮 --- const section3 = document.createElement('div'); section3.className = 'chaoyang-tool-section chaoyang-tool-last-section'; // Add a class for last section without border floatWindow.appendChild(section3); const mainDownloadBtn = document.createElement('button'); mainDownloadBtn.id = 'chaoyang-tool-main-download-btn'; mainDownloadBtn.className = 'chaoyang-tool-main-download-btn'; mainDownloadBtn.textContent = '入库'; // 将在 updateActiveAppId 中更新 mainDownloadBtn.disabled = true; // 默认禁用 section3.appendChild(mainDownloadBtn); // 更新UI元素 const currentTokenDisplay = document.getElementById('chaoyang-tool-current-token-display'); if (currentTokenDisplay) { githubTokenInput.value = githubToken; currentTokenDisplay.textContent = githubToken ? '已设置' : '未设置'; currentTokenDisplay.style.color = githubToken ? '#4CAF50' : '#f44336'; } // 清空并重新生成下载源选项和状态显示 optionGroup.innerHTML = ''; repoStatusGroup.innerHTML = ''; customManifestList.innerHTML = ''; let currentSelectedDownloadOption = setting_selectedManifestRepo; // Use loaded setting // 重新生成核心清单网站的选项和状态按钮 CORE_GITHUB_MANIFEST_REPOS.forEach(repoInfo => { const radioId = `option-${repoInfo.name.replace(/\s+/g, '-')}`; const label = document.createElement('label'); label.htmlFor = radioId; label.classList.add('chaoyang-tool-toggle-label'); // Use toggle label class for button-like appearance const radio = document.createElement('input'); radio.type = 'radio'; radio.name = 'downloadOption'; radio.value = repoInfo.name; radio.id = radioId; radio.checked = (repoInfo.name === currentSelectedDownloadOption); // Set checked based on loaded setting const spanText = document.createElement('span'); spanText.textContent = repoInfo.name; label.appendChild(radio); label.appendChild(spanText); optionGroup.appendChild(label); radio.onchange = async () => { setting_selectedManifestRepo = radio.value; await GM_setValue(GM_SETTINGS_SELECTED_MANIFEST_REPO, setting_selectedManifestRepo); mainDownloadBtn.disabled = !activeAppId; // 只有有App ID时才启用 updateDownloadOptionColors(); // Update colors after change }; const statusBtn = document.createElement('button'); statusBtn.id = `repo-status-${repoInfo.name.replace(/\s+/g, '-')}`; statusBtn.className = 'chaoyang-tool-repo-status-btn'; statusBtn.textContent = `${repoInfo.name}: Checking...`; // 初始状态 repoStatusGroup.appendChild(statusBtn); }); // 重新生成自定义清单网站的选项和状态按钮(这些只用于UI展示,不参与核心下载逻辑) userDefinedManifests.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', 'chaoyang-tool-custom-label'); const radio = document.createElement('input'); radio.type = 'radio'; radio.name = 'downloadOption'; radio.value = repoInfo.name; radio.id = radioId; radio.disabled = true; // 禁用自定义清单的下载选择,因为核心逻辑不使用 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 chaoyang-tool-custom-status-btn'; statusBtn.textContent = `${repoInfo.name}: N/A (自定义)`; repoStatusGroup.appendChild(statusBtn); }); mainDownloadBtn.disabled = !activeAppId; // 默认情况下下载按钮根据activeAppId可用性 mainDownloadBtn.onclick = () => { if (activeAppId && setting_selectedManifestRepo) { downloadManifest(activeAppId, setting_selectedManifestRepo); } 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); }); // 初始化下载源选项的视觉状态 updateDownloadOptionColors(); // 如果有App ID,则实时更新清单状态 if (activeAppId) { const appInfoForCurrent = await getAppIdManifestInfo(activeAppId); updateManifestStatusDisplay(appInfoForCurrent); } else { updateManifestStatusDisplay({ manifests: {}, latestSource: null }); } } /** * 更新下载源选项的选中状态颜色 */ function updateDownloadOptionColors() { CORE_GITHUB_MANIFEST_REPOS.forEach(repoInfo => { const radioId = `option-${repoInfo.name.replace(/\s+/g, '-')}`; const radioInput = document.getElementById(radioId); const spanText = radioInput ? radioInput.nextElementSibling : null; if (spanText) { if (repoInfo.name === setting_selectedManifestRepo) { spanText.style.backgroundColor = '#007bff'; spanText.style.color = 'white'; if (radioInput) radioInput.checked = true; } else { spanText.style.backgroundColor = '#3a3a3a'; spanText.style.color = '#e0e0e0'; if (radioInput) radioInput.checked = false; } } }); } /** * 显示设置浮窗 */ async function showSettingsPanel() { const floatWindow = document.querySelector('.chaoyang-tool-float-window'); if (!floatWindow) return; floatWindow.innerHTML = ''; // 清空内容 // --- 浮窗左上角功能区 (返回按钮) --- const topLeftControlsContainer = document.createElement('div'); topLeftControlsContainer.id = 'chaoyang-tool-top-left-controls'; // Already has border-bottom floatWindow.appendChild(topLeftControlsContainer); const backBtn = document.createElement('button'); backBtn.textContent = '返回'; backBtn.onclick = showMainDownloadPanel; // 返回主下载面板 topLeftControlsContainer.appendChild(backBtn); // 关闭浮窗按钮 const closeBtn = document.createElement('button'); closeBtn.className = 'chaoyang-tool-close-btn'; closeBtn.innerHTML = '×'; closeBtn.onclick = () => document.getElementById('chaoyang-tool-overlay').style.display = 'none'; floatWindow.appendChild(closeBtn); // --- 设置标题 --- const settingsTitle = document.createElement('h3'); settingsTitle.textContent = '设置'; settingsTitle.style.marginTop = '20px'; settingsTitle.style.marginBottom = '15px'; // Adjust margin floatWindow.appendChild(settingsTitle); // --- 设置项 1: 固定清单版本 --- const fixManifestVersionSection = document.createElement('div'); fixManifestVersionSection.className = 'chaoyang-tool-section'; // Use general section class floatWindow.appendChild(fixManifestVersionSection); const fixManifestVersionLabel = document.createElement('label'); fixManifestVersionLabel.className = 'chaoyang-tool-setting-label'; // Add a class for font sizing // 更改文字说明,更清晰地表达其功能 fixManifestVersionLabel.textContent = '固定清单版本 (仅对下载lua起效):'; fixManifestVersionSection.appendChild(fixManifestVersionLabel); const fixVersionOptions = document.createElement('div'); fixVersionOptions.className = 'chaoyang-tool-option-group'; // Reuse option group style fixManifestVersionSection.appendChild(fixVersionOptions); const createSettingToggle = (settingName, value, display, currentSetting) => { const id = `setting-${settingName}-${value}`; const label = document.createElement('label'); label.htmlFor = id; label.className = 'chaoyang-tool-toggle-label'; // Custom class for toggles const radio = document.createElement('input'); radio.type = 'radio'; radio.name = settingName; radio.value = value; radio.id = id; // 关键修改:如果settingName是fixManifestVersion,则“是”对应false,“否”对应true (与逻辑反转相符) if (settingName === 'fixManifestVersion') { // currentSetting 是存储的布尔值 (true/false) // value 是字符串 'yes' 或 'no' // 如果用户希望“否”来注释,那么当 `setting_fixManifestVersion` 是 `false` 时,UI上“否”应该被选中。 // 所以: radio.checked 为 true 对应 value === 'no' 且 currentSetting === false // radio.checked 为 true 对应 value === 'yes' 且 currentSetting === true radio.checked = (value === 'yes' && currentSetting === true) || (value === 'no' && currentSetting === false); } else { // 对于其他设置(例如downloadLuaOnly),保持原有逻辑 radio.checked = currentSetting === (value === 'yes'); } const spanText = document.createElement('span'); // Span to apply visual styles spanText.textContent = display; label.appendChild(radio); label.appendChild(spanText); radio.onchange = async () => { const newValue = radio.value === 'yes'; if (settingName === 'fixManifestVersion') { // 同样在这里反转存储的值:选择“是”时存储true,选择“否”时存储false。 // 但是,在 `downloadManifest` 中,我们将根据 `!setting_fixManifestVersion` 来决定是否注释。 setting_fixManifestVersion = newValue; await GM_setValue(GM_SETTINGS_FIX_MANIFEST_VERSION, newValue); } else if (settingName === 'downloadLuaOnly') { setting_downloadLuaOnly = newValue; await GM_setValue(GM_SETTINGS_DOWNLOAD_LUA_ONLY, newValue); } updateSettingToggleState(settingName); // Update colors for ALL options in this setting group }; return label; }; fixVersionOptions.appendChild(createSettingToggle('fixManifestVersion', 'yes', '是', setting_fixManifestVersion)); fixVersionOptions.appendChild(createSettingToggle('fixManifestVersion', 'no', '否', setting_fixManifestVersion)); // --- 设置项 2: 仅下载Lua文件 --- const downloadTypeSection = document.createElement('div'); downloadTypeSection.className = 'chaoyang-tool-section chaoyang-tool-last-section'; // Mark as last section in settings panel floatWindow.appendChild(downloadTypeSection); const downloadTypeLabel = document.createElement('label'); downloadTypeLabel.className = 'chaoyang-tool-setting-label'; // Add a class for font sizing downloadTypeLabel.textContent = '下载文件类型 (仅下载Lua,否则下载ZIP):'; downloadTypeSection.appendChild(downloadTypeLabel); const downloadTypeOptions = document.createElement('div'); downloadTypeOptions.className = 'chaoyang-tool-option-group'; downloadTypeSection.appendChild(downloadTypeOptions); downloadTypeOptions.appendChild(createSettingToggle('downloadLuaOnly', 'yes', '仅Lua', setting_downloadLuaOnly)); downloadTypeOptions.appendChild(createSettingToggle('downloadLuaOnly', 'no', '下载ZIP', setting_downloadLuaOnly)); // --- 反馈信息 --- const feedbackSection = document.createElement('div'); feedbackSection.className = 'chaoyang-tool-section chaoyang-tool-no-border'; // No border for feedback section floatWindow.appendChild(feedbackSection); const feedbackText = document.createElement('p'); feedbackText.textContent = '反馈QQ群:1035993374'; feedbackText.style.fontSize = '1.1em'; feedbackText.style.textAlign = 'center'; feedbackText.style.marginTop = '20px'; feedbackSection.appendChild(feedbackText); // 初始化设置项的视觉状态 updateSettingToggleState('fixManifestVersion'); updateSettingToggleState('downloadLuaOnly'); } /** * 更新设置项的选中状态颜色 * @param {string} settingName - 设置项名称 ('fixManifestVersion' or 'downloadLuaOnly') */ function updateSettingToggleState(settingName) { let currentStatus; if (settingName === 'fixManifestVersion') { currentStatus = setting_fixManifestVersion; } else if (settingName === 'downloadLuaOnly') { currentStatus = setting_downloadLuaOnly; } const yesInput = document.getElementById(`setting-${settingName}-yes`); const noInput = document.getElementById(`setting-${settingName}-no`); const yesSpan = yesInput ? yesInput.nextElementSibling : null; const noSpan = noInput ? noInput.nextElementSibling : null; if (yesSpan) { // 针对 fixManifestVersion 按钮的特殊逻辑 if (settingName === 'fixManifestVersion') { if (currentStatus === true) { // 内部变量为true,UI上选中“是” yesSpan.style.backgroundColor = '#007bff'; yesSpan.style.color = 'white'; if (yesInput) yesInput.checked = true; } else { // 内部变量为false,UI上选中“否” yesSpan.style.backgroundColor = '#3a3a3a'; yesSpan.style.color = '#e0e0e0'; if (yesInput) yesInput.checked = false; } } else { // 其他设置项保持原有逻辑 if (currentStatus) { yesSpan.style.backgroundColor = '#007bff'; yesSpan.style.color = 'white'; if (yesInput) yesInput.checked = true; } else { yesSpan.style.backgroundColor = '#3a3a3a'; yesSpan.style.color = '#e0e0e0'; if (yesInput) yesInput.checked = false; } } } if (noSpan) { // 针对 fixManifestVersion 按钮的特殊逻辑 if (settingName === 'fixManifestVersion') { if (currentStatus === false) { // 内部变量为false,UI上选中“否” noSpan.style.backgroundColor = '#007bff'; noSpan.style.color = 'white'; if (noInput) noInput.checked = true; } else { // 内部变量为true,UI上选中“是” noSpan.style.backgroundColor = '#3a3a3a'; noSpan.style.color = '#e0e0e0'; if (noInput) noInput.checked = false; } } else { // 其他设置项保持原有逻辑 if (!currentStatus) { noSpan.style.backgroundColor = '#007bff'; noSpan.style.color = 'white'; if (noInput) noInput.checked = true; } else { noSpan.style.backgroundColor = '#3a3a3a'; noSpan.style.color = '#e0e0e0'; if (noInput) noInput.checked = false; } } } } /** * 添加用户自定义清单网站(UI保留,但核心下载不使用) * @param {string} name - 显示名称 (可选) * @param {string} githubUrl - GitHub仓库的任意URL (例如: owner/repo) */ async function addUserManifest(name, githubUrl) { 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); allManifestRepos = [...CORE_GITHUB_MANIFEST_REPOS, ...userDefinedManifests]; // 更新所有清单列表 console.log(`朝阳的工具: 已添加自定义清单: ${displayName} (${repoUrl})`); showMainDownloadPanel(); // 刷新UI } /** * 移除用户自定义清单网站 * @param {string} url - GitHub仓库URL (owner/repo) */ async function removeUserManifest(url) { if (confirm(`朝阳的工具: 确定要移除清单网站 ${url} 吗?`)) { userDefinedManifests = userDefinedManifests.filter(repo => repo.url !== url); await GM_setValue(GM_USER_MANIFESTS, userDefinedManifests); allManifestRepos = [...CORE_GITHUB_MANIFEST_REPOS, ...userDefinedManifests]; // 更新所有清单列表 console.log(`朝阳的工具: 已移除自定义清单: ${url}`); showMainDownloadPanel(); // 刷新UI } } /** * 保存GitHub Token * @param {string} tokenValue */ async function saveGitHubToken(tokenValue) { githubToken = tokenValue.trim(); try { await GM_setValue(GM_GITHUB_TOKEN, githubToken); alert('朝阳的工具: GitHub Token已保存。'); showMainDownloadPanel(); } 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() { if (confirm('朝阳的工具: 确定要清除您的GitHub Token吗?这将可能导致API速率限制。')) { try { await GM_deleteValue(GM_GITHUB_TOKEN); githubToken = ''; // 清空内存变量 alert('朝阳的工具: GitHub Token已清除。'); showMainDownloadPanel(); // 刷新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时出错。'); } } } /** * 自动从当前页面获取所有DLC的App ID,并下载为txt文件。 */ async function fetchAndDownloadDlcIds() { if (!activeAppId) { alert('朝阳的工具: 无法获取当前游戏App ID。请在Steam或SteamDB游戏详情页使用此功能,或通过剪贴板识别。'); return; } console.log('朝阳的工具: 开始获取DLC ID...'); const dlcIds = new Set(); // 使用Set自动去重 const isSteamDB = window.location.hostname === 'steamdb.info'; const isSteamStore = window.location.hostname === 'store.steampowered.com'; const isSteamUI = window.location.hostname === 'steamui.com'; if (isSteamDB) { // SteamDB页面逻辑: DLC在ID为'dlc'的tab下的表格中 document.querySelectorAll('#dlc tr[data-appid]').forEach(row => { const dlcId = row.getAttribute('data-appid'); if (dlcId) { dlcIds.add(dlcId.trim()); } }); } else if (isSteamStore) { // Steam商店页面逻辑: DLC通常在class为'game_area_dlc_row'的div中 document.querySelectorAll('.game_area_dlc_row').forEach(row => { // Steam新版UI使用data-ds-appid属性,这是最可靠的方式 const dlcId = row.getAttribute('data-ds-appid'); if (dlcId) { dlcIds.add(dlcId.trim()); } else { // 兼容旧版或不同布局,尝试从标签的链接中解析 const link = row.querySelector('a'); if (link && link.href) { const match = link.href.match(/\/app\/(\d+)/); if (match && match[1]) { dlcIds.add(match[1]); } } } }); } else if (isSteamUI) { // steamui.com 页面逻辑(可能需要根据实际页面结构调整) // 假设 steamui.com 页面会显示 AppID 和 DLC AppID // 这部分可能需要手动检查 steamui.com 的 HTML 结构来精确提取 // 示例:如果DLC ID在一个特定的文本区域或链接中 const potentialDlcElements = document.querySelectorAll('[data-appid-dlc], .dlc-list-item a, .dlc-id-text'); // 假设的CSS选择器 potentialDlcElements.forEach(el => { let dlcId = el.getAttribute('data-appid-dlc') || el.textContent.trim(); const linkMatch = el.href ? el.href.match(/\/app\/(\d+)/) : null; if (linkMatch && linkMatch[1]) { dlcId = linkMatch[1]; } if (dlcId && APP_ID_REGEX_CLIPBOARD.test(dlcId)) { // Validate it looks like an App ID dlcIds.add(dlcId); } }); // 提醒用户 steamui.com 的DLC识别可能不准确,依赖页面结构 if (dlcIds.size === 0) { alert('朝阳的工具: 在 steamui.com 页面未找到DLC信息。请确保您当前查看的是有DLC列表的页面。该站点的DLC识别功能依赖于页面结构,可能不够完善。'); return; } } else { alert('朝阳的工具: 当前页面不支持自动获取DLC ID。请在Steam商店、SteamDB或SteamUI页面使用此功能。'); return; } const dlcIdArray = Array.from(dlcIds); console.log(`朝阳的工具: 共找到 ${dlcIdArray.length} 个唯一的DLC ID。`); if (dlcIdArray.length === 0) { alert('朝阳的工具: 未在此页面上找到任何DLC。'); return; } // 创建并下载txt文件 const fileContent = dlcIdArray.join('\n'); const blob = new Blob([fileContent], { type: 'text/plain;charset=utf-8' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `dlc_${activeAppId}.txt`; // 文件名格式: dlc_游戏ID.txt document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(a.href); GM_notification({ title: '朝阳的工具', text: `成功获取并下载了 ${dlcIdArray.length} 个DLC ID。`, image: 'https://www.tampermonkey.net/_favicon.ico', timeout: 5000 }); } // --- UI 渲染 --- function createUI() { const isSteamDB = window.location.hostname === 'steamdb.info'; // 注入CSS样式 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; } .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: #6c757d; 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; } .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; } .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; font-size: 15px; /* Default font size for float window content */ 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; } /* General section styling - no default bottom border */ .chaoyang-tool-section { margin-bottom: 20px; padding-bottom: 15px; border-bottom: none; } /* Explicitly add border to sections where needed */ #chaoyang-tool-top-left-controls, .chaoyang-tool-section:not(.chaoyang-tool-last-section) { border-bottom: 1px solid #444; } .chaoyang-tool-section:last-child { margin-bottom: 0; padding-bottom: 0; } .chaoyang-tool-section h3 { margin-top: 0; margin-bottom: 15px; color: #f0f0f0; font-size: 18px; /* Section titles are slightly larger */ } /* Setting labels font size */ .chaoyang-tool-setting-label { font-size: 15px; /* Match float window text size */ display: block; /* Ensure it takes its own line */ margin-bottom: 10px; font-weight: bold; } .chaoyang-tool-option-group { display: flex; gap: 15px; flex-wrap: wrap; margin-bottom: 15px; } /* Styles for the toggle label (used for download source and settings) */ .chaoyang-tool-toggle-label { cursor: pointer; transition: background-color 0.2s; font-size: 15px; /* Apply font size directly here */ display: inline-flex; align-items: center; gap: 0px; padding: 0; background-color: transparent; } .chaoyang-tool-toggle-label input[type="radio"] { display: none; /* Hide native radio button */ } .chaoyang-tool-toggle-label span { display: inline-block; padding: 8px 15px; border-radius: 5px; background-color: #3a3a3a; /* Default off color */ color: #e0e0e0; transition: background-color 0.2s, color 0.2s; } .chaoyang-tool-toggle-label:hover span { background-color: #4a4a4a; } .chaoyang-tool-toggle-label input[type="radio"]:checked + span { background-color: #007bff; /* On color */ color: white; } .chaoyang-tool-custom-label { background-color: #3366cc !important; cursor: not-allowed !important; /* 禁用自定义清单的选项 */ opacity: 0.8; } .chaoyang-tool-custom-label:hover { background-color: #3366cc !important; /* 禁用hover效果 */ } .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-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; } /* Token section specific styling */ #chaoyang-tool-token-section-wrapper { margin-top: 20px; padding-top: 15px; } .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-top-left-controls { position: relative; top: 0; left: 0; width: 100%; display: flex; gap: 10px; z-index: 10001; padding-bottom: 15px; /* Add padding for the border */ margin-bottom: 15px; /* Space from content below */ box-sizing: border-box; /* Include padding in width */ } #chaoyang-tool-top-left-controls button { background-color: #555; color: white; padding: 5px 10px; border-radius: 4px; font-size: 14px; cursor: pointer; border: none; transition: background-color 0.2s; } #chaoyang-tool-top-left-controls button: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'; // 刷新按钮位置 (在状态框下方,与主按钮左侧对齐) refreshStatusBtn.style.left = `${btnRect.left - refreshStatusBtn.offsetWidth - spacing}px`; refreshStatusBtn.style.top = `${btnRect.top + statusBox.offsetHeight + 5}px`; refreshStatusBtn.style.right = 'auto'; } // --- 拖动逻辑及点击检测 --- 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'; } else { // 默认位置:页面右侧20px,顶部根据Steam/SteamDB/SteamUI区分 const defaultButtonWidth = 60; const defaultRightMargin = 20; const defaultButtonLeft = window.innerWidth - defaultButtonWidth - defaultRightMargin; let defaultButtonTop = 20; // Default for store.steampowered.com and steamui.com if (isSteamDB) { defaultButtonTop = 120; // SteamDB often has fixed header } 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'; // 检查鼠标是否移动超过阈值 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) { document.getElementById('chaoyang-tool-overlay').style.display = 'flex'; showMainDownloadPanel(); // 打开浮窗时刷新内容 } // 保存按钮的最终位置 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(); // 刷新App ID存在状态 }; // 创建半透明黑色浮窗背景 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 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('朝阳的工具: 脚本初始化开始。'); // 从 Tampermonkey 存储中加载数据 githubToken = await GM_getValue(GM_GITHUB_TOKEN, ''); userDefinedManifests = await GM_getValue(GM_USER_MANIFESTS, []); setting_fixManifestVersion = await GM_getValue(GM_SETTINGS_FIX_MANIFEST_VERSION, true); // 默认 true (之前是固定,现在是默认不注释) setting_downloadLuaOnly = await GM_getValue(GM_SETTINGS_DOWNLOAD_LUA_ONLY, true); // 默认 true // 确保 selectedManifestRepo 默认值是 CORE_GITHUB_MANIFEST_REPOS 中的一个有效名称 setting_selectedManifestRepo = await GM_getValue(GM_SETTINGS_SELECTED_MANIFEST_REPO, CORE_GITHUB_MANIFEST_REPOS[0].name); // 如果存储的值不再是有效核心仓库名称,则重置为第一个 if (!CORE_GITHUB_MANIFEST_REPOS.some(repo => repo.name === setting_selectedManifestRepo)) { setting_selectedManifestRepo = CORE_GITHUB_MANIFEST_REPOS[0].name; await GM_setValue(GM_SETTINGS_SELECTED_MANIFEST_REPO, setting_selectedManifestRepo); } // 加载按钮位置,区分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); // 合并核心清单和用户自定义清单 (UI展示使用,核心下载逻辑仅针对 CORE_GITHUB_MANIFEST_REPOS) allManifestRepos = [...CORE_GITHUB_MANIFEST_REPOS, ...userDefinedManifests]; console.log('朝阳的工具: 已加载用户自定义清单:', userDefinedManifests.length, '个'); console.log('朝阳的工具: GitHub Token状态:', githubToken ? '已设置' : '未设置'); console.log('朝阳的工具: 按钮上次保存位置:', storedButtonPosition); // 更新日志输出以反映新的语义 console.log('朝阳的工具: 设置 - 注释 setManifestid (UI: 否 / True: 注释):', !setting_fixManifestVersion); console.log('朝阳的工具: 设置 - 仅下载Lua文件:', setting_downloadLuaOnly); console.log('朝阳的工具: 设置 - 选中的清单仓库:', setting_selectedManifestRepo); createUI(); // 创建UI元素 currentAppId = getAppIdFromUrl(); // 无论是何种页面,都监听剪贴板事件 document.addEventListener('paste', handlePasteEvent); // 初始化时尝试从URL获取App ID,如果URL没有,再检查剪贴板 if (currentAppId) { await updateActiveAppId(currentAppId, 'url'); } else { // 如果URL中没有App ID,尝试获取一次剪贴板内容 navigator.clipboard.readText().then(text => { if (text && APP_ID_REGEX_CLIPBOARD.test(text.trim())) { const id = text.trim(); // 仅当剪贴板ID与当前activeAppId不同时才提示 if (id !== activeAppId) { if (confirm(`朝阳的工具: 检测到剪贴板App ID: ${id},是否以此App ID进行操作?`)) { clipboardAppId = id; updateActiveAppId(clipboardAppId, 'clipboard'); } } } }).catch(err => { console.warn('朝阳的工具: 无法读取剪贴板内容 (可能需要用户授权或非HTTPS页面):', err); }); } console.log('朝阳的工具: 脚本初始化完成。'); } /** * 处理剪贴板粘贴事件 * @param {Event} event */ async function handlePasteEvent(event) { const pastedText = event.clipboardData ? event.clipboardData.getData('text') : null; if (pastedText && APP_ID_REGEX_CLIPBOARD.test(pastedText.trim())) { const newClipboardAppId = pastedText.trim(); // 只有当新识别的App ID与当前活动App ID不同时才提示 if (newClipboardAppId !== activeAppId) { if (confirm(`朝阳的工具: 检测到剪贴板App ID: ${newClipboardAppId},是否以此App ID进行操作?`)) { clipboardAppId = newClipboardAppId; await updateActiveAppId(clipboardAppId, 'clipboard'); GM_notification({ title: '朝阳的工具', text: `已识别剪贴板App ID: ${clipboardAppId}`, image: 'https://www.tampermonkey.net/_favicon.ico', timeout: 3000 }); } } else { console.log(`朝阳的工具: 剪贴板App ID ${newClipboardAppId} 与当前活动App ID相同,跳过重复处理。`); } } } // 在页面加载完成后执行初始化 window.addEventListener('load', init); })();