Add minimal home feeds

This commit is contained in:
the1812 2022-06-25 15:59:40 +08:00
parent 4bcb6d6a39
commit 2a82a24720
10 changed files with 195 additions and 96 deletions

View File

@ -109,7 +109,7 @@ import {
} from '@/ui' } from '@/ui'
import { addComponentListener } from '@/core/settings' import { addComponentListener } from '@/core/settings'
import { enableHorizontalScroll } from '@/core/horizontal-scroll' import { enableHorizontalScroll } from '@/core/horizontal-scroll'
import { cssVariableMixin, requestMixin } from './mixin' import { cssVariableMixin, requestMixin } from '../../../../mixin'
import { rankListCssVars } from './rank-list' import { rankListCssVars } from './rank-list'
import { setupScrollMask, cleanUpScrollMask } from '../../../scroll-mask' import { setupScrollMask, cleanUpScrollMask } from '../../../scroll-mask'

View File

@ -143,7 +143,7 @@ import {
VEmpty, VEmpty,
VButton, VButton,
} from '@/ui' } from '@/ui'
import { requestMixin, cssVariableMixin } from './mixin' import { requestMixin, cssVariableMixin } from '../../../../mixin'
import { rankListCssVars } from './rank-list' import { rankListCssVars } from './rank-list'
export default Vue.extend({ export default Vue.extend({

View File

@ -130,7 +130,7 @@ import { VideoCard } from '@/components/feeds/video-card'
import { getWatchlaterList, toggleWatchlater, watchlaterList } from '@/components/video/watchlater' import { getWatchlaterList, toggleWatchlater, watchlaterList } from '@/components/video/watchlater'
import { formatDuration } from '@/core/utils/formatters' import { formatDuration } from '@/core/utils/formatters'
import { DpiImage, VButton, VIcon, VLoading, VEmpty } from '@/ui' import { DpiImage, VButton, VIcon, VLoading, VEmpty } from '@/ui'
import { cssVariableMixin, requestMixin } from './mixin' import { cssVariableMixin, requestMixin } from '../../../../mixin'
export default Vue.extend({ export default Vue.extend({
components: { components: {

View File

@ -1,64 +1,72 @@
<template> <template>
<HomeRedesignBase> <HomeRedesignBase>
<div class="minimal-home"> <div class="minimal-home">
<div class="minimal-home-tabs"> <TabControl class="minimal-home-tabs" :tabs="tabs" />
<div class="default-tabs">
<MinimalHomeFeeds @tab-change="onTabChange" />
<MinimalHomeTrending @tab-change="onTabChange" />
</div>
</div>
</div> </div>
</HomeRedesignBase> </HomeRedesignBase>
</template> </template>
<script lang="ts"> <script lang="ts">
import { addComponentListener } from '@/core/settings'
import { TabControl } from '@/ui'
import { TabMappings } from '@/ui/tab-mapping'
import HomeRedesignBase from '../HomeRedesignBase.vue' import HomeRedesignBase from '../HomeRedesignBase.vue'
import { minimalHomeOptions } from './options' import { minimalHomeOptions } from './options'
import MinimalHomeFeeds from './tabs/MinimalHomeFeeds.vue'
import MinimalHomeTrending from './tabs/MinimalHomeTrending.vue'
import { MinimalHomeTabOption } from './types' import { MinimalHomeTabOption } from './types'
const tabs = [ const tabs: TabMappings = [
MinimalHomeFeeds, {
MinimalHomeTrending, name: MinimalHomeTabOption.Feeds,
displayName: '动态',
component: () => import('./tabs/MinimalHomeFeeds.vue').then(m => m.default),
activeLink: 'https://t.bilibili.com/?tab=video',
},
{
name: MinimalHomeTabOption.Trending,
displayName: minimalHomeOptions.personalized ? '推荐' : '热门',
component: () => import('./tabs/MinimalHomeTrending.vue').then(m => m.default),
activeLink: 'https://www.bilibili.com/v/popular/all',
},
] ]
export default Vue.extend({ export default Vue.extend({
components: { components: {
HomeRedesignBase, HomeRedesignBase,
MinimalHomeFeeds, TabControl,
MinimalHomeTrending,
}, },
data() { data() {
return { return {
tabs, tabs,
selectedTab: minimalHomeOptions.defaultTab,
} }
}, },
computed: { mounted() {
const columnCountKey = '--minimal-home-column-count-override'
}, addComponentListener('minimalHome.columnCount', (count: number) => {
methods: { if (count > 0) {
onTabChange(newTab: MinimalHomeTabOption) { (this.$el as HTMLElement).style.setProperty(columnCountKey, count.toString())
this.selectedTab = newTab } else {
}, (this.$el as HTMLElement).style.removeProperty(columnCountKey)
}
}, true)
}, },
}) })
</script> </script>
<style lang="scss"> <style lang="scss">
@import 'common'; @import 'common';
@import 'tabs';
/* TODO:
- 默认列数自适应
- 720P 一列
- 1080P 两列
- 21:9 四列
*/
.minimal-home { .minimal-home {
&-tabs { --minimal-home-auto-card-columns: 1;
@include tabs-style(); --card-width: 600px;
.default-tabs { --card-height: 122px;
padding: 4px 8px; @media screen and (min-width: 1080px) {
--minimal-home-auto-card-column: 2;
} }
@media screen and (min-width: 2520px) {
--minimal-home-auto-card-column: 3;
} }
--minimal-home-card-column: var(
--minimal-home-column-count-override,
var(--minimal-home-auto-card-column)
);
padding: 24px 32px;
} }
</style> </style>

View File

@ -1,14 +1,80 @@
<template> <template>
<div class="default-tab" data-name="feeds" @click="onTabChange">动态</div> <div class="minimal-home-feeds" :class="{ loading, loaded, error }">
<div class="minimal-home-feeds-cards">
<VideoCard
v-for="c of cards"
:key="c.id"
:data="c"
/>
</div>
<VEmpty v-if="loaded && cards.length === 0" />
<ScrollTrigger @trigger="loadCards" />
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { MinimalHomeTabOption } from '../types' import { getVideoFeeds } from '@/components/feeds/api'
import { VideoCard } from '@/components/feeds/video-card'
import VideoCardComponent from '@/components/feeds/VideoCard.vue'
import { logError } from '@/core/utils/log'
import { ascendingStringSort } from '@/core/utils/sort'
import {
VEmpty,
ScrollTrigger,
} from '@/ui'
import { cssVariableMixin } from '../../mixin'
export default Vue.extend({ export default Vue.extend({
components: { ScrollTrigger, VEmpty, VideoCard: VideoCardComponent },
mixins: [cssVariableMixin({
})],
data() {
return {
loading: true,
cards: [],
error: false,
}
},
computed: {
loaded() {
return !this.loading && !this.error
},
lastID() {
if (!this.cards.length) {
return null
}
const cards: VideoCard[] = [...this.cards]
return cards.sort(ascendingStringSort(c => c.id))[0].id
},
},
methods: { methods: {
onTabChange() { async loadCards() {
this.$emit('tab-change', MinimalHomeTabOption.Feeds) try {
this.error = false
this.loading = true
this.cards = lodash.uniqBy([...this.cards, ...await getVideoFeeds('video', this.lastID)], it => it.id)
} catch (error) {
logError(error)
this.error = true
} finally {
this.loading = false
}
}, },
}, },
}) })
</script> </script>
<style lang="scss">
.minimal-home-feeds {
&-cards {
display: grid;
grid-template-columns: repeat(var(--minimal-home-card-column), var(--card-width));
gap: 12px;
padding: 0 8px;
margin-bottom: 16px;
.video-card * {
transition: .2s ease-out;
}
}
}
</style>

View File

@ -1,20 +1,13 @@
<template> <template>
<div class="default-tab" data-name="trending">{{ title }}</div> <div></div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { minimalHomeOptions } from '../options'
import { MinimalHomeTabOption } from '../types'
export default Vue.extend({ export default Vue.extend({
computed: { computed: {
title() {
return minimalHomeOptions.personalized ? '推荐' : '热门'
},
}, },
methods: { methods: {
onTabChange() {
this.$emit('tab-change', MinimalHomeTabOption.Trending)
},
}, },
}) })
</script> </script>

View File

@ -2,11 +2,19 @@
<a <a
class="video-card" class="video-card"
target="_blank" target="_blank"
:href="epID ? ('https://www.bilibili.com/bangumi/play/ep' + epID) : ('https://www.bilibili.com/video/' + bvid)" :href="
epID
? 'https://www.bilibili.com/bangumi/play/ep' + epID
: 'https://www.bilibili.com/video/' + bvid
"
:class="{ vertical: orientation === 'vertical', 'no-stats': !showStats }" :class="{ vertical: orientation === 'vertical', 'no-stats': !showStats }"
> >
<div class="cover-container"> <div class="cover-container">
<DpiImage class="cover" :src="coverUrl" :size="{ height: 120, width: 200 }"></DpiImage> <DpiImage
class="cover"
:src="coverUrl"
:size="{ height: 120, width: 196 }"
></DpiImage>
<div v-if="isNew" class="new">NEW</div> <div v-if="isNew" class="new">NEW</div>
<template v-if="pubTime && pubTimeText"> <template v-if="pubTime && pubTimeText">
<div class="publish-time-summary"> <div class="publish-time-summary">
@ -34,10 +42,16 @@
<a <a
v-for="topic of topics.slice(0, 3)" v-for="topic of topics.slice(0, 3)"
:key="topic.id" :key="topic.id"
:title="topic.name"
class="topic" class="topic"
target="_blank" target="_blank"
:href="'https://t.bilibili.com/topic/name/' + topic.name + '/feed'" :href="'https://t.bilibili.com/topic/name/' + topic.name + '/feed'"
>#{{ topic.name }}#</a> >
<VIcon icon="mdi-tag-outline" :size="14" />
<div class="topic-name">
{{ topic.name }}
</div>
</a>
</div> </div>
<p v-else class="description" :title="description">{{ description }}</p> <p v-else class="description" :title="description">{{ description }}</p>
<a <a
@ -45,7 +59,7 @@
class="up" class="up"
:class="{ 'no-face': !upFaceUrl }" :class="{ 'no-face': !upFaceUrl }"
target="_blank" target="_blank"
:href="upID ? ('https://space.bilibili.com/' + upID) : null" :href="upID ? 'https://space.bilibili.com/' + upID : null"
> >
<DpiImage v-if="upFaceUrl" class="face" :src="upFaceUrl" :size="24" /> <DpiImage v-if="upFaceUrl" class="face" :src="upFaceUrl" :size="24" />
<VIcon v-else icon="up" /> <VIcon v-else icon="up" />
@ -60,15 +74,18 @@
:class="{ 'no-face': !up.faceUrl }" :class="{ 'no-face': !up.faceUrl }"
target="_blank" target="_blank"
:title="up.name" :title="up.name"
:href="up.id ? ('https://space.bilibili.com/' + up.id) : null" :href="up.id ? 'https://space.bilibili.com/' + up.id : null"
> >
<DpiImage v-if="up.faceUrl" class="face" :src="up.faceUrl" :size="24" /> <DpiImage
v-if="up.faceUrl"
class="face"
:src="up.faceUrl"
:size="24"
/>
<VIcon v-else icon="up" /> <VIcon v-else icon="up" />
</a> </a>
</div> </div>
<div class="cooperation-note"> <div class="cooperation-note">联合投稿</div>
联合投稿
</div>
</div> </div>
<div v-if="showStats" class="stats"> <div v-if="showStats" class="stats">
<template v-if="vertical"> <template v-if="vertical">
@ -198,11 +215,11 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "common"; @import 'common';
.video-card { .video-card {
display: grid; display: grid;
grid-template-columns: 200px 1fr; grid-template-columns: 196px 1fr;
grid-template-rows: 1fr 1fr 1fr; grid-template-rows: 1fr 1fr 1fr;
grid-template-areas: grid-template-areas:
'cover title' 'cover title'
@ -245,6 +262,8 @@ export default {
} }
.cover-container { .cover-container {
border-radius: $radius $radius 0 0; border-radius: $radius $radius 0 0;
width: calc(var(--card-width) - 2px);
height: calc(var(--card-width) / 20 * 12.5);
} }
.title { .title {
display: -webkit-box; display: -webkit-box;
@ -255,6 +274,7 @@ export default {
white-space: normal; white-space: normal;
line-height: 1.5; line-height: 1.5;
margin: 4px 0; margin: 4px 0;
padding: 0 10px;
font-size: 14px; font-size: 14px;
} }
.up { .up {
@ -315,8 +335,8 @@ export default {
grid-area: cover; grid-area: cover;
border-radius: $radius 0 0 $radius; border-radius: $radius 0 0 $radius;
position: relative; position: relative;
width: calc(var(--card-width) - 2px); width: calc(var(--card-height) / 12.5 * 20);
height: calc(var(--card-width) / 20 * 12.5); height: calc(var(--card-height) - 2px);
overflow: hidden; overflow: hidden;
.cover { .cover {
transition: 0.1s cubic-bezier(0.39, 0.58, 0.57, 1); transition: 0.1s cubic-bezier(0.39, 0.58, 0.57, 1);
@ -377,11 +397,11 @@ export default {
} }
.title { .title {
grid-area: title; grid-area: title;
font-size: 16px; font-size: 15px;
// font-weight: bold; // font-weight: bold;
@include semi-bold(); @include semi-bold();
color: inherit; color: inherit;
padding: 0 10px; padding: 4px 12px 0 12px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
justify-self: stretch; justify-self: stretch;
@ -391,23 +411,25 @@ export default {
} }
} }
.topics { .topics {
@include h-center();
grid-area: description; grid-area: description;
display: flex;
align-items: center;
margin-left: 12px; margin-left: 12px;
.topic { .topic {
@include h-center(4px);
color: inherit; color: inherit;
padding: 4px 8px; padding: 4px 8px;
background-color: #8882; border: 1px solid #8882;
margin-right: 8px; margin-right: 8px;
border-radius: 14px; border-radius: 14px;
white-space: nowrap; opacity: 0.75;
.topic-name {
max-width: 120px; max-width: 120px;
overflow: hidden; @include single-line();
text-overflow: ellipsis; }
&:hover { &:hover {
background-color: #8884; background-color: #8882;
color: var(--theme-color); color: var(--theme-color);
opacity: 1;
} }
} }
} }
@ -518,7 +540,7 @@ export default {
} }
} }
&-note { &-note {
opacity: .5; opacity: 0.5;
} }
} }
.stats { .stats {

View File

@ -5,7 +5,7 @@ import { watchlaterList } from '@/components/video/watchlater'
import { getData, registerData } from '@/plugins/data' import { getData, registerData } from '@/plugins/data'
import { descendingStringSort } from '@/core/utils/sort' import { descendingStringSort } from '@/core/utils/sort'
import { VideoCard } from '../video-card' import { VideoCard } from '../video-card'
import { FeedsCard, FeedsCardType } from './types' import { FeedsCard, FeedsCardType, feedsCardTypes } from './types'
export * from './types' export * from './types'
export * from './manager' export * from './manager'
@ -59,18 +59,42 @@ export const withContentFilter = <Args extends any[], Item> (
func: (...args: Args) => Promise<Item[]>, func: (...args: Args) => Promise<Item[]>,
) => (...args: Args) => func(...args).then(items => applyContentFilter(items)) ) => (...args: Args) => func(...args).then(items => applyContentFilter(items))
/**
* API
* @param type , ID列表返回最新动态
* @param afterID ID之前的动态历史,
*/
export const getFeedsUrl = (type: FeedsCardType | string, afterID?: string | number) => {
if (typeof type === 'string') {
return `https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=${getUID()}&type_list=${type}`
}
const id = type.id.toString()
let api = `https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=${getUID()}&type_list=${id}`
if (afterID) {
api = `https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_history?uid=${getUID()}&offset_dynamic_id=${afterID}&type=${id}`
}
return api
}
/**
*
* @param type , ID列表返回最新动态
* @param afterID ID之前的动态历史,
*/
export const getFeeds = async (type: FeedsCardType | string, afterID?: string | number) => (
getJsonWithCredentials(getFeedsUrl(type, afterID))
)
/** /**
* *
* @param type (video) (bangumi) , * @param type (video) (bangumi) ,
* @param afterID ID之前的动态历史,
*/ */
export const getVideoFeeds = withContentFilter( export const getVideoFeeds = withContentFilter(
async (type: 'video' | 'bangumi' = 'video'): Promise<VideoCard[]> => { async (type: 'video' | 'bangumi' = 'video', afterID?: string | number): Promise<VideoCard[]> => {
if (!getUID()) { if (!getUID()) {
return [] return []
} }
const json = await getJsonWithCredentials( const json = await getJsonWithCredentials(getFeedsUrl(type === 'video' ? feedsCardTypes.video : feedsCardTypes.bangumi, afterID))
`https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=${getUID()}&type_list=${type === 'video' ? 8 : 512}`,
)
if (json.code !== 0) { if (json.code !== 0) {
throw new Error(json.message) throw new Error(json.message)
} }
@ -138,23 +162,6 @@ export const getVideoFeeds = withContentFilter(
}, },
) )
// let mockupCalled = false
/**
*
* @param type , ID列表返回最新动态
* @param afterID ID之前的动态历史,
*/
export const getFeeds = async (type: FeedsCardType | string, afterID?: string | number) => {
if (typeof type === 'string') {
return getJsonWithCredentials(`https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=${getUID()}&type_list=${type}`)
}
const id = type.id.toString()
let api = `https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=${getUID()}&type_list=${id}`
if (afterID) {
api = `https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_history?uid=${getUID()}&offset_dynamic_id=${afterID}&type=${id}`
}
return getJsonWithCredentials(api)
}
/** /**
* *
* @param card * @param card

View File

@ -117,6 +117,9 @@ export default Vue.extend({
.header-item { .header-item {
flex: 1; flex: 1;
margin: 0 8px; margin: 0 8px;
&:empty {
display: none;
}
} }
.be-more-link { .be-more-link {
.be-button { .be-button {