Merge branch 'preview-fixes' into preview-features

This commit is contained in:
the1812 2025-02-19 08:23:55 +08:00
commit 24714b77a7
28 changed files with 5941 additions and 4847 deletions

View File

@ -28,7 +28,6 @@
"name": "Install dependencies",
"uses": "pnpm/action-setup@v4",
"with": {
"version": "8.12.1",
"run_install": true
}
},

View File

@ -17,7 +17,6 @@
"name": "Install dependencies",
"uses": "pnpm/action-setup@v4",
"with": {
"version": "8.12.1",
"run_install": true
}
},

View File

@ -22,7 +22,6 @@ jobs:
- name: Install dependencies
uses: pnpm/action-setup@v4
with:
version: 8.12.1
run_install: true
- name: Type check
run: pnpm run type

View File

@ -11,7 +11,6 @@ jobs:
- name: Install dependencies
uses: pnpm/action-setup@v4
with:
version: 8.12.1
run_install: true
- name: Type check
run: pnpm run type

View File

@ -51,7 +51,7 @@
"postcss-loader": "^4.0.4",
"postcss-preset-env": "^7.5.0",
"prettier": "^2.8.3",
"sass": "^1.25.0",
"sass": "1.25.0",
"serve": "^13.0.2",
"serve-handler": "^6.1.3",
"style-loader": "^2.0.0",
@ -60,6 +60,7 @@
"to-string-loader": "^1.2.0",
"ts-node": "^10.7.0",
"typescript": "^4.9.5",
"vue-eslint-parser": "^9.4.3",
"vue-loader": "^15.10.1",
"webpack": "^5.31.2",
"webpack-bundle-analyzer": "^4.5.0",
@ -84,7 +85,11 @@
"overrides": {
"caniuse-lite": "1.0.30001668",
"consolidate": "1.0.1"
}
},
"onlyBuiltDependencies": [
"@parcel/watcher",
"protobufjs"
]
},
"keywords": [
"userscript",
@ -92,5 +97,6 @@
"dark-theme",
"touchscreen",
"tampermonkey"
]
],
"packageManager": "pnpm@10.3.0+sha512.ee592eda8815a8a293c206bb0917c4bb0ff274c50def7cbc17be05ec641fc2d1b02490ce660061356bd0d126a4d7eb2ec8830e6959fb8a447571c631d5a2442d"
}

10450
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,6 @@
<script lang="ts">
import { addComponentListener } from '@/core/settings'
import { getUID } from '@/core/utils'
import { ascendingSort } from '@/core/utils/sort'
import { registerAndGetData } from '@/plugins/data'
import { getBuiltInItems } from './built-in-items'
@ -28,14 +27,7 @@ const [renderedItems] = registerAndGetData(CustomNavbarRenderedItems, {
items: [] as CustomNavbarItem[],
})
const getItems = () => {
const isLogin = Boolean(getUID())
const items = (initItems as CustomNavbarItemInit[])
.filter(it => {
if (it.loginRequired && !isLogin) {
return false
}
return true
})
.map(it => new CustomNavbarItem(it))
.sort(ascendingSort(it => it.order))
renderedItems.items = items

View File

@ -2,6 +2,7 @@ import { createPopper, Instance as Popper } from '@popperjs/core'
import { VueModule, Executable } from '@/core/common-types'
import { getComponentSettings, addComponentListener } from '@/core/settings'
import type { CustomNavbarOptions } from '.'
import { getUID } from '@/core/utils'
export const CustomNavbarItems = 'customNavbar.items'
export const CustomNavbarRenderedItems = 'customNavbar.renderedItems'
@ -90,7 +91,8 @@ export class CustomNavbarItem implements Required<CustomNavbarItemInit> {
},
true,
)
this.hidden = CustomNavbarItem.navbarOptions.hidden.includes(this.name)
this.hidden =
CustomNavbarItem.navbarOptions.hidden.includes(this.name) || (!getUID() && init.loginRequired)
const orderMap = CustomNavbarItem.navbarOptions.order
this.order = orderMap[this.name] || 0
this.requestedPopup = !this.lazy

View File

