Greasy Fork

来自缓存

Greasy Fork is available in English.

KamePT种子列表无限下拉瀑布流视图

KamePT种子列表无限下拉瀑布流视图(描述不能与名称相同, 乐)

当前为 2023-05-01 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name            KamePT种子列表无限下拉瀑布流视图
// @name:en         KamePT_waterfall_torrent
// @namespace       https://github.com/KesaubeEire/PT_TorrentList_Masonry
// @version         0.1
// @description     KamePT种子列表无限下拉瀑布流视图(描述不能与名称相同, 乐)
// @description:en  KamePT torrent page waterfall view
// @author          Kesa
// @match           https://kamept.com/torrents.php*
// @icon            https://kamept.com/favicon.ico
// @grant           none
// @license         MIT
// ==/UserScript==

// FIXME:
// 0. 一些顶层设计 --------------------------------------------------------------------------------------
// |-- 0.1 顶层参数&对象
/** kame 域名 */
const domain = "https://kamept.com/";

/** 瀑布流对象 */
var masonry;
window.masonry = masonry;

/** 瀑布流卡片宽度 */
var CARD_WIDTH = 200;
window.CARD_WIDTH = CARD_WIDTH;

/** 瀑布流卡片索引 */
let CARD_INDEX = 0;

/** 翻页相关参数顶层对象 */
const PAGE = {
  /** 翻页: 底部检测时间间隔 */
  GAP: 900,

  /** 翻页: 底部检测视点与底部距离 */
  DISTANCE: 300,

  /** 翻页: 是否为初始跳转页面 */
  IS_ORIGIN: true,

  /** 翻页: 当前页数 */
  PAGE_CURRENT: 0,

  /** 翻页: 下一页数 */
  PAGE_NEXT: 0,

  /** 翻页: 下一页的链接 */
  NEXT_URL: "",
};

// |-- 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_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},0);`;

    // 获取免费折扣类型
    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");
    let tags = tagSpans
      ? Array.from(tagSpans).map((span) => span.textContent.trim())
      : [];

    // console.log(index);
    // console.log(torrentName);
    // console.log(tags);

    if (
      tags.length != 0 &&
      (tags[0].includes("天") ||
        tags[0].includes("时") ||
        tags[0].includes("分钟"))
    ) {
      // console.log(tags[0]);
      tags.shift();
    }

    // 获取描述
    const descriptionCell = row.querySelector(".torrentname td:nth-child(2)");

    const str = descriptionCell.innerHTML;
    let desResult;
    // -- 前处理
    if (str.lastIndexOf("</span>") > str.lastIndexOf("<br>")) {
      desResult = str.substring(str.lastIndexOf("</span>") + 7); // 加 7 是为了去掉 "</span>" 的长度
    } else {
      desResult = str.substring(str.lastIndexOf("<br>") + 4); // 加 7 是为了去掉 "</span>" 的长度
    }
    // -- 后处理
    desResult = desResult.split("<div")[0];
    const description = desResult ? desResult.trim() : "";

    // 获取评论数量
    const commentsLink = row.querySelector("td.rowfollow:nth-child(3) a");
    // console.log(commentsLink.innerHTML);
    const comments = commentsLink ? parseInt(commentsLink.textContent) : 0;

    // 获取上传日期
    const uploadDateSpan = row.querySelector("td:nth-child(4) span");
    const uploadDate = uploadDateSpan ? uploadDateSpan.title : "";

    // 获取文件大小
    const sizeCell = row.querySelector("td:nth-child(5)");
    const size = sizeCell ? sizeCell.textContent.trim() : "";

    // 获取做种人数
    const seedersLink = row.querySelector("td:nth-child(6) a");
    const seeders = seedersLink ? parseInt(seedersLink.textContent) : 0;

    // 获取下载人数
    const leechersCell = row.querySelector("td:nth-child(7)");
    const leechers = leechersCell ? parseInt(leechersCell.textContent) : 0;

    // 获取完成下载数
    const snatchedLink = row.querySelector("td:nth-child(8) a");
    const snatched = snatchedLink ? parseInt(snatchedLink.textContent) : 0;

    // 将当前行的数据格式化为 JSON 对象
    const rowData = {
      torrentIndex,
      category,
      torrent_name: torrentName,
      torrentLink,
      torrentId,
      picLink,
      pattMsg,
      downloadLink,
      collectLink,
      free_type: freeType,
      free_remaining_time: freeRemainingTime,
      tags,
      description,
      comments,
      upload_date: uploadDate,
      size,
      seeders,
      leechers,
      snatched,
    };

    // 将当前行的 JSON 对象添加到数组中
    data.push(rowData);
  });
  return data;
}

/**
 * 将种子列表信息渲染为卡片放入瀑布流
 * @param {DOM} waterfallNode 瀑布流容器dom
 * @param {list} torrent_json 种子列表信息的 json对象列表
 * @param {boolean} isFirst 是否是第一次渲染, 默认为是, 新增渲染要写 false
 */
