Merge branch 'main' into dev

This commit is contained in:
Hakadao
2024-04-18 16:37:25 +08:00
10 changed files with 348 additions and 274 deletions

View File

@@ -13,16 +13,17 @@ import API_USER from './user'
import API_VIDEO from './video'
import API_WATCHLATER from './watchLater'
// Merge all API objects into one
const FullAPI = Object.assign({}, API_AUTH, API_ANIME, API_HISTORY, API_FAVORITE, API_MOMENT, API_NOTIFICATION, API_RANKING, API_SEARCH, API_USER, API_VIDEO, API_WATCHLATER)
// Create a message listener for each API
const handleMessage = apiListenerFactory(FullAPI)
export function setupAllMsgLstnrs() {
browser.runtime.onConnect.removeListener(handleConnect)
browser.runtime.onConnect.addListener(handleConnect)
function handleConnect() {
// Merge all API objects into one
const FullAPI = Object.assign({}, API_AUTH, API_ANIME, API_HISTORY, API_FAVORITE, API_MOMENT, API_NOTIFICATION, API_RANKING, API_SEARCH, API_USER, API_VIDEO, API_WATCHLATER)
// Create a message listener for each API
const handleMessage = apiListenerFactory(FullAPI)
browser.runtime.onMessage.removeListener(handleMessage)
browser.runtime.onMessage.addListener(handleMessage)
}
}
function handleConnect() {
browser.runtime.onMessage.removeListener(handleMessage)
browser.runtime.onMessage.addListener(handleMessage)
}

View File

