b站工具集

1. 实现首页的内容区隐藏 2. 实现视频合集页面时长统计

// ==UserScript==
// @name         b站工具集
// @namespace    http://tampermonkey.net/
// @version      0.8
// @description  1. 实现首页的内容区隐藏 2. 实现视频合集页面时长统计
// @author       yky
// @match        https://www.bilibili.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=bilibili.com
// @grant        none
// @license      MIT
// ==/UserScript==

"use strict";

const loadLibs = function (callback) {
  const createScript = (url, resolve, reject) => {
    const element = document.createElement("script");
    element.src = url;
    element.onload = () => {
      console.log(url + "加载完成");
      resolve();
    }; // 当资源加载完成时,解决Promise
    element.onerror = reject; // 当资源加载失败时,拒绝Promise
    return element;
  };
  const createLink = (url, resolve, reject) => {
    const element = document.createElement("link");
    element.rel = "stylesheet";
    element.href = url;
    element.onload = () => {
      console.log(url + "加载完成");
      resolve();
    };
    element.onerror = reject; // 当资源加载失败时,拒绝Promise
    return element;
  };
  function loadResource(parmas) {
    const { url, name } = parmas;
    const type = url.split(".")[[url.split(".").length - 1]];
    return new Promise((resolve, reject) => {
      let element;
      // 创建一个新的<script>或<link>元素
      if (type === "js" || type === "com") {
        if (name) {
          if (!window[name]) {
            element = createScript(url, resolve, reject);
          } else {
            console.log(name + "加载完成");
            resolve();
          }
        } else {
          element = createScript(url, resolve, reject);
        }
      } else {
        element = createLink(url, resolve, reject);
      }

      // 将元素添加到文档中以开始加载
      document.head.appendChild(element);
    });
  }

  // 要加载的资源数组
  const resources = [
    {
      url: "https://cdn.staticfile.net/bootstrap/5.3.2/css/bootstrap.min.css",
    },
    {
      url: "https://getbootstrap.com/docs/5.3/assets/css/docs.css",
    },
    {
      url: "https://cdn.staticfile.net/jquery/3.7.1/jquery.min.js",
      name: "jQuery",
    },
    {
      url: "https://cdn.staticfile.net/lodash.js/4.17.21/lodash.core.min.js",
      name: "_",
    },
    {
      url: "https://cdn.staticfile.net/bootstrap/5.3.2/js/bootstrap.bundle.min.js",
    },
    {
      url: "https://cdn.staticfile.net/interact.js/1.10.26/interact.min.js",
    },
    {
      url: "https://cdn.tailwindcss.com",
    },
  ];

  Promise.all(resources.map((item) => loadResource(item))).then(() => {
    console.log("全部加载完成");
    callback();
  });
};

