Bilibili-Evolved/registry/lib/components/style/custom-navbar/favorites/NavbarFavorites.vue
timongh 409b3652ff Fix image style of customNavbar(fix #4312)
原因:自定义顶栏的图片使用了 CSS 类 cover-container;该类被 B 站使用,进而影响到自定义顶栏
2023-07-24 07:27:29 +08:00

464 lines
13 KiB
Vue

<template>
<div class="favorites-list">
<div class="header">
<FavoritesFolderSelect v-model="folder"></FavoritesFolderSelect>
<div class="search">
<TextBox v-model="search" linear placeholder="搜索"></TextBox>
</div>
<a class="operation" :href="playLink" title="播放全部" target="_blank">
<VButton round class="play-all">
<VIcon icon="mdi-play" :size="18"></VIcon>
</VButton>
</a>
<a class="operation" :href="moreLink" title="查看更多" target="_blank">
<VButton round class="more-info">
<VIcon icon="mdi-dots-horizontal" :size="18"></VIcon>
</VButton>
</a>
</div>
<div class="content">
<VLoading v-if="loading && !searching"></VLoading>
<VEmpty v-else-if="!loading && !canLoadMore && filteredCards.length === 0"></VEmpty>
<transition-group v-else name="cards" tag="div" class="cards">
<div v-for="card of filteredCards" :key="card.id" class="favorite-card">
<a
class="favorites-cover-container"
target="_blank"
:href="'https://www.bilibili.com/video/' + card.bvid"
>
<DpiImage
class="cover"
:src="card.coverUrl"
:size="{ width: 130, height: 85 }"
></DpiImage>
<div class="floating duration">{{ card.durationText }}</div>
<div class="floating favorite-time">{{ card.favoriteTime }}</div>
</a>
<a
class="title"
target="_blank"
:href="'https://www.bilibili.com/video/' + card.bvid"
:title="card.title"
>{{ card.title }}</a
>
<a
v-if="card.upID"
class="up"
target="_blank"
:href="'https://space.bilibili.com/' + card.upID"
:title="card.upName"
>
<DpiImage placeholder-image class="face" :src="card.upFaceUrl" :size="20"></DpiImage>
<div class="name">{{ card.upName }}</div>
</a>
<div v-else class="description">
{{ card.description }}
</div>
</div>
<ScrollTrigger
v-if="canLoadMore"
key="scroll-trigger"
@trigger="scrollTrigger()"
></ScrollTrigger>
</transition-group>
</div>
</div>
</template>
<script lang="ts">
import { VLoading, VEmpty, VIcon, VButton, TextBox, DpiImage, ScrollTrigger } from '@/ui'
import { formatDate, formatDuration } from '@/core/utils/formatters'
import { getUID } from '@/core/utils'
import { getJsonWithCredentials } from '@/core/ajax'
import { logError } from '@/core/utils/log'
import { VideoCard } from '@/components/feeds/video-card'
import { getComponentSettings } from '@/core/settings'
import { notSelectedFolder } from './favorites-folder'
import FavoritesFolderSelect from './FavoritesFolderSelect.vue'
import { popperMixin } from '../mixins'
/*
新版收藏夹 API
https://api.bilibili.com/x/v3/fav/resource/list?media_id=media_id&pn=1&ps=20&keyword=keyword&order=mtime&type=0&tid=0&platform=web
*/
const navbarOptions = getComponentSettings('customNavbar').options
interface FavoritesItemInfo extends VideoCard {
favoriteTimestamp: number
favoriteTime: string
}
const MaxPageSize = 20
const favoriteItemFilter = (item: any): boolean => {
if (navbarOptions.showDeadVideos) {
return true
}
return item.attr !== 9 && item.attr !== 1 // 过滤掉已失效视频
}
const favoriteItemMapper = (item: any): FavoritesItemInfo => ({
id: item.id,
aid: item.id,
bvid: item.bvid,
coverUrl: item.cover.replace('http:', 'https:'),
favoriteTimestamp: item.fav_time * 1000,
favoriteTime: formatDate(new Date(item.fav_time * 1000)),
title: item.title,
description: item.intro,
duration: item.duration,
durationText: formatDuration(item.duration),
playCount: item.cnt_info.play,
danmakuCount: item.cnt_info.danmaku,
upName: item.upper.name,
upFaceUrl: item.upper.face.replace('http:', 'https:'),
upID: item.upper.mid,
})
async function searchAllList() {
if (!this.searching) {
return
}
try {
this.loading = true
const jsonCurrent = await getJsonWithCredentials(
`https://api.bilibili.com/x/v3/fav/resource/list?media_id=${this.folder.id}&pn=${this.searchPage}&ps=${MaxPageSize}&keyword=${this.search}&order=mtime&type=0&tid=0&platform=web`,
)
const jsonAll = await getJsonWithCredentials(
`https://api.bilibili.com/x/v3/fav/resource/list?media_id=${this.folder.id}&pn=${this.searchPage}&ps=${MaxPageSize}&keyword=${this.search}&order=mtime&type=1&tid=0&platform=web`,
)
if (jsonCurrent.code !== 0 && jsonAll.code !== 0) {
return
}
const currentItems = lodash.get(jsonCurrent, 'data.medias', []) || []
const allItems = lodash.get(jsonAll, 'data.medias', []) || []
this.searchPage++
const results = lodash.uniqBy(
this.filteredCards.concat(
currentItems.filter(favoriteItemFilter).map(favoriteItemMapper),
allItems.filter(favoriteItemFilter).map(favoriteItemMapper),
),
(card: FavoritesItemInfo) => card.id,
)
this.filteredCards = results
const noNewItems = currentItems.length + allItems.length === 0
const lessThanPageSize = allItems.length < MaxPageSize
if (noNewItems || lessThanPageSize) {
this.hasMoreSearchPage = false
return
}
} catch (error) {
console.error(error)
} finally {
this.loading = false
}
}
export default Vue.extend({
components: {
FavoritesFolderSelect,
VLoading,
VEmpty,
VIcon,
VButton,
TextBox,
DpiImage,
ScrollTrigger,
},
mixins: [popperMixin],
data() {
return {
loading: true,
cards: [],
filteredCards: [],
page: 1,
hasMorePage: true,
searchPage: 1,
hasMoreSearchPage: true,
search: '',
folder: notSelectedFolder,
}
},
computed: {
searching() {
return this.search !== ''
},
moreLink() {
const { id } = this.folder
if (id === 0) {
return `https://space.bilibili.com/${getUID()}/favlist`
}
return `https://space.bilibili.com/${getUID()}/favlist?fid=${id}`
},
playLink() {
const { id } = this.folder
if (id === 0) {
return undefined
}
return `https://www.bilibili.com/medialist/play/ml${id}`
},
canLoadMore() {
if (this.searching) {
return this.hasMoreSearchPage
}
return this.hasMorePage
},
},
watch: {
folder() {
this.changeList()
},
search(keyword: string) {
if (keyword === '') {
this.filteredCards = this.cards
return
}
keyword = keyword.toLowerCase()
this.hasMoreSearchPage = true
this.searchPage = 1
this.filteredCards = (this.cards as FavoritesItemInfo[]).filter(
it => it.title.toLowerCase().includes(keyword) || it.upName.toLowerCase().includes(keyword),
)
},
},
methods: {
async getCards() {
const url = `https://api.bilibili.com/x/v3/fav/resource/list?media_id=${this.folder.id}&pn=${this.page}&ps=${MaxPageSize}&keyword=&order=mtime&type=0&tid=0&platform=web`
const json = await getJsonWithCredentials(url)
if (json.code !== 0) {
throw new Error(`加载收藏夹内容失败: ${json.message}`)
}
if (!json.data.medias) {
// 超过最后一页后返回空数组
return []
}
return json.data.medias.filter(favoriteItemFilter).map(favoriteItemMapper)
},
async changeList() {
if (this.folder.id === 0) {
return
}
try {
this.search = ''
this.cards = []
this.loading = true
this.searchPage = 1
this.hasMoreSearchPage = true
this.page = 1
this.hasMorePage = true
this.cards = await this.getCards()
this.filteredCards = this.cards
} catch (error) {
logError(error)
} finally {
this.loading = false
}
},
async loadNextPage() {
try {
this.page++
const cards = await this.getCards()
this.cards.push(...cards)
this.hasMorePage = cards.length !== 0 || this.cards.length < this.folder.count
} catch (error) {
logError(error)
}
},
debounceSearchAllList: lodash.debounce(searchAllList, 200),
scrollTrigger() {
if (this.searching) {
this.debounceSearchAllList()
} else {
this.loadNextPage()
}
},
},
})
</script>
<style lang="scss">
@import 'common';
@import '../popup';
.custom-navbar .favorites-list {
width: 380px;
@include navbar-popup-height();
font-size: 12px;
@include v-stretch();
justify-content: center;
.be-empty,
.be-loading {
align-self: center;
flex: 1;
}
.be-scroll-trigger {
text-align: center;
}
.header {
@include h-stretch();
justify-content: space-between;
margin: 16px 12px;
.search {
flex: 1;
margin-left: 8px;
.be-textbox {
height: 100%;
}
}
.list-select {
flex-shrink: 0;
height: 26px;
}
.dropdown-popup {
max-height: 300px;
@include no-scrollbar();
}
.operation {
margin-left: 8px;
}
.more-info {
@include h-center();
@include round-bar(26);
padding: 4px;
}
.play-all {
@include h-center();
@include round-bar(26);
padding: 4px;
}
}
.content {
@include v-stretch();
@include no-scrollbar();
justify-content: space-between;
flex-grow: 1;
.floating {
@include round-bar(20);
@include h-center();
background-color: #000c;
color: white;
justify-content: center;
cursor: pointer;
}
.cards {
flex: 1;
scroll-behavior: smooth;
position: relative;
@include no-scrollbar();
padding: 0 12px;
padding-bottom: 12px;
&-enter,
&-leave-to {
opacity: 0;
transform: translateY(-16px) scale(0.9);
}
&-leave-active {
transition: 0.24s cubic-bezier(0.22, 0.61, 0.36, 1);
position: absolute;
&.be-scroll-trigger {
width: 100%;
padding-bottom: 12px;
}
}
.favorite-card {
cursor: pointer;
flex-shrink: 0;
border-radius: 8px;
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.05);
border: 1px solid #8882;
color: black;
background-color: #fff;
display: grid;
grid-template:
'cover title' 2fr
'cover info' 1fr / 130px 1fr;
height: 85px;
body.dark & {
background-color: #282828;
color: #eee;
}
&:not(:last-child) {
margin-bottom: 12px;
}
&:hover .cover {
transform: scale(1.05);
}
.favorites-cover-container {
grid-area: cover;
overflow: hidden;
border-radius: 8px 0 0 8px;
position: relative;
$padding: 6px;
.favorite-time {
top: $padding;
left: $padding;
padding: 0 6px;
}
.duration {
left: $padding;
bottom: $padding;
padding: 0 6px;
}
.floating {
position: absolute;
opacity: 0;
font-size: 11px;
}
.cover {
object-fit: cover;
}
}
&:hover {
.floating {
opacity: 1;
}
}
.title {
grid-area: title;
font-size: 13px;
@include semi-bold();
@include max-line(2);
-webkit-box-align: start;
margin: 0;
margin-top: 8px;
padding: 0 10px;
&:hover {
color: var(--theme-color) !important;
}
}
.up {
flex: 0 1 auto;
padding: 2px 10px 2px 2px;
margin: 0 8px 6px;
justify-self: start;
align-self: center;
max-width: calc(100% - 16px);
@include h-center();
@include round-bar(24);
border: 1px solid #8882;
&:hover {
background-color: #8882;
}
.face {
border-radius: 50%;
margin-right: 6px;
height: 20px;
width: 20px;
object-fit: cover;
body.dark &.placeholder {
filter: invert(0.9);
}
}
.name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 11px;
}
&:hover .name {
color: var(--theme-color);
}
}
.description {
@include single-line();
margin: 4px 10px;
}
}
}
}
}
</style>