@@ -10,30 +10,19 @@ const API_MOMENT: APIMAP = {
params: {},
afterHandle: AHS.J_D,
},
getTopBarNewMoments: {
url: 'https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new',
getTopBarMoments: {
url: 'https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/nav',
_fetch: {
method: 'get',
},
params: {
uid: '',
type_list: '268435455',
type: 'video',
update_baseline: '',
offset: '',
},
afterHandle: AHS.J_D,
},
getTopbarHistoryMoments: {
url: 'https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_history',
_fetch: {
method: 'get',
},
params: {
uid: '',
type_list: '268435455',
offset_dynamic_id: '',
},
afterHandle: AHS.J_D,
},
getTopbarLiveMoments: {
getTopBarLiveMoments: {
url: 'https://api.live.bilibili.com/xlive/web-ucenter/v1/xfetter/FeedList',
_fetch: {
method: 'get',

View File

@@ -28,9 +28,8 @@ enum HISTORY {
}
enum MOMENT {
GET_TOP_BAR_NEW_MOMENTS_COUNT = 'getTopBarNewMomentsCount',
GET_TOP_BAR_NEW_MOMENTS = 'getTopBarNewMoments',
GET_TOP_BAR_HISTORY_MOMENTS = 'getTopbarHistoryMoments',
GET_TOP_BAR_LIVE_MOMENTS = 'getTopbarLiveMoments',
GET_TOP_BAR_MOMENTS = 'getTopBarMoments',
GET_TOP_BAR_LIVE_MOMENTS = 'getTopBarLiveMoments',
GET_MOMENTS = 'getMoments',
}
enum NOTIFICATION {

View File

@@ -61,7 +61,7 @@ function apiListenerFactory(API_MAP: APIMAP) {
const contentScriptQuery = message.contentScriptQuery
// 检测是否有contentScriptQuery
if (!contentScriptQuery || !API_MAP[contentScriptQuery])
return console.error('no contentScriptQuery')
return console.error(`Cannot find this contentScriptQuery: ${contentScriptQuery}`)
if (API_MAP[contentScriptQuery] instanceof Function)
return (API_MAP[contentScriptQuery] as APIFunction)(message, sender, sendResponse)

View File

@@ -85,7 +85,7 @@ const notifications = useDelayedHover({
const moments = useDelayedHover({
enter: () => {
showMomentsPop.value = true
momentsPopRef.value && momentsPopRef.value.initData()
momentsPopRef.value && momentsPopRef.value.checkIfHasNewMomentsThenUpdateMoments()
},
leave: () => showMomentsPop.value = false,
})
@@ -383,7 +383,7 @@ defineExpose({
<Transition name="slide-in">
<ChannelsPop
v-if="showChannelsPop"
v-show="showChannelsPop"
class="bew-popover"
pos="!left-0 !top-70px"
transform="!translate-x-0"

View File

@@ -1,43 +1,55 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import type { Ref } from 'vue'
import { onMounted, reactive, ref, watch } from 'vue'
import { isNewArticle, isNewVideo, setLastestOffsetID } from '../notify'
import { MomentType } from '../types'
import type { MomentItem } from '../types'
import { getCSRF, getUserID, isHomePage, smoothScrollToTop } from '~/utils/main'
import { calcTimeSince } from '~/utils/dataFormatter'
// import { isNewArticle, setLastOffsetID, setLastestOffsetID } from '../notify'
import type { TopBarMomentResult } from '~/models/moment/topBarMoment'
import type { TopBarLiveMomentResult } from '~/models/moment/topBarLiveMoment'
import { getCSRF, isHomePage, smoothScrollToTop } from '~/utils/main'
import API from '~/background/msg.define'
type MomentType = 'video' | 'live' | 'article'
interface MomentTab { type: MomentType, name: any }
interface MomentCard {
type: MomentType
title: string
author: string
authorFace: string
pubTime?: string
cover: string
link: string
rid?: number
}
const { t } = useI18n()
const moments = reactive<MomentItem[]>([])
const moments = reactive<MomentCard[]>([])
const addedWatchLaterList = reactive<number[]>([])
const momentTabs = reactive([
const momentTabs = reactive<MomentTab[]>([
{
id: 0,
type: 'video',
name: t('topbar.moments_dropdown.tabs.videos'),
isSelected: true,
},
{
id: 1,
type: 'live',
name: t('topbar.moments_dropdown.tabs.live'),
isSelected: false,
},
{
id: 2,
type: 'article',
name: t('topbar.moments_dropdown.tabs.articles'),
isSelected: false,
},
])
const selectedTab = ref<number>(0)
const selectedMomentTab = ref<MomentTab>(momentTabs[0])
const isLoading = ref<boolean>(false)
// when noMoreContent is true, the user can't scroll down to load more content
const noMoreContent = ref<boolean>(false)
const noMoreContent = ref<boolean>(false) // when noMoreContent is true, the user can't scroll down to load more content
const livePage = ref<number>(1)
const momentsWrap = ref<HTMLElement>() as Ref<HTMLElement>
const momentsWrap = ref()
const momentUpdateBaseline = ref<string>('')
const momentOffset = ref<string>('')
const newMomentsCount = ref<number>(0)
watch(selectedTab, (newVal, oldVal) => {
watch(() => selectedMomentTab.value.type, (newVal, oldVal) => {
if (newVal === oldVal)
return
@@ -55,205 +67,168 @@ onMounted(() => {
>= momentsWrap.value.scrollHeight - 20
&& moments.length > 0
&& !isLoading.value
) {
if (selectedTab.value === 0 && !noMoreContent.value)
getTopbarHistoryMoments([MomentType.Video, MomentType.Bangumi])
else if (selectedTab.value === 1 && !noMoreContent.value)
getTopbarLiveMoments(livePage.value)
else if (selectedTab.value === 2 && !noMoreContent.value)
getTopbarHistoryMoments([MomentType.Article])
}
)
getData()
})
}
})
function onClickTab(tabId: number) {
function onClickTab(tab: MomentTab) {
// Prevent changing tab when loading, cuz it will cause a bug
if (isLoading.value || tabId === selectedTab.value)
if (isLoading.value || tab.type === selectedMomentTab.value.type)
return
selectedTab.value = tabId
moments.length = 0
momentTabs.forEach((tab) => {
tab.isSelected = tab.id === tabId
})
selectedMomentTab.value = tab
initData()
}
async function initData() {
if (selectedTab.value === 0) {
await getTopBarNewMoments([MomentType.Video, MomentType.Bangumi])
}
else if (selectedTab.value === 1) {
livePage.value = 1
getTopbarLiveMoments(livePage.value)
}
else if (selectedTab.value === 2) {
await getTopBarNewMoments([MomentType.Article])
}
moments.length = 0
momentUpdateBaseline.value = ''
momentOffset.value = ''
newMomentsCount.value = 0
livePage.value = 1
getData()
}
async function getTopBarNewMoments(type_list: number[]) {
isLoading.value = true
try {
const res = await browser.runtime
.sendMessage({
contentScriptQuery: API.MOMENT.GET_TOP_BAR_NEW_MOMENTS,
uid: getUserID(),
type_list,
})
if (res.code === 0) {
// If there are no new moments, do not change the data to record the scroll position
if (moments.length !== 0) {
if (res.data.new_num === 0)
return
}
moments.length = 0
if (Array.isArray(res.data.cards) && res.data.cards.length > 0) {
res.data.cards.forEach((item: any) => {
pushItemIntoMoments(item)
})
}
if (moments.length !== 0 && res.data.cards.length < 20) {
isLoading.value = false
noMoreContent.value = true
return
}
// set this lastest offset id, which will clear the new moment's marker point
// after you watch these moments.
if (selectedTab.value === 0)
setLastestOffsetID(MomentType.Video, moments[0].id)
else if (selectedTab.value === 2)
setLastestOffsetID(MomentType.Article, moments[0].id)
noMoreContent.value = false
}
}
finally {
isLoading.value = false
}
function getData() {
if (selectedMomentTab.value.type !== 'live')
getTopBarMoments()
else
getTopBarLiveMoments()
}
function getTopbarHistoryMoments(type_list: number[]) {
isLoading.value = true
browser.runtime
.sendMessage({
contentScriptQuery: API.MOMENT.GET_TOP_BAR_HISTORY_MOMENTS,
uid: getUserID(),
type_list,
offset_dynamic_id: moments[moments.length - 1].dynamic_id_str,
})
.then((res) => {
function checkIfHasNewMomentsThenUpdateMoments() {
if (selectedMomentTab.value.type === 'live')
return
browser.runtime.sendMessage({
contentScriptQuery: API.MOMENT.GET_TOP_BAR_MOMENTS,
type: selectedMomentTab.value.type,
update_baseline: momentUpdateBaseline.value || undefined,
})
.then((res: TopBarMomentResult) => {
if (res.code === 0) {
if (res.data.has_more === 0) {
isLoading.value = false
const { has_more, items, update_baseline, update_num } = res.data
if (!has_more) {
noMoreContent.value = true
return
}
if (update_num === 0)
return
for (let i = update_num - 1; i >= 0; i--) {
moments.unshift({
type: selectedMomentTab.value.type,
title: items[i].title,
author: items[i].author.name,
authorFace: items[i].author.face,
pubTime: items[i].pub_time,
cover: items[i].cover,
link: items[i].jump_url,
rid: items[i].rid,
})
}
newMomentsCount.value = update_num
momentUpdateBaseline.value = update_baseline
// newMomentsCount.value = update_num
// setLastOffsetID('video', offset)
}
})
.finally(() => isLoading.value = false)
}
function getTopBarMoments() {
if (isLoading.value)
return
isLoading.value = true
browser.runtime.sendMessage({
contentScriptQuery: API.MOMENT.GET_TOP_BAR_MOMENTS,
type: selectedMomentTab.value.type,
update_baseline: momentUpdateBaseline.value || undefined,
offset: momentOffset.value || undefined,
})
.then((res: TopBarMomentResult) => {
if (res.code === 0) {
const { has_more, items, offset, update_baseline, update_num } = res.data
if (!has_more) {
noMoreContent.value = true
return
}
res.data.cards.forEach((item: any) => {
pushItemIntoMoments(item)
})
noMoreContent.value = false
newMomentsCount.value = update_num
momentUpdateBaseline.value = update_baseline
momentOffset.value = offset
// set this lastest offset id, which will clear the new moment's marker point
// after you watch these moments.
// setLastOffsetID('video', offset)
moments.push(
...items.map(item => ({
type: selectedMomentTab.value.type,
title: item.title,
author: item.author.name,
authorFace: item.author.face,
pubTime: item.pub_time,
cover: item.cover,
link: item.jump_url,
rid: item.rid,
}),
),
)
}
isLoading.value = false
})
.finally(() => isLoading.value = false)
}
function getTopbarLiveMoments(page: number) {
function isNewMoment(index: number) {
return index < newMomentsCount.value
}
function getTopBarLiveMoments() {
isLoading.value = true
browser.runtime
.sendMessage({
contentScriptQuery: API.MOMENT.GET_TOP_BAR_LIVE_MOMENTS,
page,
page: livePage.value,
pagesize: 10,
})
.then((res) => {
.then((res: TopBarLiveMomentResult) => {
if (res.code === 0) {
// if the length of this list is less then the pageSize, it means that it have no more contents
if (moments.length !== 0 && res.data.list.length < 10) {
isLoading.value = false
noMoreContent.value = true
const { list, pagesize } = res.data
// if the length of this list is less then the pageSize, it means that it have no more contents
if (moments.length !== 0 && list.length < Number(pagesize)) {
noMoreContent.value = true
return
}
// if the length of this list is equal to the pageSize, this means that it may have the next page.
if (res.data.list.length === 10)
if (list.length === Number(pagesize))
livePage.value++
res.data.list.forEach((item: any) => {
moments.push({
id: item.roomid,
uid: item.uid,
name: item.uname,
face: item.face,
url: item.link,
moments.push(
...list.map(item => ({
type: selectedMomentTab.value.type,
title: item.title,
author: item.uname,
authorFace: item.face,
cover: item.pic,
} as MomentItem)
})
link: item.link,
}),
),
)
noMoreContent.value = false
}
isLoading.value = false
})
}
function pushItemIntoMoments(item: any) {
const card = JSON.parse(item.card)
if (item.desc.type === MomentType.Video) {
// if this is a video moment
moments.push({
type: item.desc.type,
id: item.desc.dynamic_id,
uid: item.desc.uid,
name: item.desc.user_profile.info.uname,
face: item.desc.user_profile.info.face,
aid: card.aid,
bvid: item.desc.bvid,
url: card.short_link_v2 || `https://www.bilibili.com/video/${item.desc.bvid}`,
ctime: card.ctime,
title: card.title,
cover: card.pic,
dynamic_id_str: item.desc.dynamic_id_str,
isNew: isNewVideo(item.desc.dynamic_id),
} as MomentItem)
}
else if (item.desc.type === MomentType.Bangumi) {
// bangumi moment
moments.push({
type: item.desc.type,
id: item.desc.dynamic_id,
name: card.apiSeasonInfo.title,
face: card.apiSeasonInfo.cover,
episode_id: card.episode_id,
url: card.url,
title: card.new_desc,
cover: card.cover,
dynamic_id_str: item.desc.dynamic_id_str,
isNew: isNewVideo(item.desc.dynamic_id),
} as MomentItem)
}
else if (item.desc.type === MomentType.Article) {
// article moment
moments.push({
type: item.desc.type,
id: item.desc.dynamic_id,
uid: item.desc.uid,
name: item.desc.user_profile.info.uname,
face: item.desc.user_profile.info.face,
url: `https://www.bilibili.com/read/cv${card.id}`,
ctime: card.publish_time,
title: card.title,
cover: card.image_urls[0],
dynamic_id_str: item.desc.dynamic_id_str,
isNew: isNewArticle(item.desc.dynamic_id),
} as MomentItem)
}
.finally(() => isLoading.value = false)
}
function toggleWatchLater(aid: number) {
@@ -286,7 +261,7 @@ function toggleWatchLater(aid: number) {
}
defineExpose({
initData,
checkIfHasNewMomentsThenUpdateMoments,
})
</script>
@@ -314,13 +289,13 @@ defineExpose({
<div flex="~">
<div
v-for="tab in momentTabs"
:key="tab.id"
:key="tab.type"
m="r-4"
transition="all duration-300"
class="tab"
:class="tab.isSelected ? 'tab-selected' : ''"
:class="tab.type === selectedMomentTab.type ? 'tab-selected' : ''"
cursor="pointer"
@click="onClickTab(tab.id)"
@click="onClickTab(tab)"
>
{{ tab.name }}
</div>
@@ -354,11 +329,12 @@ defineExpose({
/>
<!-- moments -->
<TransitionGroup name="list">
<a
v-for="(moment, index) in moments"
:key="index"
:href="moment.url" :target="isHomePage() ? '_blank' : '_self'" rel="noopener noreferrer"
:href="moment.link" :target="isHomePage() ? '_blank' : '_self'" rel="noopener noreferrer"
flex="~ justify-between"
m="b-2 first:t-50px" p="2"
rounded="$bew-radius"
@@ -368,7 +344,7 @@ defineExpose({
>
<!-- new moment dot -->
<div
v-if="moment.isNew"
v-if="isNewMoment(index)"
rounded="full"
w="8px"
h="8px"
@@ -377,17 +353,12 @@ defineExpose({
pos="absolute -top-12px -left-12px"
style="box-shadow: 0 0 4px var(--bew-theme-color)"
/>
<a
:href="
moment.type === MomentType.Video
? `https://space.bilibili.com/${moment.uid}`
: moment.url
"
:href="moment.link"
:target="isHomePage() ? '_blank' : '_self'" rel="noopener noreferrer"
>
<img
:src="`${moment.face}@50w_50h_1c`"
:src="`${moment.authorFace}@50w_50h_1c`"
rounded="1/2"
w="40px"
h="40px"
@@ -399,27 +370,22 @@ defineExpose({
<div>
<!-- <span v-if="selectedTab !== 1">{{ `${moment.name} ${t('topbar.moments_dropdown.uploaded')}` }}</span> -->
<!-- <span v-else>{{ `${moment.name} ${t('topbar.moments_dropdown.now_streaming')}` }}</span> -->
<span font-bold>{{ moment.name }}</span>
<span font-bold>{{ moment.author }}</span>
<div overflow-hidden text-ellipsis break-anywhere>
{{ moment.title }}
</div>
<div
v-if="moment.type !== MomentType.Bangumi"
text="$bew-text-2 sm"
m="y-2"
>
<!-- Videos and articles -->
<div v-if="selectedTab === 0 || selectedTab === 2">
{{
moment.ctime
? calcTimeSince(new Date(moment.ctime * 1000))
: moment.ctime
}}
<!-- publish time -->
<div v-if="selectedMomentTab.type !== 'live'">
{{ moment.pubTime }}
</div>
<!-- Live -->
<div
v-else-if="selectedTab === 1"
v-else
text="$bew-theme-color"
font="bold"
flex="~"
@@ -442,14 +408,14 @@ defineExpose({
rounded="$bew-radius-half"
>
<div
v-if="moment.type === MomentType.Video"
opacity-0 group-hover:opacity-100
pos="absolute" duration-300 bg="black opacity-60"
rounded="$bew-radius-half" p-1
z-1 color-white
@click.prevent="toggleWatchLater(moment.aid ?? 0)"
@click.prevent="toggleWatchLater(moment.rid || 0)"
>
<Tooltip v-if="!addedWatchLaterList.includes(moment.aid ?? 0)" :content="$t('common.save_to_watch_later')" placement="bottom" type="dark">
<Tooltip v-if="!addedWatchLaterList.includes(moment.rid || 0)" :content="$t('common.save_to_watch_later')" placement="bottom" type="dark">
<mingcute:carplay-line />
</Tooltip>
<Tooltip v-else :content="$t('common.added')" placement="bottom" type="dark">

View File

@@ -1,30 +1,29 @@
import { MomentType } from './types'
import { getCookie, getUserID, setCookie } from '~/utils/main'
// https://github.dev/the1812/Bilibili-Evolved/blob/8a4e422612a7bd0b42da9aa50c21c7bf3ea401b8/src/components/feeds/notify.ts#L1
// import { getCookie, getUserID, setCookie } from '~/utils/main'
/** Update the time interval of topbar notifications and moments counts */
export const updateInterval = 1000 * 60 * 5 // Updated every 5 minutes
const getVideoOffsetID = (): number => Number.parseInt(`${getCookie(`bp_video_offset_${getUserID()}`)}`, 10) || 0
const getArticleOffsetID = (): number => Number.parseInt(`${getCookie(`bp_article_offset_${getUserID()}`)}`, 10) || 0
// const getLastID = (): string => `${getCookie(`bp_t_offset_${getUserID()}`)}`
function compareOffsetID(currentOffsetID: number, lastestOffsetID: number): boolean {
if (currentOffsetID === lastestOffsetID)
return false
else if (currentOffsetID > lastestOffsetID)
return true
else
return false
}
// function compareID(currentID: string, lastOffsetID: string): boolean {
// if (currentID === lastOffsetID)
// return false
// else if (Number(currentID) > Number(lastOffsetID))
// return true
// else
// return false
// }
export function setLastestOffsetID(type: MomentType, offsetID: number) {
if (offsetID === null || offsetID === undefined)
return
// export function setLastId(id: string) {
// if (id === null || id === undefined)
// return
if (type === MomentType.Video || type === MomentType.Bangumi)
setCookie(`bp_video_offset_${getUserID()}`, offsetID.toString(), 30)
else if (type === MomentType.Article)
setCookie(`bp_article_offset_${getUserID()}`, offsetID.toString(), 30)
}
// if (compareID(id))
// return
export const isNewVideo = (currentOffsetID: number): boolean => compareOffsetID(currentOffsetID, getVideoOffsetID())
export const isNewArticle = (currentOffsetID: number): boolean => compareOffsetID(currentOffsetID, getArticleOffsetID())
// setCookie(`bp_t_offset_${getUserID()}`, id, 30)
// }
// export const isNewId = (id: string): boolean => compareID(id, getLastID())

View File

@@ -58,23 +58,6 @@ export enum MomentType {
Documentary = 4101,
}
export interface MomentItem {
type?: MomentType
id: number
uid: number
name: string
face: string
aid?: number
bvid?: string
episode_id?: number
url: string
ctime?: number
title: string
cover: string
dynamic_id_str?: string
isNew: boolean
}
// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/history&toview/history.md#%E8%8E%B7%E5%8F%96%E5%8E%86%E5%8F%B2%E8%AE%B0%E5%BD%95%E5%88%97%E8%A1%A8_web%E7%AB%AF
export enum HistoryType {
Archive = 'archive', // archive稿件

View File

@@ -0,0 +1,29 @@
// https://app.quicktype.io/?l=ts
export interface TopBarLiveMomentResult {
code: number
message: string
ttl: number
data: Data
}
export interface Data {
results: number
page: string
pagesize: string
list: List[]
}
export interface List {
cover: string
face: string
uname: string
title: string
roomid: number
pic: string
online: number
link: string
uid: number
parent_area_id: number
area_id: number
}

View File

@@ -0,0 +1,108 @@
// https://app.quicktype.io/?l=ts
export interface TopBarMomentResult {
code: number
message: string
ttl: number
data: Data
}
export interface Data {
has_more: boolean
items: Item[]
offset: string
update_baseline: string
update_num: number
}
export interface Item {
author: Author
cover: string
id_str: string
jump_url: string
pub_time: string
rid: number
title: string
type: number
visible: boolean
}
export interface Author {
face: string
jump_url: string
mid: number
name: string
official: Official
vip: Vip
}
export interface Official {
desc: string
role: number
title: string
type: number
}
export interface Vip {
avatar_icon: AvatarIcon
avatar_subscript: number
avatar_subscript_url: string
due_date: number
label: Label
nickname_color: Color
role: number
status: number
theme_type: number
tv_due_date: number
tv_vip_pay_type: number
tv_vip_status: number
type: number
vip_pay_type: number
}
export interface AvatarIcon {
icon_resource: IconResource
icon_type?: number
}
export interface IconResource {
type?: number
url?: string
}
export interface Label {
bg_color: Color
bg_style: number
border_color: string
img_label_uri_hans: string
img_label_uri_hans_static: string
img_label_uri_hant: string
img_label_uri_hant_static: string
label_theme: LabelTheme
path: string
text: Text
text_color: TextColor
use_img_label: boolean
}
export enum Color {
Empty = '',
Fb7299 = '#FB7299',
}
export enum LabelTheme {
AnnualVip = 'annual_vip',
Empty = '',
Vip = 'vip',
}
export enum Text {
Empty = '',
= '大会员',
= '年度大会员',
}
export enum TextColor {
Empty = '',
Ffffff = '#FFFFFF',
}