Bilibili-Evolved/registry/lib/components/style/custom-navbar/watchlater/NavbarWatchlater.vue
2023-03-19 17:42:09 +08:00

376 lines
9.7 KiB
Vue

<template>
<div class="watchlater-list">
<div class="header">
<div class="watchlater-list-summary"> {{ filteredCards.length }} </div>
<div class="search">
<TextBox v-model="search" linear placeholder="搜索"></TextBox>
</div>
<a
class="operation"
target="_blank"
href="https://www.bilibili.com/medialist/play/watchlater"
>
<VButton class="round-button" title="播放全部" round>
<VIcon icon="mdi-play" :size="18"></VIcon>
</VButton>
</a>
<a class="operation" target="_blank" href="https://www.bilibili.com/watchlater/#/list">
<VButton class="round-button" title="查看更多" round>
<VIcon icon="mdi-dots-horizontal" :size="18"></VIcon>
</VButton>
</a>
</div>
<VLoading v-if="loading"></VLoading>
<VEmpty v-else-if="!loading && cards.length === 0"></VEmpty>
<transition-group v-else name="cards" tag="div" class="watchlater-list-content">
<div v-for="(card, index) of filteredCards" :key="card.aid" class="watchlater-card">
<a class="cover-container" target="_blank" :href="card.href">
<DpiImage
class="cover"
:src="card.coverUrl"
:size="{ width: 130, height: 85 }"
></DpiImage>
<div class="floating remove" title="移除" @click.prevent="remove(card.aid, index)">
<VIcon icon="mdi-close" :size="16"></VIcon>
</div>
<div class="floating duration">{{ card.durationText }}</div>
<div v-if="card.complete" class="floating viewed">已观看</div>
</a>
<a class="title" target="_blank" :href="card.href" :title="card.title">{{ card.title }}</a>
<a
class="up"
target="_blank"
:href="'https://space.bilibili.com/' + card.upID"
:title="card.upName"
>
<DpiImage class="face" :src="card.upFaceUrl" :size="20"></DpiImage>
<div class="name">{{ card.upName }}</div>
</a>
</div>
</transition-group>
</div>
</template>
<script lang="ts">
import { getComponentSettings } from '@/core/settings'
import { formatDuration } from '@/core/utils/formatters'
import {
watchlaterList,
getWatchlaterList,
RawWatchlaterItem,
toggleWatchlater,
} from '@/components/video/watchlater'
import { VLoading, VEmpty, TextBox, VButton, VIcon, DpiImage } from '@/ui'
import { popperMixin } from '../mixins'
interface WatchlaterCard {
aid: number
href: string
coverUrl: string
durationText: string
duration: number
complete: boolean
title: string
upName: string
upFaceUrl: string
upID: number
}
export default Vue.extend({
components: {
VLoading,
VEmpty,
TextBox,
VButton,
VIcon,
DpiImage,
},
mixins: [popperMixin],
data() {
const redirect = getComponentSettings('watchlaterRedirect')
return {
watchlaterList,
loading: true,
cards: [],
filteredCards: [],
search: '',
redirect: redirect.enabled && redirect.options.navbar,
}
},
watch: {
search() {
this.updateFilteredCards()
},
},
async created() {
try {
await this.updateList()
} finally {
this.loading = false
}
},
methods: {
toggleWatchlater,
popupRefresh() {
this.updateList()
},
async updateList() {
const rawList = await getWatchlaterList(true)
if (!rawList) {
this.cards = []
return
}
const getLink = (item: RawWatchlaterItem) => {
if (this.redirect) {
return `https://www.bilibili.com/video/${item.bvid}`
}
return `https://www.bilibili.com/medialist/play/watchlater/${item.bvid}`
}
const cards = rawList.map(item => {
const href = (() => {
if (item.pages === undefined || !this.redirect) {
return getLink(item)
}
const pages = item.pages.map(it => it.cid)
const page = item.cid === 0 ? 1 : pages.indexOf(item.cid) + 1
return page <= 1 ? getLink(item) : `${getLink(item)}?p=${page}`
})()
const percent = Math.round((1000 * item.progress) / item.duration) / 1000
return {
aid: item.aid,
href,
coverUrl: item.pic.replace('http:', 'https:'),
durationText: formatDuration(item.duration),
duration: item.duration,
// percent: `${fixed(percent * 100)}%`,
complete: item.progress < 0 || percent > 0.95, // 进度过95%算看完, -1值表示100%
title: item.title,
upName: item.owner.name,
upFaceUrl: item.owner.face.replace('http:', 'https:'),
upID: item.owner.mid,
} as WatchlaterCard
})
this.cards = cards
if (this.search) {
this.updateFilteredCards()
} else {
this.filteredCards = cards
}
},
async remove(aid: number, index: number) {
this.cards.splice(index, 1)
await this.toggleWatchlater(aid)
},
updateFilteredCards: lodash.debounce(function updateFilteredCards() {
const search = this.search.toLowerCase()
const cardsList = this.$el.querySelector('.watchlater-list-content') as HTMLElement
cardsList.scrollTo(0, 0)
this.filteredCards = (this.cards as WatchlaterCard[]).filter(
card =>
card.title.toLowerCase().includes(search) || card.upName.toLowerCase().includes(search),
)
}, 100),
},
})
</script>
<style lang="scss">
@import 'common';
@import '../popup';
.custom-navbar .watchlater-list {
@include navbar-popup-height();
width: 380px;
font-size: 12px;
display: flex;
flex-flow: column nowrap;
justify-content: space-between;
align-items: center;
@mixin floating-button-background {
background-color: #000c;
}
.round-button {
@include round-bar(26);
@include h-center();
justify-content: center;
cursor: pointer;
width: 26px;
}
.floating {
@include round-bar(20);
@include h-center();
@include floating-button-background();
color: white;
justify-content: center;
cursor: pointer;
}
.header {
@include h-center();
justify-content: space-between;
align-self: stretch;
margin: 16px 12px;
.watchlater-list-summary {
margin-right: 6px;
}
.search {
position: relative;
flex-grow: 1;
align-self: stretch;
margin-right: 8px;
.be-textbox {
height: 100%;
}
}
.operations {
@include h-center();
.round-button {
&:not(:last-child) {
margin-right: 4px;
}
}
}
.operation:not(:last-child) {
margin-right: 8px;
}
.more-info {
@include h-center();
@include round-bar(28);
padding: 4px 6px 4px 10px;
}
}
.be-empty,
.be-loading {
flex: 1;
}
&-content {
flex: 1;
align-self: stretch;
scroll-behavior: smooth;
position: relative;
@include no-scrollbar();
padding: 0 12px;
padding-bottom: 12px;
.watchlater-card {
&.cards-enter,
&.cards-leave-to {
opacity: 0;
transform: translateY(-16px) scale(0.9);
}
&.cards-leave-active {
transition: 0.24s cubic-bezier(0.22, 0.61, 0.36, 1);
position: absolute;
}
cursor: pointer;
flex-shrink: 0;
border-radius: 8px;
color: black;
background-color: #fff;
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.05);
border: 1px solid #8882;
display: grid;
// grid-template:
// "cover" 120px
// "title" auto
// "info" auto / 200px;
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);
}
.cover-container {
grid-area: cover;
overflow: hidden;
border-radius: 8px 0 0 8px;
position: relative;
$padding: 6px;
.remove {
top: $padding;
left: $padding;
width: 20px;
}
.duration {
left: $padding;
bottom: $padding;
padding: 0 6px;
}
.viewed {
white-space: nowrap;
right: $padding;
top: $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();
margin: 0;
margin-top: 8px;
padding: 0 10px;
@include max-line(2);
&:hover {
color: var(--theme-color) !important;
}
}
// .info {
// grid-area: info;
// display: flex;
// align-items: center;
// justify-content: space-around;
// margin: 8px;
// }
.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;
}
.name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 11px;
}
&:hover .name {
color: var(--theme-color);
}
}
}
}
}
</style>