@ -35,6 +35,7 @@ export const entry: ComponentEntry = async ({ metadata: { name } }) => {
'https://www.bilibili.com/opus',
'https://www.bilibili.com/account/history',
'https://www.bilibili.com/v/topic/detail',
'https://www.bilibili.com/watchlater/list',
]
if (!globalFixedExclude.some(p => matchUrlPattern(p))) {
addComponentListener(

View File

@ -118,20 +118,18 @@ export default Vue.extend({
}
})
const list: HTMLElement = this.$refs.navbarSortList
const Sortable = await SortableJSLibrary
Sortable.create(list, {
delay: 100,
forceFallback: true,
onEnd: (e: SortableEvent) => {
this.onSort(e)
},
})
checkSequentialOrder(rendered.items)
if (list) {
const Sortable = await SortableJSLibrary
Sortable.create(list, {
delay: 100,
forceFallback: true,
onEnd: (e: SortableEvent) => {
this.onSort(e)
},
})
checkSequentialOrder(rendered.items)
}
this.loaded = true
// unsafeWindow.nsTest = {
// list,
// Sortable,
// }
},
methods: {
toggle() {

View File

@ -4,11 +4,11 @@ interface SimpleLinkConfig {
name: string
displayName: string
href: string
content?: string
}
const getSimpleLinkItem = (config: SimpleLinkConfig): SimpleLinkConfig & CustomNavbarItemInit => ({
...config,
content: config.displayName,
...config,
active: document.URL.startsWith(config.href),
})
export const ranking = getSimpleLinkItem({
@ -18,7 +18,8 @@ export const ranking = getSimpleLinkItem({
})
export const bangumi = getSimpleLinkItem({
name: 'bangumi',
displayName: '番剧',
displayName: '番剧 (链接)',
content: '番剧',
href: 'https://www.bilibili.com/anime/',
})
export const music = getSimpleLinkItem({

View File

@ -0,0 +1,42 @@
<template>
<div class="">{{ content }}</div>
</template>
<script lang="ts">
import {
addComponentListener,
getComponentSettings,
removeComponentListener,
} from '@/core/settings'
import type { CustomNavbarOptions } from '..'
export default Vue.extend({
data() {
return {
isBangumiLinkHidden:
getComponentSettings<CustomNavbarOptions>('customNavbar').options.hidden.includes(
'bangumi',
),
}
},
computed: {
content() {
if (this.isBangumiLinkHidden) {
return '番剧'
}
return '追番追剧'
},
},
mounted() {
addComponentListener('customNavbar.hidden', this.updateBangumiLinkStatus)
},
beforeDestroy() {
removeComponentListener('customNavbar.hidden', this.updateBangumiLinkStatus)
},
methods: {
updateBangumiLinkStatus(hiddenItems: string[]) {
const isBangumiLinkHidden = hiddenItems.includes('bangumi')
this.isBangumiLinkHidden = isBangumiLinkHidden
},
},
})
</script>

View File

@ -8,8 +8,8 @@ export enum SubscriptionTypes {
const uid = getUID()
export const subscriptions: CustomNavbarItemInit = {
name: 'subscriptions',
displayName: '番剧',
content: '番剧',
displayName: '番剧 (弹窗)',
content: () => import('./Content.vue').then(m => m.default),
href: `https://space.bilibili.com/${uid}/bangumi`,
touch: true,

View File

@ -40,7 +40,7 @@
<div v-if="card.percent" class="progress" :style="{ width: card.percent * 100 + '%' }" />
</a>
<a class="title" target="_blank" :href="card.href" :title="card.title">{{ card.title }}</a>
<div class="info">
<div class="info-row">
<a
class="up"
target="_blank"
@ -352,7 +352,7 @@ export default Vue.extend({
color: var(--theme-color) !important;
}
}
.info {
.info-row {
display: flex;
justify-content: space-between;
align-items: flex-end;

View File

@ -16,5 +16,6 @@
@import "./dark-slice-16.scss";
@import "./dark-slice-17.scss";
@import "./dark-slice-18.scss";
@import "./dark-slice-19.scss";
@import "./dark-navbar.scss";
@import "./dark-variables.scss";

View File

@ -619,7 +619,7 @@ html {
&:not(.iframe) {
&,
body {
@include background-color('2');
background-color: var(--bg1, #222) !important;
}
}
}

View File

@ -0,0 +1,8 @@
@import './dark-definitions';
@import 'common';
.relation-card-info-option {
.follow-btn {
@include background-color();
}
}

View File

@ -1,6 +1,6 @@
import { postTextWithCredentials, getJsonWithCredentials } from '@/core/ajax'
import { postTextWithCredentials } from '@/core/ajax'
import { Toast } from '@/core/toast'
import { formData, getCsrf } from '@/core/utils'
import { getCsrf } from '@/core/utils'
import { registerAndGetData } from '@/plugins/data'
export interface CheckInItem {
@ -19,7 +19,7 @@ const builtInItems: CheckInItem[] = [
const seedsToCoinsApi = 'https://api.live.bilibili.com/xlive/revenue/v1/wallet/silver2coin'
const text = await postTextWithCredentials(
seedsToCoinsApi,
formData({
new URLSearchParams({
csrf: getCsrf(),
csrf_token: getCsrf(),
}),
@ -40,31 +40,5 @@ const builtInItems: CheckInItem[] = [
}
},
},
{
name: 'live-check-in',
displayName: '直播间签到',
icon: 'mdi-calendar-check',
action: async () => {
const liveCheckInApi = 'https://api.live.bilibili.com/xlive/web-ucenter/v1/sign/DoSign'
const json = (await getJsonWithCredentials(liveCheckInApi)) as {
code: number
message: string
data: {
text: string
specialText: string
allDays: number
hadSignDays: number
isBonusDay: number
}
}
if (json.code !== 0) {
Toast.info(json.message, '直播间签到', 3000)
} else {
const { text, specialText, allDays, hadSignDays } = json.data
const message = `签到成功, 获得了${text} ${specialText}\n本月进度: ${hadSignDays} / ${allDays}`
Toast.success(message, '直播间签到', 3000)
}
},
},
]
export const [checkInItems] = registerAndGetData('checkInCenter.items', builtInItems)

View File

@ -23,7 +23,7 @@ export const setupFeedImageExporter: ComponentEntry<Options> = async ({
const imageUrls: { url: string; extension: string }[] = []
dqa(
card.element,
'.main-content .img-content, .bili-album__preview__picture__img, .bili-album .preview__picture__img, .bili-dyn-gallery__image img',
'.main-content .img-content, .bili-album__preview__picture__img, .bili-album .preview__picture__img, .bili-dyn-gallery__image img, .bili-album__watch__track__item img',
).forEach((img: HTMLImageElement | HTMLDivElement) => {
const urlData = retrieveImageUrl(img)
if (urlData && !imageUrls.some(({ url }) => url === urlData.url)) {

View File

@ -0,0 +1,7 @@
对 200% 以上的屏幕 DPI 缩放请求更高分辨率的图片, 加载时间也会相应变长一些. (也会导致某些浏览器里出现图片闪动, 因为本质上是更换了图片源)
由于 b 站在很多地方没有设置图片维持原比例, 如果计算后的图片尺寸超出原图尺寸则会产生错误的比例, 可以考虑在选项中手动降低缩放级别.
`缩放级别` 选项:
- `auto`: 根据 DPI 缩放超出 200% 的比例自动计算, 例如 250% 的缩放下会请求 1.5 倍尺寸的图片.
- `数字`: 自定义尺寸的缩放倍率.

View File

@ -9,7 +9,6 @@ const options = defineOptionsMetadata({
scale: {
displayName: '缩放级别',
defaultValue: 'auto',
hidden: true,
},
originalImageInArticles: {
displayName: '在专栏中请求原图',
@ -25,9 +24,5 @@ export const component = defineComponentMetadata({
tags: [componentsTags.utils],
enabledByDefault: window.devicePixelRatio > 1,
entry: startResolution,
description: {
'zh-CN':
'根据屏幕 DPI 请求更高分辨率的图片, 例如 DPI 缩放 200% 则请求 2 倍的分辨率, 加载时间也会相应变长一些. (也会导致某些浏览器里出现图片闪动, 因为本质上是更换了图片源)',
},
options,
})

View File

@ -19,6 +19,13 @@ const widthAndHeightSelectors = [
]
const originalImageInArticlesSelectors = ['.article-detail .article-content img']
const getAutoScale = () => {
if (window.devicePixelRatio <= 2) {
return 1
}
return window.devicePixelRatio / 2
}
const walk = (rootElement: Node, action: (node: HTMLElement) => void) => {
const walker = document.createNodeIterator(rootElement, NodeFilter.SHOW_ELEMENT)
let node = walker.nextNode()
@ -123,10 +130,11 @@ export const startResolution = styledComponentEntry<Options>(
() => import('./fix.scss'),
async ({ settings }) => {
const { allMutations } = await import('@/core/observer')
const dpi =
settings.options.scale === 'auto'
? window.devicePixelRatio
: parseFloat(settings.options.scale)
const scale =
settings.options.scale === 'auto' ? getAutoScale() : parseFloat(settings.options.scale)
if (scale === 1) {
return
}
const handleResolution: ImageResolutionHandler = {
getWidth: (currentWidth, element) => {
if (
@ -135,7 +143,7 @@ export const startResolution = styledComponentEntry<Options>(
) {
return Infinity
}
return Math.round(dpi * currentWidth)
return Math.round(scale * currentWidth)
},
getHeight: (currentHeight, element) => {
if (
@ -144,7 +152,7 @@ export const startResolution = styledComponentEntry<Options>(
) {
return Infinity
}
return Math.round(dpi * currentHeight)
return Math.round(scale * currentHeight)
},
}

View File

@ -1,4 +1,4 @@
import { isTyping, matchUrlPattern } from '@/core/utils'
import { getActiveElement, isTyping, matchUrlPattern } from '@/core/utils'
import { mediaListUrls, watchlaterUrls } from '@/core/utils/urls'
import { clickElement, changeVideoTime, showTip } from './actions'
import { shadowDomObserver } from '@/core/shadow-root'
@ -17,6 +17,7 @@ export interface KeyBindingAction {
run: (context: KeyBindingActionContext) => unknown
prevent?: boolean
ignoreTyping?: boolean
ignoreFocus?: boolean
}
export interface KeyBinding {
keys: string[]
@ -43,6 +44,15 @@ export const loadKeyBindings = lodash.once((bindings: KeyBinding[]) => {
if (binding.action.ignoreTyping !== false && isTyping()) {
return
}
// 忽略其他可聚焦元素
const hasElementFocus = !([document.body, null] as (Element | null)[]).includes(
getActiveElement(),
)
if (binding.action.ignoreFocus !== false && hasElementFocus) {
return
}
const key = e.key.toLowerCase()
// 全景视频禁用 WASD 快捷键

View File

@ -36,7 +36,8 @@ body:not(.preserve-feed-goods) .dyn-goods,
.desktop-download-tip,
.adcard,
.palette-button-adcard,
.palette-button-wrap .adcard-content {
.palette-button-wrap .adcard-content,
.video-card-ad-small {
display: none !important;
}
.recommend-list .rec-list > :not(.video-page-card) + .video-page-card {

View File

@ -1,53 +1,87 @@
import { ComponentEntry } from '@/components/types'
import { defineComponentMetadata } from '@/components/define'
import type { RawWatchlaterItem } from '@/components/video/watchlater'
const entry: ComponentEntry = async ({ settings }) => {
if (settings.options.page) {
const { select } = await import('@/core/spin-query')
const { childList } = await import('@/core/observer')
const { getWatchlaterList } = await import('@/components/video/watchlater')
const list = await getWatchlaterList(true)
const listBox = await select('.watch-later-list .list-box > span')
if (!listBox) {
return
}
const redirect = (item: Element, index: number) => {
try {
const watchlaterItem = list[index]
const { bvid, cid, pages } = watchlaterItem
const page = pages.find(p => p.cid === cid)?.page ?? 1
const url =
page > 1
? `https://www.bilibili.com/video/${bvid}?p=${page}`
: `https://www.bilibili.com/video/${bvid}`
const pic = item.querySelector('.av-pic') as HTMLAnchorElement
pic.target = '_blank'
pic.href = url
const title = item.querySelector('.av-about .t') as HTMLAnchorElement
title.target = '_blank'
title.href = url
} catch (error) {
console.error(`[watchlater redirect] error at index ${index}`, item, error)
}
}
const runRedirect = () => {
const avItems = listBox.querySelectorAll('.av-item')
avItems.forEach(redirect)
}
childList(listBox, records => {
records.forEach(record => {
record.removedNodes.forEach(node => {
if (node instanceof HTMLElement && !node.classList.contains('itemlist-move')) {
const index = parseInt(dq(node, '.key').textContent) - 1
console.log('remove index', index)
list.splice(index, 1)
}
})
})
runRedirect()
})
const getBvidFromElement = (element: Element) => {
const pic = element.querySelector('.av-pic, .bili-cover-card') as HTMLAnchorElement
const bvid = pic.href.match(/bvid=([^&]+)/)?.[1]
return bvid
}
const redirect = (element: Element, watchlaterItem: RawWatchlaterItem) => {
try {
const { bvid, cid, pages } = watchlaterItem
const page = pages.find(p => p.cid === cid)?.page ?? 1
const url =
page > 1
? `https://www.bilibili.com/video/${bvid}?p=${page}`
: `https://www.bilibili.com/video/${bvid}`
const pic = element.querySelector('.av-pic, .bili-cover-card') as HTMLAnchorElement
pic.target = '_blank'
pic.href = url
const title = element.querySelector(
'.av-about .t, .bili-video-card__title a, .video-card__right .title',
) as HTMLAnchorElement
title.target = '_blank'
title.href = url
} catch (error) {
console.error(`[watchlater redirect] error at ${watchlaterItem.bvid}`, element, error)
}
}
const entry: ComponentEntry = async ({ settings }) => {
if (!settings.options.page) {
return
}
const { select } = await import('@/core/spin-query')
const { childListSubtree } = await import('@/core/observer')
const { getWatchlaterList } = await import('@/components/video/watchlater')
const { useScopedConsole } = await import('@/core/utils/log')
const console = useScopedConsole('稍后再看重定向')
const list: RawWatchlaterItem[] = await getWatchlaterList(true)
const listContainer = await select('.watch-later-list .list-box > span, .watchlater-list')
if (!listContainer) {
return
}
const tryRedirect = (element: Element) => {
const bvid = getBvidFromElement(element)
if (bvid === undefined) {
console.warn('bvid not found for', element)
return
}
const listItem = list.find(it => it.bvid === bvid)
if (listItem === undefined) {
console.warn('bvid no match for', bvid)
return
}
redirect(element, listItem)
}
const runRedirect = lodash.debounce(() => {
const videoCards = listContainer.querySelectorAll('.av-item, .video-card')
console.log('run redirect, length =', videoCards.length)
videoCards.forEach(card => {
tryRedirect(card)
})
}, 200)
runRedirect()
childListSubtree(listContainer, async records => {
records.forEach(r => {
const hasVideoCardChange = [...r.addedNodes].some(
node =>
node instanceof HTMLElement &&
(node.classList.contains('bili-video-card__wrap') ||
node.classList.contains('watchlater-list-container')),
)
if (hasVideoCardChange) {
runRedirect()
}
})
})
}
export const component = defineComponentMetadata({
name: 'watchlaterRedirect',
displayName: '稍后再看重定向',
@ -62,6 +96,9 @@ export const component = defineComponentMetadata({
defaultValue: true,
},
},
urlInclude: ['https://www.bilibili.com/watchlater/#/list'],
urlInclude: [
'https://www.bilibili.com/watchlater/#/list',
'https://www.bilibili.com/watchlater/list#/list',
],
tags: [componentsTags.utils, componentsTags.video],
})

View File

@ -263,7 +263,7 @@ export const addMenuItem = (
if (isCascader) {
;(async () => {
const cascader = await select(() => dq(morePanel, '.bili-cascader'))
const [observer] = childList(cascader, () => {
childList(cascader, (_, observer) => {
const cascaderOptions = dq(cascader, '.bili-cascader-options')
if (cascaderOptions === null) {
return

View File

@ -49,7 +49,7 @@ addData(ListAdaptorKey, (adaptors: FeedsCardsListAdaptor[]) => {
name: 'space',
match: ['https://space.bilibili.com/'],
watchCardsList: async manager => {
const container = (await select('.s-space')) as HTMLDivElement
const container = (await select('.s-space, .space-main')) as HTMLDivElement
if (!container) {
return false
}

View File

@ -1,6 +1,9 @@
@import 'common';
@mixin markdown {
line-height: normal;
> * {
font-size: inherit;
}
h1,
h2,
h3,