mirror of
https://github.com/the1812/Bilibili-Evolved.git
synced 2025-11-04 21:22:45 +08:00
192 lines
5.7 KiB
TypeScript
192 lines
5.7 KiB
TypeScript
import { defineComponentMetadata } from '@/components/define'
|
||
import { useScopedConsole } from '@/core/utils/log'
|
||
import { bilibiliApi, getJsonWithCredentials } from '@/core/ajax'
|
||
import { select } from '@/core/spin-query'
|
||
import { allMutations } from '@/core/observer'
|
||
import { getUserInfo } from '@/core/user-info'
|
||
|
||
const displayName = '关注时间显示'
|
||
const console = useScopedConsole(displayName)
|
||
|
||
const SELECTORS = {
|
||
profileFollowContainer: '.space-head-follow.b-follow',
|
||
profileTextClass: 'subscribe-time-text',
|
||
relationCardAnchor: '.relation-card-info__uname',
|
||
relationCardContainer: '.relation-card-info',
|
||
relationCardTimeClass: 'relation-card-info__time',
|
||
relationCardSign: '.relation-card-info__sign',
|
||
}
|
||
|
||
type FollowTimeInfo = {
|
||
mtime: number
|
||
label: string
|
||
}
|
||
|
||
const midMap: Record<number, FollowTimeInfo> = {}
|
||
|
||
const insertFollowTimeToCard = (mid: number, dateStr: string, label: string) => {
|
||
const anchor = dq(`${SELECTORS.relationCardAnchor}[href*="/${mid}"]`)
|
||
const card = anchor?.closest(SELECTORS.relationCardContainer)
|
||
if (!card || card.querySelector(`.${SELECTORS.relationCardTimeClass}`)) {
|
||
return
|
||
}
|
||
|
||
const div = document.createElement('div')
|
||
div.className = SELECTORS.relationCardTimeClass
|
||
div.textContent = `${label}:${dateStr}`
|
||
div.dataset.mid = mid.toString()
|
||
|
||
const sign = card.querySelector(SELECTORS.relationCardSign)
|
||
if (sign?.parentNode) {
|
||
sign.parentNode.insertBefore(div, sign.nextSibling)
|
||
}
|
||
}
|
||
|
||
const updateCards = (() => {
|
||
let scheduled = false
|
||
return () => {
|
||
if (scheduled) {
|
||
return
|
||
}
|
||
scheduled = true
|
||
requestAnimationFrame(() => {
|
||
scheduled = false
|
||
dqa(SELECTORS.relationCardAnchor).forEach(el => {
|
||
const match = (el as HTMLAnchorElement).href.match(/\/(\d+)/)
|
||
if (!match) {
|
||
return
|
||
}
|
||
const mid = Number(match[1])
|
||
if (Number.isNaN(mid)) {
|
||
return
|
||
}
|
||
const data = midMap[mid]
|
||
if (data) {
|
||
const dateStr = new Date(data.mtime * 1000).toLocaleString()
|
||
insertFollowTimeToCard(mid, dateStr, data.label)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
})()
|
||
|
||
const insertSubscribeTime = (followTimeStr: string) => {
|
||
const container = dq(SELECTORS.profileFollowContainer)
|
||
if (!container || container.querySelector(`.${SELECTORS.profileTextClass}`)) {
|
||
return
|
||
}
|
||
|
||
if (getComputedStyle(container).position === 'static') {
|
||
;(container as HTMLElement).style.position = 'relative'
|
||
}
|
||
|
||
const infoEl = document.createElement('div')
|
||
infoEl.className = SELECTORS.profileTextClass
|
||
infoEl.textContent = `关注于 ${followTimeStr}`
|
||
container.appendChild(infoEl)
|
||
}
|
||
|
||
if (!(unsafeWindow as any).subscribeTimeHooked) {
|
||
;(unsafeWindow as any).subscribeTimeHooked = true
|
||
const originalFetch = unsafeWindow.fetch
|
||
unsafeWindow.fetch = new Proxy(originalFetch, {
|
||
apply(target, thisArg, args) {
|
||
const url = typeof args[0] === 'string' ? args[0] : args[0].url
|
||
const urlObj = new URL(url, location.origin)
|
||
|
||
if (
|
||
urlObj.hostname === 'api.bilibili.com' &&
|
||
(urlObj.pathname.includes('/x/relation/fans') ||
|
||
urlObj.pathname.includes('/x/relation/followings'))
|
||
) {
|
||
return target
|
||
.apply(thisArg, args)
|
||
.then(res => {
|
||
res
|
||
.clone()
|
||
.json()
|
||
.then(json => {
|
||
const list = json?.data?.list
|
||
if (!Array.isArray(list)) {
|
||
console.warn('接口数据结构异常:', json)
|
||
return
|
||
}
|
||
list.forEach(user => {
|
||
if (typeof user.mid === 'number' && typeof user.mtime === 'number') {
|
||
const label = url.includes('/fans') ? 'Ta 关注你的时间' : '你关注 Ta 的时间'
|
||
midMap[user.mid] = { mtime: user.mtime, label }
|
||
const dateStr = new Date(user.mtime * 1000).toLocaleString()
|
||
insertFollowTimeToCard(user.mid, dateStr, label)
|
||
}
|
||
})
|
||
})
|
||
.catch(err => {
|
||
console.warn('JSON 解析失败:', err)
|
||
})
|
||
return res
|
||
})
|
||
.catch(err => {
|
||
console.warn('fetch 请求失败:', err)
|
||
throw err
|
||
})
|
||
}
|
||
|
||
return target.apply(thisArg, args)
|
||
},
|
||
})
|
||
}
|
||
|
||
const entry = async () => {
|
||
try {
|
||
const { addImportantStyle } = await import('@/core/style')
|
||
const { default: style } = await import('./subscribe-time.scss')
|
||
addImportantStyle(style, 'subscribe-time-style')
|
||
} catch (e) {
|
||
console.error('样式加载失败:', e)
|
||
}
|
||
|
||
allMutations(updateCards)
|
||
|
||
const userMid = (await getUserInfo()).mid
|
||
const match = location.href.match(/space\.bilibili\.com\/(\d+)/)
|
||
if (!match) {
|
||
return
|
||
}
|
||
|
||
const mid = Number(match[1])
|
||
if (mid === userMid) {
|
||
console.log('当前为本人空间,跳过关注时间显示')
|
||
return
|
||
}
|
||
|
||
const info = await bilibiliApi(
|
||
getJsonWithCredentials(`https://api.bilibili.com/x/web-interface/relation?mid=${mid}`),
|
||
)
|
||
|
||
const mtime = info?.relation?.mtime
|
||
if (!mtime) {
|
||
return
|
||
}
|
||
|
||
await select(SELECTORS.profileFollowContainer)
|
||
insertSubscribeTime(new Date(mtime * 1000).toLocaleString())
|
||
}
|
||
|
||
export const component = defineComponentMetadata({
|
||
name: 'subscribeTimeShow',
|
||
displayName: '关注时间显示',
|
||
author: {
|
||
name: 'CNOCM',
|
||
link: 'https://github.com/CNOCM',
|
||
},
|
||
tags: [componentsTags.utils],
|
||
urlInclude: [
|
||
/^https:\/\/space\.bilibili\.com\/\d+\/(relation|fans)\/(fans|follow)/,
|
||
/https:\/\/space\.bilibili\.com\/\d+/,
|
||
],
|
||
entry,
|
||
description: {
|
||
'zh-CN': '在粉丝/关注列表及用户主页显示关注的具体时间。',
|
||
},
|
||
})
|