mirror of
https://github.com/BewlyBewly/BewlyBewly.git
synced 2025-04-14 13:15:29 +00:00
Merge branch 'main' of https://github.com/hakadao/BewlyBewly into main
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
575
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -17,7 +17,7 @@ const imgURL = browser.runtime.getURL('/assets/loading.gif')
|
||||
w="46px"
|
||||
h="46px"
|
||||
m="r-2"
|
||||
/>
|
||||
>
|
||||
{{ $t('common.loading') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
16
src/components/Progress.vue
Normal file
16
src/components/Progress.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
104
src/components/Topbar/types.ts
Normal file
104
src/components/Topbar/types.ts
Normal 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:已开播
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
export * from './topbar/moments'
|
||||
|
||||
export enum LanguageType {
|
||||
English = 'en',
|
||||
Mandarin_SC = 'cmn-SC',
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
5
volar.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const vetur = require('@volar-plugins/vetur')
|
||||
|
||||
module.exports = {
|
||||
plugins: [vetur()],
|
||||
}
|
||||
Reference in New Issue
Block a user