Greasy Fork is available in English.

B站 Antfu

B站首页显示Antfu专区!

// ==UserScript==
// @name         B站 Antfu
// @namespace    https://github.com/lilei2603/bilibili-follow
// @version      2.1
// @description  B站首页显示Antfu专区!
// @author       Lei
// @match        https://www.bilibili.com/*
// @icon         https://avatars.githubusercontent.com/u/11247099?v=4
// @grant        GM_addStyle
// @license      MIT

// ==/UserScript==
; (function () {
  'use strict'

  // 用户列表
  let USERS = []
  // 当前用户索引
  let currentUserIndex = 0

  let currentPage = 1
  let page = 0
  // 视频列表
  let videoList = []
  // 是否显示我的关注窗口
  let isShowFavorite = false
  // 窗口滚动距离
  let scrollTop = 0
  let totalCount = 0
  const API = {
    // 获取用户视频列表
    getNewVideo: async () => {
      let res = await fetch(
        `https://api.bilibili.com/x/space/arc/search?mid=${USERS[currentUserIndex].mid}&ps=30&tid=0&pn=${currentPage}&order=pubdate&jsonp=jsonp`
      )
      const json = await res.json()
      totalCount = json.data.page.count
      videoList = videoList.concat(json.data.list.vlist)
    },
    // 根据用户ID获取用户信息
    getUserInfoByMid: async (mid) => {
      let res = await fetch(`https://api.bilibili.com/x/space/acc/info?mid=${mid}&jsonp=jsonp`)
      const json = await res.json()
      return json
    }
  }
  // 用户初始化
  function initUser() {
    // 从缓存中获取用户列表
    if(!localStorage.getItem('favorites')) {
      USERS = [
        {
          key_words: 'AnthonyFu一个托尼',
          mid: 668380,
          avatar: 'http://i1.hdslb.com/bfs/face/519cb17285e6b9450a738472cb0b95aeb8676547.jpg',
        }
      ]
      localStorage.setItem('favorites', JSON.stringify(USERS))
    }else {
      USERS = JSON.parse(localStorage.getItem('favorites'))
    }
  }
  // 播放量格式化
  function bigNumber(num) {
    return num > 10000 ? `${(num / 10000).toFixed(2)}万` : num
  }

  function s2d(string) {
    return new DOMParser().parseFromString(string, 'text/html').body
      .childNodes[0]
  }
  // 播放时长格式化
  function timeFormat(time) {
    let res = []
    let [s = 0, m = 0] = time.split(':').reverse()
    res.unshift(String(s).padStart(2, '0'))
    res.unshift(String(m % 60).padStart(2, '0'))
    res.unshift(String(parseInt(m / 60)).padStart(2, '0'))

    return res.join(':')
  }
  // 日期时间格式化
  function timeago(dateTimeStamp) {
    const minute = 60;      //把分,时,天,周,半个月,一个月用毫秒表示
    const hour = minute * 60;
    const day = hour * 24;
    // const week = day * 7;
    const month = day * 30;
    const now = parseInt(new Date().getTime() / 1000);   //获取当前时间毫秒
    const diffValue = now - dateTimeStamp;//时间差
    let result = ''
    if (diffValue < 0) {
      return;
    }
    const minC = diffValue / minute;  //计算时间差的分,时,天,周,月
    const hourC = diffValue / hour;
    const dayC = diffValue / day;
    const monthC = diffValue / month;
    const datetime = new Date();
    datetime.setTime(dateTimeStamp * 1000);
    const Nyear = datetime.getFullYear();
    const Nmonth = datetime.getMonth() + 1;
    const Ndate = datetime.getDate();
    if (dayC >= 1 && dayC < 2) {
      result = '昨天'
    } else if (hourC >= 1 && hourC <= 23) {
      result = ' ' + parseInt(hourC) + '小时前'
    } else if (minC >= 1 && minC <= 59) {
      result = ' ' + parseInt(minC) + '分钟前'
    } else if (diffValue >= 0 && diffValue <= minute) {
      result = '刚刚'
    } else if (monthC < new Date().getMonth() + 1) {
      result = Nmonth + '-' + Ndate
    } else {
      result = Nyear + '-' + Nmonth + '-' + Ndate
    }
    return result;
  }
  // 换一换
  async function refresh() {
    page++
    if(videoList.length < totalCount){
      currentPage++
      await API.getNewVideo()
    }else {
      if(videoList.length % totalCount == 0){
        currentPage = 1
      }else{
        currentPage++
      }
      await API.getNewVideo()
    }
    drawVideos()
  }
  // 绘制视频
  function drawVideos() {
    const VIDEO_DOM = document.querySelector('#bili_custom .variety-body')
    VIDEO_DOM.innerHTML = ''

    videoList
      .slice(page * 10, page * 10 + 14)
      .forEach((item) => {
        const title = item.title.replace(/<em class="keyword">(.*?)<\/em>/g, '$1')
        const pic = item.pic.replace(/http/g, 'https') + '@672w_378h_1c'
        const webp = pic + '.webp'
        let DOM = s2d(`
        <div class="bili-video-card" data-report="partition_recommend.content">
  <div class="bili-video-card__skeleton hide">
    <div class="bili-video-card__skeleton--cover"></div>
    <div class="bili-video-card__skeleton--info">
      <div class="bili-video-card__skeleton--right">
        <p class="bili-video-card__skeleton--text"></p>
        <p class="bili-video-card__skeleton--text short"></p>
        <p class="bili-video-card__skeleton--light"></p>
      </div>
    </div>
  </div>
  <div class="bili-video-card__wrap __scale-wrap"><a href="//www.bilibili.com/video/${item.bvid}" target="_blank"
      data-mod="partition_recommend" data-idx="content" data-ext="click">
      <div class="bili-video-card__image __scale-player-wrap">
        <div class="bili-video-card__image--wrap">
          <div class="bili-watch-later" style="display: none;"><svg class="bili-watch-later__icon">
              <use xlink:href="#widget-watch-later"></use>
            </svg><span class="bili-watch-later__tip" style="display: none;"></span></div>
          <picture class="v-img bili-video-card__cover">
            <!---->
            <source srcset="${webp}"
              type="image/webp"><img
              src="${pic}"
              alt="${title}" loading="lazy" onload="">
          </picture>
          <div class="v-inline-player"></div>
        </div>
        <div class="bili-video-card__mask">
          <div class="bili-video-card__stats">
            <div class="bili-video-card__stats--left"><span class="bili-video-card__stats--item"><svg
                  class="bili-video-card__stats--icon">
                  <use xlink:href="#widget-video-play-count"></use>
                </svg><span class="bili-video-card__stats--text">${bigNumber(item.play)}</span></span><span
                class="bili-video-card__stats--item"><svg class="bili-video-card__stats--icon">
                  <use xlink:href="#widget-video-danmaku"></use>
                </svg><span class="bili-video-card__stats--text">${item.comment}</span></span></div><span
              class="bili-video-card__stats__duration">${timeFormat(item.length)}</span>
          </div>
        </div>
      </div>
    </a>
    <div class="bili-video-card__info __scale-disable">
      <div class="bili-video-card__info--right">
        <h3 class="bili-video-card__info--tit" title="${title}"><a href="//www.bilibili.com/video/${item.bvid}"
            target="_blank" data-mod="partition_recommend" data-idx="content" data-ext="click">${title}</a></h3>
        <div class="bili-video-card__info--bottom">
          <a class="bili-video-card__info--owner" href="//space.bilibili.com/${item.mid}" target="_blank"
            data-mod="partition_recommend" data-idx="content" data-ext="click"><svg
              class="bili-video-card__info--owner__up">
              <use xlink:href="#widget-up"></use>
            </svg><span class="bili-video-card__info--author">${item.author}</span><span
            class="bili-video-card__info--date">· ${timeago(item.created)}</span></a>
        </div>
      </div>
    </div>
  </div>
</div>
`)
        VIDEO_DOM.append(DOM)
      })
  }
  // 绘制我的关注按钮
  function drawFavorites() {

    const refreshBtn = document.querySelector('.custom-refresh')
    const refreshBtnParent = refreshBtn.parentNode
    const favorite = s2d(`
    <button class="primary-btn roll-btn favorite-btn" id="favorite-btn">
      <svg style="transform: rotate(180deg);">
        <use xlink:href="#widget-arrow"></use>
      </svg>
      <span>我的关注</span>
    </button>
  `)
  refreshBtnParent.insertBefore(favorite, refreshBtn)
  const modal = s2d(`
  <div class="custom-modal">
  <div class="modal-mask"></div>
  <div class="modal-wrap">
    <div class="modal" role="document">
      <div tabindex="0" aria-hidden="true" style="width: 0px; height: 0px; overflow: hidden; outline: none;"></div>
      <div class="modal-content"><button type="button" aria-label="Close" class="modal-close"><span
            class="modal-close-x"><span role="img" aria-label="close"
              class="anticon anticon-close modal-close-icon"><svg focusable="false" class="" data-icon="close"
                width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896">
                <path
                  d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z">
                </path>
              </svg></span></span></button>
        <div class="modal-header">
          <div class="modal-title">我的关注</div>
        </div>
        <div class="modal-body"></div>
        <div class="modal-footer">
          <div>
            <span>用户ID:</span>
            <input id="mid" style="height: 25px; padding: 5px;" />
          </div>
          <button class="btn btn-primary" type="button">
            <span>添 加</span>
          </button>
        </div>
      </div>
      <div tabindex="0" aria-hidden="true" style="width: 0px; height: 0px; overflow: hidden; outline: none;"></div>
    </div>
  </div>
</div>
    `)
    document.querySelector('#bili_custom').append(modal)
    appendFavoriteItem()
  }
  // 添加关注用户项
  function appendFavoriteItem() {
    document.querySelector('.modal-body').innerHTML = ''
    USERS.forEach((item, index) => {
      const DOM = s2d(`
      <div class="modal-content-item">
        <img src="${item.avatar}" style="width: 34px; height: 34px; border-radius: 17px; margin-right: 12px;">
        <span>${item.key_words}</span>
      </div>
      `)
      document.querySelector('.modal-body').append(DOM)
      DOM.addEventListener('click', () => {
        currentUserIndex = index
        showFavorite()
        injectDOM()
      })
    })
  }
  // 操作我的关注弹窗
  function showFavorite() {
    isShowFavorite = !isShowFavorite
    // 获取点击按钮时,当前窗口滚动距离
    if(window.pageYOffset != 0) {
      scrollTop = window.pageYOffset
    }
    // 如果显示弹窗,则禁用页面滚动
    document.body.style.position = isShowFavorite ? 'fixed' : 'static'
    // 设置页面滚动高度
    document.body.style.top = isShowFavorite ? `-${scrollTop}px` : '0';
    window.scrollTo(0, scrollTop)

    document.querySelector('.custom-modal').style.display = isShowFavorite ? 'block' : 'none'
    appendFavoriteItem()
  }
  // 添加用户信息
  async function appendFavorite() {
    const mid = document.querySelector('#mid')
    const isUserExist = USERS.findIndex(item => item.mid == mid)
    if(isUserExist == -1) {
      const userInfo = await API.getUserInfoByMid(mid.value)
      if(userInfo.code === 0) {
        USERS.push({
          mid: mid.value,
          key_words: userInfo.data.name,
          avatar: userInfo.data.face
        })
      }else{
        alert('获取用户信息失败')
      }
      localStorage.setItem('favorites', JSON.stringify(USERS))
      getMyFavorite()
      mid.value = ''
    }else{
      alert('用户已存在')
    }
  }
  // 获取用户信息
  function getMyFavorite() {
    const favorites = localStorage.getItem('favorites')
    if(favorites) {
      USERS = JSON.parse(favorites)
    }
    appendFavoriteItem()
  }
  // 关注信息
  async function useLive() {
    const { data } = await API.getUserInfoByMid(USERS[currentUserIndex].mid);
    return {
        liveRoomUrl: data.live_room.url,
        isLive: data.live_room.liveStatus === 1
    }
  }
  // 包装根据bool切换值
  function wrapperSwitchValue(flag) {
    return (t, f) => flag ? t : f
  }
  // 初始化容器
  async function injectDOM() {
    // currentUserIndex = mid ? USERS.findIndex(item => item.mid == mid) : 0
    videoList = []
    currentPage = 1
    page = 0
    const spaceVideoUrl = `https://space.bilibili.com/${USERS[currentUserIndex].mid}/video`
    const { liveRoomUrl, isLive } = await useLive();
    const switchLiving = wrapperSwitchValue(isLive);

    const DOM = `
    <div id="bili_custom">
      <section class="bili-grid">
        <div class="variety-area">
          <div class="area-header">
            <div class="left">
              <div class="avatar-container">
                <a href="${switchLiving(liveRoomUrl,spaceVideoUrl)}" target="_blank" class="space-user-avatar">
                <div class="avatar-wrap ${switchLiving('live-ani','')}">
                  <div>
                    <div class="bili-avatar" style="width: 34px;height:34px;">
                      <img class="bili-avatar-img bili-avatar-face bili-avatar-img-radius"
                        data-src="${USERS[currentUserIndex].avatar}"
                        alt=""
                        src="${USERS[currentUserIndex].avatar}">
                      <span class="bili-avatar-icon bili-avatar-right-icon  bili-avatar-size-60"></span>
                    </div>
                  </div>
                  <div class="a-cycle a-cycle-1"></div>
                  <div class="a-cycle a-cycle-2"></div>
                  <div class="a-cycle a-cycle-3"></div>
                </div>
                <div class="live-tab" style="${switchLiving('', 'display:none;')}"><img style="width: 15px;height: 15px;"
                    src="//s1.hdslb.com/bfs/static/jinkela/space/assets/live.gif" alt="live" class="live-gif">
                </div>
                </a>
            </div>
                <a class="title" href="https://space.bilibili.com/${USERS[currentUserIndex].mid}/video" target="_blank">
                  <span>${USERS[currentUserIndex].key_words}</span>
                </a>
            </div>
            <div class="right">
              <button class="primary-btn roll-btn custom-refresh">
                <svg style="transform: rotate(0deg);">
                  <use xlink:href="#widget-roll"></use>
                </svg>
                <span>换一换</span>
              </button>
              <a class="primary-btn see-more" href="https://space.bilibili.com/${USERS[currentUserIndex].mid}/video" target="_blank">
                <span>查看更多</span>
                <svg>
                  <use xlink:href="#widget-arrow"></use>
                </svg>
              </a>
            </div>
          </div>
          <div class="variety-body"></div>
        </div>
      </section>
    </div>`
    let content = document.querySelector('.bili-layout')
    if (document.querySelector('#bili_custom')) {
      content.removeChild(document.querySelector('#bili_custom'))
    }
    let anchor = document.querySelectorAll('.bili-grid')[2]
    let init = s2d(DOM)
    document.querySelector('#bili_custom') &&
    document.querySelector('#bili_custom').remove()
    // 插入初始模版
    console.log(anchor)
    content.insertBefore(init, anchor)
    // 插入关注UP主按钮
    drawFavorites()
    // 插入最新视频
    await API.getNewVideo()
    drawVideos()
    // 点击事件
    document.querySelector('.custom-refresh').addEventListener('click', refresh)
    document.querySelector('.favorite-btn').addEventListener('click', showFavorite)
    document.querySelector('.anticon').addEventListener('click', showFavorite)
    document.querySelector('.btn-primary').addEventListener('click', appendFavorite)
  }

  window.addEventListener(
    'load',
    async () => {
      initUser()
      await injectDOM()
    },
    false,
  )

  GM_addStyle(`

  .space-user-avatar {
    width: 34px;
    min-width: 34px;
  }
  .space-user-avatar .avatar-wrap {
    position: relative;
    width: 100%;
    height: 34px;
  }
  .space-user-avatar .avatar-wrap.live-ani .a-cycle {
      position: absolute;
      left: 50%;
      top: 50%;
      transform: translate(-50%,-50%);
      width: 34px;
      height: 34px;
      border: 1px solid #f69;
      border-radius: 50%;
      z-index: 1;
      opacity: 0;
      animation: scaleUpCircle 1.5s linear;
      animation-iteration-count: infinite;
  }
  @keyframes scaleUpCircle{
    0%{
      transform:translate(-50%,-50%) scale(1);
      opacity:1
    }
    to
    {
    transform:translate(-50%,-50%) scale(1.5);
    opacity:0
    }
  }
  .space-user-avatar .avatar-wrap.live-ani .a-cycle-1 {
      animation-delay: 0s;
  }
  .space-user-avatar .avatar-wrap.live-ani .a-cycle-2 {
      animation-delay: .5s;
  }
  .space-user-avatar .avatar-wrap.live-ani .a-cycle-3 {
      animation-delay: 1s;
  }
  .avatar-container {
      position: relative;
      margin-right:15px;
  }
  .space-user-avatar .live-tab {
    position: absolute;
    left: 50%;
    bottom: 0;
    transform: translate(-50%,50%);
    height: 15px;
    white-space: nowrap;
    display: -ms-flexbox;
    display: flex;
    -ms-flex-align: center;
    align-items: center;
    -ms-flex-pack: center;
    justify-content: center;
    background: #f69;
    color: #fff;
    border-radius: 8px;
    border: 1.5px solid #fff;
    padding: 5px 10px;
    font-size: 12px;
    z-index: 2;
}


    .favorite-btn {
      background-color: #fb7299!important;
      color: #ffffff!important;
    }
    .favorite-btn:hover {
      background-color: #fc8bab!important;
    }
    #bili_custom {
      position: relative;
    }
    .custom-modal {

      display: none;
    }
    .modal-mask {
      position: fixed;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      z-index: 1000;
      height: 100%;
      background-color: #00000073;
    }
    .modal-wrap {
      position: fixed;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      overflow: auto;
      outline: 0;
      z-index: 1000;
    }
    .modal {
      box-sizing: border-box;
      padding: 0 0 24px;
      color: #000000d9;
      font-size: 14px;
      font-variant: tabular-nums;
      line-height: 1.5715;
      list-style: none;
      font-feature-settings: "tnum";
      pointer-events: none;
      position: relative;
      top: 100px;
      width: 500px;
      height: 500px;
      margin: 0 auto;
      transform-origin: 9px 204px;
  }
  .modal-content {
      position: relative;
      background-color: #fff;
      background-clip: padding-box;
      border: 0;
      border-radius: 2px;
      box-shadow: 0 3px 6px -4px #0000001f, 0 6px 16px #00000014, 0 9px 28px 8px #0000000d;
      pointer-events: auto;
  }
  .modal-close {
      position: absolute;
      top: 0;
      right: 0;
      z-index: 10;
      padding: 0;
      color: #00000073;
      font-weight: 700;
      line-height: 1;
      text-decoration: none;
      background: transparent;
      border: 0;
      outline: 0;
      cursor: pointer;
      transition: color .3s;
  }
  .modal-close-x {
      display: block;
      width: 56px;
      height: 56px;
      font-size: 16px;
      font-style: normal;
      line-height: 56px;
      text-align: center;
      text-transform: none;
      text-rendering: auto;
  }
  .anticon {
      display: inline-block;
      color: inherit;
      font-style: normal;
      line-height: 0;
      text-align: center;
      text-transform: none;
      vertical-align: -0.125em;
      text-rendering: optimizelegibility;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
  }
  .anticon:hover {
    color: #fb7299;
  }
  .modal-header {
      padding: 16px 24px;
      color: #000000d9;
      background: #fff;
      border-bottom: 1px solid #f0f0f0;
      border-radius: 2px 2px 0 0;
  }
  .modal-title {
      margin: 0;
      color: #000000d9;
      font-weight: 500;
      font-size: 16px;
      line-height: 22px;
      word-wrap: break-word;
  }
  .modal-body {
      padding: 24px;
      font-size: 14px;
      line-height: 1.5715;
      word-wrap: break-word;
  }
  .modal-footer {
      padding: 10px 16px;
      text-align: right;
      background: transparent;
      border-top: 1px solid #f0f0f0;
      border-radius: 0 0 2px 2px;
      display: flex;
      justify-content: space-between;
      align-items: center;
  }
  .btn {
      line-height: 1.5715;
      position: relative;
      display: inline-block;
      font-weight: 400;
      white-space: nowrap;
      text-align: center;
      background-image: none;
      border: 1px solid transparent;
      box-shadow: 0 2px #00000004;
      cursor: pointer;
      transition: all .3s cubic-bezier(.645,.045,.355,1);
      -webkit-user-select: none;
      -moz-user-select: none;
      user-select: none;
      touch-action: manipulation;
      height: 32px;
      padding: 4px 15px;
      font-size: 14px;
      border-radius: 2px;
      color: #000000d9;
      border-color: #d9d9d9;
      background: #fff;
      outline: 0;
  }
  .btn:hover {
    border-color: #e3e5e7!important;
    background-color: #e3e5e7!important;
  }
  .btn-primary {
      margin-left: 20px;
      color: #fff;
      border-color: #fb7299;
      background: #fb7299!important;
      text-shadow: 0 -1px 0 rgb(0 0 0 / 12%);
      box-shadow: 0 2px #0000000b;
  }
  .btn-primary:hover {
    border-color: #fc8bab!important;
    background-color: #fc8bab!important;
  }
  .modal-body {
    display: flex;
    flex-wrap: wrap;
    align-content: start;
    justify-content: space-between;
    height: 300px;
    overflow-y: auto;
  }
  .modal-content-item {
    cursor: pointer;
    box-shadow: 0px 1px 4px 0px rgba(0, 0, 0, 0.16);
    padding: 0 12px;
    width: 45%;
    height: 60px;
    margin-bottom: 12px;
    display: flex;
    align-items: center;
  }
  .modal-content-item:hover {
    background-color: #efefef;
  }
  `)
})()