// ==UserScript==
// @name bilibili订阅+
// @namespace https://github.com/YanxinTang/Tampermonkey
// @version 0.7.9
// @description bilibili导航添加订阅按钮以及订阅列表
// @author tyx1703
// @license MIT
// @noframes
// @require https://cdn.jsdelivr.net/npm/vue@2/dist/vue.min.js
// @match *.bilibili.com/*
// @exclude *://live.bilibili.com/*
// @exclude *://manga.bilibili.com/*
// @exclude *://bw.bilibili.com/*
// @exclude *://show.bilibili.com/*
// ==/UserScript==
(async function () {
const DedeUserID = getCookie("DedeUserID");
const loginStatus = DedeUserID !== "";
if (!loginStatus) {
log("少侠请先登录~ 哔哩哔哩 (゜-゜)つロ 干杯~");
return;
}
const PER_PAGE = 15;
try {
const lastPopoverButton = await getLastPopoverButton();
const subscribeMenuEl = document.createElement("li");
subscribeMenuEl.setAttribute("id", "subscribe");
lastPopoverButton.after(subscribeMenuEl);
const getBangumis = (page) => {
return fetch(
`//api.bilibili.com/x/space/bangumi/follow/list?type=1&follow_status=0&pn=${page}&ps=${PER_PAGE}&vmid=${DedeUserID}`,
{
method: "GET",
credentials: "include",
}
)
.then((response) => response.json())
.then((response) => response.data)
.then(({ list, ...rest }) => {
return {
list: list.map((item) => ({ ...item, id: item.media_id })),
...rest,
};
});
};
const getCinemas = (page) => {
return fetch(
`//api.bilibili.com/x/space/bangumi/follow/list?type=2&follow_status=0&pn=${page}&ps=${PER_PAGE}&vmid=${DedeUserID}`,
{
method: "GET",
credentials: "include",
}
)
.then((response) => response.json())
.then((response) => response.data)
.then(({ list, ...rest }) => {
return {
list: list.map((item) => ({ ...item, id: item.media_id })),
...rest,
};
});
};
const getFloowings = (page) => {
return fetch(
`//api.bilibili.com/x/relation/followings?&pn=${page}&ps=${PER_PAGE}&vmid=${DedeUserID}&order=desc`,
{
method: "GET",
credentials: "include",
}
)
.then((response) => response.json())
.then((response) => {
return {
list: response.data.list.map((item) => ({
...item,
id: item.mid,
})),
total: response.data.total,
pn: page,
};
});
};
const VideoItem = {
props: ["item"],
computed: {
coverURL() {
return this.item.cover.replace("http:", "");
},
},
template: `
<a
target="_blank"
class="header-history-card header-history-video"
:href="item.url"
>
<div class="header-history-video__image">
<picture class="v-img">
<source :srcset="coverURL + '@256w_144h_1c.webp'" type="image/webp" />
<img :src="coverURL + '@256w_144h_1c'" />
</picture>
<div
class="header-history-live__tag header-history-live__tag--red"
v-if="item?.new_ep?.index_show ?? false"
>
<span class="header-history-live__tag--text">
{{item.new_ep.index_show}}
</span>
</div>
</div>
<div class="header-history-card__info">
<div :title="item.title" class="header-history-card__info--title">
{{item.title}}
</div>
<div class="header-history-card__info--date">
<span>{{item.time}}</span>
</div>
<div class="header-history-card__info--name">
<span>{{item?.new_ep?.long_title ?? '' }}</span>
</div>
</div>
</a>
`,
};
const UserItem = {
props: ["item"],
computed: {
spaceURL() {
return `https://space.bilibili.com/${this.item.mid}`;
},
avatarURL() {
return this.item.face.replace("http:", "");
},
},
template: `
<a
target="_blank"
class="header-history-card header-history-video"
:href="spaceURL"
>
<div class="header-history-video__image">
<picture class="v-img"">
<source :srcset="avatarURL + '@256w_144h_1c.webp'" type="image/webp" />
<img :src="avatarURL + '@256w_144h_1c'" />
</picture>
</div>
<div class="header-history-card__info">
<div :title="item.title" class="header-history-card__info--title">
{{item.uname}}
</div>
<div class="header-history-card__info--name">
<span>{{item.sign }}</span>
</div>
</div>
</a>
`,
};
new Vue({
el: subscribeMenuEl,
components: { VideoItem, UserItem },
data() {
return {
isPanelVisible: false,
loading: false,
inLeaveAnimation: false,
activeTab: "bangumis",
tabs: [
{ key: "bangumis", name: "追番" },
{ key: "cinemas", name: "追剧" },
{ key: "floowings", name: "关注" },
],
dataset: {
bangumis: {
list: [],
total: 0,
page: 0,
component: "VideoItem",
},
cinemas: {
list: [],
total: 0,
page: 0,
component: "VideoItem",
},
floowings: {
list: [],
total: 0,
page: 0,
component: "UserItem",
},
},
};
},
created() {
this.load();
},
computed: {
list() {
return this.dataset[this.activeTab].list;
},
total() {
return this.dataset[this.activeTab].total;
},
page() {
return this.dataset[this.activeTab].page;
},
tabComponent() {
return this.dataset[this.activeTab].component;
},
},
methods: {
async load() {
const tab = this.activeTab;
let request;
if (tab === "bangumis") {
request = getBangumis;
}
if (tab === "cinemas") {
request = getCinemas;
}
if (tab === "floowings") {
request = getFloowings;
}
try {
this.loading = true;
const { list, total, pn } = await request(this.page + 1);
this.dataset[tab].list = [...this.dataset[tab].list, ...list];
this.dataset[tab].total = total;
this.dataset[tab].page = pn;
} catch (error) {
throw error;
} finally {
this.loading = false;
}
},
changeTabHandler(tab) {
this.activeTab = tab.key;
if (this.list.length <= 0) {
this.load();
}
},
onMouseoverHandler() {
if (!this.inLeaveAnimation) {
this.isPanelVisible = true;
}
},
onMouseleaveHandler() {
this.isPanelVisible = false;
},
onContentBeforeLeaveHandler() {
this.inLeaveAnimation = true;
},
onContentAfterLeaveHandler() {
this.inLeaveAnimation = false;
},
onScrollHandler() {
const panelContent = this.$refs.panelContent;
if (
!this.loading &&
this.list.length < this.total &&
panelContent.scrollHeight - panelContent.scrollTop - 50 <=
panelContent.clientHeight
) {
this.load();
}
},
},
template: `
<li
class="v-popover-wrap"
@mouseover="onMouseoverHandler"
@mouseleave="onMouseleaveHandler"
>
<a
href="//www.bilibili.com/account/history"
target="_blank" class="right-entry__outside"
>
<svg class="right-entry-icon" viewBox="0 0 1182 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2974" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="21"><path d="M1088.792893 96.259987A330.610168 330.610168 0 0 0 622.44343 96.259987l-31.600682 31.600683L560.199662 96.259987A330.370769 330.370769 0 0 0 93.132002 96.259987c-128.557321 128.557321-121.854146 345.692312 6.703175 474.249634l23.939911 23.939911 401.472304 402.429901a92.886854 92.886854 0 0 0 131.190712 0L1058.149807 595.167729l23.939911-23.939911c128.317922-128.317922 135.260496-345.452913 6.703175-474.967831z m-23.939911 247.299279a220.486579 220.486579 0 0 1-66.313553 140.527277l-25.136906 26.333902-383.038573 383.038573-383.038573-383.038573-24.41871-25.376306a220.725978 220.725978 0 0 1-66.552952-140.527276A210.671215 210.671215 0 0 1 340.191882 120.199898a219.528982 219.528982 0 0 1 140.527276 67.031751l25.615705 25.376305L550.623698 256.896789l63.201364 63.201365a62.483167 62.483167 0 1 0 88.338271-88.57767l-22.742915-23.939911 26.8127-25.615705a210.671215 210.671215 0 0 1 359.098662 162.551995z" fill="currentColor" p-id="2975"></path><path d="M249.030112 413.615829m42.320183-42.320184l0 0q42.320183-42.320183 84.640366 0l107.323985 107.323985q42.320183 42.320183 0 84.640367l0 0q-42.320183 42.320183-84.640367 0l-107.323984-107.323985q-42.320183-42.320183 0-84.640367Z" fill="currentColor" p-id="2976"></path></svg>
<span class="right-entry-text">订阅</span>
</a>
<transition
name="v-popover_bottom"
enter-active-class="v-popover_bottom-enter-from"
leave-active-class="v-popover_bottom-leave-from"
@before-leave="onContentBeforeLeaveHandler"
@after-leave="onContentAfterLeaveHandler"
>
<div
v-show="isPanelVisible"
class="v-popover is-bottom"
style="padding-top: 15px; margin-left: -50px;"
>
<div class="v-popover-content">
<div class="history-panel-popover">
<div class="header-tabs-panel">
<div
v-for="tab in tabs"
:key="tab.key"
class="header-tabs-panel__item"
:class="{'header-tabs-panel__item--active': activeTab === tab.key }"
@click="changeTabHandler(tab)"
>{{tab.name}}</div>
</div>
<div class="header-tabs-panel__content" ref="panelContent" @scroll="onScrollHandler">
<component
:is="tabComponent"
v-for="item in list"
:item="item"
:key="item.id"
/>
</div>
</div>
</div>
</div>
</transition>
</li>
`,
});
} catch (error) {
log(error);
}
function getLastPopoverButton(count = 1) {
if (count >= 30) {
return Promise.reject("获取顶部按列表超时");
}
return new Promise((resolve) => {
const popoverButtons = document.body.querySelectorAll(
".bili-header .bili-header__bar .right-entry>.v-popover-wrap"
);
if (popoverButtons.length) {
resolve(popoverButtons[popoverButtons.length - 1]);
return;
}
setTimeout(() => {
resolve(getLastPopoverButton(count++));
}, 100);
});
}
/**
* Get cookie by name
* @param {string} name
*/
function getCookie(name) {
const value = "; " + document.cookie;
let parts = value.split("; " + name + "=");
if (parts.length == 2) {
return parts.pop().split(";").shift();
}
return "";
}
/**
* print something in console with custom style
* @param {*} stuff
*/
function log(stuff) {
console.log(
"%cbilibili订阅+:",
"background: #f25d8e; border-radius: 3px; color: #fff; padding: 0 8px",
stuff
);
}
})();