mirror of
https://github.com/BewlyBewly/BewlyBewly.git
synced 2025-04-14 13:15:29 +00:00
add: moments dropdown
This commit is contained in:
@@ -172,6 +172,33 @@ chrome.runtime.onMessage.addListener((message: any, sender: chrome.runtime.Messa
|
||||
.catch(error => console.error(error))
|
||||
return true
|
||||
}
|
||||
if (message.contentScriptQuery === 'getNewMoments') {
|
||||
const url = `https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=${message.uid}
|
||||
&type_list=${message.typeList}`
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => sendResponse(data))
|
||||
.catch(error => console.error(error))
|
||||
return true
|
||||
}
|
||||
if (message.contentScriptQuery === 'getHistoryMoments') {
|
||||
const url = `https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_history?uid=${message.uid}
|
||||
&type_list=${message.typeList}
|
||||
&offset_dynamic_id=${message.offsetDynamicID}`
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => sendResponse(data))
|
||||
.catch(error => console.error(error))
|
||||
return true
|
||||
}
|
||||
if (message.contentScriptQuery === 'getLiveMoments') {
|
||||
const url = `https://api.live.bilibili.com/xlive/web-ucenter/v1/xfetter/FeedList?page=${message.page}&pagesize=${message.pageSize}`
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => sendResponse(data))
|
||||
.catch(error => console.error(error))
|
||||
return true
|
||||
}
|
||||
if (message.contentScriptQuery === 'submitDislike') {
|
||||
// https://github.com/indefined/UserScripts/blob/master/bilibiliHome/bilibiliHome.API.md#%E6%8F%90%E4%BA%A4%E4%B8%8D%E5%96%9C%E6%AC%A2
|
||||
let url = `${APP_URL}/x/feed/dislike?access_key=${message.accessKey}
|
||||
|
||||
382
src/components/Topbar/MomentsDropdown.vue
Normal file
382
src/components/Topbar/MomentsDropdown.vue
Normal file
@@ -0,0 +1,382 @@
|
||||
<script lang="ts">
|
||||
import { isNewArticle, isNewVideo, setLastestOffsetID } from './notify'
|
||||
import { getUserID, calcTimeSince } from '~/utils'
|
||||
import { MomentItem, MomentType } from '~/types'
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
moments: [] as MomentItem[],
|
||||
calcTimeSince,
|
||||
MomentType,
|
||||
momentTabs: [
|
||||
{
|
||||
id: 0,
|
||||
name: 'Videos',
|
||||
isSelected: true,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Live',
|
||||
isSelected: false,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Articles',
|
||||
isSelected: false,
|
||||
},
|
||||
],
|
||||
selectedTab: 0,
|
||||
isLoading: false,
|
||||
// when noMoreContent is true, the user can't scroll down to load more content
|
||||
noMoreContent: false,
|
||||
livePage: 1,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedTab(newVal: number, oldVal: number) {
|
||||
if (newVal === oldVal) return
|
||||
|
||||
this.scrollToTop(this.$refs.momentsWrap as HTMLElement, 300)
|
||||
this.moments = []
|
||||
if (newVal === 0) {
|
||||
this.getNewMoments([MomentType.Video, MomentType.Bangumi])
|
||||
}
|
||||
else if (newVal === 1) {
|
||||
this.livePage = 1
|
||||
this.getLiveMoments(this.livePage)
|
||||
}
|
||||
else if (newVal === 2) {
|
||||
this.getNewMoments([MomentType.Article])
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getNewMoments([MomentType.Video, MomentType.Bangumi])
|
||||
|
||||
const momentsWrap = this.$refs.momentsWrap as HTMLDivElement
|
||||
momentsWrap.addEventListener('scroll', () => {
|
||||
if (momentsWrap.clientHeight + momentsWrap.scrollTop >= momentsWrap.scrollHeight
|
||||
&& this.moments.length > 0 && !this.isLoading) {
|
||||
if (this.selectedTab === 0 && !this.noMoreContent)
|
||||
this.getHistoryMoments([MomentType.Video, MomentType.Bangumi])
|
||||
else if (this.selectedTab === 1 && !this.noMoreContent)
|
||||
this.getLiveMoments(this.livePage)
|
||||
else if (this.selectedTab === 2 && !this.noMoreContent)
|
||||
this.getHistoryMoments([MomentType.Article])
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
onClickTab(tabId: number) {
|
||||
// Prevent changing tab when loading, cuz it will cause a bug
|
||||
if (this.isLoading) return
|
||||
|
||||
this.selectedTab = tabId
|
||||
this.momentTabs.forEach((tab) => {
|
||||
tab.isSelected = tab.id === tabId
|
||||
})
|
||||
},
|
||||
getNewMoments(typeList: number[]) {
|
||||
this.isLoading = true
|
||||
browser.runtime.sendMessage({
|
||||
contentScriptQuery: 'getNewMoments',
|
||||
uid: getUserID(),
|
||||
typeList,
|
||||
}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
if (this.moments.length !== 0 && res.data.cards.length < 20) {
|
||||
this.isLoading = false
|
||||
this.noMoreContent = true
|
||||
return
|
||||
}
|
||||
|
||||
res.data.cards.forEach((item: any) => {
|
||||
this.pushItemIntoMoments(item)
|
||||
})
|
||||
|
||||
// set this lastest offset id, which will clear the new moment's marker point
|
||||
// after you watch these moments.
|
||||
if (this.selectedTab === 0)
|
||||
setLastestOffsetID(MomentType.Video, this.moments[0].id)
|
||||
else if (this.selectedTab === 2)
|
||||
setLastestOffsetID(MomentType.Article, this.moments[0].id)
|
||||
|
||||
this.noMoreContent = false
|
||||
}
|
||||
this.isLoading = false
|
||||
})
|
||||
},
|
||||
getHistoryMoments(typeList: number[]) {
|
||||
this.isLoading = true
|
||||
browser.runtime.sendMessage({
|
||||
contentScriptQuery: 'getHistoryMoments',
|
||||
uid: getUserID(),
|
||||
typeList,
|
||||
offsetDynamicID: this.moments[this.moments.length - 1].dynamic_id_str,
|
||||
}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
if (res.data.has_more === 0) {
|
||||
this.isLoading = false
|
||||
this.noMoreContent = true
|
||||
return
|
||||
}
|
||||
|
||||
res.data.cards.forEach((item: any) => {
|
||||
this.pushItemIntoMoments(item)
|
||||
})
|
||||
this.noMoreContent = false
|
||||
}
|
||||
this.isLoading = false
|
||||
})
|
||||
},
|
||||
getLiveMoments(page: number) {
|
||||
this.isLoading = true
|
||||
browser.runtime.sendMessage({
|
||||
contentScriptQuery: 'getLiveMoments',
|
||||
page,
|
||||
pageSize: 10,
|
||||
}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
// if the length of this list is less then the pageSize, it means that it have no more contents
|
||||
if (this.moments.length !== 0 && res.data.list.length < 10) {
|
||||
this.isLoading = false
|
||||
this.noMoreContent = 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)
|
||||
this.livePage++
|
||||
res.data.list.forEach((item: any) => {
|
||||
this.moments.push({
|
||||
id: item.roomid,
|
||||
uid: item.uid,
|
||||
name: item.uname,
|
||||
face: item.face,
|
||||
url: item.link,
|
||||
title: item.title,
|
||||
cover: item.pic,
|
||||
} as MomentItem)
|
||||
})
|
||||
this.noMoreContent = false
|
||||
}
|
||||
this.isLoading = false
|
||||
})
|
||||
},
|
||||
pushItemIntoMoments(item: any) {
|
||||
const card = JSON.parse(item.card)
|
||||
|
||||
if (item.desc.type === MomentType.Video) {
|
||||
// if this is a video moment
|
||||
this.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,
|
||||
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
|
||||
this.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
|
||||
this.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)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* smooth scroll to the top of the html element
|
||||
*/
|
||||
scrollToTop(element: HTMLElement, duration: number) {
|
||||
// cancel if already on top
|
||||
if (element.scrollTop === 0) return
|
||||
|
||||
const cosParameter = element.scrollTop / 2
|
||||
let scrollCount = 0
|
||||
let oldTimestamp = 0
|
||||
|
||||
function step(newTimestamp: number) {
|
||||
if (oldTimestamp !== 0) {
|
||||
// if duration is 0 scrollCount will be Infinity
|
||||
scrollCount += Math.PI * (newTimestamp - oldTimestamp) / duration
|
||||
if (scrollCount >= Math.PI) return element.scrollTop = 0
|
||||
element.scrollTop = cosParameter + cosParameter * Math.cos(scrollCount)
|
||||
}
|
||||
oldTimestamp = newTimestamp
|
||||
window.requestAnimationFrame(step)
|
||||
}
|
||||
window.requestAnimationFrame(step)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
bg="$bew-content-solid-1"
|
||||
w="380px"
|
||||
rounded="$bew-radius"
|
||||
pos="relative"
|
||||
style="box-shadow: var(--bew-shadow-2)"
|
||||
>
|
||||
<!-- top bar -->
|
||||
<div
|
||||
flex="~"
|
||||
justify="between"
|
||||
p="y-4 x-6"
|
||||
pos="fixed top-0 left-0"
|
||||
w="full"
|
||||
bg="$bew-content-1"
|
||||
z="1"
|
||||
border="!rounded-t-$bew-radius"
|
||||
style="backdrop-filter: var(--bew-filter-glass)"
|
||||
>
|
||||
<div flex="~">
|
||||
<div
|
||||
v-for="tab in momentTabs"
|
||||
:key="tab.id"
|
||||
m="r-4"
|
||||
transition="all duration-300"
|
||||
class="tab"
|
||||
:class="tab.isSelected ? 'tab-selected' : ''"
|
||||
cursor="pointer"
|
||||
@click="onClickTab(tab.id)"
|
||||
>
|
||||
{{ tab.name }}
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://t.bilibili.com/" target="_blank" flex="~" items="center">
|
||||
<span text="sm">View ALL</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- moments wrapper -->
|
||||
<div ref="momentsWrap" h="430px" overflow="y-scroll" p="x-4">
|
||||
<!-- loading -->
|
||||
<loading v-if="isLoading && moments.length === 0" h="full" flex="~" items="center"></loading>
|
||||
|
||||
<!-- empty -->
|
||||
<empty v-if="!isLoading && moments.length === 0" w="full" h="full"></empty>
|
||||
|
||||
<!-- moments -->
|
||||
<transition-group name="list">
|
||||
<a
|
||||
v-for="(moment, index) in moments"
|
||||
:key="index"
|
||||
:href="moment.url"
|
||||
target="_blank"
|
||||
flex="~"
|
||||
justify="between"
|
||||
m="b-4"
|
||||
first:m="t-16"
|
||||
p="2"
|
||||
rounded="$bew-radius"
|
||||
hover:bg="$bew-fill-2"
|
||||
transition="all duration-300"
|
||||
cursor="pointer"
|
||||
pos="relative"
|
||||
>
|
||||
<!-- new moment dot -->
|
||||
<div
|
||||
v-if="moment.isNew"
|
||||
rounded="full"
|
||||
w="8px"
|
||||
h="8px"
|
||||
m="t-2 l-2"
|
||||
bg="$bew-theme-color"
|
||||
pos="absolute -top-10px -left-10px"
|
||||
style="box-shadow: 0 0 4px var(--bew-theme-color)"
|
||||
></div>
|
||||
|
||||
<a
|
||||
:href="moment.type === MomentType.Video ? 'https://space.bilibili.com/' + moment.uid : moment.url"
|
||||
target="_blank"
|
||||
>
|
||||
<img :src="moment.face + '@60w_60h_1c'" rounded="$bew-radius" w="40px" h="40px" m="r-4" />
|
||||
</a>
|
||||
|
||||
<div flex="~" justify="between" w="full">
|
||||
<div>
|
||||
<span>{{ moment.name }}</span> uploaded: {{ moment.title }}
|
||||
<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">{{ calcTimeSince(new Date(moment.ctime * 1000)) }} ago
|
||||
</div>
|
||||
<!-- Live -->
|
||||
<div v-else-if="selectedTab === 1" text="$bew-theme-color" font="bold" flex="~" items="center">
|
||||
<fluent:live-24-filled m="r-2" /> LIVE
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<img :src="moment.cover + '@128w_72h_1c'" w="82px" h="46px" m="l-4" rounded="$bew-radius" />
|
||||
</div>
|
||||
</a>
|
||||
</transition-group>
|
||||
|
||||
<!-- loading -->
|
||||
<loading v-if="isLoading && moments.length !== 0" m="-t-4"></loading>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
@apply opacity-0 transform translate-y-2 transform-gpu;
|
||||
}
|
||||
|
||||
.tab {
|
||||
@apply relative text-$bew-text-2;
|
||||
|
||||
&::after {
|
||||
@apply absolute bottom-0 left-0 w-full h-12px bg-$bew-theme-color opacity-0 transform scale-x-0 -z-1 transition-all duration-300;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
.tab-selected {
|
||||
@apply font-bold text-$bew-text-1;
|
||||
|
||||
&::after {
|
||||
@apply scale-x-80 opacity-40;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,9 @@
|
||||
<script lang="ts">
|
||||
import MomentsDropdown from './MomentsDropdown.vue'
|
||||
import { getUserID } from '~/utils'
|
||||
|
||||
export default defineComponent({
|
||||
components: { MomentsDropdown },
|
||||
data() {
|
||||
return {
|
||||
mid: getUserID() || '',
|
||||
@@ -10,9 +12,10 @@ export default defineComponent({
|
||||
showUserPanel: false,
|
||||
showTopbarMask: false,
|
||||
showNotificationsDropDown: false,
|
||||
showMomentsDropDown: false,
|
||||
showUploadDropDown: false,
|
||||
showSearchBar: true,
|
||||
showRightContent: true,
|
||||
// showSearchBar: true,
|
||||
// showRightContent: true,
|
||||
isLogin: !!getUserID(),
|
||||
unReadmessage: {},
|
||||
unReadDm: {},
|
||||
@@ -60,6 +63,14 @@ export default defineComponent({
|
||||
avatarImg.classList.remove('hover')
|
||||
avatarShadow.classList.remove('hover')
|
||||
},
|
||||
openNotificationsDropdown() {
|
||||
this.showNotificationsDropDown = true
|
||||
// update the unread message count
|
||||
this.getUnreadMessageCount()
|
||||
},
|
||||
closeNotificationsDropdown() {
|
||||
this.showNotificationsDropDown = false
|
||||
},
|
||||
getUserInfo() {
|
||||
browser.runtime
|
||||
.sendMessage({
|
||||
@@ -84,6 +95,7 @@ export default defineComponent({
|
||||
contentScriptQuery: 'getUnreadDm',
|
||||
}).then(res => this.unReadDm = res.data)
|
||||
|
||||
this.unReadmessageCount = 0
|
||||
for (const [key, value] of Object.entries(this.unReadmessage))
|
||||
if (key !== 'up') this.unReadmessageCount += parseInt(`${value}`)
|
||||
for (const [, value] of Object.entries(this.unReadDm))
|
||||
@@ -223,8 +235,8 @@ export default defineComponent({
|
||||
|
||||
<div
|
||||
class="right-side-item"
|
||||
@mouseenter="showNotificationsDropDown = true"
|
||||
@mouseleave="showNotificationsDropDown = false"
|
||||
@mouseenter="openNotificationsDropdown"
|
||||
@mouseleave="closeNotificationsDropdown"
|
||||
>
|
||||
<div
|
||||
v-if="unReadmessageCount !== 0"
|
||||
@@ -243,12 +255,17 @@ export default defineComponent({
|
||||
<transition name="slide">
|
||||
<notifications-dropdown
|
||||
v-if="showNotificationsDropDown"
|
||||
ref="notificationsDropdown"
|
||||
class="bew-popover"
|
||||
></notifications-dropdown>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<div class="right-side-item">
|
||||
<div
|
||||
class="right-side-item"
|
||||
@mouseenter="showMomentsDropDown = true"
|
||||
@mouseleave="showMomentsDropDown = false"
|
||||
>
|
||||
<div
|
||||
v-if="newMomentsCount !== 0"
|
||||
class="unread-message"
|
||||
@@ -262,6 +279,14 @@ export default defineComponent({
|
||||
>
|
||||
<tabler:windmill />
|
||||
</a>
|
||||
|
||||
<transition name="slide">
|
||||
<moments-dropdown
|
||||
v-if="showMomentsDropDown"
|
||||
class="bew-popover"
|
||||
>
|
||||
</moments-dropdown>
|
||||
</transition>
|
||||
</div>
|
||||
<div class="right-side-item">
|
||||
<a
|
||||
@@ -299,8 +324,9 @@ export default defineComponent({
|
||||
href="https://member.bilibili.com/platform/upload/video/frame"
|
||||
target="_blank"
|
||||
class="bg-$bew-theme-color rounded-full !text-white !text-base !px-4 mx-1"
|
||||
flex="~ justify-center"
|
||||
w="xl:120px <xl:42px"
|
||||
flex="~"
|
||||
justify="center"
|
||||
w="xl:100px <xl:42px"
|
||||
h="xl:auto <xl:42px"
|
||||
p="xl:auto <xl:unset"
|
||||
>
|
||||
@@ -328,7 +354,7 @@ export default defineComponent({
|
||||
<style lang="scss" scoped>
|
||||
.slide-enter-active,
|
||||
.slide-leave-active {
|
||||
@apply transition-all duration-300;
|
||||
@apply transition-all duration-300 pointer-events-none;
|
||||
}
|
||||
|
||||
.slide-leave-to,
|
||||
|
||||
27
src/components/Topbar/notify.ts
Normal file
27
src/components/Topbar/notify.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { MomentType } from '~/types'
|
||||
import { getUserID, getCookie, setCookie } from '~/utils'
|
||||
|
||||
const getVideoOffsetID = (): number => parseInt(`${getCookie(`bp_video_offset_${getUserID()}`)}`, 10) || 0
|
||||
const getArticleOffsetID = (): number => parseInt(`${getCookie(`bp_article_offset_${getUserID()}`)}`, 10) || 0
|
||||
|
||||
const compareOffsetID = (currentOffsetID: number, lastestOffsetID: number): boolean => {
|
||||
if (currentOffsetID === lastestOffsetID)
|
||||
return false
|
||||
else if (currentOffsetID > lastestOffsetID)
|
||||
return true
|
||||
else
|
||||
return false
|
||||
}
|
||||
|
||||
export const setLastestOffsetID = (type: MomentType, offsetID: number) => {
|
||||
if (offsetID === null || offsetID === 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)
|
||||
}
|
||||
|
||||
export const isNewVideo = (currentOffsetID: number): boolean => compareOffsetID(currentOffsetID, getVideoOffsetID())
|
||||
export const isNewArticle = (currentOffsetID: number): boolean => compareOffsetID(currentOffsetID, getArticleOffsetID())
|
||||
1
src/types/index.ts
Normal file
1
src/types/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './topbar/moments'
|
||||
27
src/types/topbar/moments.ts
Normal file
27
src/types/topbar/moments.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export enum MomentType {
|
||||
Video = 8,
|
||||
Article = 64,
|
||||
Bangumi = 512,
|
||||
PGC = 4097,
|
||||
Movie = 4098,
|
||||
TvShow = 4099,
|
||||
ChineseAnime = 4100,
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user