Add infinite scroll

This commit is contained in:
the1812 2020-06-13 14:54:15 +08:00
parent 3d6961a1ee
commit a1dd164f21
10 changed files with 494 additions and 347 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -33,10 +33,10 @@
"compact-layout.min.js": "B20609A7CBBDB1845FA0156FB5BE6B1E1A1B8B069EA85F65D16241DD2C12D738",
"custom-control-background.min.css": "1981FD2BF3B17ECF33F98D5DEDAF0D32ACBE9532A51FDB70822286991AB98EF3",
"custom-control-background.min.js": "B9F366B251D1572ACE801FF6ECF15D4D1AAB3EA5A5B04B961B6F4310E2AB1A84",
"custom-navbar.min.css": "9B21F32F0BD66613AED9E52F666BF5088BB1A7B1CFEBBDABAD0B7B74C64878D7",
"custom-navbar.min.css": "813761561B78BBBD7AF662FC661FBF948ABEE69676A9454AC540444B53638971",
"custom-navbar.min.html": "183B1F7182633044CCB20A3710524492118308CF55915AA9AA9F36850500F457",
"custom-navbar.min.js": "0D53FECB162F73C77B305CB2324ECA956D91F43BBDF3AA9B1B01D6727EDF2D99",
"custom-navbar-activities.min.js": "802226FE2C24D56379A6EAD13D3697A4D5A46109D0C63EDACAE8E667ED267EA1",
"custom-navbar-activities.min.js": "D18DDC1B01B5F91E147539955A6400092564891114CF5C38771D5F71F67C454B",
"custom-navbar-blank.min.js": "DC2B75D5760EECD3A257785FDFBB7E5DEBF61CD9083C68DD5BCE468B093F54F3",
"custom-navbar-category.min.js": "2D1A6189540E5DE22A9741AFCFDA2FED50EA960B129547B1E756F2D5E53C0C67",
"custom-navbar-component.min.js": "1E19E02F7BFCC3397C14907EC8390F03522E128B528CB82412F202BAF57C28E9",
@ -58,12 +58,11 @@
"danmaku-send-bar.min.css": "52149D46CF3A15CF6EB186CB49F298DE18B0723E52D8693228F66099847EDCCB",
"danmaku-send-bar.min.js": "7108E72F00C169F26E20409F4AA79F05553310375BABC392EB7E2166224B800A",
"dark.min.css": "BCC164174A579D1D2B4CC4F993611B65B283F71D4F0679299FF8C3D4A8FDDE7F",
"dark.user.css": "8068EB2B2AC92ADAD8E0E904C4F9CF07D3394EA2CD196CEDC674B28D19CF1556",
"dark.user.css": "1946312C75FD486219173E12B95847FCA382E66AF9BA89268B45B0B8FC1EE2BB",
"dark-color-scheme.min.js": "6D353AC738180317ACF9D2E046BFFBA0ACC0FA7A8D22B8E16C51127CDE4FE96B",
"dark-important.min.css": "5369385A2C16188FB11E71C4C5B91275646F22F0F09E518E37813CC4FA23A072",
"dark-navbar.min.css": "7FAD547E326B768904B3739C978DF86FFDDD5E5064FB8F490710D197CCC3018B",
"dark-schedule.min.js": "853C446547603F4F0425F19F09F73335C9EC451A790C1C07E5E5B88A09E9B453",
"dark-slice-9.min.css": "7B717361A3252C90B7BA6B02DEA097B328F72DC3781133D94FE56A2B01DB2B2A",
"dark-styles.min.js": "74E6D655F88E50A27D60F189554F6656CCD2A6EA2686FD9946CA9A89EABF7719",
"debounce.min.js": "54D33E1273C1F3FE19550BF1844339C3D54D6B01DF8A39C3162D95B93B079CFA",
"default-danmaku-settings.min.css": "D9942B184FEDA7B08CFA0C34920E97D7A83B81762DCBF757642EBB60F95FF25D",

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -34,20 +34,19 @@
.activity-tab {
position: relative;
cursor: pointer;
}
.activity-tab.selected {
opacity: 1;
transform: scale(1.2);
}
.activity-tab .tab-name {
.tab-name {
line-height: normal;
opacity: 0.5;
}
.activity-tab.selected .tab-name {
&.selected {
opacity: 1;
transform: scale(1.2);
.tab-name {
opacity: 1;
font-weight: bold;
}
.activity-tab[data-count]::before {
}
&[data-count]::before {
content: attr(data-count);
position: absolute;
top: 0;
@ -61,6 +60,7 @@
border-radius: 10px;
white-space: nowrap;
}
}
.activity-tab::after,
.subscriptions .tab::after {
content: "";
@ -78,6 +78,10 @@
.subscriptions .tab.selected::after {
transform: scaleX(1);
}
.activity-popup-content::-webkit-scrollbar,
.subscriptions .content::-webkit-scrollbar {
width: 0px !important;
}
.activity-popup-content {
overflow: auto;
overscroll-behavior: contain;
@ -88,12 +92,7 @@
align-items: center;
justify-content: space-between;
scrollbar-width: none !important;
}
.activity-popup-content::-webkit-scrollbar,
.subscriptions .content::-webkit-scrollbar {
width: 0px !important;
}
.activity-popup-content .view-more {
.view-more {
display: flex;
align-items: center;
flex-shrink: 0;
@ -111,21 +110,22 @@
border-radius: 14px;
box-sizing: border-box;
z-index: 2;
}
.activity-popup-content .view-more:hover {
&:hover {
background-color: #fff;
}
&.dark .activity-popup-content .view-more {
background-color: #333e;
}
&.dark .activity-popup-content .view-more:hover {
background-color: #333;
}
.activity-popup-content .view-more .mdi {
.mdi {
line-height: 1;
margin-left: 8px;
font-size: 12pt;
}
body.dark & {
background-color: #333e;
&:hover {
background-color: #333;
}
}
}
}
.activity-popup-content .loading,
.bangumi-subscriptions .loading,
.bangumi-subscriptions .empty {
@ -140,19 +140,34 @@
line-height: 1;
margin-right: 6px;
}
.video-activity {
padding: 0 12px;
display: flex;
justify-content: space-between;
// display: flex;
// justify-content: space-between;
align-self: stretch;
}
.video-activity.center {
display: grid;
grid-template:
"left right" auto
"trigger trigger" auto / 1fr 1fr;
&.center {
display: flex;
height: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
}
.video-activity-card {
&-column {
&.left {
grid-area: left;
justify-self: start;
}
&.right {
grid-area: right;
justify-self: end;
}
}
&-card {
--card-width: 172px;
width: var(--card-width);
display: flex;
@ -161,14 +176,7 @@
margin-bottom: 12px;
break-inside: avoid;
flex-shrink: 0;
}
&.dark .video-activity-card,
&.dark .activity-popup-content .view-more,
&.dark .column-card {
box-shadow: rgba(0, 0, 0, 0.3) 0 4px 8px 0px;
background-color: #2d2d2d;
}
.video-activity-card .cover {
.cover {
width: var(--card-width);
background-color: #8884;
height: calc(var(--card-width) / 16 * 10);
@ -176,7 +184,7 @@
display: block;
min-height: 100px;
}
.video-activity-card .title {
.title {
font-size: 10pt;
font-weight: bold;
margin: 8px 8px 2px 8px;
@ -190,37 +198,37 @@
max-height: 3em;
word-break: break-all;
}
.video-activity-card .title:hover,
.video-activity-card .up:hover .name {
color: var(--theme-color);
}
.video-activity-card .up {
.up {
display: flex;
justify-content: space-between;
align-items: center;
margin: 6px 8px;
border-radius: 13px;
padding: 2px;
}
.video-activity-card .up:hover {
&:hover {
background-color: #8882;
}
.video-activity-card .up .face {
.face {
width: 24px;
border-radius: 50%;
background-color: #8884;
}
.video-activity-card .up .name {
.name {
padding: 0 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.video-activity-card .cover-container {
}
.title:hover,
.up:hover .name {
color: var(--theme-color);
}
.cover-container {
position: relative;
}
.video-activity-card .time,
.video-activity-card .watchlater {
.time,
.watchlater {
position: absolute;
bottom: 4px;
display: flex;
@ -233,34 +241,43 @@
box-sizing: border-box;
opacity: 0;
}
.video-activity-card:hover .time,
.video-activity-card:hover .watchlater {
.time,
.watchlater {
opacity: 1;
}
.video-activity-card .time {
.time {
left: 4px;
}
.video-activity-card .watchlater {
.watchlater {
padding: 0 8px 0 3px;
right: 4px;
font-size: 11px;
}
.video-activity-card .watchlater .mdi {
.mdi {
line-height: 1;
margin-right: 4px;
font-size: 16px;
}
}
}
}
&.dark .video-activity-card,
&.dark .activity-popup-content .view-more,
&.dark .column-card {
box-shadow: rgba(0, 0, 0, 0.3) 0 4px 8px 0px;
background-color: #2d2d2d;
}
.bangumi-activity {
display: flex;
flex-direction: column;
padding-top: 4px;
width: 100%;
}
.bangumi-activity.center {
&.center {
height: 100%;
justify-content: center;
align-items: center;
}
}
.bangumi-card {
--cover-width: 100px;
margin: 0 12px 12px 12px;
@ -271,32 +288,26 @@
grid-template-rows: 6fr 5fr;
position: relative;
flex-shrink: 0;
}
&.dark .bangumi-card,
&.dark .live-card {
box-shadow: rgba(0, 0, 0, 0.3) 0 2px 8px 0px;
background-color: #2d2d2d;
}
.bangumi-card .up {
.up {
grid-area: title;
display: flex;
align-items: center;
padding: 0 12px;
align-self: start;
overflow: hidden;
}
.bangumi-card .up .cover {
.cover {
height: 18px;
border-radius: 50%;
}
.bangumi-card .up .title {
.title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin: 0 6px;
line-height: normal;
}
.bangumi-card .ep-title {
}
.ep-title {
grid-area: epTitle;
font-size: 11pt;
font-weight: bold;
@ -308,36 +319,43 @@
color: inherit;
line-height: normal;
}
.bangumi-card:hover .ep-title {
&:hover .ep-title {
color: var(--theme-color);
}
.bangumi-card .ep-cover {
.ep-cover {
width: var(--cover-width);
min-height: 62.5px;
background-color: #8884;
grid-area: cover;
}
}
&.dark .bangumi-card,
&.dark .live-card {
box-shadow: rgba(0, 0, 0, 0.3) 0 2px 8px 0px;
background-color: #2d2d2d;
}
.column-activity {
display: flex;
flex-direction: column;
align-items: stretch;
padding: 0 12px;
}
.column-activity.center {
&.center {
height: 100%;
align-items: center;
justify-content: center;
}
}
.column-card {
width: 356px;
display: flex;
flex-direction: column;
margin-bottom: 12px;
box-shadow: rgba(0, 0, 0, 0.12) 0 4px 8px 0px;
position: relative;
flex-shrink: 0;
}
.column-card .up {
.up {
position: absolute;
left: 8px;
bottom: 6px;
@ -348,26 +366,26 @@
border-radius: 14px;
height: 28px;
box-sizing: border-box;
}
.column-card .face {
border-radius: 50%;
height: 24px;
}
.column-card .up .name {
.name {
margin: 0 6px;
color: #fff;
}
.column-card .title {
}
.face {
border-radius: 50%;
height: 24px;
}
.title {
padding: 10px 10px 0;
font-size: 11pt;
font-weight: bold;
color: inherit;
line-height: normal;
}
.column-card:hover .title {
&:hover .title {
color: var(--theme-color);
}
.column-card .description {
.description {
margin: 8px 10px;
max-height: 3em;
overflow: hidden;
@ -378,27 +396,28 @@
-webkit-box-orient: vertical;
display: -webkit-box;
}
.column-card .covers {
.covers {
position: relative;
display: flex;
}
.column-card .cover {
width: 0;
flex-grow: 1;
.cover {
flex: 1;
object-fit: cover;
}
}
.live-activity {
display: flex;
flex-direction: column;
align-items: stretch;
width: 100%;
padding-top: 4px;
}
.live-activity.center {
&.center {
height: 100%;
align-items: center;
justify-content: center;
}
}
.live-card {
margin: 0 12px 12px 12px;
box-shadow: rgba(0, 0, 0, 0.12) 0 2px 8px 0px;
@ -412,13 +431,12 @@
padding: 2px;
width: auto;
flex-shrink: 0;
}
.live-card .face {
.face {
grid-area: face;
border-radius: 50%;
height: 48px;
}
.live-card .live-title {
.live-title {
grid-area: title;
font-size: 11pt;
font-weight: bold;
@ -430,10 +448,10 @@
color: inherit;
line-height: normal;
}
.live-card:hover .live-title {
&:hover .live-title {
color: var(--theme-color);
}
.live-card .name {
.name {
grid-area: name;
align-self: start;
padding: 0 12px;
@ -442,6 +460,20 @@
white-space: nowrap;
line-height: normal;
}
}
.video-activity,
.bangumi-activity,
.column-activity,
.live-activity {
.trigger {
text-align: center;
grid-area: trigger;
align-self: stretch;
margin-bottom: 12px;
}
}
.activity-content-enter-active,
.activity-content-leave-active,
.subscriptions-content-enter-active,

View File

@ -3,19 +3,22 @@ import { VideoCardInfo } from '../../simplify-home/video-card-info'
let componentUpdate = async () => { }
let tabUpdate = async () => { }
let latestID: string
// feeds history API https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_history?uid=${getUID()}&offset_dynamic_id=${lastCardID}&type=${feedsType}
// live list https://api.live.bilibili.com/relation/v1/feed/feed_list?page=${page}&pagesize=${pageSize}
interface ActivityTabComponentConfig {
dataObject: {}
apiUrl: string
name: string
handleJson: (json: any) => void
handleJson: (json: any) => Promise<void>
template: string
nextPage?: () => Promise<boolean>
}
interface ActivityTab {
name: string
moreUrl: string
}
const getActivityTabComponent = ({ dataObject, apiUrl, name, handleJson, template }: ActivityTabComponentConfig) => {
return {
const getActivityTabComponent = ({ dataObject, apiUrl, name, handleJson, nextPage, template }: ActivityTabComponentConfig) => {
const component = {
template,
components: {
'dpi-img': () => import('../dpi-img.vue'),
@ -42,16 +45,44 @@ const getActivityTabComponent = ({ dataObject, apiUrl, name, handleJson, templat
data() {
return Object.assign({
loading: true,
hasMoreContent: true,
scrollObserver: null,
}, dataObject)
},
mounted() {
this.fetchData()
async mounted() {
componentUpdate = async () => await this.fetchData(true)
await this.fetchData()
if (this.$refs.trigger && typeof this.nextPage === 'function') {
console.log('infinite scroll')
const trigger = this.$refs.trigger as HTMLElement
const observer = new IntersectionObserver(async (entries) => {
console.log(entries)
if (entries.some(e => e.intersectionRatio > 0)) {
const hasMoreContent = await this.nextPage()
if (!hasMoreContent) {
console.log('disconnect')
observer.disconnect()
this.hasMoreContent = false
}
}
})
this.scrollObserver = observer
observer.observe(trigger)
}
},
destroyed() {
beforeDestroy() {
componentUpdate = async () => { }
if (this.scrollObserver) {
console.log('destroy')
this.scrollObserver.disconnect()
this.scrollObserver = null
}
},
}
if (nextPage) {
(component.methods as any).nextPage = nextPage
}
return component
}
export class Activities extends NavbarComponent {
constructor() {
@ -80,6 +111,9 @@ export class Activities extends NavbarComponent {
}
this.getNotifyCount()
setInterval(async () => {
if (!navigator.onLine) {
return
}
await this.getNotifyCount()
await tabUpdate()
await componentUpdate()
@ -214,18 +248,19 @@ export class Activities extends NavbarComponent {
<div class="video-activity" :class="{center: loading || (leftCards.length + rightCards.length) === 0}">
<activity-loading :loading="loading"></activity-loading>
<activity-empty v-if="!loading && leftCards.length + rightCards.length === 0"></activity-empty>
<div v-if="!loading" class="video-activity-column">
<div v-if="!loading" class="video-activity-column left">
<video-card v-for="card of leftCards" :key="card.id" :card="card" :watchlaterInit="card.watchlater"></video-card>
</div>
<div v-if="!loading" class="video-activity-column">
<div v-if="!loading" class="video-activity-column right">
<video-card v-for="card of rightCards" :key="card.id" :card="card" :watchlaterInit="card.watchlater"></video-card>
</div>
<div v-if="!loading && hasMoreContent" class="trigger" ref="trigger">...</div>
</div>
`,
handleJson: async function (json) {
async handleJson(json) {
// const { getWatchlaterList } = await import('../../video/watchlater-api')
// const watchlaterList = await getWatchlaterList()
const cards = _.uniqBy(_.get(json, 'data.cards', []).map((card: any) => {
const jsonCards = _.get(json, 'data.cards', []).map((card: any) => {
const cardJson = JSON.parse(card.card)
return {
coverUrl: cardJson.pic,
@ -242,14 +277,38 @@ export class Activities extends NavbarComponent {
watchlater: true,
get new() { return Activities.isNewID(this.id) },
}
}), (it: VideoCardInfo) => it.aid)
}) as VideoCardInfo[]
const cards = _.uniqBy(jsonCards.concat(this.leftCards, this.rightCards), it => it.id)
.sort((a, b) => {
return b.id > a.id ? 1 : -1
})
if (cards.length === 0) {
this.hasMoreContent = false
}
this.leftCards = cards.filter((_, index) => index % 2 === 0)
this.rightCards = cards.filter((_, index) => index % 2 === 1)
if (this.leftCards.length !== this.rightCards.length) {
this.leftCards.pop()
}
// if (this.leftCards.length !== this.rightCards.length) {
// this.leftCards.pop()
// }
Activities.updateLatestID(cards)
},
async nextPage() {
const cards = [...this.leftCards, ...this.rightCards].sort((a, b) => {
return b.id > a.id ? 1 : -1
}) as VideoCardInfo[]
if (cards.length === 0) {
return false
}
let lastCardID = cards.pop()!.id
const api = `https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_history?uid=${getUID()}&offset_dynamic_id=${lastCardID}&type=8`
const json = await Ajax.getJsonWithCredentials(api)
console.log('lastCardID', lastCardID, json)
if (json.code !== 0) {
return false
}
await this.handleJson(json)
return Boolean(_.get(json, 'data.has_more', true))
},
}), {
components: {
'video-card': {
@ -313,10 +372,11 @@ export class Activities extends NavbarComponent {
<div class="title">{{card.title}}</div>
</div>
</a>
<div v-if="!loading && hasMoreContent" class="trigger" ref="trigger">...</div>
</div>
`,
handleJson: async function (json) {
this.cards = _.get(json, 'data.cards', []).map((card: any) => {
const cards = _.get(json, 'data.cards', []).map((card: any) => {
const cardJson = JSON.parse(card.card)
return {
title: cardJson.apiSeasonInfo.title,
@ -327,8 +387,30 @@ export class Activities extends NavbarComponent {
id: card.desc.dynamic_id_str,
get new() { return Activities.isNewID(this.id) },
}
}) as { id: string }[]
this.cards = _.uniqBy(cards.concat(this.cards), it => it.id)
.sort((a, b) => {
return b.id > a.id ? 1 : -1
})
Activities.updateLatestID(this.cards)
if (cards.length === 0) {
this.hasMoreContent = false
}
Activities.updateLatestID(cards)
},
async nextPage() {
const cards = [...this.cards] as VideoCardInfo[]
if (cards.length === 0) {
return false
}
let lastCardID = cards.pop()!.id
const api = `https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_history?uid=${getUID()}&offset_dynamic_id=${lastCardID}&type=512`
const json = await Ajax.getJsonWithCredentials(api)
console.log('lastCardID', lastCardID, json)
if (json.code !== 0) {
return false
}
await this.handleJson(json)
return Boolean(_.get(json, 'data.has_more', true))
},
}),
'column-activity': getActivityTabComponent({
@ -341,19 +423,22 @@ export class Activities extends NavbarComponent {
<activity-empty v-if="!loading && cards.length === 0"></activity-empty>
<a v-if="!loading" class="column-card" :class="{new: card.new}" v-for="card of cards" :key="card.id" target="_blank" :href="card.url">
<div class="covers">
<dpi-img class="cover" v-for="cover of card.covers" :key="cover" :size="{height: 120}" :src="cover"></dpi-img>
<!--<dpi-img class="cover" v-for="cover of card.covers" :key="cover" :size="{ height: 120, width: 0 }" :src="cover"></dpi-img>-->
<img class="cover" v-for="cover of card.covers" :key="cover" height="120" width="0" :src="cover"/>
<a class="up" target="_blank" :href="card.upUrl">
<dpi-img class="face" :size="24" :src="card.faceUrl"></dpi-img>
<img class="face" height="24" width="24" :src="card.faceUrl"/>
<!--<dpi-img class="face" :size="24" :src="card.faceUrl"></dpi-img>-->
<div class="name">{{card.upName}}</div>
</a>
</div>
<h1 class="title" :title="card.title">{{card.title}}</h1>
<div class="description" :title="card.description">{{card.description}}</div>
</a>
<div v-if="!loading && hasMoreContent" class="trigger" ref="trigger">...</div>
</div>
`,
handleJson: async function (json) {
this.cards = _.get(json, 'data.cards', []).map((card: any) => {
const cards = _.get(json, 'data.cards', []).map((card: any) => {
const cardJson = JSON.parse(card.card)
return {
covers: cardJson.image_urls,
@ -367,9 +452,31 @@ export class Activities extends NavbarComponent {
id: card.desc.dynamic_id_str,
get new() { return Activities.isNewID(this.id) },
}
}) as { id: string }[]
this.cards = _.uniqBy(cards.concat(this.cards), it => it.id)
.sort((a, b) => {
return b.id > a.id ? 1 : -1
})
if (cards.length === 0) {
this.hasMoreContent = false
}
Activities.updateLatestID(this.cards)
},
async nextPage() {
const cards = [...this.cards] as VideoCardInfo[]
if (cards.length === 0) {
return false
}
let lastCardID = cards.pop()!.id
const api = `https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_history?uid=${getUID()}&offset_dynamic_id=${lastCardID}&type=64`
const json = await Ajax.getJsonWithCredentials(api)
console.log('lastCardID', lastCardID, json)
if (json.code !== 0) {
return false
}
await this.handleJson(json)
return Boolean(_.get(json, 'data.has_more', true))
},
}),
'live-activity': getActivityTabComponent({
dataObject: { cards: [] },
@ -387,7 +494,7 @@ export class Activities extends NavbarComponent {
</div>
`,
handleJson: async function (json) {
this.cards = _.get(json, 'data.list', []).map((card: any) => {
const parseLiveCard = (card: any) => {
return {
faceUrl: card.face,
title: card.title,
@ -395,7 +502,20 @@ export class Activities extends NavbarComponent {
id: card.roomid,
url: card.link,
}
}
this.cards = _.get(json, 'data.list', []).map(parseLiveCard)
const fullList = await Ajax.getPages({
api: page => {
return Ajax.getJsonWithCredentials(`https://api.live.bilibili.com/relation/v1/feed/feed_list?page=${page}&pagesize=24`)
},
getList: json => {
return _.get(json, 'data.list', [])
},
getTotal: json => {
return _.get(json, 'data.results', 0)
},
})
this.cards = fullList.map(parseLiveCard)
},
}),
},