// ==UserScript== // @name KamePT种子列表无限下拉瀑布流视图 // @name:en KamePT_waterfall_torrent // @namespace https://github.com/KesaubeEire/PT_TorrentList_Masonry // @version 0.2.4 // @description KamePT种子列表无限下拉瀑布流视图(描述不能与名称相同, 乐) // @description:en KamePT torrent page waterfall view // @author Kesa // @match https://kamept.com/torrents.php* // @match https://kamept.com/special.php* // @icon https://kamept.com/favicon.ico // @grant none // @license MIT // @downloadURL none // ==/UserScript== // FIXME: // 0. 一些顶层设计 -------------------------------------------------------------------------------------- // |-- 0.1 顶层参数&对象 /** 获取主题背景色 */ const mainOuterDOM = document.querySelector("table.mainouter"); const themeColor = window.getComputedStyle(mainOuterDOM)["background-color"]; console.log("背景颜色:", themeColor); /** kame 域名 */ const domain = "https://kamept.com/"; /** 瀑布流对象 */ var masonry; window.masonry = masonry; /** 瀑布流卡片相关参数顶层对象 */ const CARD = { /** 瀑布流卡片宽度 */ CARD_WIDTH: 200, /** 瀑布流卡片边框宽度 -> 这个2是真值, 但是边框好像是会随着分辨率和缩放变化, 给高有利大分辨率, 给低有利于小分辨率 */ CARD_BORDER: 3, /** 瀑布流卡片索引 */ CARD_INDEX: 0, }; window.CARD_WIDTH = CARD.CARD_WIDTH; /** 翻页相关参数顶层对象 */ const PAGE = { /** 翻页: 底部检测时间间隔 */ GAP: 900, /** 翻页: 底部检测视点与底部距离 */ DISTANCE: 300, /** 翻页: 是否为初始跳转页面 */ IS_ORIGIN: true, /** 翻页: 当前页数 */ PAGE_CURRENT: 0, /** 翻页: 下一页数 */ PAGE_NEXT: 0, /** 翻页: 下一页的链接 */ NEXT_URL: "", }; /** 网站图标链接 */ const ICON = { /** 大小图标 */ SIZE: 'size', /** 评论图标 */ COMMENT: 'comments', /** 上传人数图标 */ SEEDERS: 'seeders', /** 下载人数图标 */ LEECHERS: 'leechers', /** 已完成人数图标 */ SNATCHED: 'snatched', /** 下载图标 */ DOWNLOAD: 'download', /** 未收藏图标 */ COLLET: 'Unbookmarked', /** 已收藏图标 */ COLLETED: 'Bookmarked', }; // |-- 0.1 顶层方法 /** * 将 种子列表dom 的信息变为 json对象列表 * @param {DOM} torrent_list_Dom 种子列表dom * @returns {list} 种子列表信息的 json对象列表 */ function TORRENT_LIST_TO_JSON(torrent_list_Dom) { // 获取表格中的所有行 const rows = torrent_list_Dom.querySelectorAll("tr"); // const rows = div.querySelectorAll('tr'); // 种子信息 -> 存储所有行数据的数组 const data = []; // index // let index = 0; // 遍历每一行并提取数据 rows.forEach((row) => { // 获取种子分类 const categoryImg = row.querySelector("td:nth-child(1) > a > img"); const category = categoryImg ? categoryImg.alt : ""; // 若没有分类则退出 if (!category) return; // 加index const torrentIndex = CARD.CARD_INDEX++; // 获取种子名称 const torrentNameLink = row.querySelector(".torrentname a"); const torrentName = torrentNameLink ? torrentNameLink.textContent.trim() : ""; // 获取种子详情链接 const torrentLink = torrentNameLink.href; // console.log(torrentLink); // 获取种子id const pattern = /id=(\d+)&hit/; const match = torrentLink.match(pattern); const torrentId = match ? parseInt(match[1]) : null; // 获取预览图片链接 let picLink = row .querySelector(".torrentname img") .getAttribute("data-src"); // -- 没有加域名前缀的加上 if (!picLink.includes("http")) picLink = domain + picLink; // 获取置顶信息 const place_at_the_top = row.querySelector(".torrentname img.sticky"); const pattMsg = place_at_the_top ? place_at_the_top.title : ""; // 获取下载链接 const downloadLink = `${domain}download.php?id=${torrentId}`; // 获取收藏链接 const collectLink = `javascript: bookmark(${torrentId},${torrentIndex});`; // 获取收藏状态 const collectDOM = row.querySelector(".torrentname a[id^='bookmark']"); const collectState = collectDOM.children[0].alt; // console.log(collectState); // 获取免费折扣类型 const freeTypeImg = row.querySelector('img[class^="pro_"]'); // console.log(freeTypeImg); // console.log(freeTypeImg.alt); const freeType = freeTypeImg ? freeTypeImg.alt : ""; // 获取免费剩余时间 const freeRemainingTimeSpan = row.querySelector("font"); const freeRemainingTime = freeRemainingTimeSpan ? freeRemainingTimeSpan.innerText : ""; // 获取标签 const tagSpans = row.querySelectorAll(".torrentname span"); // const raw_tags = row.querySelector(".torrentname"); const tagsDOM = Array.from(tagSpans); let tags = tagSpans ? tagsDOM.map((span) => span.textContent.trim()) : []; // console.log(index); // console.log(torrentName); // console.log(tags); if (freeType != "") { // console.log(tags[0]); tags.shift(); tagsDOM.shift(); } const raw_tags = tagsDOM.map((el) => el.outerHTML).join(""); // console.log(raw_tags); // 获取描述 const descriptionCell = row.querySelector(".torrentname td:nth-child(2)"); const str = descriptionCell.innerHTML; let desResult; // -- 前处理 if (str.lastIndexOf("") > str.lastIndexOf("
")) { desResult = str.substring(str.lastIndexOf("") + 7); // 加 7 是为了去掉 "" 的长度 } else { desResult = str.substring(str.lastIndexOf("
") + 4); // 加 7 是为了去掉 "" 的长度 } // -- 后处理 desResult = desResult.split(" { const { torrentIndex, category, torrent_name: torrentName, torrentLink, torrentId, picLink, pattMsg, downloadLink, collectLink, collectState, free_type: freeType, free_remaining_time: freeRemainingTime, raw_tags, tags, description, comments, upload_date: uploadDate, size, seeders, leechers, snatched, } = data; return `
${category}
${torrentName}
${torrentIndex + 1}
${ description ? ` ${description}` : "" }
${raw_tags}
${ freeType ? `
${freeType}: ${freeRemainingTime}
` : "" } ${pattMsg ? `
置顶等级: ${pattMsg}
` : ""}
${ICON.SIZE} ${size}
  
${ICON.DOWNLOAD}  下载
  
上传时间: ${uploadDate}
${ICON.COMMENT} ${comments}   ${ICON.SEEDERS} ${seeders}   ${ICON.LEECHERS} ${leechers}   ${ICON.SNATCHED} ${snatched}
`; }; for (const rowData of torrent_json) { const card = document.createElement("div"); card.classList.add("card"); card.innerHTML = cardTemplate(rowData); // 生成新的时候再改一次图片宽度 card.style.width = `${CARD.CARD_WIDTH}px`; // z-index 逐渐变小, 使得展开标题时本卡片的内容可以不被下面的卡片遮挡 card.style.zIndex = 10000 - rowData.torrentIndex; // |--|-- 3.1.1 渲染完成图片后调整构图 const card_img = card.querySelector(".card-image--img"); card_img.onload = function () { // 加载完图片后重新布局 Masonry if (masonry) { // TODO: 这里可以写个防抖优化性能 // TODO: 人好像自带防抖的, 哈哈...... masonry.layout(); } // // TODO:加载完图片添加鼠标触摸预览 // var imgEle, // selector = "img.preview", // imgPosition; // jQuery("body") // .on("mouseover", selector, function (e) { // imgEle = jQuery(card_img); // // previewEle = jQuery('').appendTo(imgEle.parent()) // imgPosition = getImgPosition(e, imgEle); // let position = getPosition(e, imgPosition); // let src = imgEle.attr("src"); // if (src) { // previewEle.attr("src", src).css(position).fadeIn("fast"); // } // }) // .on("mouseout", selector, function (e) { // // previewEle.remove() // // previewEle = null // previewEle.hide(); // }) // .on("mousemove", selector, function (e) { // let position = getPosition(e, imgPosition); // previewEle.css(position); // }); }; // |--|-- 3.1.2 插入生成的元素 // |--|--|-- 3.1.2.1 第一次默认生成 waterfallNode.appendChild(card); // |--|--|-- 3.1.2.2 非第一次生成 if (!isFirst) { // console.log("not first ----------------------------"); // console.log(card); masonry.appended(card); } } } /** * 整合上面两个函数: 将种子列表转为瀑布流 * @param {DOM} torrent_list_Dom 种子列表dom * @param {DOM} waterfallNode 瀑布流容器dom * @param {boolean} isFirst 是否是第一次渲染, 默认为是, 新增渲染要写 false */ function PUT_TORRENT_INTO_MASONRY( torrent_list_Dom, waterfallNode, isFirst = true ) { /** 种子列表信息的 json对象列表 */ const data = TORRENT_LIST_TO_JSON(torrent_list_Dom); // DEBUG:打印获得的数据 console.log(`渲染行数: ${data.length}`); // console.log(data); // 将种子列表信息渲染为卡片放入瀑布流 RENDER_TORRENT_JSON_IN_MASONRY(waterfallNode, data, isFirst); } /** * 根据容器宽度和卡片宽度动态调整卡片间隔 gutter * @param {DOM} containerDom 容器dom * @param {number} card_width 卡片宽度 */ function GET_CARD_GUTTER(containerDom, card_width) { // 获取容器宽度 const _width = containerDom.clientWidth; // 获取一个合适的 gutter const card_real_width = card_width + CARD.CARD_BORDER; const columns = Math.floor(_width / card_real_width); const gutter = (_width - columns * card_real_width) / (columns - 1); // console.log(`列数:${columns} 间隔:${gutter}`); // console.log( // `容器宽:${_width} 列宽:${masonry ? masonry.columnWidth : "对象"}` // ); return Math.floor(gutter); } /** * 动态调整卡片宽度 * @param {number} targetWidth * @param {DOM} containerDom 容器dom * @param {object} masonry 瀑布流对象 */ function CHANGE_CARD_WIDTH(targetWidth, containerDom, masonry) { // 改变卡片宽度 for (const card of containerDom.childNodes) { // console.log(CARD.CARD_WIDTH); card.style.width = `${targetWidth}px`; } // 调整卡片间隔 gutter masonry.options.gutter = GET_CARD_GUTTER(containerDom, targetWidth); // 重新布局瀑布流 masonry.layout(); } /** * 执行收藏动作并对制定卡片切换图标 * @param {string} jsCodeLink js的收藏代码 * @param {string} card_id 种子卡片id */ function COLLET_AND_ICON_CHANGE(jsCodeLink, card_id) { // console.log(jsCodeLink, card_id); try { // 收藏链接 window.location.href = jsCodeLink; // 操作相应的收藏图片 const btn = document.querySelector(`button#${card_id}`); const img = btn.children[0]; img.className = img.className == "delbookmark" ? "bookmark" : "delbookmark"; // console.log(btn); } catch (error) { // GUI 通知一下捏 console.error(error); } } window.COLLET_AND_ICON_CHANGE = COLLET_AND_ICON_CHANGE; /** * 防抖函数 * @param {function} func 操作函数 * @param {number} delay 延迟 * @returns */ function debounce(func, delay) { var timer; return function () { var context = this; var args = arguments; clearTimeout(timer); timer = setTimeout(function () { func.apply(context, args); }, delay); }; } (function () { "use strict"; // FIXME: // 1. 隐藏原种子列表并进行前置操作 -------------------------------------------------------------------------------------- // 表格节点 const tableNode = document.querySelector("table.torrents"); // 表格父节点 const parentNode = tableNode.parentNode; // 删除原有视图 // parentNode.removeChild(tableNode); // 隐藏原有视图 tableNode.style.display = "none"; // 放置瀑布流的节点 const waterfallNode = document.createElement("div"); // 添加class waterfallNode.classList.add("waterfall"); // 将瀑布流节点放置在表格节点上面 parentNode.insertBefore(waterfallNode, tableNode.nextSibling); // 生成按钮 -> 可以随时显示原来的表格 const btnViewOrigin = document.getElementById("btnViewOrigin"); // 创建一个按钮元素 const toggleBtn = document.createElement("button"); toggleBtn.classList.add("debug"); toggleBtn.setAttribute("id", "toggle_oldTable"); toggleBtn.innerText = "显示原种子表格"; toggleBtn.style.zIndex = 10001; // 为按钮添加事件监听器 toggleBtn.addEventListener("click", function () { if (tableNode.style.display === "none") { tableNode.style.display = "block"; toggleBtn.innerText = "隐藏原种子表格"; } else { tableNode.style.display = "none"; toggleBtn.innerText = "显示原种子表格"; } }); // 将按钮插入到文档中 document.body.appendChild(toggleBtn); // 生成按钮 -> Masonry 重新排列 const btnReLayout = document.getElementById("btnReLayout"); // 创建一个按钮元素 const reLayoutBtn = document.createElement("button"); reLayoutBtn.classList.add("debug"); reLayoutBtn.setAttribute("id", "btnReLayout"); reLayoutBtn.innerText = "单列宽度切换(200/300)"; reLayoutBtn.style.zIndex = 10002; // 为按钮添加事件监听器 reLayoutBtn.addEventListener("click", function () { if (masonry) { masonry.layout(); } // 动态调整卡片宽度 CARD.CARD_WIDTH = CARD.CARD_WIDTH == 200 ? 300 : 200; CHANGE_CARD_WIDTH(CARD.CARD_WIDTH, waterfallNode, masonry); masonry.layout(); // // 改变卡片宽度 // for (const card of waterfallNode.childNodes) { // console.log(CARD.CARD_WIDTH); // card.style.width = `${CARD.CARD_WIDTH}px`; // } // // 调整卡片间隔 gutter // masonry.options.gutter = GET_CARD_GUTTER(waterfallNode, CARD.CARD_WIDTH); // // 重新布局瀑布流 // masonry.layout(); }); // 将按钮插入到文档中 document.body.appendChild(reLayoutBtn); // FIXME: // 2. 将种子列表信息搞下来 html -> json 对象 -------------------------------------------------------------------------------------- // 获取表格 Dom const table = document.querySelector("table.torrents"); // /** 种子列表信息的 json对象列表 */ // const data = TORRENT_LIST_TO_JSON(table); // // FIXME: // // 3. 开整瀑布流 -------------------------------------------------------------------------------------- // // -- 3.1 搞定卡片模板 // // 将种子列表信息渲染为卡片放入瀑布流 // RENDER_TORRENT_JSON_IN_MASONRY(waterfallNode, data); // ----------- // 一步到位整合上面步骤: 将种子列表转为瀑布流 PUT_TORRENT_INTO_MASONRY(table, waterfallNode); // -- 3.2 调整 css // 使用中的css const css = ` /* 瀑布流主容器 */ div.waterfall{ width: 100%; padding-top: 20px; padding-bottom: 20px; border-radius: 20px; height: 100%; /* margin: 0 auto; */ margin: 20px auto; } /* 调试按键统一样式 */ button.debug { position: fixed; top: 10px; right: 10px; padding: 4px; background-color: #333; color: #fff; border: none; border-radius: 5px; cursor: pointer; } /* 调试按键1: 显示隐藏原种子列表 */ button#toggle_oldTable { top: 10px; } /* 调试按键2: Masonry 重新排列 */ button#btnReLayout { top: 40px; } /* 卡片 */ .card { width: ${CARD.CARD_WIDTH}px; border: 1px solid rgba(255, 255, 255, 0.5); border-radius: 5px; background-color: ${themeColor}; /* box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3); */ /* margin: 10px; */ margin: 6px 0; overflow: hidden; transition: all 0.1s; } .card:hover { } .card-holder{ background-color: rgba(255, 255, 255, 0.5); background: linear-gradient(to bottom, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0)); padding-bottom: 6px; } /* 卡片行默认样式 */ .card-line{ margin-bottom: 1px; display: flex; justify-content: space-evenly; align-items: center; } /* 卡片标题: 默认两行 */ .two-lines { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; transition: color 0.3s; } /* 卡片标题: hover时变为正常 */ .two-lines:hover { -webkit-line-clamp: 100; } /* 卡片信息: flex 居中 */ .cl-center{ display: flex; justify-content: space-evenly; align-items: center; } /* 卡片信息行: 标签行 */ .cl-tags{ display: flex; justify-content: left; align-items: center; transform: translateX(4px); } /* 卡片简介总容器 */ .card-details{ display: flex; justify-content: center; align-items: center; flex-direction: column; } /* 卡片图像div */ .card-image { height: 100%; position: relative; margin-bottom: 2px; } /* 卡片图像div -> img标签 */ .card-image img { width: 100%; object-fit: cover; } /* 卡片可选信息 */ .card-alter{ text-align: center; } /* 卡片索引 */ .card-index{ position: absolute; top: 0; left: 0; padding-right: 9px; padding-left: 2px; margin: 0; height: 20px; line-height: 16px; font-size: 16px; background-color: rgba(0,0,0,0.7); color: yellow; border-top-right-radius: 100px; border-bottom-right-radius: 100px; display: flex; align-items: center; } /* 卡片索引 */ .btnCollet{ padding: 1px 2px; } `; // 注释中的css const css_commented = ` `; // -- 3.3 引入 Masonry 库 const style = document.createElement("style"); style.textContent = css; document.head.appendChild(style); // 创建script标签 var script = document.createElement("script"); // 设置script标签的src属性为Masonry库的地址 script.src = "https://cdnjs.cloudflare.com/ajax/libs/masonry/4.2.2/masonry.pkgd.min.js"; // 将script标签添加到head标签中 document.getElementsByTagName("head")[0].appendChild(script); // -- 3.3.1 初始化 Masonry 参数 script.onload = function () { // 初始化瀑布流布局 masonry = new Masonry(waterfallNode, { itemSelector: ".card", columnWidth: ".card", gutter: GET_CARD_GUTTER(waterfallNode, CARD.CARD_WIDTH), }); // console.log(masonry); // -- 3.3.2 监听窗口大小变化事件 window.addEventListener("resize", function () { // 调整卡片间隔 gutter masonry.options.gutter = GET_CARD_GUTTER(waterfallNode, CARD.CARD_WIDTH); // 重新布局瀑布流 masonry.layout(); }); // 重新布局瀑布流 masonry.layout(); // 绑定 Masonry 对象到 window window.masonry = masonry; }; // FIXME: // 4. 底部检测 & 加载下一页 -------------------------------------------------------------------------------------- // |-- 4.1 检测是否到了底部 /** 延迟加载事件变量名 */ let debounceLoad; window.addEventListener("scroll", function () { const scrollHeight = document.body.scrollHeight; const clientHeight = document.documentElement.clientHeight; const scrollTop = document.documentElement.scrollTop || document.body.scrollTop; if (scrollTop + clientHeight >= scrollHeight - PAGE.DISTANCE) { debounceLoad(); } }); // |-- 4.2 加载下一页 debounceLoad = debounce(function () { console.log("到页面底部啦!!! Scrolled to bottom!"); // |--|-- 4.2.1 获取下一页的链接 // 使用 URLSearchParams 对象获取当前网页的查询参数 const urlSearchParams = new URLSearchParams(window.location.search); // 获取名为 "page" 的参数的值 -> 初始为页面值, 更新为更新值 PAGE.PAGE_CURRENT = PAGE.IS_ORIGIN ? urlSearchParams.get("page") : PAGE.PAGE_CURRENT; // 如果 "page" 参数不存在,则将页数设为 0,否则打印当前页数 if (!PAGE.PAGE_CURRENT) { console.log( `网页链接没有page参数, 无法跳转下一页, 生成PAGE.PAGE_CURRENT为0` ); PAGE.PAGE_CURRENT = 0; } else { console.log("当前页数: " + PAGE.PAGE_CURRENT); } // 将页数加 1,并设置为新的 "page" 参数的值 PAGE.PAGE_NEXT = parseInt(PAGE.PAGE_CURRENT) + 1; urlSearchParams.set("page", PAGE.PAGE_NEXT); // 生成新的链接,包括原网页的域名、路径和新的查询参数 PAGE.NEXT_URL = window.location.origin + window.location.pathname + "?" + urlSearchParams.toString(); // 打印新的链接 console.log("New URL:", PAGE.NEXT_URL); // TODO: 搞个 list 放入所有生成的新链接, 如果新链接存在就不 fetch 新数据 // |--|-- 4.2.2 加载下一页 html 获取 json 信息对象 fetch(PAGE.NEXT_URL) .then((response) => response.text()) .then((html) => { const parser = new DOMParser(); const doc = parser.parseFromString(html, "text/html"); const table = doc.querySelector("table.torrents"); // console.log(table); // |--|-- 4.2.3 渲染 下一页信息 并 加到 waterfallNode 里面来 PUT_TORRENT_INTO_MASONRY(table, waterfallNode, false); // 生成新的时候再改一次图片宽度 CHANGE_CARD_WIDTH(CARD.CARD_WIDTH, waterfallNode, masonry); // 页数更新, 在上面几行更新会导致没有下一页的情况下仍然触发 PAGE.IS_ORIGIN = false; PAGE.PAGE_CURRENT = PAGE.PAGE_NEXT; }) .catch((error) => { // console.error(error); console.warn("获取不到下页信息, 可能到头了"); console.warn(error); }); }, PAGE.DISTANCE); })();