This commit is contained in:
Hakadao
2022-12-08 00:11:13 +08:00
22 changed files with 1623 additions and 921 deletions

View File

@@ -6,6 +6,9 @@
<p align="center">Bringing you the suggested videos on the bilibili homepage, clean and simple.</p>
<details>
<summary>開發中, 佛系更新 = =</summary>
<p align="center">
<img width="655" src="https://user-images.githubusercontent.com/33394391/160250313-6a3db903-53c5-431a-8ddd-80a50725087a.png"><br/>
</p>
@@ -72,3 +75,5 @@ And then pack files under `extension`
[Bilibili-Evolved](https://github.com/the1812/Bilibili-Evolved)
[bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect)
</details>

View File

@@ -35,6 +35,7 @@
"@types/webextension-polyfill": "^0.8.3",
"@typescript-eslint/eslint-plugin": "^5.25.0",
"@vitejs/plugin-vue": "^2.3.3",
"@volar-plugins/vetur": "^0.1.0",
"@vue/compiler-sfc": "^3.2.35",
"@vue/test-utils": "^2.0.0",
"@vueuse/core": "^8.5.0",

575
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ import { API_URL } from '.'
export const setupHistoryAPIs = () => {
browser.runtime.onMessage.addListener((message) => {
if (message.contentScriptQuery === 'getHistoryList') {
const url = `https://${API_URL}/x/web-interface/history/cursor`
const url = `${API_URL}/x/web-interface/history/cursor?ps=20&type=${message.type}&view_at=${message.viewAt}`
return fetch(url)
.then(response => response.json())
.then(data => (data))

View File

@@ -4,6 +4,7 @@ import { setupUserAPIs } from './user'
import { setupSearchAPIs } from './search'
import { setupNotificationsAPIs } from './notifications'
import { setupMomentsAPIs } from './moments'
import { setupHistoryAPIs } from './history'
export const APP_URL = 'https://app.bilibili.com'
export const API_URL = 'https://api.bilibili.com'
@@ -15,4 +16,5 @@ export const setupAllAPIs = () => {
setupSearchAPIs()
setupNotificationsAPIs()
setupMomentsAPIs()
setupHistoryAPIs()
}

View File

@@ -81,6 +81,7 @@ browser.tabs.onUpdated.addListener((tabId: number, changInfo: Tabs.OnUpdatedChan
css: `
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
@@ -95,6 +96,10 @@ browser.tabs.onUpdated.addListener((tabId: number, changInfo: Tabs.OnUpdatedChan
background-color: var(--bew-fill-3);
border-radius: 20px;
}
::-webkit-scrollbar-corner {
background: var(--bew-bg)
}
`,
// runAt: 'document_start',
target: { tabId },

View File

@@ -17,7 +17,7 @@ const imgURL = browser.runtime.getURL('/assets/loading.gif')
w="46px"
h="46px"
m="r-2"
/>
>
{{ $t('common.loading') }}
</div>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
defineProps<{
percentage: number
}>()
</script>
<template>
<div
bg="$bew-theme-color"
h="6px"
border="rounded-$bew-radius"
:style="{ width: `${percentage}%` }"
/>
</template>
<style lang="scss" scoped></style>

View File

@@ -1,250 +1,183 @@
<script lang="ts">
import { isNewArticle, isNewVideo, setLastestOffsetID } from './notify'
import { language } from '~/logic'
import { getUserID, calcTimeSince } from '~/utils'
import { MomentItem, MomentType, LanguageType } from '~/types'
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import type { Ref } from 'vue'
import { onMounted, reactive, ref, watch } from 'vue'
import { useDateFormat } from '@vueuse/core'
import type { HistoryItem } from './types'
import { HistoryType } from './types'
import { calcCurrentTime } from '~/utils'
const { t } = useI18n()
export default defineComponent({
data() {
return {
moments: [] as MomentItem[],
calcTimeSince,
MomentType,
momentTabs: [
{
id: 0,
name: this.$t('topbar.moments_dropdown.tabs.videos'),
isSelected: true,
},
{
id: 1,
name: this.$t('topbar.moments_dropdown.tabs.live'),
isSelected: false,
},
{
id: 2,
name: this.$t('topbar.moments_dropdown.tabs.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,
LanguageType,
language,
}
const historys = reactive<Array<HistoryItem>>([])
const historyTabs = reactive([
{
id: 0,
name: t('topbar.moments_dropdown.tabs.videos'),
isSelected: true,
},
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])
}
},
{
id: 1,
name: t('topbar.moments_dropdown.tabs.live'),
isSelected: false,
},
mounted() {
this.getNewMoments([MomentType.Video, MomentType.Bangumi])
{
id: 2,
name: t('topbar.moments_dropdown.tabs.articles'),
isSelected: false,
},
])
/**
* Active tab (0: archive, 1: live, 2: article)
*/
const activatedTab = ref<number>(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 livePage = ref<number>(1)
const historysWrap = ref<HTMLElement>() as Ref<HTMLElement>
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])
watch(activatedTab, (newVal: number, oldVal: number) => {
if (newVal === oldVal)
return
historys.length = 0
if (historysWrap.value)
scrollToTop(historysWrap.value, 300)
if (newVal === 0) {
getHistoryList(HistoryType.Archive)
}
else if (newVal === 1) {
livePage.value = 1
getHistoryList(HistoryType.Live)
}
else if (newVal === 2) {
getHistoryList(HistoryType.Article)
}
})
onMounted(() => {
getHistoryList(HistoryType.Archive)
if (historysWrap.value) {
historysWrap.value.addEventListener('scroll', () => {
// When you scroll to the bottom, they will automatically
// add the next page of data to the history list
if (
historysWrap.value.clientHeight + historysWrap.value.scrollTop
>= historysWrap.value.scrollHeight - 20
&& historys.length > 0
&& !isLoading.value
) {
if (activatedTab.value === 0 && !noMoreContent.value) {
getHistoryList(
HistoryType.Archive,
historys[historys.length - 1].view_at,
)
}
else if (activatedTab.value === 1 && !noMoreContent.value) {
getHistoryList(
HistoryType.Live,
historys[historys.length - 1].view_at,
)
}
else if (activatedTab.value === 2 && !noMoreContent.value) {
getHistoryList(
HistoryType.Article,
historys[historys.length - 1].view_at,
)
}
}
})
},
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)
},
},
}
})
function onClickTab(tabId: number) {
// Prevent changing tab when loading, cuz it will cause a bug
if (isLoading.value)
return
activatedTab.value = tabId
historyTabs.forEach((tab) => {
tab.isSelected = tab.id === tabId
})
}
/**
* Return the URL of the history item
* @param item history item
* @return {string} url
*/
function getHistoryUrl(item: HistoryItem) {
// Video
if (activatedTab.value === 0)
return item.history.bvid
// Live
else if (activatedTab.value === 1)
return `//live.bilibili.com/${item.history.oid}`
// Article
else if (activatedTab.value === 2)
return `/read/cv${item.history.oid}`
return ''
}
/**
* Get history list
* @param {HistoryType} type
* @param {number} viewAt Last viewed timestamp
*/
function getHistoryList(type: HistoryType, viewAt = 0 as number) {
isLoading.value = true
browser.runtime
.sendMessage({
contentScriptQuery: 'getHistoryList',
type,
viewAt,
})
.then((res) => {
if (res.code === 0) {
if (historys.length !== 0 && res.data.list.length < 20) {
isLoading.value = false
noMoreContent.value = true
return
}
res.data.list.forEach((item: HistoryItem) => {
historys.push(item)
})
noMoreContent.value = false
}
isLoading.value = false
})
}
/**
* smooth scroll to the top of the html element
*/
function 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>
@@ -263,13 +196,13 @@ export default defineComponent({
pos="fixed top-0 left-0"
w="full"
bg="$bew-content-1"
z="1"
z="2"
border="!rounded-t-$bew-radius"
style="backdrop-filter: var(--bew-filter-glass)"
>
<div flex="~">
<div
v-for="tab in momentTabs"
v-for="tab in historyTabs"
:key="tab.id"
m="r-4"
transition="all duration-300"
@@ -286,73 +219,176 @@ export default defineComponent({
</a>
</div>
<!-- moments wrapper -->
<div ref="momentsWrap" h="430px" overflow="y-scroll" p="x-4">
<!-- historys wrapper -->
<div
ref="historysWrap"
flex="~ col gap-4"
h="430px"
overflow="y-scroll"
p="x-4"
>
<!-- loading -->
<loading v-if="isLoading && moments.length === 0" h="full" flex="~" items="center"></loading>
<Loading
v-if="isLoading && historys.length === 0"
pos="absolute left-0"
bg="$bew-content-1"
z="1"
w="full"
h="full"
flex="~"
items="center"
border="rounded-$bew-radius"
/>
<!-- empty -->
<empty v-if="!isLoading && moments.length === 0" w="full" h="full"></empty>
<Empty v-if="!isLoading && historys.length === 0" w="full" h="full" />
<!-- moments -->
<!-- historys -->
<transition-group name="list">
<a
v-for="(moment, index) in moments"
:key="index"
:href="moment.url"
v-for="historyItem in historys"
:key="historyItem.kid"
:href="getHistoryUrl(historyItem)"
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"
border="rounded-$bew-radius"
p="2"
m="first:t-50px last:b-4"
class="group"
transition="duration"
>
<!-- 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> {{ $t('topbar.moments_dropdown.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)) }}{{ language === LanguageType.English ? ' ' + $t('common.ago') : $t('common.ago') }}
<section flex="~ gap-4" align="item-start">
<!-- Video cover, live cover, ariticle cover -->
<div
bg="$bew-fill-1"
w="150px"
flex="shrink-0"
border="rounded-$bew-radius-half"
overflow="hidden"
>
<!-- Video -->
<template v-if="activatedTab === 0">
<div pos="relative">
<img
w="150px"
class="aspect-video"
:src="`${historyItem.cover}@256w_144h_1c`"
:alt="historyItem.title"
bg="contain"
>
<div
pos="absolute bottom-0 right-0"
bg="black opacity-60"
m="1"
p="x-2 y-1"
text="white xs"
border="rounded-full"
opacity="0"
group-hover:opacity="100"
>
{{
`${calcCurrentTime(historyItem.progress)} /
${calcCurrentTime(historyItem.duration)}`
}}
</div>
</div>
<!-- Live -->
<div v-else-if="selectedTab === 1" text="$bew-theme-color" font="bold" flex="~" items="center">
<fluent:live-24-filled m="r-2" /> {{ $t('topbar.moments_dropdown.live_status') }}
<Progress
:percentage="
(historyItem.progress / historyItem.duration) * 100
"
/>
</template>
<!-- Live -->
<template v-else-if="activatedTab === 1">
<div pos="relative">
<img
w="150px"
class="aspect-video"
:src="`${historyItem.cover}@256w_144h_1c`"
:alt="historyItem.title"
bg="contain"
>
<div
v-if="historyItem.live_status === 1"
pos="absolute top-0 left-0"
bg="rose-600"
text="xs white"
p="x-2 y-1"
m="1"
border="rounded-$bew-radius-half"
font="semibold"
>
LIVE
</div>
<div
v-else
pos="absolute top-0 left-0"
bg="gray-500 opacity-55"
text="xs white"
p="x-2 y-1"
m="1"
border="rounded-$bew-radius-half"
>
Offline
</div>
</div>
</template>
<!-- Article -->
<div v-else-if="activatedTab === 2">
<img
w="150px"
class="aspect-video"
:src="`${
Array.isArray(historyItem.covers)
? historyItem.covers[0]
: ''
}@256w_144h_1c`"
:alt="historyItem.title"
bg="contain"
>
</div>
</div>
<img :src="moment.cover + '@128w_72h_1c'" w="82px" h="46px" m="l-4" rounded="$bew-radius" />
</div>
<!-- Description -->
<div>
<h3
style="
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
display: -webkit-box;
"
overflow="hidden"
text="overflow-ellipsis"
>
{{ historyItem.title }}
</h3>
<div text="$bew-text-2 sm" m="t-4" flex="~" align="items-center">
{{ historyItem.author_name }}
<span
v-if="historyItem.live_status === 1"
text="$bew-theme-color"
m="l-2"
><tabler:live-photo />
Live
</span>
</div>
<p text="$bew-text-2 sm">
{{
useDateFormat(
historyItem.view_at * 1000,
'YYYY-MM-DD HH:mm:ss',
).value
}}
</p>
</div>
</section>
</a>
</transition-group>
<!-- loading -->
<loading v-if="isLoading && moments.length !== 0" m="-t-4"></loading>
<loading v-if="isLoading && historys.length !== 0" m="-t-4" />
</div>
</div>
</template>

View File

@@ -1,255 +1,269 @@
<script lang="ts">
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import type { Ref, UnwrapNestedRefs } 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 { language } from '~/logic'
import { calcTimeSince, getUserID } from '~/utils'
import type { MomentItem } from '~/types'
import { LanguageType, MomentType } from '~/types'
import { LanguageType } from '~/enums/appEnums'
const { t } = useI18n()
export default defineComponent({
data() {
return {
moments: [] as MomentItem[],
calcTimeSince,
MomentType,
momentTabs: [
{
id: 0,
name: this.$t('topbar.moments_dropdown.tabs.videos'),
isSelected: true,
},
{
id: 1,
name: this.$t('topbar.moments_dropdown.tabs.live'),
isSelected: false,
},
{
id: 2,
name: this.$t('topbar.moments_dropdown.tabs.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,
LanguageType,
language,
}
const moments = reactive<Array<MomentItem>>([]) as UnwrapNestedRefs<
Array<MomentItem>
>
const momentTabs = reactive([
{
id: 0,
name: t('topbar.moments_dropdown.tabs.videos'),
isSelected: true,
},
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])
}
},
{
id: 1,
name: t('topbar.moments_dropdown.tabs.live'),
isSelected: false,
},
mounted() {
this.getNewMoments([MomentType.Video, MomentType.Bangumi])
{
id: 2,
name: t('topbar.moments_dropdown.tabs.articles'),
isSelected: false,
},
])
const selectedTab = ref<number>(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 livePage = ref<number>(1)
const momentsWrap = ref<HTMLElement>() as Ref<HTMLElement>
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])
watch(selectedTab, (newVal: number, oldVal: number) => {
if (newVal === oldVal)
return
if (momentsWrap.value)
scrollToTop(momentsWrap.value, 300)
moments.length = 0
if (newVal === 0) {
getNewMoments([MomentType.Video, MomentType.Bangumi])
}
else if (newVal === 1) {
livePage.value = 1
getLiveMoments(livePage.value)
}
else if (newVal === 2) {
getNewMoments([MomentType.Article])
}
})
onMounted(() => {
getNewMoments([MomentType.Video, MomentType.Bangumi])
if (momentsWrap.value) {
momentsWrap.value.addEventListener('scroll', () => {
if (
momentsWrap.value.clientHeight + momentsWrap.value.scrollTop
>= momentsWrap.value.scrollHeight - 20
&& moments.length > 0
&& !isLoading.value
) {
if (selectedTab.value === 0 && !noMoreContent.value)
getHistoryMoments([MomentType.Video, MomentType.Bangumi])
else if (selectedTab.value === 1 && !noMoreContent.value)
getLiveMoments(livePage.value)
else if (selectedTab.value === 2 && !noMoreContent.value)
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)
},
},
}
})
function onClickTab(tabId: number) {
// Prevent changing tab when loading, cuz it will cause a bug
if (isLoading.value)
return
selectedTab.value = tabId
momentTabs.forEach((tab) => {
tab.isSelected = tab.id === tabId
})
}
function getNewMoments(typeList: number[]) {
isLoading.value = true
browser.runtime
.sendMessage({
contentScriptQuery: 'getNewMoments',
uid: getUserID(),
typeList,
})
.then((res) => {
if (res.code === 0) {
if (moments.length !== 0 && res.data.cards.length < 20) {
isLoading.value = false
noMoreContent.value = true
return
}
res.data.cards.forEach((item: any) => {
pushItemIntoMoments(item)
})
// 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
}
isLoading.value = false
})
}
function getHistoryMoments(typeList: number[]) {
isLoading.value = true
browser.runtime
.sendMessage({
contentScriptQuery: 'getHistoryMoments',
uid: getUserID(),
typeList,
offsetDynamicID: moments[moments.length - 1].dynamic_id_str,
})
.then((res) => {
if (res.code === 0) {
if (res.data.has_more === 0) {
isLoading.value = false
noMoreContent.value = true
return
}
res.data.cards.forEach((item: any) => {
pushItemIntoMoments(item)
})
noMoreContent.value = false
}
isLoading.value = false
})
}
function getLiveMoments(page: number) {
isLoading.value = 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 (moments.length !== 0 && res.data.list.length < 10) {
isLoading.value = false
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)
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,
title: item.title,
cover: item.pic,
} as MomentItem)
})
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,
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)
}
}
/**
* smooth scroll to the top of the html element
*/
function 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>
@@ -294,10 +308,25 @@ export default defineComponent({
<!-- 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
v-if="isLoading && moments.length === 0"
h="full"
flex="~"
items="center"
/>
<!-- empty -->
<empty v-if="!isLoading && moments.length === 0" w="full" h="full" />
<empty
v-if="!isLoading && moments.length === 0"
pos="absolute left-0"
bg="$bew-content-1"
z="1"
w="full"
h="full"
flex="~"
items="center"
border="rounded-$bew-radius"
/>
<!-- moments -->
<transition-group name="list">
@@ -330,28 +359,66 @@ export default defineComponent({
/>
<a
:href="moment.type === MomentType.Video ? `https://space.bilibili.com/${moment.uid}` : moment.url"
: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">
<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> {{ $t('topbar.moments_dropdown.uploaded') }}{{ moment.title }}
<div v-if="moment.type !== MomentType.Bangumi" text="$bew-text-2 sm" m="y-2">
<span>{{ `${moment.name} ${t('topbar.moments_dropdown.uploaded')}` }}</span>
<div overflow="hidden" text="overflow-ellipsis">
{{ 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">
{{ calcTimeSince(new Date(moment.ctime * 1000)) }}{{ language === LanguageType.English ? ` ${$t('common.ago')}` : $t('common.ago') }}
{{
moment.ctime
? calcTimeSince(new Date(moment.ctime * 1000))
: moment.ctime
}}{{
language === LanguageType.English
? ` ${$t('common.ago')}`
: $t('common.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" /> {{ $t('topbar.moments_dropdown.live_status') }}
<div
v-else-if="selectedTab === 1"
text="$bew-theme-color"
font="bold"
flex="~"
items="center"
>
<fluent:live-24-filled m="r-2" />
{{ $t('topbar.moments_dropdown.live_status') }}
</div>
</div>
</div>
<img :src="`${moment.cover}@128w_72h_1c`" w="82px" h="46px" m="l-4" rounded="$bew-radius">
<img
:src="`${moment.cover}@128w_72h_1c`"
w="82px"
h="46px"
m="l-4"
rounded="$bew-radius"
>
</div>
</a>
</transition-group>

View File

@@ -1,124 +1,142 @@
<script lang="ts">
<script setup lang="ts">
import type { Ref, UnwrapNestedRefs } from 'vue'
import { Transition, onMounted, watch } from 'vue'
import type { UnReadDm, UnReadMessage, UserInfo } from '../topbar/types'
import MomentsDropdown from './MomentsDropdown.vue'
import HistoryDropdown from './HistoryDropdown.vue'
import { updateInterval } from './notify'
import { getUserID } from '~/utils'
export default defineComponent({
components: { MomentsDropdown },
data() {
return {
mid: getUserID() || '',
userInfo: {} as any,
showLogoDropDown: false,
showUserPanel: false,
showTopbarMask: false,
showNotificationsDropDown: false,
showMomentsDropDown: false,
showUploadDropDown: false,
isLogin: !!getUserID(),
unReadmessage: {},
unReadDm: {},
unReadmessageCount: 0,
newMomentsCount: 0,
}
const mid = getUserID() || ''
const userInfo = reactive<UserInfo | {}>({}) as UnwrapNestedRefs<UserInfo>
const showLogoDropDown = ref<boolean>(false)
const showUserPanel = ref<boolean>(false)
const showTopbarMask = ref<boolean>(false)
const showNotificationsDropDown = ref<boolean>(false)
const showMomentsDropDown = ref<boolean>(false)
const showUploadDropDown = ref<boolean>(false)
const showHistoryDropDown = ref<boolean>(false)
const isLogin = ref<boolean>(!!getUserID())
const unReadMessage = reactive<UnReadMessage | {}>({}) as UnwrapNestedRefs<UnReadMessage>
const unReadDm = reactive<UnReadDm | {}>({} as UnwrapNestedRefs<UnReadDm>)
const unReadMessageCount = ref<number>(0)
const newMomentsCount = ref<number>(0)
const logo = ref<HTMLElement>() as Ref<HTMLElement>
const avatarImg = ref<HTMLImageElement>() as Ref<HTMLImageElement>
const avatarShadow = ref<HTMLImageElement>() as Ref<HTMLImageElement>
watch(
() => showNotificationsDropDown,
(newValue, oldValue) => {
getUnreadMessageCount()
},
watch: {
showNotificationsDropDown(val) {
this.getUnreadMessageCount()
},
showMomentsDropDown(val) {
this.getNewMomentsCount()
},
)
watch(
() => showMomentsDropDown,
(newValue, oldValue) => {
getNewMomentsCount()
},
mounted() {
this.initUserPanel()
)
document.addEventListener('scroll', () => {
if (window.scrollY > 0)
this.showTopbarMask = true
else
this.showTopbarMask = false
})
},
methods: {
initUserPanel() {
this.getUserInfo()
this.getUnreadMessageCount()
this.getNewMomentsCount()
onMounted(() => {
initUserPanel()
// automatically update notifications and moments count
setInterval(() => {
this.getUnreadMessageCount()
this.getNewMomentsCount()
}, updateInterval)
},
showLogoMenuDropdown() {
const logo = this.$refs.logo as HTMLElement
logo.classList.add('hover')
this.showLogoDropDown = true
},
closeLogoMenuDropdown() {
const logo = this.$refs.logo as HTMLElement
logo.classList.remove('hover')
this.showLogoDropDown = false
},
openUserPanel() {
this.showUserPanel = true
const avatarImg = this.$refs.avatarImg as HTMLImageElement
const avatarShadow = this.$refs.avatarShadow as HTMLImageElement
avatarImg.classList.add('hover')
avatarShadow.classList.add('hover')
},
closeUserPanel() {
this.showUserPanel = false
const avatarImg = this.$refs.avatarImg as HTMLImageElement
const avatarShadow = this.$refs.avatarShadow as HTMLImageElement
avatarImg.classList.remove('hover')
avatarShadow.classList.remove('hover')
},
getUserInfo() {
browser.runtime
.sendMessage({
contentScriptQuery: 'getUserInfo',
})
.then((res) => {
if (res.code === 0)
this.userInfo = res.data
})
},
async getUnreadMessageCount() {
if (!this.isLogin)
return
await browser.runtime
.sendMessage({
contentScriptQuery: 'getUnreadMsg',
}).then(res => this.unReadmessage = res.data)
await browser.runtime
.sendMessage({
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))
this.unReadmessageCount += parseInt(`${value}`)
},
getNewMomentsCount() {
if (!this.isLogin)
return
browser.runtime
.sendMessage({
contentScriptQuery: 'getNewMomentsCount',
}).then((res) => {
this.newMomentsCount = res.data.update_info.item.count
})
},
},
document.addEventListener('scroll', () => {
if (window.scrollY > 0)
showTopbarMask.value = true
else showTopbarMask.value = false
})
})
function initUserPanel() {
getUserInfo()
getUnreadMessageCount()
getNewMomentsCount()
// automatically update notifications and moments count
setInterval(() => {
getUnreadMessageCount()
getNewMomentsCount()
}, updateInterval)
}
function showLogoMenuDropdown() {
logo.value.classList.add('hover')
showLogoDropDown.value = true
}
function closeLogoMenuDropdown() {
logo.value.classList.remove('hover')
showLogoDropDown.value = false
}
function openUserPanel() {
showUserPanel.value = true
avatarImg.value.classList.add('hover')
avatarShadow.value.classList.add('hover')
}
function closeUserPanel() {
showUserPanel.value = false
avatarImg.value.classList.remove('hover')
avatarShadow.value.classList.remove('hover')
}
function getUserInfo() {
browser.runtime
.sendMessage({
contentScriptQuery: 'getUserInfo',
})
.then((res) => {
if (res.code === 0)
Object.assign(userInfo, res.data)
})
}
async function getUnreadMessageCount() {
if (!isLogin)
return
await browser.runtime
.sendMessage({
contentScriptQuery: 'getUnreadMsg',
})
.then((res) => {
Object.assign(unReadMessage, res.data)
})
await browser.runtime
.sendMessage({
contentScriptQuery: 'getUnreadDm',
})
.then(res => Object.assign(unReadDm, res.data))
unReadMessageCount.value = 0
Object.keys(unReadMessage).forEach((key) => {
if (key !== 'up')
unReadMessageCount.value += unReadMessage[key as keyof typeof unReadMessage]
})
Object.keys(unReadDm).forEach((key) => {
unReadMessageCount.value += unReadDm[key as keyof typeof unReadDm]
})
}
function getNewMomentsCount() {
if (!isLogin)
return
browser.runtime
.sendMessage({
contentScriptQuery: 'getNewMomentsCount',
})
.then((res) => {
newMomentsCount.value = res.data.update_info.item.count
})
}
</script>
<template>
@@ -129,7 +147,7 @@ export default defineComponent({
p="lg:x-23 <lg:x-16 y-2"
w="screen"
>
<transition name="topbar">
<Transition name="topbar">
<div
v-show="showTopbarMask"
class="fixed top-0 left-0"
@@ -138,18 +156,15 @@ export default defineComponent({
opacity="100"
pointer="none"
style="background: linear-gradient(var(--bew-bg), transparent)"
></div>
</transition>
/>
</Transition>
<div
class="left-side"
@mouseenter.self="showLogoMenuDropdown()"
@mouseleave.self="closeLogoMenuDropdown()"
>
<div
ref="logo"
class="logo"
>
<div ref="logo" class="logo">
<svg
t="1645466458357"
class="icon"
@@ -166,24 +181,25 @@ export default defineComponent({
/>
</svg>
<tabler:chevron-down w="!4" h="!4" m="l-4" icon="stroke-4 fill-$bew-text-1" />
<tabler:chevron-down
w="!4"
h="!4"
m="l-4"
icon="stroke-4 fill-$bew-text-1"
/>
</div>
<transition name="slide">
<logo-menu-dropdown
<Transition name="slide">
<LogoMenuDropdown
v-if="showLogoDropDown"
class="bew-popover"
pos="!left-0 !top-70px"
transform="!translate-x-0"
></logo-menu-dropdown>
</transition>
/>
</Transition>
</div>
<!-- search bar -->
<div
flex="~"
w="full"
justify="md:center <md:end"
>
<div flex="~" w="full" justify="md:center <md:end">
<!-- <button
class="icon-btn"
text="$bew-text-1 2xl"
@@ -192,16 +208,16 @@ export default defineComponent({
>
<tabler:search />
</button> -->
<search-bar
ref="searchBar"
></search-bar>
<SearchBar ref="searchBar" />
</div>
<!-- right content -->
<div class="right-side">
<div v-if="!isLogin" class="right-side-item">
<a href="https://passport.bilibili.com/login" class="login">
<ic-outline-account-circle class="text-base mr-2" />{{ $t('topbar.sign_in') }}
<ic:outline-account-circle class="text-base mr-2" />{{
$t('topbar.sign_in')
}}
</a>
</div>
<template v-if="isLogin">
@@ -214,27 +230,37 @@ export default defineComponent({
<a
id="avatar-img"
ref="avatarImg"
:href="'https://space.bilibili.com/' + mid"
:href="`https://space.bilibili.com/${mid}`"
target="_blank"
class="rounded-full z-1 w-40px h-40px bg-$bew-fill-3 bg-cover bg-center"
:style="{ backgroundImage: `url(${(userInfo.face + '').replace('http:', '')})` }"
></a>
:style="{
backgroundImage: `url(${`${userInfo.face}`.replace(
'http:',
'',
)})`,
}"
/>
<div
id="avatar-shadow"
ref="avatarShadow"
class="absolute top-0 rounded-full z-0 w-40px h-40px filter blur-sm bg-cover bg-center"
opacity="80"
:style="{ backgroundImage: `url(${(userInfo.face + '').replace('http:', '')})` }"
></div>
<transition name="slide">
<user-panel-dropdown
:style="{
backgroundImage: `url(${`${userInfo.face}`.replace(
'http:',
'',
)})`,
}"
/>
<Transition name="slide">
<UserPanelDropdown
v-if="showUserPanel"
ref="userPanelDropdown"
:user-info="userInfo"
after:h="!0"
class="bew-popover"
></user-panel-dropdown>
</transition>
/>
</Transition>
</div>
<!-- Notifications -->
@@ -243,11 +269,8 @@ export default defineComponent({
@mouseenter="showNotificationsDropDown = true"
@mouseleave="showNotificationsDropDown = false"
>
<div
v-if="unReadmessageCount !== 0"
class="unread-message"
>
{{ unReadmessageCount > 999 ? '999+' : unReadmessageCount }}
<div v-if="unReadMessageCount !== 0" class="unread-message">
{{ unReadMessageCount > 999 ? '999+' : unReadMessageCount }}
</div>
<a
href="https://message.bilibili.com"
@@ -257,13 +280,13 @@ export default defineComponent({
<tabler:bell />
</a>
<transition name="slide">
<notifications-dropdown
<Transition name="slide">
<NotificationsDropdown
v-if="showNotificationsDropDown"
ref="notificationsDropdown"
class="bew-popover"
></notifications-dropdown>
</transition>
/>
</Transition>
</div>
<!-- Moments -->
@@ -272,10 +295,7 @@ export default defineComponent({
@mouseenter="showMomentsDropDown = true"
@mouseleave="showMomentsDropDown = false"
>
<div
v-if="newMomentsCount !== 0"
class="unread-message"
>
<div v-if="newMomentsCount !== 0" class="unread-message">
{{ newMomentsCount > 999 ? '999+' : newMomentsCount }}
</div>
<a
@@ -286,19 +306,15 @@ export default defineComponent({
<tabler:windmill />
</a>
<transition name="slide">
<moments-dropdown
v-if="showMomentsDropDown"
class="bew-popover"
>
</moments-dropdown>
</transition>
<Transition name="slide">
<MomentsDropdown v-if="showMomentsDropDown" class="bew-popover" />
</Transition>
</div>
<!-- Favorites -->
<div class="right-side-item">
<a
:href="'https://space.bilibili.com/' + mid + '/favlist'"
:href="`https://space.bilibili.com/${mid}/favlist`"
target="_blank"
:title="$t('topbar.favorites')"
>
@@ -307,7 +323,11 @@ export default defineComponent({
</div>
<!-- History -->
<div class="right-side-item">
<div
class="right-side-item"
@mouseenter="showHistoryDropDown = true"
@mouseleave="showHistoryDropDown = false"
>
<a
href="https://www.bilibili.com/account/history"
target="_blank"
@@ -315,6 +335,10 @@ export default defineComponent({
>
<tabler:clock />
</a>
<Transition name="slide">
<HistoryDropdown v-if="showHistoryDropDown" class="bew-popover" />
</Transition>
</div>
<!-- Createive center -->
@@ -343,20 +367,19 @@ export default defineComponent({
p="xl:auto <xl:unset"
>
<tabler:upload />
<span
m="l-2"
display="xl:block <xl:hidden"
>{{ $t('topbar.upload') }}</span>
<span m="l-2" display="xl:block <xl:hidden">{{
$t('topbar.upload')
}}</span>
</a>
<transition name="slide">
<upload-dropdown
<Transition name="slide">
<UploadDropdown
v-if="showUploadDropDown"
class="bew-popover"
pos="!left-auto !-right-2"
transform="!translate-x-0"
></upload-dropdown>
</transition>
/>
</Transition>
</div>
</template>
</div>
@@ -451,7 +474,7 @@ export default defineComponent({
@apply duration-300;
&.hover {
@apply transform scale-230 translate-y-4;
@apply transform scale-230 "!translate-y-30px";
}
}

View File

@@ -1,5 +1,5 @@
import { MomentType } from '~/types'
import { getUserID, getCookie, setCookie } from '~/utils'
import { MomentType } from './types'
import { getCookie, getUserID, setCookie } from '~/utils'
/** Update the time interval of topbar notifications and moments counts */
export const updateInterval = 1000 * 60 * 5 // Updated every 5 minutes

View File

@@ -0,0 +1,104 @@
export interface UserInfo {
face: string // avatar
level_info: {
current_level: number
current_min: number
current_exp: number
next_exp: string
}
mid: number
money: number // 硬幣
uname: string // username
wallet: {
mid: number
bcoin_balance: number // b幣
}
}
/**
* Number of follower, following and published posts by user
*/
export interface UserStat {
dynamic_count: number
follower: number
following: number
}
// https://github.com/SocialSisterYi/bilibili-API-collect/blob/63da4454309e2599269125e24a6940b1feecedef/message/msg.md#%E6%9C%AA%E8%AF%BB%E6%B6%88%E6%81%AF%E6%95%B0
export interface UnReadMessage {
at: number
chat: number
like: number
reply: number
sys_msg: number
up: number
}
export interface UnReadDm {
// https://api.vc.bilibili.com/session_svr/v1/session_svr/single_unread?build=0&mobi_app=web&unread_type=0
unfollow_unread: number
follow_unread: number
unfollow_push_msg: number
dustbin_push_msg: number
dustbin_unread: number
biz_msg_unfollow_unread: number
biz_msg_follow_unread: number
}
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
}
// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/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稿件
PGC = 'pgc', // pgc剧集 (番剧 / 影视)
Live = 'live', // live直播
ArticleList = 'article-list', // article-list文集
Article = 'article', // article文章
}
export interface HistoryItem {
title: string
cover: string
covers?: Array<string>
history: {
business: HistoryType
epid?: number
bvid?: string
part?: string
oid: number
}
author_name: string
author_face: string
author_mid: string
view_at: number
progress: number
duration: number
kid: number
live_status: 0 | 1 // 0未开播 1已开播
}

View File

@@ -1,9 +1,40 @@
<script setup lang="ts">
import { useDark, useToggle } from '@vueuse/core'
import { apperance, isShowTopbar } from '~/logic/storage'
import { language } from '~/logic'
import 'virtual:windi.css'
import '~/styles/index.ts'
import Home from './home/Home.vue'
import { useI18n } from 'vue-i18n'
const [showSettings, toggle] = useToggle(false)
const isDark = useDark()
const toggleDark = useToggle(isDark)
const { locale } = useI18n()
window.onload = () => {
// if there is first-time load extension, set the default language by browser display language
if (language.value === '') {
if (browser.i18n.getUILanguage() === 'zh-CN')
language.value = 'cmn-SC'
else if (browser.i18n.getUILanguage() === 'zh-TW')
language.value = 'cmn-TC'
else
language.value = 'en'
}
locale.value = language.value
// locale.value = language.value
}
</script>
<template>
<transition>
<topbar v-if="isShowTopbar" class="fixed z-50"></topbar>
</transition>
<Transition>
<Topbar v-if="isShowTopbar" class="fixed z-50" />
</Transition>
<!-- is home page -->
<home></home>
<Home />
<!-- button -->
<div
flex="~ col"
@@ -40,44 +71,12 @@
style="box-shadow: var(--bew-shadow-2); backdrop-filter: var(--bew-filter-glass);"
@click="toggle()"
>
<tabler-settings />
<tabler:settings />
</button>
</div>
<settings v-if="showSettings" @close="showSettings = false"></settings>
<Settings v-if="showSettings" @close="showSettings = false" />
</template>
<script setup lang="ts">
import { useToggle, useDark } from '@vueuse/core'
import { apperance, isShowTopbar } from '~/logic/storage'
import { language } from '~/logic'
import 'virtual:windi.css'
import '~/styles/index.ts'
import Home from './Home/index.vue'
import { useI18n } from 'vue-i18n'
const [showSettings, toggle] = useToggle(false)
const isDark = useDark()
const toggleDark = useToggle(isDark)
const { locale } = useI18n()
window.onload = () => {
// if there is first-time load extension, set the default language by browser display language
if (language.value === '') {
if (browser.i18n.getUILanguage() === 'zh-CN')
language.value = 'cmn-SC'
else if (browser.i18n.getUILanguage() === 'zh-TW')
language.value = 'cmn-TC'
else
language.value = 'en'
}
locale.value = language.value
// locale.value = language.value
}
</script>
<style lang="scss">
.v-enter-active,
.v-leave-active {
@@ -88,5 +87,4 @@ window.onload = () => {
.v-leave-to {
@apply opacity-0 transform -translate-y-full;
}
</style>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { accessKey, language } from '~/logic/index'
import { calcCurrentTime, calcTimeSince, numFormatter } from '~/utils'
import { LanguageType } from '~/types'
import { LanguageType } from '~/enums/appEnums'
export default defineComponent({
data() {
@@ -25,13 +25,17 @@ export default defineComponent({
}, 2000)
window.onscroll = async () => {
if ((window.innerHeight + window.scrollY) >= document.body.scrollHeight) {
if (window.innerHeight + window.scrollY >= document.body.scrollHeight) {
if (!this.isLoading)
return
if (this.videoList.length <= (this.MAX_LIMIT - 20)) {
await this.getRecommendVideos(this.videoList[this.videoList.length - 1]?.idx)
await this.getRecommendVideos(this.videoList[this.videoList.length - 1]?.idx)
if (this.videoList.length <= this.MAX_LIMIT - 20) {
await this.getRecommendVideos(
this.videoList[this.videoList.length - 1]?.idx,
)
await this.getRecommendVideos(
this.videoList[this.videoList.length - 1]?.idx,
)
}
else {
this.isLoading = false
@@ -41,12 +45,11 @@ export default defineComponent({
},
methods: {
async getRecommendVideos(idx?: number) {
const response = await browser.runtime
.sendMessage({
contentScriptQuery: 'getRecommendVideos',
idx,
accessKey: this.accessKey,
})
const response = await browser.runtime.sendMessage({
contentScriptQuery: 'getRecommendVideos',
idx,
accessKey: this.accessKey,
})
if (response.code === 0) {
const resData = [] as any[]
@@ -72,7 +75,15 @@ export default defineComponent({
gotoVideo(uri: string) {
window.open(`/video/av${uri.split('/')[3]}`)
},
submitDislike(videoIndex: number, reasonID: number, goto: string, id: string, mid: string, rid: string, tagID: string) {
submitDislike(
videoIndex: number,
reasonID: number,
goto: string,
id: string,
mid: string,
rid: string,
tagID: string,
) {
browser.runtime
.sendMessage({
contentScriptQuery: 'submitDislike',
@@ -91,7 +102,15 @@ export default defineComponent({
}
})
},
undoDislike(videoIndex: number, reasonID: number, goto: string, id: string, mid: string, rid: string, tagID: string) {
undoDislike(
videoIndex: number,
reasonID: number,
goto: string,
id: string,
mid: string,
rid: string,
tagID: string,
) {
browser.runtime
.sendMessage({
contentScriptQuery: 'undoDislike',
@@ -121,12 +140,14 @@ export default defineComponent({
done()
},
},
})
</script>
<template>
<div m="lg:x-22 <lg:x-16 b-0 t-0" grid="~ xl:cols-4 lg:cols-3 md:cols-2 gap-4">
<div
m="lg:x-22 <lg:x-16 b-0 t-0"
grid="~ xl:cols-4 lg:cols-3 md:cols-2 gap-4"
>
<transition-group name="list" @enter="onEnter">
<div
v-for="(video, index) in videoList"
@@ -148,22 +169,24 @@ export default defineComponent({
border="solid $bew-fill-1"
text="$bew-text-3 sm center"
rounded="$bew-radius"
style="aspect-ratio: 16/9;"
style="aspect-ratio: 16/9"
>
{{ $t('home.video_removed') }}
<button
text="$bew-theme-color base"
font="bold"
m="t-4"
@click="undoDislike(
index,
video.selectedReasonID,
video.goto,
video.param,
video.mid,
video.tid,
video.tag.tag_id,
)"
@click="
undoDislike(
index,
video.selectedReasonID,
video.goto,
video.param,
video.mid,
video.tid,
video.tag.tag_id,
)
"
>
{{ $t('common.undo') }}
</button>
@@ -177,16 +200,28 @@ export default defineComponent({
class="overflow-hidden w-full relative rounded-$bew-radius z-1"
style="aspect-ratio: 16/9"
>
<img class="cover" :src="`${video.cover.replace('http:', '')}@672w_378h_1c`" loading="lazy">
<img
class="cover"
:src="`${video.cover.replace('http:', '')}@672w_378h_1c`"
loading="lazy"
>
</div>
<img class="cover-shadow" :src="`${video.cover.replace('http:', '')}@672w_378h_1c`" loading="lazy">
<img
class="cover-shadow"
:src="`${video.cover.replace('http:', '')}@672w_378h_1c`"
loading="lazy"
>
</div>
</a>
<div class="detail">
<div class="flex">
<a class="avatar" cursor="pointer" @click="gotoChannel(video.mid)">
<a
class="avatar"
cursor="pointer"
@click="gotoChannel(video.mid)"
>
<img
:src="`${(`${video.face}`).replace('http:', '')}@60w_60h_1c`"
:src="`${`${video.face}`.replace('http:', '')}@60w_60h_1c`"
width="48"
height="48"
loading="lazy"
@@ -232,7 +267,10 @@ export default defineComponent({
w="180px"
bg="$bew-content-1"
rounded="$bew-radius"
style="box-shadow: var(--bew-shadow-2); backdrop-filter: var(--bew-filter-glass);"
style="
box-shadow: var(--bew-shadow-2);
backdrop-filter: var(--bew-filter-glass);
"
>
<p p="2" text="$bew-text-3">
{{ $t('home.not_interested_in') }}
@@ -247,13 +285,17 @@ export default defineComponent({
hover:bg="$bew-fill-2"
transition="all duration-300"
rounded="$bew-radius"
@click.stop="submitDislike(index,
reason.reason_id,
video.goto,
video.param,
video.mid,
video.tid,
video.tag.tag_id)"
@click.stop="
submitDislike(
index,
reason.reason_id,
video.goto,
video.param,
video.mid,
video.tid,
video.tag.tag_id,
)
"
>
{{ reason.reason_name }}
</li>
@@ -265,13 +307,19 @@ export default defineComponent({
{{ video.name }}
</div>
<div class="video-info">
{{ numFormatter(video.play) }}{{ language === LanguageType.English
? ` ${$t('common.view', video.play)}`
: $t('common.view', video.play) }}
{{ numFormatter(video.play)
}}{{
language === LanguageType.English
? ` ${$t('common.view', video.play)}`
: $t('common.view', video.play)
}}
<span class="text-xs font-light"></span>
{{ calcTimeSince(new Date(video.ctime * 1000)) }}{{ language === LanguageType.English
? ` ${$t('common.ago')}`
: $t('common.ago') }}
{{ calcTimeSince(new Date(video.ctime * 1000))
}}{{
language === LanguageType.English
? ` ${$t('common.ago')}`
: $t('common.ago')
}}
</div>
</div>
</div>
@@ -292,7 +340,7 @@ export default defineComponent({
justify="center"
>
<button
style="box-shadow: var(--bew-shadow-1);"
style="box-shadow: var(--bew-shadow-1)"
bg="!$bew-theme-color"
w="60px"
h="60px"

View File

@@ -1,5 +1,3 @@
export * from './topbar/moments'
export enum LanguageType {
English = 'en',
Mandarin_SC = 'cmn-SC',

View File

@@ -1,11 +1,15 @@
:root {
--bew-radius: 12px;
--bew-radius-half: calc(var(--bew-radius) / 2);
--bew-filter-glass: blur(20px) saturate(180%);
--bew-shadow-1: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--bew-shadow-2: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--bew-shadow-3: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
--bew-shadow-1: 0 4px 6px -1px rgb(0 0 0 / 0.1),
0 2px 4px -2px rgb(0 0 0 / 0.1);
--bew-shadow-2: 0 10px 15px -3px rgb(0 0 0 / 0.1),
0 4px 6px -4px rgb(0 0 0 / 0.1);
--bew-shadow-3: 0 20px 25px -5px rgb(0 0 0 / 0.1),
0 8px 10px -6px rgb(0 0 0 / 0.1);
--bew-shadow-4: 0 25px 50px -12px rgb(0 0 0 / 0.25);
--bew-theme-color: rgb(0, 161, 214);
@@ -47,7 +51,7 @@
@supports not (backdrop-filter: blur(15px)) {
:root,
:root.dark {
--bew-content-1: var(--bew-content-solid-1);
--bew-content-1: var(--bew-content-solid-1);
}
}
@@ -58,6 +62,10 @@ body,
padding: 0;
}
html {
filter: none !important;
}
body {
background: var(--bew-bg);
color: var(--bew-text-1);

View File

@@ -1,27 +0,0 @@
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
}

View File

@@ -36,8 +36,8 @@ export const numFormatter = (num: number) => {
return item ? (num / item.value).toFixed(digits).replace(rx, '$1') + item.symbol : '0'
}
export const calcTimeSince = (date: any) => {
const seconds = Math.floor(((new Date() as any) - date) / 1000)
export const calcTimeSince = (date: number | string) => {
const seconds = Math.floor(((Number(new Date())) - Number(date)) / 1000)
let interval = seconds / 31536000
if (interval > 1)
return `${Math.floor(interval)} ${t('common.year', Math.floor(interval))}`

5
volar.config.js Normal file
View File

@@ -0,0 +1,5 @@
const vetur = require('@volar-plugins/vetur')
module.exports = {
plugins: [vetur()],
}