loadLibs(() => {
  const element = `<div id="card" class="card w-32 fixed bottom-32 right-5 z-50">
      <ul class="list-group list-group-flush">
        <li class="list-group-item">
          <div class="form-check form-switch flex items-center gap-2">
            <input
              class="form-check-input cursor-pointer"
              type="checkbox"
              role="switch"
              id="mainChecked"
              value="false"
            />
            <label class="form-check-label cursor-pointer" for="mainChecked"
              ><svg
                xmlns="http://www.w3.org/2000/svg"
                width="16"
                height="16"
                fill="currentColor"
                class="bi bi-house"
                viewBox="0 0 16 16"
              >
                <path
                  fill-rule="evenodd"
                  d="M8.707 1.5a1 1 0 0 0-1.414 0L.646 8.146a.5.5 0 0 0 .708.708L2 8.207V13.5A1.5 1.5 0 0 0 3.5 15h9a1.5 1.5 0 0 0 1.5-1.5V8.207l.646.647a.5.5 0 0 0 .708-.708L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.707 1.5ZM13 7.207l-5-5-5 5V13.5a.5.5 0 0 0 .5.5h9a.5.5 0 0 0 .5-.5V7.207Z"
                /></svg
            ></label>
          </div>
        </li>
        <li class="list-group-item">
          <div class="form-check form-switch flex items-center gap-2">
            <input
              class="form-check-input cursor-pointer"
              type="checkbox"
              role="switch"
              id="rightChecked"
              value="false"
            />
            <label class="form-check-label cursor-pointer" for="rightChecked"
              ><svg
                xmlns="http://www.w3.org/2000/svg"
                width="16"
                height="16"
                fill="currentColor"
                class="bi bi-blockquote-right"
                viewBox="0 0 16 16"
              >
                <path
                  d="M2.5 3a.5.5 0 0 0 0 1h11a.5.5 0 0 0 0-1h-11zm0 3a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6zm0 3a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6zm0 3a.5.5 0 0 0 0 1h11a.5.5 0 0 0 0-1h-11zm10.113-5.373a6.59 6.59 0 0 0-.445-.275l.21-.352c.122.074.272.17.452.287.18.117.35.26.51.428.156.164.289.351.398.562.11.207.164.438.164.692 0 .36-.072.65-.216.873-.145.219-.385.328-.721.328-.215 0-.383-.07-.504-.211a.697.697 0 0 1-.188-.463c0-.23.07-.404.211-.521.137-.121.326-.182.569-.182h.281a1.686 1.686 0 0 0-.123-.498 1.379 1.379 0 0 0-.252-.37 1.94 1.94 0 0 0-.346-.298zm-2.168 0A6.59 6.59 0 0 0 10 6.352L10.21 6c.122.074.272.17.452.287.18.117.35.26.51.428.156.164.289.351.398.562.11.207.164.438.164.692 0 .36-.072.65-.216.873-.145.219-.385.328-.721.328-.215 0-.383-.07-.504-.211a.697.697 0 0 1-.188-.463c0-.23.07-.404.211-.521.137-.121.327-.182.569-.182h.281a1.749 1.749 0 0 0-.117-.492 1.402 1.402 0 0 0-.258-.375 1.94 1.94 0 0 0-.346-.3z"
                /></svg
            ></label>
          </div>
        </li>
        <li class="list-group-item">
          <div class="flex flex-col" id="collectionTime" class="cursor-pointer">
            <div class="flex gap-2 items-center cursor-pointer">
              <span
                ><svg
                  xmlns="http://www.w3.org/2000/svg"
                  width="16"
                  height="16"
                  fill="currentColor"
                  class="bi bi-watch"
                  viewBox="0 0 16 16"
                >
                  <path
                    d="M8.5 5a.5.5 0 0 0-1 0v2.5H6a.5.5 0 0 0 0 1h2a.5.5 0 0 0 .5-.5V5z"
                  />
                  <path
                    d="M5.667 16C4.747 16 4 15.254 4 14.333v-1.86A5.985 5.985 0 0 1 2 8c0-1.777.772-3.374 2-4.472V1.667C4 .747 4.746 0 5.667 0h4.666C11.253 0 12 .746 12 1.667v1.86a5.99 5.99 0 0 1 1.918 3.48.502.502 0 0 1 .582.493v1a.5.5 0 0 1-.582.493A5.99 5.99 0 0 1 12 12.473v1.86c0 .92-.746 1.667-1.667 1.667H5.667zM13 8A5 5 0 1 0 3 8a5 5 0 0 0 10 0z"
                  /></svg
              ></span>
              <svg
                id="refresh"
                xmlns="http://www.w3.org/2000/svg"
                width="16"
                height="16"
                fill="currentColor"
                class="bi bi-arrow-clockwise"
                viewBox="0 0 16 16"
              >
                <path
                  fill-rule="evenodd"
                  d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"
                />
                <path
                  d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"
                />
              </svg>
            </div>
            <div class="text-[12px] mt-[5px]">
              <div>总时长: <span id="totalTime"></span></div>
              <div>已看: <span id="seen"></span></div>
            </div>
          </div>
        </li>
      </ul>
    </div>`;

  // 控制首页内容区展示
  function mainDisplay(display) {
    localStorage.mainDisplay = display;
    $("main").css("display", display);
    $(".bili-header__channel").css("display", display);
    let count = 0;
    let interval = setInterval(() => {
      count += 1;
      if (count == 5) {
        clearInterval(interval);
        interval = null;
        return;
      }
      $(".header-channel").css("display", display);
    }, 1000);
  }

  function calculateTotalTime(timeStrings) {
    let totalSeconds = 0;

    timeStrings.forEach((timeStr) => {
      // 检查时间字符串的格式并分割
      let parts = timeStr.split(":");
      let hours = 0,
        minutes = 0,
        seconds = 0;

      if (parts.length === 3) {
        // hh:mm:ss 格式
        hours = parseInt(parts[0], 10);
        minutes = parseInt(parts[1], 10);
        seconds = parseInt(parts[2], 10);
      } else if (parts.length === 2) {
        // mm:ss 格式
        minutes = parseInt(parts[0], 10);
        seconds = parseInt(parts[1], 10);
      } else if (parts.length === 1) {
        // ss 格式
        seconds = parseInt(parts[0], 10);
      } else {
        throw new Error("无效的时间格式");
      }

      // 将时间转换为秒并累加
      totalSeconds += hours * 3600 + minutes * 60 + seconds;
    });

    // 转换总秒数回 hh:mm:ss 格式
    let totalHrs = Math.floor(totalSeconds / 3600);
    totalSeconds %= 3600;
    let totalMins = Math.floor(totalSeconds / 60);
    let totalSecs = totalSeconds % 60;

    return {
      totalSeconds: totalSeconds,
      formattedTime: `${totalHrs.toString().padStart(2, "0")}:${totalMins
        .toString()
        .padStart(2, "0")}:${totalSecs.toString().padStart(2, "0")}`,
    };
  }
  function calculateTime() {
    var multi_page = document.querySelector(".video-pod__list");
    var sections_page = document.querySelector(".video-sections-content-list");
    if (multi_page || sections_page) {
      var durations = Array.from(
        (multi_page || sections_page).querySelectorAll(
          multi_page ? ".duration" : ".video-episode-card__info-duration"
        ) || []
      ).map((item) => item.innerText);
      var totalTime = "";
      totalTime = calculateTotalTime(durations);
      $("#totalTime").text(totalTime.formattedTime);
      var seenTime = "";
      var index = Array.from(
        document
          .querySelector(
            multi_page ? ".video-pod__list" : ".video-section-list"
          )
          .querySelectorAll(
            multi_page ? ".simple-base-item" : ".video-episode-card__info"
          ) || []
      ).findIndex((item) =>
        item.classList.contains(
          multi_page ? "active" : "video-episode-card__info-playing"
        )
      );
      var array = durations.slice(0, index + 1);
      seenTime = calculateTotalTime(array);
      console.log("已看时长", seenTime);
      $("#seen").text(seenTime.formattedTime);
    }
  }

  function showRecoList(val) {
    const reco_list = $(".rec-list");
    if (reco_list) {
      if (val) {
        reco_list.css({ display: "block" });
      } else {
        reco_list.css({ display: "none" });
      }
    }
  }

  $("body").append(element);
  window.addEventListener("load", function () {
    (function () {
      $("#mainChecked").prop(
        "checked",
        localStorage.mainDisplay === "block" ? true : false
      );
      mainDisplay(localStorage.mainDisplay || "none");
      showRecoList(false);
      calculateTime();
      $("#mainChecked").change(function (e) {
        e.stopPropagation();
        console.log("$(this).prop", $(this).prop("checked"));
        if ($(this).prop("checked")) {
          mainDisplay("block");
        } else {
          mainDisplay("none");
        }
      });
      $("#rightChecked").change(function (e) {
        e.stopPropagation();
        showRecoList($(this).prop("checked"));
      });
      $("#refresh").on("click", function (e) {
        e.stopPropagation();
        calculateTime();
      });
    })();
    (function dragFn() {
      const position = {
        x: +localStorage.positionX || 0,
        y: +localStorage.positionY || 0,
      };
      if (localStorage.positionX && localStorage.positionY) {
        $("#card").css({
          transform: `translate(${position.x}px, ${position.y}px)`,
        });
      }
      interact("#card").draggable({
        listeners: {
          start(event) {
            console.log("e", event.type);
            console.log(event.type, event.target);
          },
          move(event) {
            position.x += event.dx;
            position.y += event.dy;
            localStorage.setItem("positionX", Math.round(position.x, 2));
            localStorage.setItem("positionY", Math.round(position.y, 2));

            event.target.style.transform = `translate(${position.x}px, ${position.y}px)`;
          },
        },
      });
    })();
    (() => {
      // --------
      // Tooltips
      // --------
      // Instantiate all tooltips in a docs or StackBlitz page
      document
        .querySelectorAll('[data-bs-toggle="tooltip"]')
        .forEach((tooltip) => {
          new bootstrap.Tooltip(tooltip);
        });

      // --------
      // Popovers
      // --------
      // Instantiate all popovers in a docs or StackBlitz page
      document
        .querySelectorAll('[data-bs-toggle="popover"]')
        .forEach((popover) => {
          new bootstrap.Popover(popover);
        });

      // -------------------------------
      // Toasts
      // -------------------------------
      // Used by 'Placement' example in docs or StackBlitz
      const toastPlacement = document.getElementById("toastPlacement");
      if (toastPlacement) {
        document
          .getElementById("selectToastPlacement")
          .addEventListener("change", function () {
            if (!toastPlacement.dataset.originalClass) {
              toastPlacement.dataset.originalClass = toastPlacement.className;
            }

            toastPlacement.className = `${toastPlacement.dataset.originalClass} ${this.value}`;
          });
      }

      // Instantiate all toasts in a docs page only
      document.querySelectorAll(".bd-example .toast").forEach((toastNode) => {
        const toast = new bootstrap.Toast(toastNode, {
          autohide: false,
        });

        toast.show();
      });

      // Instantiate all toasts in a docs page only
      const toastTrigger = document.getElementById("liveToastBtn");
      const toastLiveExample = document.getElementById("liveToast");
      if (toastTrigger) {
        toastTrigger.addEventListener("click", () => {
          const toast = new bootstrap.Toast(toastLiveExample);

          toast.show();
        });
      }

      // -------------------------------
      // Alerts
      // -------------------------------
      // Used in 'Show live toast' example in docs or StackBlitz
      const alertPlaceholder = document.getElementById("liveAlertPlaceholder");
      const alertTrigger = document.getElementById("liveAlertBtn");

      const appendAlert = (message, type) => {
        const wrapper = document.createElement("div");
        wrapper.innerHTML = [
          `<div class="alert alert-${type} alert-dismissible" role="alert">`,
          `   <div>${message}</div>`,
          '   <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
          "</div>",
        ].join("");

        alertPlaceholder.append(wrapper);
      };

      if (alertTrigger) {
        alertTrigger.addEventListener("click", () => {
          appendAlert("Nice, you triggered this alert message!", "success");
        });
      }

      // --------
      // Carousels
      // --------
      // Instantiate all non-autoplaying carousels in a docs or StackBlitz page
      document
        .querySelectorAll('.carousel:not([data-bs-ride="carousel"])')
        .forEach((carousel) => {
          bootstrap.Carousel.getOrCreateInstance(carousel);
        });

      // -------------------------------
      // Checks & Radios
      // -------------------------------
      // Indeterminate checkbox example in docs and StackBlitz
      document
        .querySelectorAll('.bd-example-indeterminate [type="checkbox"]')
        .forEach((checkbox) => {
          if (checkbox.id.includes("Indeterminate")) {
            checkbox.indeterminate = true;
          }
        });

      // -------------------------------
      // Links
      // -------------------------------
      // Disable empty links in docs examples only
      document.querySelectorAll('.bd-content [href="#"]').forEach((link) => {
        link.addEventListener("click", (event) => {
          event.preventDefault();
        });
      });

      // -------------------------------
      // Modal
      // -------------------------------
      // Modal 'Varying modal content' example in docs and StackBlitz
      const exampleModal = document.getElementById("exampleModal");
      if (exampleModal) {
        exampleModal.addEventListener("show.bs.modal", (event) => {
          // Button that triggered the modal
          const button = event.relatedTarget;
          // Extract info from data-bs-* attributes
          const recipient = button.getAttribute("data-bs-whatever");

          // Update the modal's content.
          const modalTitle = exampleModal.querySelector(".modal-title");
          const modalBodyInput =
            exampleModal.querySelector(".modal-body input");

          modalTitle.textContent = `New message to ${recipient}`;
          modalBodyInput.value = recipient;
        });
      }

      // -------------------------------
      // Offcanvas
      // -------------------------------
      // 'Offcanvas components' example in docs only
      const myOffcanvas = document.querySelectorAll(
        ".bd-example-offcanvas .offcanvas"
      );
      if (myOffcanvas) {
        myOffcanvas.forEach((offcanvas) => {
          offcanvas.addEventListener(
            "show.bs.offcanvas",
            (event) => {
              event.preventDefault();
            },
            false
          );
        });
      }
    })();
  });
});