// ==UserScript== // @name XHS-Downloader // @namespace https://github.com/JoeanAmier/XHS-Downloader // @version 2.0.1 // @description 提取小红书作品/用户链接,下载小红书无水印图文/视频作品文件 // @author JoeanAmier // @match http*://xhslink.com/* // @match http*://www.xiaohongshu.com/explore* // @match http*://www.xiaohongshu.com/user/profile/* // @match http*://www.xiaohongshu.com/search_result* // @match http*://www.xiaohongshu.com/board/* // @icon data:image/png;base64,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 // @grant GM_getValue // @grant GM_setValue // @grant unsafeWindow // @grant GM_setClipboard // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @license GNU General Public License v3.0 // @run-at document-end // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.9.1/jszip.min.js // @downloadURL https://update.greasyfork.icu/scripts/483847/XHS-Downloader.user.js // @updateURL https://update.greasyfork.icu/scripts/483847/XHS-Downloader.meta.js // ==/UserScript== (function () { 'use strict'; const iconBase64 = "data:image/png;base64,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"; let config = { disclaimer: GM_getValue("disclaimer", false), packageDownloadFiles: GM_getValue("packageDownloadFiles", true), autoScrollSwitch: GM_getValue("autoScrollSwitch", false), maxScrollCount: GM_getValue("maxScrollCount", 50), fileNameFormat: undefined, imageFileFormat: undefined, icon: { type: 'image', // 可选: image/svg/font image: { url: iconBase64, // 图片URL或Base64 size: 64, // 图标尺寸 borderRadius: '50%' // 形状(50%为圆形) }, }, // 位置配置 position: { bottom: '8rem', left: '2rem' }, // 动画配置 animation: { duration: 0.35, // 动画时长(s) easing: 'cubic-bezier(0.4, 0, 0.2, 1)' } }; const readme = () => { const instructions = ` XHS-Downloader 用户脚本 功能清单: 1. 下载小红书无水印作品文件 2. 提取推荐页面作品链接 3. 提取账号发布作品链接 4. 提取账号收藏作品链接 5. 提取账号专辑作品链接 6. 提取账号点赞作品链接 7. 提取搜索结果作品链接 8. 提取搜索结果用户链接 XHS-Downloader 用户脚本 详细说明: 1. 下载小红书无水印作品文件时,脚本需要花费时间处理文件,请等待片刻,请勿多次点击下载按钮 2. 无水印作品文件较大,可能需要较长的时间处理,页面跳转可能会导致下载失败 3. 提取账号发布、收藏、点赞、专辑作品链接时,脚本可以自动滚动页面直至加载全部作品 4. 提取推荐作品链接、搜索作品、用户链接时,脚本可以自动滚动指定次数加载更多内容,默认滚动次数:50 次 5. 自动滚动页面功能默认关闭;用户可以自由开启,并修改滚动页面次数,修改后立即生效 6. 如果未开启自动滚动页面功能,用户需要手动滚动页面以便加载更多内容后再进行其他操作 7. 支持作品文件打包下载;该功能默认开启,多个文件的作品将会以压缩包格式下载 项目开源地址:https://github.com/JoeanAmier/XHS-Downloader ` const disclaimer_content = ` 关于 XHS-Downloader 的 免责声明: 1. 使用者对本项目的使用由使用者自行决定,并自行承担风险。作者对使用者使用本项目所产生的任何损失、责任、或风险概不负责。 2. 本项目的作者提供的代码和功能是基于现有知识和技术的开发成果。作者尽力确保代码的正确性和安全性,但不保证代码完全没有错误或缺陷。 3. 使用者在使用本项目时必须严格遵守 GNU General Public License v3.0 的要求,并在适当的地方注明使用了 GNU General Public License v3.0 的代码。 4. 使用者在任何情况下均不得将本项目的作者、贡献者或其他相关方与使用者的使用行为联系起来,或要求其对使用者使用本项目所产生的任何损失或损害负责。 5. 使用者在使用本项目的代码和功能时,必须自行研究相关法律法规,并确保其使用行为合法合规。任何因违反法律法规而导致的法律责任和风险,均由使用者自行承担。 6. 本项目的作者不会提供 XHS-Downloader 项目的付费版本,也不会提供与 XHS-Downloader 项目相关的任何商业服务。 7. 基于本项目进行的任何二次开发、修改或编译的程序与原创作者无关,原创作者不承担与二次开发行为或其结果相关的任何责任,使用者应自行对因二次开发可能带来的各种情况负全部责任。 在使用本项目的代码和功能之前,请您认真考虑并接受以上免责声明。如果您对上述声明有任何疑问或不同意,请不要使用本项目的代码和功能。如果您使用了本项目的代码和功能,则视为您已完全理解并接受上述免责声明,并自愿承担使用本项目的一切风险和后果。 是否已阅读 XHS-Downloader 功能说明与免责声明(YES/NO) ` alert(instructions); if (!config.disclaimer) { const answer = prompt(disclaimer_content, ""); if (!answer) { GM_setValue("disclaimer", false); config.disclaimer = false; } else { GM_setValue("disclaimer", answer.toUpperCase() === "YES" || answer.toUpperCase() === "Y"); config.disclaimer = GM_getValue("disclaimer"); } } }; if (!config.disclaimer) { readme(); } console.info("用户接受 XHS-Downloader 免责声明", config.disclaimer) GM_registerMenuCommand("阅读脚本说明和免责声明", function () { readme(); }); const updatePackageDownloadFiles = (value) => { config.packageDownloadFiles = value; GM_setValue("packageDownloadFiles", config.packageDownloadFiles); }; const updateAutoScrollSwitch = (value) => { config.autoScrollSwitch = value; GM_setValue("autoScrollSwitch", config.autoScrollSwitch); }; const updateMaxScrollCount = (value) => { config.maxScrollCount = parseInt(value) || 50; GM_setValue("maxScrollCount", config.maxScrollCount); }; const updateFileNameFormat = (value) => { config.fileNameFormat = value; GM_setValue("fileNameFormat", config.fileNameFormat); }; const about = () => { window.open('https://github.com/JoeanAmier/XHS-Downloader', '_blank'); } const abnormal = (text) => { alert(`${text}请向作者反馈!\n项目开源地址:https://github.com/JoeanAmier/XHS-Downloader`); }; const generateVideoUrl = note => { try { return [`https://sns-video-bd.xhscdn.com/${note.video.consumer.originVideoKey}`]; } catch (error) { console.error("Error generating video URL:", error); return []; } }; const generateImageUrl = note => { let images = note.imageList; const regex = /http:\/\/sns-webpic-qc\.xhscdn.com\/\d+\/[0-9a-z]+\/(\S+)!/; let urls = []; try { images.forEach((item) => { let match = item.urlDefault.match(regex); if (match && match[1]) { urls.push(`https://ci.xiaohongshu.com/${match[1]}?imageView2/format/png`); } }) return urls } catch (error) { console.error("Error generating image URLs:", error); return []; } }; const extractImageWebpUrls = (note, urls,) => { try { let items = [] let {imageList} = note; if (urls.length !== imageList.length) { console.error("图片数量不一致!") return [] } for (const [index, item] of imageList.entries()) { if (item.urlDefault) { items.push({ webp: item.urlDefault, index: index + 1, url: urls[index], }) } else { console.error("提取图片预览链接失败", item) break } } return items; } catch (error) { console.error("Error occurred in generating image object:", error); return [] } }; const download = async (urls, note) => { const name = extractName(); console.info(`文件名称 ${name}`); if (note.type === "video") { await downloadVideo(urls[0], name); } else { let items = extractImageWebpUrls(note, urls); if (items.length === 0) { console.error("解析图文作品数据失败", note) abnormal("解析图文作品数据发生异常!") } else if (urls.length > 1) { showImageSelectionModal(items, name,) } else { await downloadImage(items, name); } } }; const exploreDeal = async note => { try { let links; if (note.type === "normal") { links = generateImageUrl(note); } else { links = generateVideoUrl(note); } if (links.length > 0) { console.info("下载链接", links); await download(links, note); } else { abnormal("处理下载链接发生异常!") } } catch (error) { console.error("Error in exploreDeal function:", error); abnormal("下载作品文件发生异常!"); } }; const extractNoteInfo = () => { const regex = /\/explore\/([^?]+)/; const match = currentUrl.match(regex); if (match) { return unsafeWindow.__INITIAL_STATE__.note.noteDetailMap[match[1]] } else { console.error("从链接提取作品 ID 失败", currentUrl,); } }; const extractDownloadLinks = async () => { if (currentUrl.includes("https://www.xiaohongshu.com/explore/")) { let note = extractNoteInfo(); if (note.note) { await exploreDeal(note.note); } else { abnormal("读取作品数据发生异常!"); } } }; const triggerDownload = (name, blob) => { // 创建 Blob 对象的 URL const blobUrl = URL.createObjectURL(blob); // 创建一个临时链接元素 const tempLink = document.createElement("a"); tempLink.href = blobUrl; tempLink.download = name; // 将链接添加到 DOM 并模拟点击 document.body.appendChild(tempLink); // 避免某些浏览器安全限制 tempLink.click(); // 清理临时链接元素 document.body.removeChild(tempLink); // 从 DOM 中移除临时链接 URL.revokeObjectURL(blobUrl); // 释放 URL console.info(`文件已成功下载: ${name}`); } const downloadFile = async (link, name, trigger = true, retries = 5) => { for (let attempt = 1; attempt <= retries; attempt++) { try { // 使用 fetch 获取文件数据 const response = await fetch(link, { "headers": { "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "accept-language": "zh-SG,zh;q=0.9", }, "method": "GET", }); // 检查响应状态码 if (!response.ok) { console.error(`下载失败,状态码: ${response.status},URL: ${link},尝试次数: ${attempt}`); continue; // 继续下一次尝试 } const blob = await response.blob(); if (trigger) { triggerDownload(name, blob); return true; } else { return blob; } } catch (error) { console.error(`下载失败 (${name}),错误信息:`, error, `尝试次数: ${attempt}`); if (attempt === retries) { return false; // 如果达到最大重试次数,返回失败 } } } return false; // 如果所有尝试都失败,返回失败 }; const downloadFiles = async (items, name,) => { const downloadResults = []; // 用于存储下载结果 const downloadPromises = items.map(async (item) => { let fileName; if (item.index) { fileName = `${name}_${item.index}.png`; // 根据索引生成文件名 } else { fileName = `${name}.png`; } const result = await downloadFile(item.url, fileName, false); // 调用单个文件下载方法 if (result) { downloadResults.push({name: fileName, file: result}); return true; // 成功 } else { return false; // 失败 } }); // 等待所有下载操作完成 const results = await Promise.all(downloadPromises); if (results.every(result => result === true)) { try { const zip = new JSZip(); downloadResults.forEach((item) => { zip.file(item.name, item.file); }); const content = await zip.generateAsync({type: "blob", compression: "STORE"}); triggerDownload(`${name}.zip`, content,) return true; } catch (error) { console.error('生成 ZIP 文件或保存失败,错误信息:', error); return false; } } else { return false; } }; const truncateString = (str, maxLength) => { if (str.length > maxLength) { const halfLength = Math.floor(maxLength / 2) - 1; // 减去 1 留出省略号的空间 return str.slice(0, halfLength) + '...' + str.slice(-halfLength); } return str; }; const extractName = () => { let name = document.title.replace(/ - 小红书$/, "").replace(/[^\u4e00-\u9fa5a-zA-Z0-9 ~!@#$%&()_\-+=\[\];"',.!()【】:“”,。《》?]/g, ""); name = truncateString(name, 64,); let match = currentUrl.match(/\/([^\/]+)$/); let id = match ? match[1] : null; return name === "" ? id : name }; const downloadVideo = async (url, name) => { if (!await downloadFile(url, `${name}.mp4`)) { abnormal("下载视频作品文件发生异常!"); } }; const downloadImage = async (items, name) => { let success; if (!config.packageDownloadFiles && items.length > 1) { let result = []; for (let item of items) { result.push(await downloadFile(item.url, `${name}_${item.index}.png`)); } success = result.every(item => item === true); } else if (items.length === 1) { success = await downloadFile(items[0].url, `${name}.png`); } else { success = await downloadFiles(items, name,); } if (!success) { abnormal("下载图文作品文件发生异常!"); } }; const window_scrollBy = (x, y,) => { window.scrollBy(x, y,); } // 随机整数生成函数 const getRandomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; // 判断是否需要暂停,模拟用户的停顿行为 const shouldPause = () => Math.random() < 0.2; // 20%几率停顿 // 执行一次增量滚动 const scrollOnce = () => { const scrollDistanceMin = 100; // 最小滚动距离 const scrollDistanceMax = 300; // 最大滚动距离 const scrollDistance = getRandomInt(scrollDistanceMin, scrollDistanceMax); window_scrollBy(0, scrollDistance); // 增量滚动 }; // 检查是否已经滚动到底部 const isAtBottom = () => { const docHeight = document.documentElement.scrollHeight; const winHeight = window.innerHeight; const scrollPos = window.scrollY; return (docHeight - winHeight - scrollPos <= 10); // 如果距离底部小于10px,认为滚动到底部 }; // 自动滚动主函数 const scrollScreen = (callback, endless = false, scrollCount = 0,) => { const timeoutMin = 250; // 最小滚动间隔 const timeoutMax = 500; // 最大滚动间隔 const scrollInterval = setInterval(() => { if (shouldPause()) { // 停顿,模拟用户的休息 clearInterval(scrollInterval); setTimeout(() => { scrollScreen(callback, endless, scrollCount,); // 重新启动滚动 }, getRandomInt(timeoutMin, timeoutMax,)); // 随机停顿时间 } else if (endless) { // 无限滚动至底部模式 if (!isAtBottom()) { scrollOnce(); // 执行一次滚动 } else { // 到达底部,停止滚动 clearInterval(scrollInterval); callback(); // 调用回调函数 } } else if (scrollCount < config.maxScrollCount && !isAtBottom()) { scrollOnce(); // 执行一次滚动 scrollCount++; } else { // 如果到达底部或滚动次数已满,停止滚动 clearInterval(scrollInterval); callback(); // 调用回调函数 } }, getRandomInt(timeoutMin, timeoutMax)); // 随机滚动间隔 }; const scrollScreenEvent = (callback, endless = false) => { if (config.autoScrollSwitch) { scrollScreen(callback, endless,); } else { callback(); } }; const extractNotesInfo = order => { const notesRawValue = unsafeWindow.__INITIAL_STATE__.user.notes._rawValue[order]; return notesRawValue.map(item => [item.id, item.xsecToken,]); }; const extractBoardInfo = () => { // 定义正则表达式来匹配 URL 中的 ID const regex = /\/board\/([a-z0-9]+)\?/; // 使用 exec 方法执行正则表达式 const match = regex.exec(currentUrl); // 检查是否有匹配 if (match) { // 提取 ID const id = match[1]; // match[0] 是整个匹配的字符串,match[1] 是第一个括号内的匹配 const notesRawValue = unsafeWindow.__INITIAL_STATE__.board.boardFeedsMap._rawValue[id].notes; return notesRawValue.map(item => [item.noteId, item.xsecToken,]); } else { console.error("从链接提取专辑 ID 失败", currentUrl,); return []; } }; const extractFeedInfo = () => { const notesRawValue = unsafeWindow.__INITIAL_STATE__.feed.feeds._rawValue; return notesRawValue.map(item => [item.id, item.xsecToken,]); }; const extractSearchNotes = () => { const notesRawValue = unsafeWindow.__INITIAL_STATE__.search.feeds._rawValue; return notesRawValue.map(item => [item.id, item.xsecToken,]); } const extractSearchUsers = () => { const notesRawValue = unsafeWindow.__INITIAL_STATE__.search.userLists._rawValue; return notesRawValue.map(item => item.id); } const generateNoteUrls = data => data.map(([id, token,]) => `https://www.xiaohongshu.com/discovery/item/${id}?source=webshare&xhsshare=pc_web&xsec_token=${token}&xsec_source=pc_share`).join(" "); const generateUserUrls = data => data.map(id => `https://www.xiaohongshu.com/user/profile/${id}`).join(" "); const extractAllLinks = (callback, order) => { scrollScreenEvent(() => { let data; if (order >= 0 && order <= 2) { data = extractNotesInfo(order); } else if (order === 3) { data = extractSearchNotes(); } else if (order === 4) { data = extractSearchUsers(); } else if (order === -1) { data = extractFeedInfo() } else if (order === 5) { data = extractBoardInfo() } else { data = []; } let urlsString = order !== 4 ? generateNoteUrls(data) : generateUserUrls(data); callback(urlsString); }, [0, 1, 2, 5].includes(order)) }; const extractAllLinksEvent = (order = 0) => { extractAllLinks(urlsString => { if (urlsString) { GM_setClipboard(urlsString, "text", () => { alert('作品/用户链接已复制到剪贴板!'); }); } else { alert("未提取到任何作品/用户链接!") } }, order); }; if (typeof JSZip === 'undefined') { alert("XHS-Downloader 用户脚本依赖库 JSZip 加载失败,作品文件打包下载功能无法使用,请尝试刷新网页或者向作者反馈!"); } /* ==================== 样式定义 ==================== */ let style = document.createElement('style'); style.textContent = ` /* 弹窗基础样式 */ #SettingsOverlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.32); backdrop-filter: blur(4px); display: flex; justify-content: center; align-items: center; z-index: 10000; animation: fadeIn 0.3s; } .optimized-scroll-modal { background: white; border-radius: 16px; width: 380px; /* 缩小窗口宽度 */ max-width: 95vw; box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); overflow: hidden; animation: scaleUp 0.3s; } /* 头部样式 */ .modal-header { padding: 1rem; border-bottom: 1px solid #eee; text-align: center; } .modal-header span { font-size: 1.25rem; font-weight: 500; color: #212121; } /* 内容区域 */ .modal-body { padding: 1rem; /* 减小内边距 */ } /* 设置项样式 */ .setting-item { margin: 0.5rem 0; /* 减少设置项间距 */ padding: 10px; border-radius: 8px; transition: background 0.2s; } .setting-item:hover { background: #f0f0f0; } .setting-item label { display: flex; justify-content: space-between; align-items: center; width: 100%; } /* 设置项标题 */ .setting-item label span { font-size: 1rem; /* 增大标题文字 */ font-weight: 500; color: #333; } /* 开关样式 */ .toggle-switch { position: relative; width: 40px; height: 20px; } .toggle-switch input { opacity: 0; width: 0; height: 0; } .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background: #ccc; transition: 0.4s; border-radius: 34px; } .slider:before { content: ""; position: absolute; height: 16px; width: 16px; left: 2px; bottom: 2px; background: white; border-radius: 50%; transition: 0.4s; } input:checked + .slider { background: #2196F3; } input:checked + .slider:before { transform: translateX(20px); } /* 数值输入 */ .number-input { display: flex; align-items: center; border: 1px solid #ddd; border-radius: 8px; overflow: hidden; margin: 6px 0; } .number-input input { width: 60px; text-align: center; border: none; } .number-button { padding: 4px 8px; background: #f0f0f0; border: none; cursor: pointer; transition: all 0.2s; } /* 文本输入框 */ .text-input { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 0.9rem; margin-top: 8px; /* 增加与标题的距离 */ transition: border-color 0.2s; } .text-input:focus { outline: none; border-color: #2196F3; box-shadow: 0 0 4px rgba(33, 150, 243, 0.3); } /* 设置项说明 */ .setting-description { font-size: 0.875rem; color: #757575; margin-top: 4px; line-height: 1.4; text-align: left; /* 左对齐 */ } /* 底部按钮 */ .modal-footer { padding: 1rem; border-top: 1px solid #eee; display: flex; justify-content: flex-end; gap: 12px; } .primary-btn { background: #2196F3; color: white; padding: 8px 24px; border-radius: 24px; cursor: pointer; transition: all 0.2s; } .secondary-btn { background: #f0f0f0; color: #666; padding: 8px 24px; border-radius: 24px; cursor: pointer; transition: all 0.2s; } /* 动画 */ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes scaleUp { from { transform: scale(0.98); } to { transform: scale(1); } } `; document.head.appendChild(style); // 创建开关项 const createSettingItem = ({label, description, checked}) => { const item = document.createElement('div'); item.className = 'setting-item'; item.innerHTML = `
${description}
`; return item; }; // 创建数值输入项 const createNumberInput = ({label, description, value, min, max, disabled}) => { const item = document.createElement('div'); item.className = 'setting-item'; const numberInput = document.createElement('div'); numberInput.className = 'number-input'; numberInput.style.opacity = disabled ? 0.6 : 1; numberInput.innerHTML = ` `; item.innerHTML = `
${description}
`; // 绑定数值按钮事件 const container = item.querySelector('.number-input'); container.querySelectorAll('.number-button').forEach(btn => { btn.addEventListener('click', () => { const input = container.querySelector('input'); if (input.disabled) { return; } let val = parseInt(input.value); if (btn.dataset.action === 'increment') { val = Math.min(val + 1, max); } else { val = Math.max(val - 1, min); } input.value = val; }); }); return item; }; // 创建文本输入项 const createTextInput = ({label, description, placeholder, value}) => { const item = document.createElement('div'); item.className = 'setting-item'; item.innerHTML = `
${label}
${description}
`; return item; }; // 关闭弹窗函数 const closeSettingsModal = () => { const overlay = document.getElementById('SettingsOverlay'); if (overlay) { overlay.style.animation = 'fadeOut 0.2s'; setTimeout(() => overlay.remove(), 200); } }; /* ==================== 弹窗逻辑 ==================== */ const showSettings = () => { if (document.getElementById('SettingsOverlay')) { return; } // 创建覆盖层 const overlay = document.createElement('div'); overlay.id = 'SettingsOverlay'; // 创建弹窗 const modal = document.createElement('div'); modal.className = 'optimized-scroll-modal'; // 创建头部 const header = document.createElement('div'); header.className = 'modal-header'; header.innerHTML = ` 用户脚本设置 `; // 创建内容区域 const body = document.createElement('div'); body.className = 'modal-body'; // 自动滚动开关 const autoScroll = createSettingItem({ label: '自动滚动页面', description: '启用后,页面将根据规则自动滚动以便加载更多内容', checked: GM_getValue("autoScrollSwitch", false), }); // 文件打包开关 const filePack = createSettingItem({ label: '文件打包下载', description: '启用后,多个文件的作品将会以压缩包格式下载', checked: GM_getValue("packageDownloadFiles", true) }); // 滚动次数设置 const scrollCount = createNumberInput({ label: '自动滚动次数', description: '自动滚动页面的次数(仅在启用自动滚动页面时可用)', value: GM_getValue("maxScrollCount", 50), min: 10, max: 5000, disabled: !GM_getValue("autoScrollSwitch", false), }); // 名称格式设置 // const nameFormat = createTextInput({ // label: '文件名称格式', // description: '设置文件的名称格式(例如:{date}-{title})。', // placeholder: '{date}-{title}', // value: GM_getValue("fileNameFormat",) // }); // 绑定自动滚动开关控制次数输入 autoScroll.querySelector('input').addEventListener('change', (e) => { scrollCount.querySelector('input').disabled = !e.target.checked; scrollCount.querySelector('.number-input').style.opacity = e.target.checked ? 1 : 0.6; }); // 组合内容 body.appendChild(filePack); body.appendChild(autoScroll); body.appendChild(scrollCount); // body.appendChild(nameFormat); // 创建底部按钮 const footer = document.createElement('div'); footer.className = 'modal-footer'; const saveBtn = document.createElement('button'); saveBtn.className = 'primary-btn'; saveBtn.textContent = '保存设置'; const cancelBtn = document.createElement('button'); cancelBtn.className = 'secondary-btn'; cancelBtn.textContent = '放弃修改'; footer.appendChild(saveBtn); footer.appendChild(cancelBtn); // 组装弹窗 modal.appendChild(header); modal.appendChild(body); modal.appendChild(footer); overlay.appendChild(modal); document.body.appendChild(overlay); // 保存事件 saveBtn.addEventListener('click', () => { updateAutoScrollSwitch(autoScroll.querySelector('input').checked); updatePackageDownloadFiles(filePack.querySelector('input').checked); updateMaxScrollCount(parseInt(scrollCount.querySelector('input').value) || 50) // updateFileNameFormat(nameFormat.querySelector('.text-input').value.trim() || null); closeSettingsModal(); }); // 关闭事件 cancelBtn.addEventListener('click', closeSettingsModal); overlay.addEventListener('click', (e) => e.target === overlay && closeSettingsModal()); }; /* ==================== 样式定义 ==================== */ style = document.createElement('style'); style.textContent = ` /* 弹窗基础样式 */ #imageSelectionOverlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.32); backdrop-filter: blur(4px); display: flex; justify-content: center; align-items: center; z-index: 10000; animation: fadeIn 0.3s; } .image-selection-modal { background: white; border-radius: 16px; width: 80%; max-width: 900px; max-height: 90vh; box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); overflow: hidden; animation: scaleUp 0.3s; display: flex; flex-direction: column; } /* 头部样式 */ .modal-header { padding: 1rem; border-bottom: 1px solid #eee; text-align: center; } .modal-header span { font-size: 1.25rem; font-weight: 500; color: #212121; } /* 内容区域 */ .modal-body { flex: 1; padding: 1rem; overflow-y: auto; } /* 图片网格 */ .image-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 12px; } .image-item { position: relative; border-radius: 8px; overflow: hidden; cursor: pointer; transition: all 0.2s; border: 2px solid transparent; } .image-item img { width: 100%; height: 100px; object-fit: cover; border-radius: 6px; } .image-item.selected { border-color: #2196F3; } .image-checkbox { position: absolute; top: 8px; right: 8px; width: 20px; height: 20px; opacity: 0; } .image-checkbox + label { position: absolute; top: 8px; right: 8px; width: 20px; height: 20px; background: white; border: 1px solid #ccc; border-radius: 50%; cursor: pointer; display: flex; justify-content: center; align-items: center; transition: all 0.2s; } .image-checkbox:checked + label { background: #2196F3; border-color: #2196F3; } .image-checkbox:checked + label::after { content: "✓"; color: white; font-size: 12px; } /* 底部按钮 */ .modal-footer { padding: 1rem; border-top: 1px solid #eee; display: flex; justify-content: flex-end; gap: 12px; } .primary-btn { background: #2196F3; color: white; padding: 8px 24px; border-radius: 24px; cursor: pointer; transition: all 0.2s; } .secondary-btn { background: #f0f0f0; color: #666; padding: 8px 24px; border-radius: 24px; cursor: pointer; transition: all 0.2s; } /* 动画 */ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes scaleUp { from { transform: scale(0.98); } to { transform: scale(1); } } `; document.head.appendChild(style); // 关闭弹窗函数 const closeImagesModal = () => { const overlay = document.getElementById('imageSelectionOverlay'); if (overlay) { overlay.style.animation = 'fadeOut 0.2s'; setTimeout(() => overlay.remove(), 200); } }; /* ==================== 弹窗逻辑 ==================== */ const showImageSelectionModal = (imageUrls, name) => { if (document.getElementById('imageSelectionOverlay')) { return; } // 创建覆盖层 const overlay = document.createElement('div'); overlay.id = 'imageSelectionOverlay'; // 创建弹窗 const modal = document.createElement('div'); modal.className = 'image-selection-modal'; // 创建头部 const header = document.createElement('div'); header.className = 'modal-header'; header.innerHTML = ` 请选中需要下载的图片 `; // 创建内容区域 const body = document.createElement('div'); body.className = 'modal-body'; // 创建图片网格 const imageGrid = document.createElement('div'); imageGrid.className = 'image-grid'; // 动态生成图片项 imageUrls.forEach((image) => { const item = document.createElement('div'); item.className = 'image-item'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.className = 'image-checkbox'; checkbox.id = `image-checkbox-${image.index}`; checkbox.checked = true; const label = document.createElement('label'); label.htmlFor = `image-checkbox-${image.index}`; const img = document.createElement('img'); img.src = image.webp; img.index = image.index; img.url = image.url; img.alt = `图片_${image.index}`; item.appendChild(checkbox); item.appendChild(label); item.appendChild(img); // 绑定点击事件 item.addEventListener('click', (e) => { if (e.target.tagName !== 'INPUT') { checkbox.checked = !checkbox.checked; item.classList.toggle('selected', checkbox.checked); } }); imageGrid.appendChild(item); }); body.appendChild(imageGrid); // 创建底部按钮 const footer = document.createElement('div'); footer.className = 'modal-footer'; const confirmBtn = document.createElement('button'); confirmBtn.className = 'primary-btn'; confirmBtn.textContent = '开始下载'; const closeBtn = document.createElement('button'); closeBtn.className = 'secondary-btn'; closeBtn.textContent = '关闭窗口'; footer.appendChild(confirmBtn); footer.appendChild(closeBtn); // 组装弹窗 modal.appendChild(header); modal.appendChild(body); modal.appendChild(footer); overlay.appendChild(modal); document.body.appendChild(overlay); // 确认事件 confirmBtn.addEventListener('click', async () => { const selectedImages = Array.from(document.querySelectorAll('.image-checkbox:checked')).map((checkbox) => { let item = checkbox.parentElement.querySelector('img'); return { index: item.index, url: item.url, } }); if (selectedImages.length === 0) { alert('请至少选择一张图片!'); return; } closeImagesModal(); await downloadImage(selectedImages, name) }); // 关闭事件 closeBtn.addEventListener('click', closeImagesModal); overlay.addEventListener('click', (e) => e.target === overlay && closeImagesModal()); }; // 创建主图标 const createIcon = () => { const icon = document.createElement('div'); icon.style = ` position: fixed; bottom: ${config.position.bottom}; left: ${config.position.left}; width: ${config.icon[config.icon.type].size}px; height: ${config.icon[config.icon.type].size}px; background: white; border-radius: ${config.icon.image.borderRadius || '8px'}; cursor: pointer; z-index: 9999; box-shadow: 0 3px 5px rgba(0,0,0,0.12), 0 3px 5px rgba(0,0,0,0.24); display: flex; align-items: center; justify-content: center; transition: all ${config.animation.duration}s ${config.animation.easing}; `; icon.style.backgroundImage = `url(${config.icon.image.url})`; icon.style.backgroundSize = 'cover'; return icon; }; // 创建菜单容器 const menu = document.createElement('div'); menu.style = ` position: fixed; bottom: calc(${config.position.bottom} + ${config.icon[config.icon.type].size}px + 1rem); left: ${config.position.left}; width: 255px; max-width: calc(100vw - 4rem); background: white; border-radius: 16px; box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); overflow: hidden; display: none; z-index: 9998; transform-origin: bottom left; transition: all ${config.animation.duration}s ${config.animation.easing}; opacity: 0; transform: translateY(10px) scaleY(0.95); will-change: transform, opacity; `; // 创建菜单内容容器 const menuContent = document.createElement('div'); menuContent.style = ` max-height: 400px; overflow-y: auto; overscroll-behavior: contain; `; menu.appendChild(menuContent); // 初始化样式 style = document.createElement('style'); style.textContent = ` :root { --primary: #2196F3; --surface: #ffffff; --on-surface: #212121; --ripple-color: rgba(33, 150, 243, 0.15); --border-radius: 12px; } .menu-item { display: flex; padding: 1rem 1.5rem; cursor: pointer; position: relative; transition: all 0.2s ease; align-items: center; } .menu-item:hover { background: var(--ripple-color); } .menu-item:not(:last-child) { border-bottom: 1px solid #eee; } .icon-container { margin-right: 1rem; display: flex; align-items: center; } .material-icons { font-size: 24px; color: var(--primary); } .content { flex: 1; } .title { font-size: 0.95rem; color: var(--on-surface); font-weight: 500; margin-bottom: 2px; } .subtitle { font-size: 0.75rem; color: #757575; line-height: 1.4; } .menu-enter { animation: slideIn ${config.animation.duration}s ${config.animation.easing}; } .menu-exit { animation: slideOut ${config.animation.duration}s ${config.animation.easing}; } @keyframes slideIn { from { opacity: 0; transform: translateY(10px) scaleY(0.95); } to { opacity: 1; transform: translateY(0) scaleY(1); } } @keyframes slideOut { from { opacity: 1; transform: translateY(0) scaleY(1); } to { opacity: 0; transform: translateY(10px) scaleY(0.95); } } .ripple { position: absolute; border-radius: 50%; transform: scale(0); animation: ripple 0.6s linear; background: var(--ripple-color); pointer-events: none; } @keyframes ripple { to { transform: scale(2); opacity: 0; } } /* 滚动条样式 */ ::-webkit-scrollbar { width: 8px; } ::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 10px; } ::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 10px; } ::-webkit-scrollbar-thumb:hover { background: #a8a8a8; } `; document.head.appendChild(style); // 涟漪效果 const createRipple = e => { const target = e.currentTarget; const rect = target.getBoundingClientRect(); const size = Math.max(rect.width, rect.height); const x = e.clientX - rect.left - size / 2; const y = e.clientY - rect.top - size / 2; const ripple = document.createElement('span'); ripple.className = 'ripple'; ripple.style.width = ripple.style.height = `${size}px`; ripple.style.left = `${x}px`; ripple.style.top = `${y}px`; target.appendChild(ripple); setTimeout(() => ripple.remove(), 600); }; // 隐藏菜单 let hideTimeout; const hideMenu = () => { hideTimeout = setTimeout(() => { menu.classList.remove('menu-enter'); menu.classList.add('menu-exit'); menu.style.opacity = 0; menu.style.transform = 'translateY(10px) scaleY(0.95)'; setTimeout(() => { menu.style.display = 'none'; menu.classList.remove('menu-exit'); isMenuVisible = false; }, config.animation.duration * 1000); }, 100); }; let currentUrl; // 动态生成菜单内容 const updateMenuContent = () => { menuContent.innerHTML = ''; // 根据URL生成不同菜单项 currentUrl = window.location.href; const menuItems = []; if (!config.disclaimer) { menuItems.push({ text: 'README', icon: ' 📄 ', action: readme, description: '阅读脚本说明和免责声明' },); } else if (currentUrl === "https://www.xiaohongshu.com/explore" || currentUrl.includes("https://www.xiaohongshu.com/explore?")) { menuItems.push({ text: '提取推荐作品链接', icon: ' ⛓ ', action: () => extractAllLinksEvent(-1), description: '提取当前页面的作品链接至剪贴板' },); } else if (currentUrl.includes("https://www.xiaohongshu.com/explore/")) { menuItems.push({ text: '下载作品文件', icon: ' 📦 ', action: extractDownloadLinks, description: '下载当前作品的无水印文件' },); } else if (currentUrl.includes("https://www.xiaohongshu.com/user/profile/")) { menuItems.push({ text: '提取发布作品链接', icon: ' ⛓ ', action: () => extractAllLinksEvent(0), description: '提取账号发布作品链接至剪贴板' }, { text: '提取点赞作品链接', icon: ' ⛓ ', action: () => extractAllLinksEvent(2), description: '提取账号点赞作品链接至剪贴板' }, { text: '提取收藏作品链接', icon: ' ⛓ ', action: () => extractAllLinksEvent(1), description: '提取账号收藏作品链接至剪贴板' },); } else if (currentUrl.includes("https://www.xiaohongshu.com/search_result")) { menuItems.push({ text: '提取作品链接', icon: ' ⛓ ', action: () => extractAllLinksEvent(3), description: '提取搜索结果的作品链接至剪贴板' }, { text: '提取用户链接', icon: ' ⛓ ', action: () => extractAllLinksEvent(4), description: '提取搜索结果的用户链接至剪贴板' },); } else if (currentUrl.includes("https://www.xiaohongshu.com/board/")) { menuItems.push({ text: "提取专辑作品链接", icon: ' ⛓ ', action: () => extractAllLinksEvent(5), description: '提取当前专辑的作品链接至剪贴板' },); } // 常用功能 menuItems.push({ separator: true }, { text: '修改用户脚本设置', icon: ' ⚙️ ', action: showSettings, description: '修改用户脚本设置' }, { text: '访问项目开源仓库', icon: ' 📒 ', action: about, description: '访问项目 GitHub 开源仓库' }); // 创建菜单项 menuItems.forEach(item => { if (item.separator) { const divider = document.createElement('div'); divider.style = ` height: 8px; background: #f5f5f5; `; menuContent.appendChild(divider); return; } const btn = document.createElement('div'); btn.className = 'menu-item'; btn.innerHTML = `
${item.icon}
${item.text}
${item.description}
`; btn.addEventListener('click', (e) => { e.stopPropagation(); item.action(); hideMenu(); }); btn.addEventListener('mousedown', createRipple); menuContent.appendChild(btn); }); }; // URL监测相关 let lastUrl = window.location.href; let isMenuVisible = false; // 显示菜单 const showMenu = () => { clearTimeout(hideTimeout); menu.style.display = 'block'; void menu.offsetHeight; // 触发重绘 menu.classList.add('menu-enter'); menu.style.opacity = 1; menu.style.transform = 'translateY(0) scaleY(1)'; updateMenuContent(); isMenuVisible = true; }; // 事件监听 const icon = createIcon(); icon.addEventListener('mouseenter', showMenu); icon.addEventListener('mouseleave', hideMenu); menu.addEventListener('mouseenter', () => clearTimeout(hideTimeout)); menu.addEventListener('mouseleave', hideMenu); // URL变化监听 const setupUrlListener = () => { const observeUrl = () => { if (window.location.href !== lastUrl) { lastUrl = window.location.href; if (isMenuVisible) { updateMenuContent(); } } requestAnimationFrame(observeUrl); }; observeUrl(); }; // 添加到页面 document.body.appendChild(icon); document.body.appendChild(menu); document.head.appendChild(style); setupUrlListener(); })();