function RENDER_TORRENT_JSON_IN_MASONRY(
  waterfallNode,
  torrent_json,
  isFirst = true
) {
  const cardTemplate = (data) => {
    const {
      torrentIndex,
      category,
      torrent_name: torrentName,
      torrentLink,
      torrentId,
      picLink,
      pattMsg,
      downloadLink,
      collectLink,
      free_type: freeType,
      free_remaining_time: freeRemainingTime,
      tags,
      description,
      comments,
      upload_date: uploadDate,
      size,
      seeders,
      leechers,
      snatched,
    } = data;

    return `
<div class="card-header">
  <a src="${torrentLink}" href="${torrentLink}" target="_blank">${torrentName}</a>
</div>
<div class="card-body">
  <div class="card-image">
    <img class="card-image--img" src="${picLink}" alt="${torrentName}" />
    <div class="card-index">
      ${torrentIndex + 1}
    </div>  
  </div>
  <div class="card-details">
    <div class="card-line"><strong>Category:</strong> ${category}</div>
    <!--<div class="card-line"><strong>Torrent ID:</strong> ${torrentId}</div> -->
    <div class="card-line">
      <strong>Tags:</strong> ${tags.join(", ")}
    </div>
    <div class="card-line"><strong>Description:</strong> ${description}</div>
    <div class="card-line"><strong>Comments:</strong> ${comments}</div>
    <div class="card-line"><strong>Uploaded:</strong> ${uploadDate}</div>
    <div class="card-line"><strong>Size:</strong> ${size}</div>
    <div class="card-line"><strong>Seeders:</strong> ${seeders}</div>
    <div class="card-line"><strong>Leechers:</strong> ${leechers}</div>
    <div class="card-line"><strong>Snatched:</strong> ${snatched}</div>
    <div class="card-line"><strong>Download Link:</strong> <a src="${downloadLink}">下载</a></div>
    <div class="card-line"><strong>Collect Link:</strong> <a href="${collectLink}">Collect</a></div>
  </div>
</div>
<div class="card-footer">
  <div><strong>Free Type:</strong> ${freeType}</div>
  <div><strong>Free Remaining Time:</strong> ${freeRemainingTime}</div>
  <div><strong>Patt Msg:</strong> ${pattMsg}</div>
</div>
    `;
  };

  for (const rowData of torrent_json) {
    const card = document.createElement("div");
    card.classList.add("card");
    card.innerHTML = cardTemplate(rowData);

    //  |--|-- 3.1.1 渲染完成图片后调整构图
    const card_img = card.querySelector(".card-image--img");
    card_img.onload = function () {
      // 加载完图片后重新布局 Masonry
      if (masonry) {
        // TODO: 这里可以写个防抖优化性能
        masonry.layout();
      }

      // // TODO:加载完图片添加鼠标触摸预览
      // var imgEle,
      //   selector = "img.preview",
      //   imgPosition;
      // jQuery("body")
      //   .on("mouseover", selector, function (e) {
      //     imgEle = jQuery(card_img);
      //     // previewEle = jQuery('<img style="display: none;position:absolute;">').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 + 2;
  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 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_WIDTH);
    card.style.width = `${targetWidth}px`;
  }

  // 调整卡片间隔 gutter
  masonry.options.gutter = GET_CARD_GUTTER(containerDom, targetWidth);

  // 重新布局瀑布流
  masonry.layout();
}

/**
 * 防抖函数
 * @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.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.addEventListener("click", function () {
    if (masonry) {
      masonry.layout();
    }

    // 动态调整卡片宽度
    CARD_WIDTH = CARD_WIDTH == 200 ? 300 : 200;
    CHANGE_CARD_WIDTH(CARD_WIDTH, waterfallNode, masonry);
    masonry.layout();

    // // 改变卡片宽度
    // for (const card of waterfallNode.childNodes) {
    //   console.log(CARD_WIDTH);
    //   card.style.width = `${CARD_WIDTH}px`;
    // }

    // // 调整卡片间隔 gutter
    // masonry.options.gutter = GET_CARD_GUTTER(waterfallNode, 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;
  background-color: grey;
  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_WIDTH}px;
  border: 1px solid #ccc;
  border-radius: 5px;
  box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
  /* margin: 10px; */
  margin: 6px 0;
}

/* 卡片图像div */
.card-image {
  height: 100%;
  position: relative;

  position: relative;
}

/* 卡片图像div -> img标签 */
.card-image img {
  width: 100%;
  object-fit: cover;
}

/* 卡片索引 */
.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;
}
`;

  // 注释中的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_WIDTH),
    });

    // console.log(masonry);

    //    -- 3.3.2 监听窗口大小变化事件
    window.addEventListener("resize", function () {
      // 调整卡片间隔 gutter
      masonry.options.gutter = GET_CARD_GUTTER(waterfallNode, 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_NEXT;

    // 如果 "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);

        // 页数更新
        PAGE.IS_ORIGIN = false;

        // |--|-- 4.2.3 渲染 下一页信息 并 加到 waterfallNode 里面来
        PUT_TORRENT_INTO_MASONRY(table, waterfallNode, false);
      })
      .catch((error) => console.error(error));
  }, PAGE.DISTANCE);
})();