mirror of
https://github.com/the1812/Bilibili-Evolved.git
synced 2025-11-04 21:22:45 +08:00
Merge pull request #2960 from timongh/feat-default-location
添加新组件“视频页默认定位”(videoDefaultLocation)
This commit is contained in:
commit
22916eaef7
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -92,6 +92,7 @@
|
||||
"Violentmonkey",
|
||||
"Vuex",
|
||||
"Wasm",
|
||||
"watchlater",
|
||||
"woff",
|
||||
"xmlhttp"
|
||||
],
|
||||
|
||||
166
registry/lib/components/video/default-location/Advanced.vue
Normal file
166
registry/lib/components/video/default-location/Advanced.vue
Normal file
@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="video-default-location-form-line">
|
||||
<div class="video-default-location-form-item-not-grow">
|
||||
当前位置
|
||||
</div>
|
||||
<TextBox
|
||||
class="video-default-location-form-item-grow"
|
||||
:text="String(curPosition)"
|
||||
change-on-blur
|
||||
readonly
|
||||
linear
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="video-default-location-vertical-space"></div>
|
||||
|
||||
<div class="video-default-location-form-line">
|
||||
<VButton
|
||||
class="video-default-location-form-item-grow"
|
||||
@click="$emit('set-default-location', curPosition)"
|
||||
>
|
||||
将当前位置设为默认值
|
||||
</VButton>
|
||||
</div>
|
||||
|
||||
<div class="video-default-location-vertical-space"></div>
|
||||
|
||||
<div class="video-default-location-form-line">
|
||||
<TextBox
|
||||
v-model="locationInput"
|
||||
class="video-default-location-form-item-grow"
|
||||
linear
|
||||
change-on-blur
|
||||
@change="onLocationInput"
|
||||
/>
|
||||
<VButton @click="locateTo">
|
||||
定位
|
||||
</VButton>
|
||||
</div>
|
||||
|
||||
<div class="video-default-location-vertical-space"></div>
|
||||
|
||||
<div class="video-default-location-form-line">
|
||||
<TextBox
|
||||
v-model="offsetInput"
|
||||
class="video-default-location-form-item-grow"
|
||||
linear
|
||||
change-on-blur
|
||||
@change="onOffsetInput"
|
||||
/>
|
||||
<VButton @click="offsetTo">
|
||||
偏移
|
||||
</VButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { VButton, TextBox } from '@/ui'
|
||||
|
||||
let scrollObserver = null
|
||||
|
||||
const getScrollY = (): number => Math.round(window.scrollY)
|
||||
|
||||
const stringIntoInt = (value: string): number | null => {
|
||||
const num = parseFloat(value)
|
||||
if (isNaN(num)) {
|
||||
return null
|
||||
}
|
||||
return Math.round(num)
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
components: { VButton, TextBox },
|
||||
props: {
|
||||
observePosition: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
locationLimit: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
curPosition: getScrollY(),
|
||||
locationInput: '0',
|
||||
offsetInput: '0',
|
||||
location: 0,
|
||||
offset: 0,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.setupObserveScroll()
|
||||
},
|
||||
beforeDestroy() {
|
||||
scrollObserver.stop()
|
||||
},
|
||||
methods: {
|
||||
setLocation(value: number) {
|
||||
this.location = value
|
||||
this.locationInput = String(value)
|
||||
},
|
||||
onLocationInput(value: string) {
|
||||
let num = stringIntoInt(value)
|
||||
if (num === null) {
|
||||
this.setLocation(0)
|
||||
} else {
|
||||
num = lodash.clamp(num, 0, this.locationLimit)
|
||||
this.setLocation(num)
|
||||
}
|
||||
},
|
||||
locateTo() {
|
||||
unsafeWindow.scrollTo(0, this.location)
|
||||
},
|
||||
setOffset(value: number) {
|
||||
this.offset = value
|
||||
this.offsetInput = String(value)
|
||||
},
|
||||
onOffsetInput(value) {
|
||||
let num = stringIntoInt(value)
|
||||
if (num === null) {
|
||||
this.setOffset(0)
|
||||
} else {
|
||||
num = lodash.clamp(num, -this.locationLimit, this.locationLimit)
|
||||
this.setOffset(num)
|
||||
}
|
||||
},
|
||||
offsetTo() {
|
||||
unsafeWindow.scrollBy(0, this.offset)
|
||||
},
|
||||
setupObserveScroll() {
|
||||
const updateCurPosition = () => {
|
||||
this.curPosition = getScrollY()
|
||||
}
|
||||
let observing = false
|
||||
scrollObserver = {
|
||||
start: () => {
|
||||
if (!observing) {
|
||||
updateCurPosition()
|
||||
window.addEventListener('scroll', updateCurPosition)
|
||||
observing = true
|
||||
}
|
||||
},
|
||||
stop: () => {
|
||||
if (observing) {
|
||||
window.removeEventListener('scroll', updateCurPosition)
|
||||
observing = false
|
||||
}
|
||||
},
|
||||
}
|
||||
this.$watch(
|
||||
'observePosition',
|
||||
shouldObserve => scrollObserver[shouldObserve ? 'start' : 'stop'](),
|
||||
{ immediate: true },
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
</style>
|
||||
159
registry/lib/components/video/default-location/ExtendBox.vue
Normal file
159
registry/lib/components/video/default-location/ExtendBox.vue
Normal file
@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div
|
||||
class="video-default-location-extend-box"
|
||||
:class="{ 'video-default-location-extend-box-hidden': realHidden }"
|
||||
>
|
||||
<div class="video-default-location-extend-box-bar" @click="setRealHidden">
|
||||
<div class="video-default-location-extend-box-bar-text">
|
||||
位置测试
|
||||
</div>
|
||||
<div
|
||||
class="video-default-location-extend-box-bar-btn"
|
||||
:class="btnClass"
|
||||
@animationend="onBarBtnAnimationEnd"
|
||||
>
|
||||
<VIcon :icon="btnIcon" :size="15" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="video-default-location-extend-box-content-wrap">
|
||||
<transition name="video-default-location-extend-box-content-transition">
|
||||
<div v-show="!realHidden" class="video-default-location-extend-box-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { VIcon } from '@/ui'
|
||||
|
||||
const getIconName = (hidden: boolean): string => (hidden ? 'mdi-unfold-more-horizontal' : 'mdi-unfold-less-horizontal')
|
||||
|
||||
const btnAnimationClass = 'video-default-location-extend-box-bar-btn-animation'
|
||||
|
||||
export default Vue.extend({
|
||||
components: { VIcon },
|
||||
model: {
|
||||
prop: 'hidden',
|
||||
event: 'change',
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 12,
|
||||
},
|
||||
hidden: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
realHidden: this.hidden,
|
||||
barBottom: !this.hidden,
|
||||
btnIcon: getIconName(this.hidden),
|
||||
btnClass: {
|
||||
[btnAnimationClass]: false,
|
||||
},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
hidden(value: boolean) {
|
||||
this.setRealHidden(value)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setRealHidden(value: boolean) {
|
||||
if (value !== this.realHidden) {
|
||||
this.realHidden = !this.realHidden
|
||||
this.$emit('change', this.realHidden)
|
||||
|
||||
this.btnClass[btnAnimationClass] = false
|
||||
this.$nextTick(() => {
|
||||
this.btnClass[btnAnimationClass] = true
|
||||
setTimeout(() => {
|
||||
this.btnIcon = getIconName(this.realHidden)
|
||||
}, 150)
|
||||
})
|
||||
}
|
||||
},
|
||||
onBarBtnAnimationEnd() {
|
||||
this.btnClass[btnAnimationClass] = false
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "bar";
|
||||
|
||||
$border-color: #8884;
|
||||
$border-radius: 4px;
|
||||
|
||||
.video-default-location-extend-box {
|
||||
border-radius: $border-radius;
|
||||
box-shadow: 0 0 0 1px $border-color;
|
||||
}
|
||||
|
||||
.video-default-location-extend-box-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: $border-radius;
|
||||
box-shadow: 0 1px $border-color;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.video-default-location-extend-box-bar-text {
|
||||
@include title-container;
|
||||
}
|
||||
|
||||
.video-default-location-extend-box-bar-btn {
|
||||
@include icon-container;
|
||||
}
|
||||
|
||||
.video-default-location-extend-box-bar-btn-animation {
|
||||
animation: video-default-location-extend-box-bar-btn-animation-keyframes 0.3s;
|
||||
}
|
||||
|
||||
@keyframes video-default-location-extend-box-bar-btn-animation-keyframes {
|
||||
50% {
|
||||
transform: rotateX(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.video-default-location-extend-box-bar {
|
||||
transition: box-shadow .2s ease-out;
|
||||
}
|
||||
|
||||
.video-default-location-extend-box-hidden {
|
||||
.video-default-location-extend-box-bar {
|
||||
box-shadow: 0 0 $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
.video-default-location-extend-box-content-wrap {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.video-default-location-extend-box-content-transition-enter-active,
|
||||
.video-default-location-extend-box-content-transition-leave-active {
|
||||
transition: margin-top .2s ease-out;
|
||||
}
|
||||
|
||||
.video-default-location-extend-box-content-transition-enter,
|
||||
.video-default-location-extend-box-content-transition-leave-to {
|
||||
margin-top: -100%;
|
||||
}
|
||||
|
||||
// .video-default-location-extend-box-content-transition-enter-to,
|
||||
// .video-default-location-extend-box-content-transition-leave {
|
||||
// margin-top: 0;
|
||||
// }
|
||||
</style>
|
||||
163
registry/lib/components/video/default-location/Options.vue
Normal file
163
registry/lib/components/video/default-location/Options.vue
Normal file
@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<div class="video-default-location-options">
|
||||
<div class="video-default-location-form-line">
|
||||
<div class="video-default-location-form-item-not-grow">
|
||||
页面
|
||||
</div>
|
||||
<PageTypeSelector
|
||||
v-model="pageType"
|
||||
class="video-default-location-form-item-grow"
|
||||
@change="onChangePageType"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="video-default-location-vertical-space"></div>
|
||||
|
||||
<div class="video-default-location-form-line">
|
||||
<div class="video-default-location-form-item-not-grow">
|
||||
默认位置
|
||||
</div>
|
||||
<TextBox
|
||||
v-model="defaultLocation"
|
||||
class="video-default-location-form-item-grow"
|
||||
linear
|
||||
change-on-blur
|
||||
@change="onChangeDefaultLocation"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="video-default-location-vertical-space"></div>
|
||||
|
||||
<div class="video-default-location-options-test">
|
||||
<ExtendBox v-model="hiddenAdvance" @change="resetObservePosition">
|
||||
<div class="video-default-location-options-advanced">
|
||||
<Advanced
|
||||
:observe-position="observePosition"
|
||||
:location-limit="locationLimit"
|
||||
@set-default-location="setDefaultLocation"
|
||||
/>
|
||||
</div>
|
||||
</ExtendBox>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { getComponentSettings } from '@/core/settings'
|
||||
import { TextBox } from '@/ui'
|
||||
import ExtendBox from './ExtendBox.vue'
|
||||
import Advanced from './Advanced.vue'
|
||||
import PageTypeSelector from './PageTypeSelector.vue'
|
||||
import { pageTypeInfos, getCurrentPageType } from '.'
|
||||
|
||||
const maxLocation = 4000
|
||||
|
||||
let panelObserver = null
|
||||
|
||||
const stringIntoInt = (value: string): number | null => {
|
||||
const num = parseFloat(value)
|
||||
if (isNaN(num)) {
|
||||
return null
|
||||
}
|
||||
return Math.round(num)
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
TextBox,
|
||||
ExtendBox,
|
||||
Advanced,
|
||||
PageTypeSelector,
|
||||
},
|
||||
props: {
|
||||
componentData: {
|
||||
type: Object, // ComponentMetadata
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const { options: { locations } } = getComponentSettings(this.componentData)
|
||||
const currentPageType = getCurrentPageType() ?? Object.keys(pageTypeInfos)[0]
|
||||
return {
|
||||
locations,
|
||||
defaultLocation: String(locations[currentPageType]),
|
||||
hiddenAdvance: true,
|
||||
observePosition: false,
|
||||
locationLimit: maxLocation,
|
||||
pageType: currentPageType,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.setupPanelSwitch()
|
||||
},
|
||||
mounted() {
|
||||
if (panelObserver) {
|
||||
panelObserver.start()
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (panelObserver) {
|
||||
panelObserver.stop()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChangePageType(value: string) {
|
||||
this.defaultLocation = String(this.locations[value])
|
||||
},
|
||||
setDefaultLocation(value: number) {
|
||||
this.locations[this.pageType] = value
|
||||
this.defaultLocation = String(value)
|
||||
},
|
||||
onChangeDefaultLocation(value: string) {
|
||||
let num = stringIntoInt(value)
|
||||
if (num === null) {
|
||||
this.setDefaultLocation(0)
|
||||
} else {
|
||||
num = lodash.clamp(num, 0, maxLocation)
|
||||
this.setDefaultLocation(num)
|
||||
}
|
||||
},
|
||||
resetObservePosition() {
|
||||
this.observePosition = !this.hiddenAdvance
|
||||
},
|
||||
// 启用监视设置面板的开启与关闭,以控制是否监视页面的滚动
|
||||
setupPanelSwitch() {
|
||||
const panel = dq('.component-detail-panel')
|
||||
if (!panel) {
|
||||
console.error(
|
||||
"[videoPageOrientation] Could not find element '.component-detail-panel'",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const callback = () => {
|
||||
if (panel.classList.contains('open')) {
|
||||
this.resetObservePosition()
|
||||
} else {
|
||||
this.observePosition = false
|
||||
}
|
||||
}
|
||||
const mutationObserver = new MutationObserver(callback)
|
||||
const options = {
|
||||
attributeFilter: ['class'],
|
||||
attributes: true,
|
||||
}
|
||||
panelObserver = {
|
||||
start: () => {
|
||||
callback()
|
||||
mutationObserver.observe(panel, options)
|
||||
},
|
||||
stop: () => mutationObserver.disconnect(),
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'form';
|
||||
|
||||
.video-default-location-options-advanced {
|
||||
margin: 8px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="video-default-location-page-type-selector">
|
||||
<VDropdown v-model="curItem" :items="items" @change="onChange">
|
||||
<template #arrow>
|
||||
<div class="video-default-location-page-type-selector-icon">
|
||||
<VIcon :size="15" icon="mdi-chevron-down" />
|
||||
</div>
|
||||
</template>
|
||||
</VDropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { VDropdown, VIcon } from '@/ui'
|
||||
import { pageTypeInfos } from '.'
|
||||
|
||||
const itemsMap = lodash.mapValues(pageTypeInfos, (v, k) => ({
|
||||
name: k,
|
||||
displayName: v.displayName,
|
||||
}))
|
||||
|
||||
interface Item {
|
||||
name: string,
|
||||
displayName: string,
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
components: { VDropdown, VIcon },
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'change',
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
items: Object.values(itemsMap),
|
||||
curItem: itemsMap[this.value],
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(value: string) {
|
||||
if (this.curItem.name !== value) {
|
||||
this.curItem = itemsMap[value]
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onChange(item: string | Item) {
|
||||
this.$emit('change', item.name)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "bar";
|
||||
|
||||
.video-default-location-page-type-selector-icon {
|
||||
@include icon-container;
|
||||
}
|
||||
</style>
|
||||
14
registry/lib/components/video/default-location/_bar.scss
Normal file
14
registry/lib/components/video/default-location/_bar.scss
Normal file
@ -0,0 +1,14 @@
|
||||
@mixin title-container {
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
@mixin icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
color: #888a;
|
||||
}
|
||||
18
registry/lib/components/video/default-location/_form.scss
Normal file
18
registry/lib/components/video/default-location/_form.scss
Normal file
@ -0,0 +1,18 @@
|
||||
.video-default-location-form-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
.video-default-location-form-item-grow {
|
||||
flex: 1 auto;
|
||||
}
|
||||
|
||||
.video-default-location-form-item-not-grow {
|
||||
flex: 0 auto;
|
||||
}
|
||||
|
||||
.video-default-location-vertical-space {
|
||||
height: 8px;
|
||||
}
|
||||
18
registry/lib/components/video/default-location/desc.md
Normal file
18
registry/lib/components/video/default-location/desc.md
Normal file
@ -0,0 +1,18 @@
|
||||
打开视频/番剧时自动定位到指定位置
|
||||
|
||||
<div class="video-default-location-desc-detail">
|
||||
|
||||
位置:距离页面顶部的像素距离\
|
||||
默认位置:打开视频时自动定位到此处\
|
||||
位置测试:查看、调整当前页面所在位置
|
||||
|
||||
当前版本限制默认位置的最大值为 4000。\
|
||||
脚本不会等待评论完全加载,因此较大的默认位置将无法正确定位。
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.video-default-location-desc-detail.video-default-location-desc-detail p {
|
||||
margin-top: 1ex;
|
||||
}
|
||||
</style>
|
||||
155
registry/lib/components/video/default-location/index.ts
Normal file
155
registry/lib/components/video/default-location/index.ts
Normal file
@ -0,0 +1,155 @@
|
||||
import { ComponentMetadata } from '@/components/types'
|
||||
import {
|
||||
allVideoUrls, bangumiUrls, bnjUrls, cheeseUrls, matchCurrentPage, mediaListUrls,
|
||||
} from '@/core/utils/urls'
|
||||
import { select } from '@/core/spin-query'
|
||||
import desc from './desc.md'
|
||||
|
||||
const commonVideoUrlPattern = '//www.bilibili.com/video/'
|
||||
|
||||
export const pageTypeInfos = {
|
||||
withTitle: {
|
||||
displayName: '带标题视频页',
|
||||
urls: [
|
||||
commonVideoUrlPattern,
|
||||
...mediaListUrls,
|
||||
],
|
||||
},
|
||||
noTitle: {
|
||||
displayName: '无标题视频页',
|
||||
urls: [
|
||||
...bangumiUrls,
|
||||
...cheeseUrls,
|
||||
],
|
||||
},
|
||||
bnj: {
|
||||
displayName: '拜年纪视频页',
|
||||
urls: [
|
||||
...bnjUrls,
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export const getCurrentPageType = lodash.once((): string | null => {
|
||||
for (const [name, { urls }] of Object.entries(pageTypeInfos)) {
|
||||
if (matchCurrentPage(urls)) {
|
||||
return name
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
interface WaitMoment {
|
||||
time: number
|
||||
callback: (time: number) => Promise<void>
|
||||
}
|
||||
|
||||
class WaitResult<R> {
|
||||
constructor(
|
||||
// promise 完成前经过的最后一个 moments 中的时刻,
|
||||
// 如果没有任何一个时刻经过,该值为 0
|
||||
public lastMoment: number,
|
||||
// promise 的返回值
|
||||
public result: R,
|
||||
) {}
|
||||
}
|
||||
|
||||
// 等待 promise 执行完成。当经过 moments 所指定的时刻时,调用对应的函数。
|
||||
// moments 的时刻必须是递增顺序
|
||||
async function waitWithMoments<R>(
|
||||
promise: Promise<R>,
|
||||
moments: Iterable<WaitMoment>,
|
||||
): Promise<WaitResult<R>> {
|
||||
let lastMoment = { time: 0, callback: none as unknown } as WaitMoment
|
||||
let timeoutId = null
|
||||
|
||||
const momentIt = moments[Symbol.iterator]()
|
||||
const setNextMoment = () => {
|
||||
const yielded = momentIt.next()
|
||||
if (!yielded.done) {
|
||||
const nextMoment = yielded.value
|
||||
timeoutId = setTimeout(() => {
|
||||
timeoutId = null
|
||||
lastMoment.callback(lastMoment.time)
|
||||
lastMoment = nextMoment
|
||||
setNextMoment()
|
||||
}, nextMoment.time - lastMoment.time)
|
||||
}
|
||||
}
|
||||
setNextMoment()
|
||||
|
||||
const result = await promise
|
||||
timeoutId !== null && clearTimeout(timeoutId)
|
||||
return new WaitResult(lastMoment.time, result)
|
||||
}
|
||||
|
||||
// 等待评论区元素加载,以提供足够的高度
|
||||
// 在特定的时刻输出 warn 日志。超时时输出 error 日志。
|
||||
// 返回元素是否在超时前加载完成
|
||||
const waitComment = async (): Promise<boolean> => {
|
||||
const minute = 60_000
|
||||
const promise = select('.bb-comment', { maxRetry: 50, queryInterval: 600 })
|
||||
const moments = [minute / 2, minute, 3 * minute].map(time => ({
|
||||
time,
|
||||
callback: (async theTime => {
|
||||
console.warn(`[videoDefaultLocation] waiting more than ${theTime}ms for the page to load`)
|
||||
}) as WaitMoment['callback'],
|
||||
} as WaitMoment))
|
||||
const res = (await waitWithMoments(promise, moments)).result
|
||||
if (res === null) {
|
||||
console.error('[videoDefaultLocation] waiting for page load timeout')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const entry = async ({ settings: { options: { locations } } }) => {
|
||||
// 仅在初次进入页面时进行定位,即刷新、回退等操作不会执行定位
|
||||
const navigationArr = window?.performance?.getEntriesByType('navigation')
|
||||
if (navigationArr?.length !== 1) {
|
||||
console.error(`[videoDefaultLocation] 无法处理 PerformanceNavigationTiming 不是一个的情况。url: ${window.location.href}`)
|
||||
return
|
||||
}
|
||||
const nav = navigationArr[0] as PerformanceNavigationTiming
|
||||
if (nav.type !== 'navigate') {
|
||||
return
|
||||
}
|
||||
|
||||
if (matchCurrentPage(commonVideoUrlPattern)) {
|
||||
// 屏蔽初始化时的滚动行为
|
||||
const org = unsafeWindow.scrollTo
|
||||
unsafeWindow.scrollTo = () => {
|
||||
unsafeWindow.scrollTo = org
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前页面类型的默认位置
|
||||
const pageType = getCurrentPageType()
|
||||
if (pageType === null) {
|
||||
console.error(`[videoDefaultLocation] unknown page type. url: ${window.location.href}`)
|
||||
return
|
||||
}
|
||||
const defaultLocation = locations[pageType]
|
||||
|
||||
// 滚动到默认位置
|
||||
const html = document.documentElement
|
||||
if (defaultLocation < html.scrollHeight - html.clientHeight || await waitComment()) {
|
||||
window.scrollTo(0, defaultLocation)
|
||||
}
|
||||
}
|
||||
|
||||
export const component: ComponentMetadata = {
|
||||
name: 'videoDefaultLocation',
|
||||
displayName: '视频页默认定位',
|
||||
tags: [componentsTags.video],
|
||||
urlInclude: allVideoUrls,
|
||||
description: { 'zh-CN': desc },
|
||||
extraOptions: () => import('./Options.vue').then(m => m.default),
|
||||
options: {
|
||||
locations: {
|
||||
defaultValue: lodash.mapValues(pageTypeInfos, () => 0),
|
||||
hidden: true,
|
||||
},
|
||||
},
|
||||
entry: entry as any,
|
||||
}
|
||||
@ -3,21 +3,30 @@ import { TestPattern } from '../common-types'
|
||||
|
||||
/** 稍后再看页面 */
|
||||
export const watchlaterUrls = [
|
||||
'//www.bilibili.com/medialist/play/watchlater/',
|
||||
'//www.bilibili.com/medialist/play/watchlater',
|
||||
]
|
||||
/** 收藏夹连播页面 */
|
||||
export const favoriteListUrls = [
|
||||
'//www.bilibili.com/medialist/play/ml',
|
||||
]
|
||||
/** UP 主视频连播页面 */
|
||||
export const upListUrls = [
|
||||
/\/\/www\.bilibili\.com\/medialist\/play\/\d+/,
|
||||
]
|
||||
/** 合集类页面 */
|
||||
export const mediaListUrls = [
|
||||
...watchlaterUrls,
|
||||
...favoriteListUrls,
|
||||
...upListUrls,
|
||||
]
|
||||
/** 拜年纪页面 */
|
||||
export const bnjUrls = [
|
||||
/\/\/www\.bilibili\.com\/festival\/(\d+)bnj/,
|
||||
]
|
||||
/** 含有普通视频的页面 */
|
||||
export const videoUrls = [
|
||||
'//www.bilibili.com/video/',
|
||||
/\/\/www\.bilibili\.com\/festival\/(\d+)bnj/,
|
||||
...bnjUrls,
|
||||
...mediaListUrls,
|
||||
]
|
||||
/** 含有番剧的页面 */
|
||||
|
||||
Loading…
Reference in New Issue
Block a user