Add pinned and hidden list options

This commit is contained in:
the1812 2025-05-28 09:41:43 +08:00
parent f2a1525d06
commit 1d10e48edb
7 changed files with 297 additions and 24 deletions

View File

@ -99,6 +99,7 @@
"superchat",
"Swiper",
"tabindex",
"tagid",
"Tampermonkey",
"Touhou",
"truetype",

View 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>

View File

@ -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>

View File

@ -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">

View File

@ -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,
})

View 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>

View 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
}