mirror of
https://github.com/the1812/Bilibili-Evolved.git
synced 2025-09-26 22:49:14 +08:00
Add pinned and hidden list options
This commit is contained in:
parent
f2a1525d06
commit
1d10e48edb
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -99,6 +99,7 @@
|
||||
"superchat",
|
||||
"Swiper",
|
||||
"tabindex",
|
||||
"tagid",
|
||||
"Tampermonkey",
|
||||
"Touhou",
|
||||
"truetype",
|
||||
|
57
registry/lib/components/feeds/extend-live/ExtraOptions.vue
Normal file
57
registry/lib/components/feeds/extend-live/ExtraOptions.vue
Normal file
@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="extend-feeds-live-extra-options">
|
||||
<div class="extend-feeds-live-extra-option">
|
||||
<div class="extend-feeds-live-extra-option-header">置顶列表</div>
|
||||
<div class="extend-feeds-live-extra-option-description">
|
||||
配置直播信息扩充要展示的置顶列表,置顶列表中的用户会排序到上方。
|
||||
</div>
|
||||
<FollowingListSelect :value="pinnedListID" @change="handlePinnedListChange" />
|
||||
</div>
|
||||
<div class="extend-feeds-live-extra-option">
|
||||
<div class="extend-feeds-live-extra-option-header">隐藏列表</div>
|
||||
<div class="extend-feeds-live-extra-option-description">
|
||||
配置直播信息扩充要展示的隐藏列表,隐藏列表中的用户会被隐藏,优先级高于置顶列表。
|
||||
</div>
|
||||
<FollowingListSelect :value="hiddenListID" @change="handleHiddenListChange" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import FollowingListSelect from './FollowingListSelect.vue'
|
||||
import { getComponentSettings } from '@/core/settings'
|
||||
import { ExtendFeedsLiveOptions } from './options'
|
||||
import { FollowingListID } from './types'
|
||||
|
||||
const { options } = getComponentSettings<ExtendFeedsLiveOptions>('extendFeedsLive')
|
||||
|
||||
const pinnedListID = ref(options.pinnedListID)
|
||||
const handlePinnedListChange = (value: FollowingListID) => {
|
||||
options.pinnedListID = value
|
||||
pinnedListID.value = value
|
||||
}
|
||||
|
||||
const hiddenListID = ref(options.hiddenListID)
|
||||
const handleHiddenListChange = (value: FollowingListID) => {
|
||||
options.hiddenListID = value
|
||||
hiddenListID.value = value
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import 'common';
|
||||
|
||||
.extend-feeds-live-extra-options {
|
||||
margin-top: 8px;
|
||||
@include v-stretch(12px);
|
||||
.extend-feeds-live-extra-option {
|
||||
@include v-stretch(8px);
|
||||
&-header {
|
||||
@include semi-bold();
|
||||
}
|
||||
&-description {
|
||||
font-size: 12px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<VDropdown
|
||||
:items="followingList"
|
||||
:key-mapper="item => item.id"
|
||||
class="be-live-following-list-select"
|
||||
:value="selectedItem"
|
||||
@change="handleFollowingListChange"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<span class="be-live-following-list-item"> {{ item.displayName }} </span>
|
||||
</template>
|
||||
</VDropdown>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { VDropdown } from '@/ui'
|
||||
import { bilibiliApi, getJsonWithCredentials } from '@/core/ajax'
|
||||
import { FollowingListID, FollowingListItem, RawFollowingListItem } from './types'
|
||||
import { UnselectedListID } from './options'
|
||||
|
||||
const props = defineProps<{
|
||||
value: FollowingListID
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'change', value: FollowingListID): void
|
||||
}>()
|
||||
|
||||
const followingListID = computed<FollowingListID>(() => props.value)
|
||||
const handleFollowingListChange = (value: FollowingListItem) => {
|
||||
emit('change', value.id)
|
||||
}
|
||||
|
||||
const unselectedListItem = reactive<FollowingListItem>({
|
||||
id: UnselectedListID,
|
||||
displayName: '<未选择>',
|
||||
count: 0,
|
||||
})
|
||||
|
||||
const followingList = ref<FollowingListItem[]>([])
|
||||
const selectedItem = computed(() => {
|
||||
return followingList.value.find(item => item.id === followingListID.value) ?? unselectedListItem
|
||||
})
|
||||
const loadFollowingList = async () => {
|
||||
const list = await bilibiliApi<RawFollowingListItem[]>(
|
||||
getJsonWithCredentials('https://api.bilibili.com/x/relation/tags'),
|
||||
)
|
||||
followingList.value = [
|
||||
unselectedListItem,
|
||||
...list.map(item => ({
|
||||
id: item.tagid,
|
||||
displayName: item.name,
|
||||
count: item.count,
|
||||
})),
|
||||
]
|
||||
}
|
||||
|
||||
loadFollowingList()
|
||||
</script>
|
@ -6,7 +6,7 @@
|
||||
<div class="be-live-list-count">({{ items.length }})</div>
|
||||
</div>
|
||||
<div class="be-live-list-actions">
|
||||
<div v-if="loaded" class="be-live-list-refresh" title="刷新" @click="refresh">
|
||||
<div v-if="loaded" class="be-live-list-refresh" title="刷新" @click="fetchLiveList">
|
||||
<VIcon icon="mdi-refresh" :size="16" />
|
||||
</div>
|
||||
<a
|
||||
@ -49,21 +49,17 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { VIcon, TextBox, DpiImage, VEmpty, VLoading } from '@/ui'
|
||||
import { getJsonWithCredentials, responsiveGetPages } from '@/core/ajax'
|
||||
|
||||
interface LiveInfo {
|
||||
cover: string
|
||||
face: string
|
||||
uname: string
|
||||
title: string
|
||||
roomid: number
|
||||
pic: string
|
||||
online: number
|
||||
uid: number
|
||||
link: string
|
||||
}
|
||||
import { bilibiliApi, getJsonWithCredentials, getPages, responsiveGetPages } from '@/core/ajax'
|
||||
import { FollowingListID, FollowingUserInfo, LiveInfo, RawFollowingListItem } from './types'
|
||||
import { getUID } from '@/core/utils'
|
||||
import { ExtendFeedsLiveOptions, UnselectedListID } from './options'
|
||||
import {
|
||||
addComponentListener,
|
||||
getComponentSettings,
|
||||
removeComponentListener,
|
||||
} from '@/core/settings'
|
||||
|
||||
const decodeTitle = (title: string) => {
|
||||
const textArea = document.createElement('textarea')
|
||||
@ -71,7 +67,10 @@ const decodeTitle = (title: string) => {
|
||||
return textArea.value
|
||||
}
|
||||
|
||||
const { options } = getComponentSettings<ExtendFeedsLiveOptions>('extendFeedsLive')
|
||||
const items = ref<LiveInfo[]>([])
|
||||
const pinnedUsers = ref<FollowingUserInfo[]>([])
|
||||
const hiddenUsers = ref<FollowingUserInfo[]>([])
|
||||
const loaded = ref(false)
|
||||
const keyword = ref('')
|
||||
|
||||
@ -88,7 +87,7 @@ const filteredItems = computed(() => {
|
||||
return items.value
|
||||
})
|
||||
|
||||
const fetchRecommendItems = async (): Promise<LiveInfo[]> => {
|
||||
const fetchRecommendLiveInfos = async (): Promise<LiveInfo[]> => {
|
||||
// 动态 portal 接口会获取推荐的 top30 直播。同时这个接口不会忽略悄悄关注的 up 的直播。
|
||||
const portalList = await getJsonWithCredentials(
|
||||
'https://api.bilibili.com/x/polymer/web-dynamic/v1/portal',
|
||||
@ -110,15 +109,69 @@ const fetchRecommendItems = async (): Promise<LiveInfo[]> => {
|
||||
return recommendLiveItems
|
||||
}
|
||||
|
||||
const sortByRecommend = (feedItems: LiveInfo[], recommendItems: LiveInfo[]) => {
|
||||
const mergeRecommendLiveInfos = (liveInfos: LiveInfo[], recommendLiveInfos: LiveInfo[]) => {
|
||||
// recommendItems 里的 pic 和 online 为默认值,但是 ui 也没用到。
|
||||
// 所以方便起见,直接拼接没出现在 recommendItems 里的 item
|
||||
const recommendRoomIds = recommendItems.map(item => item.roomid)
|
||||
const feedConcatItems = feedItems.filter(item => !recommendRoomIds.includes(item.roomid))
|
||||
return lodash.concat(recommendItems, feedConcatItems)
|
||||
const recommendRoomIds = recommendLiveInfos.map(item => item.roomid)
|
||||
const feedConcatItems = liveInfos.filter(item => !recommendRoomIds.includes(item.roomid))
|
||||
return lodash.concat(recommendLiveInfos, feedConcatItems)
|
||||
}
|
||||
|
||||
const refresh = async () => {
|
||||
const sortByUsers = (users: FollowingUserInfo[], descending = false) => {
|
||||
return (a: LiveInfo, b: LiveInfo) => {
|
||||
const aPinnedIndex = users.findIndex(it => it.mid === a.uid)
|
||||
const bPinnedIndex = users.findIndex(it => it.mid === b.uid)
|
||||
if (bPinnedIndex === -1 && aPinnedIndex === -1) {
|
||||
return 0
|
||||
}
|
||||
if (aPinnedIndex === -1) {
|
||||
return descending ? -1 : 1
|
||||
}
|
||||
if (bPinnedIndex === -1) {
|
||||
return descending ? 1 : -1
|
||||
}
|
||||
return aPinnedIndex - bPinnedIndex
|
||||
}
|
||||
}
|
||||
|
||||
const fetchFollowingLists = async () => {
|
||||
const fetchFollowingList = async (id: FollowingListID) => {
|
||||
if (id === UnselectedListID || id === undefined) {
|
||||
return []
|
||||
}
|
||||
const list = await bilibiliApi<RawFollowingListItem[]>(
|
||||
getJsonWithCredentials('https://api.bilibili.com/x/relation/tags'),
|
||||
)
|
||||
const response = await getPages<FollowingUserInfo>({
|
||||
api: page =>
|
||||
getJsonWithCredentials(
|
||||
`https://api.bilibili.com/x/relation/tag?tagid=${id}&pn=${page}&ps=500&mid=${getUID()}`,
|
||||
),
|
||||
getList: json => lodash.get(json, 'data', []),
|
||||
getTotal: () => list.find(it => it.tagid === id)?.count ?? 0,
|
||||
})
|
||||
return response
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
fetchFollowingList(options.pinnedListID)
|
||||
.then(users => {
|
||||
pinnedUsers.value = users
|
||||
})
|
||||
.catch(() => {
|
||||
pinnedUsers.value = []
|
||||
}),
|
||||
fetchFollowingList(options.hiddenListID)
|
||||
.then(users => {
|
||||
hiddenUsers.value = users
|
||||
})
|
||||
.catch(() => {
|
||||
hiddenUsers.value = []
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
const fetchLiveList = async () => {
|
||||
try {
|
||||
items.value = []
|
||||
loaded.value = false
|
||||
@ -145,15 +198,28 @@ const refresh = async () => {
|
||||
getTotal: json => lodash.get(json, 'data.count', 0),
|
||||
})
|
||||
|
||||
const [allItems, recommendItems] = await Promise.all([promise, fetchRecommendItems()])
|
||||
items.value = sortByRecommend(allItems, recommendItems)
|
||||
const [allItems, recommendItems] = await Promise.all([promise, fetchRecommendLiveInfos()])
|
||||
const sortedItems = mergeRecommendLiveInfos(allItems, recommendItems)
|
||||
.sort(sortByUsers(pinnedUsers.value, false))
|
||||
.filter(item => !hiddenUsers.value.some(user => user.mid === item.uid))
|
||||
items.value = sortedItems
|
||||
} finally {
|
||||
loaded.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const initializeLiveList = async () => {
|
||||
await fetchFollowingLists()
|
||||
await fetchLiveList()
|
||||
}
|
||||
addComponentListener('extendFeedsLive.pinnedListID', initializeLiveList)
|
||||
addComponentListener('extendFeedsLive.hiddenListID', initializeLiveList)
|
||||
onMounted(() => {
|
||||
refresh()
|
||||
initializeLiveList()
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
removeComponentListener('extendFeedsLive.pinnedListID', initializeLiveList)
|
||||
removeComponentListener('extendFeedsLive.hiddenListID', initializeLiveList)
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
|
@ -3,6 +3,7 @@ import { styledComponentEntry } from '@/components/styled-component'
|
||||
import { select } from '@/core/spin-query'
|
||||
import { mountVueComponent } from '@/core/utils'
|
||||
import { useScopedConsole } from '@/core/utils/log'
|
||||
import { options } from './options'
|
||||
|
||||
const entry = async () => {
|
||||
const console = useScopedConsole('extendFeedsLive')
|
||||
@ -46,4 +47,6 @@ export const component = defineComponentMetadata({
|
||||
entry: styledComponentEntry(() => import('./extend-feeds-live.scss'), entry),
|
||||
tags: [componentsTags.feeds, componentsTags.live],
|
||||
urlInclude: [/^https:\/\/t\.bilibili\.com\/$/],
|
||||
extraOptions: () => import('./ExtraOptions.vue').then(m => m.default),
|
||||
options,
|
||||
})
|
||||
|
19
registry/lib/components/feeds/extend-live/options.ts
Normal file
19
registry/lib/components/feeds/extend-live/options.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { defineOptionsMetadata, OptionsOfMetadata } from '@/components/define'
|
||||
import { FollowingListID } from './types'
|
||||
|
||||
export const UnselectedListID: FollowingListID = -1
|
||||
|
||||
export const options = defineOptionsMetadata({
|
||||
pinnedListID: {
|
||||
defaultValue: UnselectedListID as FollowingListID,
|
||||
displayName: '置顶列表 ID',
|
||||
hidden: true,
|
||||
},
|
||||
hiddenListID: {
|
||||
defaultValue: UnselectedListID as FollowingListID,
|
||||
displayName: '隐藏列表 ID',
|
||||
hidden: true,
|
||||
},
|
||||
})
|
||||
|
||||
export type ExtendFeedsLiveOptions = OptionsOfMetadata<typeof options>
|
69
registry/lib/components/feeds/extend-live/types.ts
Normal file
69
registry/lib/components/feeds/extend-live/types.ts
Normal file
@ -0,0 +1,69 @@
|
||||
export type FollowingListID = number
|
||||
|
||||
export interface LiveInfo {
|
||||
cover: string
|
||||
face: string
|
||||
uname: string
|
||||
title: string
|
||||
roomid: number
|
||||
pic: string
|
||||
online: number
|
||||
uid: number
|
||||
link: string
|
||||
}
|
||||
|
||||
export interface FollowingUserInfo {
|
||||
mid: number
|
||||
attribute: number
|
||||
special: number
|
||||
contract_info: Record<string, unknown>
|
||||
uname: string
|
||||
face: string
|
||||
sign: string
|
||||
face_nft: number
|
||||
official_verify: {
|
||||
type: number
|
||||
desc: string
|
||||
}
|
||||
vip: {
|
||||
vipType: number
|
||||
vipDueDate: number
|
||||
dueRemark: string
|
||||
accessStatus: number
|
||||
vipStatus: number
|
||||
vipStatusWarn: string
|
||||
themeType: number
|
||||
label: {
|
||||
path: string
|
||||
text: string
|
||||
label_theme: string
|
||||
text_color: string
|
||||
bg_style: number
|
||||
bg_color: string
|
||||
border_color: string
|
||||
}
|
||||
avatar_subscript: number
|
||||
nickname_color: string
|
||||
avatar_subscript_url: string
|
||||
}
|
||||
name_render: Record<string, unknown>
|
||||
live: {
|
||||
live_status: number
|
||||
jump_url: string
|
||||
}
|
||||
nft_icon: string
|
||||
rec_reason: string
|
||||
track_id: string
|
||||
follow_time: string
|
||||
}
|
||||
export interface RawFollowingListItem {
|
||||
tagid: number
|
||||
name: string
|
||||
count: number
|
||||
tip: string
|
||||
}
|
||||
export interface FollowingListItem {
|
||||
id: FollowingListID
|
||||
displayName: string
|
||||
count: number
|
||||
}
|
Loading…
Reference in New Issue
Block a user