Merge branch 'preview-features' into preview

This commit is contained in:
the1812 2024-12-28 11:29:53 +08:00
commit bb5ab37698
34 changed files with 578 additions and 232 deletions

View File

@ -51,6 +51,7 @@
"Fullscreen",
"githubusercontent",
"Greasemonkey",
"guochuang",
"haruna",
"hdslb",
"HEVC",

View File

@ -1,6 +1,41 @@
<!-- spell-checker: disable -->
# 更新日志
## v2.9.6-preview
`2024-12-28`
包含 [v2.9.6](https://github.com/the1812/Bilibili-Evolved/releases/tag/v2.9.6) 的所有更新内容.
- 新增插件 `自定义顶栏 - 版权内容`. (#5045)
> 为自定义顶栏扩充版权内容相关的快速入口, 包括国创 / 电影 / 电视剧 /综艺 / 纪录片
- 视频截图在还有截图未处理 (保存 / 丢弃) 时, 离开页面增加二次确认. (#5066)
## v2.9.6
`2024-12-28`
✨新增
- `自定义顶栏` 更新:
- (来自 v2.9.5-preview) 历史支持手动刷新. (#4948)
- 全局固定适配话题页. (#5039)
- 新歌热榜 URL 更新. (#5056)
- `订阅` 重命名为 `番剧`, 功能不变. (#4777)
- 增加 VLOG, 搞笑, 综艺, 单机游戏入口. (#1511)
- `传统连播模式` 支持番剧页面. (#5067)
- `下载视频` 增加选项 `DASH 回退编码`, 默认回退到 AVC, 可以避免 HEVC 回退到 AV1 的情况. (#5080)
- 新增功能 `隐藏首页轮播图`. (从上一版本废弃的 `简化首页` 中提取) (#5038)
- `夜间模式` 优化对动态和新版首页的适配. (#5075)
🐛修复
- 修复 `隐藏头像框` 对部分角标删除不完全. (#5040)
- 修复 `自定义顶栏` 删除搜索历史时失焦. (#4732)
- 修复 `自定义顶栏` 未登录时仍能通过功能面板进行排序. (#5051)
- 修复 `自定义顶栏` 收藏夹的音频链接错误. (#5069)
- 修复在搜索页的样式冲突. (#5073)
- 修复 `直播首页静音` 在关闭时仍会导致首页静音. (#5045)
- 修复 `禁止滚轮调音量` 在 Firefox 中不生效. (#5047)
- 回退 `直播信息扩充` 的 API 变更. (#4964)
## v2.9.5-preview
`2024-11-12`

View File

@ -30,6 +30,12 @@ https://afdian.com/a/the1812?tab=sponsor
| 时间 | 用户名 | 单号后4位 | 金额 |
| ------------------- | --------------------- | --------- | ------- |
| 2024.12.27 11:01:16 | *o | 2835 | ¥5.00 |
| 2024.12.20 18:05:09 | *言 | 6840 | ¥3.00 |
| 2024.12.17 18:30:53 | x*x | 3632 | ¥10.00 |
| 2024.12.10 12:20:10 | *吗 | 8875 | ¥5.00 |
| 2024.12.06 14:33:35 | 匿名 | 8998 | ¥5.00 |
| 2024.11.17 05:46:36 | *树 | 0442 | ¥20.00 |
| 2024.10.24 19:59:56 | 匿名 | 5126 | ¥20.00 |
| 2024.10.24 03:07:40 | S*a | 8703 | ¥20.00 |
| 2024.10.11 11:19:52 | 匿名 | 8738 | ¥100.00 |

View File

@ -295,6 +295,14 @@
"fullRelativePath": "../../registry/dist/components/style/hide/banner.js",
"fullAbsolutePath": "registry/dist/components/style/hide/banner.js"
},
{
"type": "component",
"name": "hideHomeCarousel",
"displayName": "隐藏首页轮播图",
"description": "隐藏首页的轮播图区域.\r\n",
"fullRelativePath": "../../registry/dist/components/style/hide/home-carousel.js",
"fullAbsolutePath": "registry/dist/components/style/hide/home-carousel.js"
},
{
"type": "component",
"name": "hideTrendingSearch",
@ -1048,6 +1056,14 @@
"fullRelativePath": "../../registry/dist/plugins/style/custom-navbar-dark-mode.js",
"fullAbsolutePath": "registry/dist/plugins/style/custom-navbar-dark-mode.js"
},
{
"type": "plugin",
"name": "customNavbar.items.pgc",
"displayName": "自定义顶栏 - 版权内容",
"description": "为自定义顶栏扩充版权内容相关的快速入口, 包括国创 / 电影 / 电视剧 /综艺 / 纪录片",
"fullRelativePath": "../../registry/dist/plugins/style/custom-navbar-pgc.js",
"fullAbsolutePath": "registry/dist/plugins/style/custom-navbar-pgc.js"
},
{
"type": "plugin",
"name": "keymap.actions.darkMode",
@ -1116,7 +1132,7 @@
"type": "plugin",
"name": "downloadVideo.outputs.idm",
"displayName": "下载视频 - IDM 输出支持",
"description": "为下载视频增加 IDM 输出支持,建议配合 [ef2.exe](https://github.com/MotooriKashin/ef2) 以简化操作、保留文件名.",
"description": "为下载视频增加 IDM 输出支持.",
"fullRelativePath": "../../registry/dist/plugins/video/download/idm-output.js",
"fullAbsolutePath": "registry/dist/plugins/video/download/idm-output.js"
},

View File

@ -387,6 +387,15 @@ by [@Tinhone](https://github.com/Tinhone)
隐藏首页顶部横幅.
### [隐藏首页轮播图](../../registry/dist/components/style/hide/home-carousel.js)
`hideHomeCarousel`
**jsDelivr:** [`Stable`](https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/components/style/hide/home-carousel.js) / [`Preview`](https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/components/style/hide/home-carousel.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/style/hide/home-carousel.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/style/hide/home-carousel.js)
隐藏首页的轮播图区域.
### [隐藏热搜](../../registry/dist/components/style/hide/trending-search.js)
`hideTrendingSearch`
@ -1457,6 +1466,15 @@ by FoundTheWOUT
为自定义顶栏添加一个夜间模式开关, 方便快速切换夜间模式.
### [自定义顶栏 - 版权内容](../../registry/dist/plugins/style/custom-navbar-pgc.js)
`customNavbar.items.pgc`
**jsDelivr:** [`Stable`](https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/plugins/style/custom-navbar-pgc.js) / [`Preview`](https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/plugins/style/custom-navbar-pgc.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/plugins/style/custom-navbar-pgc.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/plugins/style/custom-navbar-pgc.js)
为自定义顶栏扩充版权内容相关的快速入口, 包括国创 / 电影 / 电视剧 /综艺 / 纪录片
### [快捷键扩展 - 夜间模式](../../registry/dist/plugins/utils/keymap-dark-mode.js)
`keymap.actions.darkMode`
@ -1536,7 +1554,7 @@ by FoundTheWOUT
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/plugins/video/download/idm-output.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/plugins/video/download/idm-output.js)
为下载视频增加 IDM 输出支持,建议配合 [ef2.exe](https://github.com/MotooriKashin/ef2) 以简化操作、保留文件名.
为下载视频增加 IDM 输出支持.
### [下载视频 - 手动输入](../../registry/dist/plugins/video/download/manual-input.js)
`downloadVideo.inputs.manual`

View File

@ -50,7 +50,7 @@
</template>
<script lang="ts">
import { VIcon, TextBox, DpiImage, VEmpty, VLoading } from '@/ui'
import { getJsonWithCredentials } from '@/core/ajax'
import { getJsonWithCredentials, responsiveGetPages } from '@/core/ajax'
const decodeTitle = (title: string) => {
const textArea = document.createElement('textarea')
@ -109,26 +109,65 @@ export default Vue.extend({
try {
this.items = []
this.loaded = false
const url = `https://api.live.bilibili.com/xlive/web-ucenter/v1/xfetter/GetWebList?hit_ab=true&_=${Date.now()}`
const portalList = await getJsonWithCredentials(url)
this.items = portalList.data.rooms.map(item => {
const { room_id, face, title, uname, uid } = item
return {
cover: face,
face,
uname,
title,
roomid: room_id,
pic: '', // portal
online: 0, // portal
uid,
link: `https://live.bilibili.com/${room_id}`,
}
const [, promise] = responsiveGetPages<LiveInfo>({
api: page =>
getJsonWithCredentials(
`https://api.live.bilibili.com/xlive/web-ucenter/v1/xfetter/GetWebList?page=${page}`,
),
getList: json =>
lodash.get(json, 'data.list', []).map(item => {
const { face, uname, title, room_id, cover_from_user, online, uid, link } = item
return {
cover: face,
face,
uname,
title,
roomid: room_id,
pic: cover_from_user,
online,
uid,
link,
}
}),
getTotal: json => lodash.get(json, 'data.count', 0),
})
const [allItems, recommendItems] = await Promise.all([promise, this.fetchRecommendItems()])
this.items = this.sortByRecommend(allItems, recommendItems)
} finally {
this.loaded = true
}
},
async fetchRecommendItems(): Promise<LiveInfo[]> {
// portaltop30up
const portalList = await getJsonWithCredentials(
'https://api.bilibili.com/x/polymer/web-dynamic/v1/portal',
)
const recommendLiveItems = portalList.data.live_users.items.map(item => {
const { jump_url, room_id, face, title, uname, mid } = item
return {
cover: face,
face,
uname,
title,
roomid: room_id,
pic: '', // portal
online: 0, // portal
uid: mid,
link: jump_url,
}
})
return recommendLiveItems
},
sortByRecommend(feedItems: LiveInfo[], recommendItems: LiveInfo[]) {
// recommendItemspiconlineui
// 便recommendItemsitem
const recommendRoomIds = recommendItems.map(item => item.roomid)
const feedConcatItems = feedItems.filter(item => !recommendRoomIds.includes(item.roomid))
return lodash.concat(recommendItems, feedConcatItems)
},
},
})
</script>

View File

@ -37,6 +37,10 @@ export const component = defineComponentMetadata({
'zh-CN': '提前执行代码以尽快静音.',
},
async setup() {
const { isComponentEnabled } = await import('@/core/settings')
if (!isComponentEnabled('liveHomeMute')) {
return
}
const { matchUrlPattern } = await import('@/core/utils')
if (!matchUrlPattern(liveHome)) {
return

View File

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

View File

@ -21,11 +21,7 @@
<VEmpty v-else-if="!loading && !canLoadMore && filteredCards.length === 0"></VEmpty>
<transition-group v-else name="cards" tag="div" class="cards">
<div v-for="card of filteredCards" :key="card.id" class="favorite-card">
<a
class="favorites-cover-container"
target="_blank"
:href="'https://www.bilibili.com/video/' + card.bvid"
>
<a class="favorites-cover-container" target="_blank" :href="getItemPlayLink(card)">
<DpiImage
class="cover"
:src="card.coverUrl"
@ -34,13 +30,9 @@
<div class="floating duration">{{ card.durationText }}</div>
<div class="floating favorite-time">{{ card.favoriteTime }}</div>
</a>
<a
class="title"
target="_blank"
:href="'https://www.bilibili.com/video/' + card.bvid"
:title="card.title"
>{{ card.title }}</a
>
<a class="title" target="_blank" :href="getItemPlayLink(card)" :title="card.title">{{
card.title
}}</a>
<a
v-if="card.upID"
class="up"
@ -82,7 +74,8 @@ https://api.bilibili.com/x/v3/fav/resource/list?media_id=media_id&pn=1&ps=20&key
*/
const navbarOptions = getComponentSettings('customNavbar').options
interface FavoritesItemInfo extends VideoCard {
interface FavoritesItemInfo extends Omit<VideoCard, 'type'> {
type: number
favoriteTimestamp: number
favoriteTime: string
}
@ -94,6 +87,7 @@ const favoriteItemFilter = (item: any): boolean => {
return item.attr !== 9 && item.attr !== 1 //
}
const favoriteItemMapper = (item: any): FavoritesItemInfo => ({
type: item.type,
id: item.id,
aid: item.id,
bvid: item.bvid,
@ -269,6 +263,17 @@ export default Vue.extend({
this.loadNextPage()
}
},
getItemPlayLink(item: FavoritesItemInfo) {
switch (item.type) {
default:
case 2: {
return `https://www.bilibili.com/video/${item.bvid}`
}
case 12: {
return `https://www.bilibili.com/audio/au${item.id}`
}
}
},
},
})
</script>

View File

@ -6,6 +6,11 @@
<TextBox v-model="search" placeholder="搜索" linear></TextBox>
</div>
<div class="operations">
<div class="operation">
<VButton title="刷新" round @click="reloadHistoryItems()">
<VIcon icon="mdi-refresh" :size="16"></VIcon>
</VButton>
</div>
<div class="operation" @click="toggleHistoryPause">
<VButton v-if="!paused" title="暂停记录历史" round>
<VIcon icon="mdi-pause" :size="14"></VIcon>

View File

@ -1,17 +1,11 @@
<template>
<div class="custom-navbar-extra-options">
<VButton
v-if="login"
ref="button"
@mouseover="loadNavbarSettings()"
@click="toggleNavbarSettings()"
>
<VButton ref="button" @mouseover="loadNavbarSettings()" @click="toggleNavbarSettings()">
布局设置<VIcon icon="right-arrow" :size="16"></VIcon>
</VButton>
</div>
</template>
<script lang="ts">
import { getUID } from '@/core/utils'
import { VIcon, VButton } from '@/ui'
import { setTriggerElement, loadNavbarSettings, toggleNavbarSettings } from './vm'
@ -20,11 +14,6 @@ export default Vue.extend({
VIcon,
VButton,
},
data() {
return {
login: Boolean(getUID()),
}
},
methods: {
async loadNavbarSettings() {
const isFirstLoad = await loadNavbarSettings()

View File

@ -31,7 +31,7 @@
<div class="padding-value">{{ padding.toFixed(1) }}%</div>
</div>
</div>
<div class="navbar-settings-section">
<div v-if="isLogin" class="navbar-settings-section">
<div class="navbar-settings-section-title">元素呈现</div>
<div class="navbar-settings-section-description">
按住并拖动可以调整顺序, 点击眼睛图标可以切换隐藏/显示.
@ -71,7 +71,7 @@
import { SortableEvent } from 'sortablejs'
import { VPopup, VIcon, VSlider, VLoading } from '@/ui'
import { addComponentListener } from '@/core/settings'
import { dqa } from '@/core/utils'
import { dqa, getUID } from '@/core/utils'
import { SortableJSLibrary } from '@/core/runtime-library'
import { getData } from '@/plugins/data'
import { CustomNavbarItem, CustomNavbarRenderedItems } from '../custom-navbar-item'
@ -103,6 +103,7 @@ export default Vue.extend({
rendered,
hidden: navbarOptions.hidden,
loaded: false,
isLogin: Boolean(getUID()),
}
},
watch: {

View File

@ -123,7 +123,7 @@ export default Vue.extend({
`https://api.bilibili.com/x/space/bangumi/follow/list?${params}`,
)
if (json.code !== 0) {
logError(`加载订阅信息失败: ${json.message}`)
logError(`加载番剧信息失败: ${json.message}`)
return
}
const newCards: SubscriptionItem[] = lodash

View File

@ -8,8 +8,8 @@ export enum SubscriptionTypes {
const uid = getUID()
export const subscriptions: CustomNavbarItemInit = {
name: 'subscriptions',
displayName: '订阅',
content: '订阅',
displayName: '番剧',
content: '番剧',
href: `https://space.bilibili.com/${uid}/bangumi`,
touch: true,

View File

@ -1,13 +1,37 @@
import { addComponentListener, getComponentSettings } from '@/core/settings'
import { sq } from '@/core/spin-query'
import { matchUrlPattern } from '@/core/utils'
import { mainSiteUrls, matchCurrentPage } from '@/core/utils/urls'
const setupTransparentFill = (vm: { toggleStyle: (value: boolean, style: string) => void }) => {
addComponentListener(
'customNavbar.transparent',
value => {
if (!getComponentSettings('hideBanner').enabled) {
vm.toggleStyle(value, 'transparent')
}
},
true,
)
addComponentListener('hideBanner', value => {
if (getComponentSettings('customNavbar').options.transparent) {
vm.toggleStyle(!value, 'transparent')
}
})
}
const transparentFillUrls = ['//music.bilibili.com/', '//space.bilibili.com/']
/**
* ()
*/
export const checkTransparentFill = async (vm: {
toggleStyle: (value: boolean, style: string) => void
}) => {
if (transparentFillUrls.some(url => matchUrlPattern(url))) {
setupTransparentFill(vm)
return
}
if (!matchCurrentPage(mainSiteUrls)) {
return
}
@ -41,19 +65,6 @@ export const checkTransparentFill = async (vm: {
if (banner.length === 0) {
return
}
addComponentListener(
'customNavbar.transparent',
value => {
if (!getComponentSettings('hideBanner').enabled) {
vm.toggleStyle(value, 'transparent')
}
},
true,
)
addComponentListener('hideBanner', value => {
if (getComponentSettings('customNavbar').options.transparent) {
vm.toggleStyle(!value, 'transparent')
}
})
setupTransparentFill(vm)
})
}

View File

@ -11,6 +11,7 @@ export const urlInclude = [
'//app.bilibili.com',
'//passport.bilibili.com',
'//live.bilibili.com/blackboard/',
'//music.bilibili.com',
]
export const urlExclude = [
'//t.bilibili.com/vote/h5/index/#/result',

View File

@ -223,11 +223,20 @@
@include theme-color();
}
}
&__more {
z-index: 1;
}
.bili-dyn-more {
&:hover {
@include background-color();
}
&__container:hover {
@include background-color('3');
}
&__btn {
&.tp {
@include color('8');
}
&:hover {
@include to-theme('blue');
}

View File

@ -726,9 +726,7 @@
@include set-color('border-top-color', '4');
}
#{contains('mediainfo_mediaRight')} {
#{contains('mediainfo_media_desc_section')}
#{contains('mediainfo_display_area')}
#{contains('mediainfo_ellipsis')} {
#{contains('mediainfo_media_desc_section')} #{contains('mediainfo_display_area')} #{contains('mediainfo_ellipsis')} {
background: linear-gradient(90deg, hsla(0, 0%, 100%, 0), #222 20%, #222);
@include theme-color();
}
@ -995,7 +993,35 @@
background-color: rgba(0, 0, 0, 0) !important;
@include color('cf');
}
h1,h2,h3,h4,h5,h6{
h1,
h2,
h3,
h4,
h5,
h6 {
@include color('e');
}
}
.bili-header {
.notify-list-item,
.hot-list-item {
&:hover {
@include background-color('3');
}
}
}
.home-empty-content__pic {
.b-img {
@include background-color();
}
}
.header.space-header {
.header-toutu,
.header-upinfo-bg-shadow {
z-index: 0 !important;
}
.header-upinfo {
z-index: 1 !important;
}
}

View File

@ -0,0 +1,3 @@
.recommended-swipe.grid-anchor {
display: none !important;
}

View File

@ -0,0 +1 @@
隐藏首页的轮播图区域.

View File

@ -0,0 +1,15 @@
import { defineComponentMetadata } from '@/components/define'
export const component = defineComponentMetadata({
name: 'hideHomeCarousel',
displayName: '隐藏首页轮播图',
entry: none,
tags: [componentsTags.style],
urlInclude: [/^https:\/\/www\.bilibili\.com\/$/, /^https:\/\/www\.bilibili\.com\/index\.html$/],
instantStyles: [
{
name: 'hide-home-carousel',
style: () => import('./hide-home-carousel.scss'),
},
],
})

View File

@ -3,10 +3,10 @@
container-name: avatar;
// .layers:first-child,
.layer:not(:first-child),
.layer:not(:has(picture), :has([style*="/face/"])) {
.layer:not(:has(picture [src*="/face/"]), :has([style*="/face/"])) {
display: none;
}
.layer:is(:has(picture), :has([style*="/face/"])) {
.layer:is(:has(picture [src*="/face/"]), :has([style*="/face/"])) {
min-width: 100cqw;
min-height: 100cqh;
max-width: 100cqw;

View File

@ -188,13 +188,31 @@ const downloadDash = async (
videoDashes,
videoCodec: codec,
})
const qualities = (data.accept_quality as number[])
// .filter(qn => (
// // 去掉当前编码不匹配的 quality
// (video as any[]).some(d => d.id === qn && parseVideoCodec(d.codecid) === codec)
// ))
.map(qn => allQualities.find(q => q.value === qn))
.filter(q => q !== undefined)
const qualities = (() => {
const filterByCodec = (preferredCodec: DashCodec | null) => {
return (data.accept_quality as number[])
.filter(qn => {
if (preferredCodec !== null) {
return (video as any[]).some(
d => d.id === qn && parseVideoCodec(d.codecid) === preferredCodec,
)
}
return true
})
.map(qn => allQualities.find(q => q.value === qn))
.filter(q => q !== undefined)
}
const allAvailableQualities = filterByCodec(codec)
if (allAvailableQualities.length > 0) {
return allAvailableQualities
}
const { options } = getComponentSettings<Options>('downloadVideo')
const fallbackQualities = filterByCodec(options.dashCodecFallback)
if (fallbackQualities.length > 0) {
return fallbackQualities
}
return filterByCodec(null)
})()
const info = new DownloadVideoInfo({
input,
jsonData: data,
@ -209,21 +227,21 @@ export const videoDashAvc: DownloadVideoApi = {
name: 'video.dash.avc',
displayName: 'dash (AVC/H.264)',
description:
'音画分离的 mp4 格式, 编码为 H.264, 体积较大, 兼容性较好. 下载后可以合并为单个 mp4 文件. 如果视频源没有此编码, 则会自动选择其他同清晰度的编码格式.',
'音画分离的 mp4 格式, 编码为 H.264, 体积较大, 兼容性较好. 下载后可以合并为单个 mp4 文件. 如果视频源没有此编码, 则会回退到设置中设定的 DASH 回退编码.',
downloadVideoInfo: async input => downloadDash(input, { codec: DashCodec.Avc }),
}
export const videoDashHevc: DownloadVideoApi = {
name: 'video.dash.hevc',
displayName: 'dash (HEVC/H.265)',
description:
'音画分离的 mp4 格式, 编码为 H.265, 体积中等, 兼容性较差. 下载后可以合并为单个 mp4 文件. 如果视频源没有此编码, 则会自动选择其他同清晰度的编码格式.',
'音画分离的 mp4 格式, 编码为 H.265, 体积中等, 兼容性较差. 下载后可以合并为单个 mp4 文件. 如果视频源没有此编码, 则会回退到设置中设定的 DASH 回退编码.',
downloadVideoInfo: async input => downloadDash(input, { codec: DashCodec.Hevc }),
}
export const videoDashAv1: DownloadVideoApi = {
name: 'video.dash.av1',
displayName: 'dash (AV1)',
description:
'音画分离的 mp4 格式, 编码为 AV1, 体积较小, 兼容性中等. 下载后可以合并为单个 mp4 文件. 如果视频源没有此编码, 则会自动选择其他同清晰度的编码格式.',
'音画分离的 mp4 格式, 编码为 AV1, 体积较小, 兼容性中等. 下载后可以合并为单个 mp4 文件. 如果视频源没有此编码, 则会回退到设置中设定的 DASH 回退编码.',
downloadVideoInfo: async input => downloadDash(input, { codec: DashCodec.Av1 }),
}
export const videoAudioDash: DownloadVideoApi = {

View File

@ -4,7 +4,7 @@ import {
defineOptionsMetadata,
} from '@/components/define'
import { hasVideo } from '@/core/spin-query'
import { DefaultDashExtensions } from './apis/dash'
import { DashCodec, DefaultDashExtensions } from './apis/dash'
const options = defineOptionsMetadata({
basicConfig: {
@ -24,6 +24,11 @@ const options = defineOptionsMetadata({
defaultValue: DefaultDashExtensions.flacAudio,
displayName: 'DASH FLAC 音频扩展名',
},
dashCodecFallback: {
defaultValue: DashCodec.Avc,
dropdownEnum: DashCodec,
displayName: 'DASH 回退编码',
},
})
export type Options = OptionsOfMetadata<typeof options>
export const component = defineComponentMetadata({

View File

@ -4,7 +4,7 @@ import { allVideoUrls } from '@/core/utils/urls'
let cancel: () => void
export const disableScrollVolume = () => {
return preventEvent(unsafeWindow, 'mousewheel', () => {
const shouldPrevent = () => {
const isFullscreen = [
'player-mode-full',
'player-mode-web',
@ -12,7 +12,13 @@ export const disableScrollVolume = () => {
'player-full-win',
].some(token => document.body.classList.contains(token))
return isFullscreen
})
}
const cancelFirefox = preventEvent(unsafeWindow, 'DOMMouseScroll', shouldPrevent)
const cancelStandard = preventEvent(unsafeWindow, 'wheel', shouldPrevent)
return () => {
cancelFirefox()
cancelStandard()
}
}
export const component = defineComponentMetadata({
name: 'disableScrollVolume',

View File

@ -3,137 +3,172 @@ import { getVueData } from '@/components/feeds/api'
import { playerAgent } from '@/components/video/player-agent'
import { childListSubtree, videoChange } from '@/core/observer'
import { select } from '@/core/spin-query'
import { playerReady } from '@/core/utils'
import { matchUrlPattern, playerReady } from '@/core/utils'
import { useScopedConsole } from '@/core/utils/log'
import { videoUrls } from '@/core/utils/urls'
import { bangumiUrls, videoUrls } from '@/core/utils/urls'
abstract class LegacyAutoPlay {
isChecked(container: HTMLElement) {
return (
Boolean(container.querySelector('.switch-button.on') || container.matches(':checked')) ||
container.classList.contains('on')
)
}
async onRightPanelLoaded(callback: (rightPanel: Element) => Promise<void> | void) {
await playerReady()
const rightPanel = await Promise.any([
select('.right-container-inner'),
select('.playlist-container--right'),
select('.video-pod'),
])
if (!rightPanel) {
const console = useScopedConsole('传统连播模式')
console.warn('未找到 rightPanelContainer 或 playListContainer')
return
}
callback(rightPanel)
}
async onVideoChange(callback: (video: HTMLVideoElement) => Promise<void> | void) {
videoChange(async () => {
const video = (await playerAgent.query.video.element()) as HTMLVideoElement
video?.addEventListener('play', () => callback(video), { once: true })
})
}
abstract setupAutoPlay(): Promise<void>
}
class VideoLegacyAutoPlay extends LegacyAutoPlay {
readonly autoPlayControls = {
/** 命中时应该打开连播: 传统分 P, 合集 */
enable: [
':is(.base-video-sections, .base-video-sections-v1) .next-button',
':is(.multi-page, .multi-page-v1) .next-button',
'.player-auxiliary-autoplay-switch input',
'.video-pod .auto-play .switch-btn',
],
/** 命中时应该关闭连播: 关联视频推荐 */
disable: [
':is(.recommend-list, .recommend-list-v1) .next-button',
'.recommend-list-v1 .switch-btn',
],
}
/** 最后 1P 时不能开启连播 */
readonly disableConditions = [
/** 传统分 P */
() =>
Boolean(
dq(
':is(.multi-page, .multi-page-v1) .list-box li.on:last-child, .video-pod__list .simple-base-item.active:last-child',
),
),
/** 替代分 P 的合集 & 可分子合集的分 P 合集, 布局长得比下面那个丑一点 */
() =>
Boolean(
dq(
'.video-sections-item:last-child .video-episode-card:last-child .video-episode-card__info-playing',
),
),
/** 可分子合集的合集 */
() =>
Boolean(
dq(
'.video-sections-item:last-child .video-episode-card:last-child .video-episode-card__info-title-playing',
),
),
]
async checkPlayMode() {
const element = (await select(
[...this.autoPlayControls.disable, ...this.autoPlayControls.enable].join(','),
)) as HTMLElement
if (!element) {
return
}
const shouldChecked =
this.autoPlayControls.enable.some(selector => element.matches(selector)) &&
this.disableConditions.every(condition => !condition())
const checked = this.isChecked(element)
console.log('checkPlayMode', { shouldChecked, checked })
if (shouldChecked !== checked) {
element.click()
}
}
async checkPlayListPlayMode(rightPanel: Element) {
// spell-checker: disable
const videoSequentialNumber = dq(rightPanel, '.list-count, .video-pod__header .amt')
const sequentialNumbers = videoSequentialNumber.innerHTML
.replace(/[]/g, '')
.split('/')
.map(it => parseInt(it))
const lastVideoElement = dq(
'.action-list .action-list-item-wrap:last-child .action-list-item .actionlist-item-inner, .video-pod__list .pod-item:last-child',
)
const isSingleList =
lastVideoElement.classList.contains('singlep-list-item-inner') ||
lastVideoElement.querySelector('.single-p') !== null
const isLastVideo = (() => {
if (isSingleList) {
return (
lastVideoElement.classList.contains('siglep-active') ||
lastVideoElement.querySelector('.simple-base-item.active') !== null
)
}
return lastVideoElement.children[1].lastElementChild.classList.contains(
'multip-list-item-active',
)
})()
const shouldContinue = !(sequentialNumbers[0] >= sequentialNumbers[1] && isLastVideo)
console.log('checkPlayListPlayMode', { isLastVideo, sequentialNumbers, shouldContinue })
const app = document.getElementById('app')
const vueInstance = getVueData(app)
// 不用判断当前状态是什么,直接将将是否需要继续播放赋值 vue 实例中的 continuousPlay
vueInstance.setContinuousPlay(shouldContinue)
// spell-checker: enable
}
async setupAutoPlay() {
this.onRightPanelLoaded(rightPanel => {
const check = () => {
const isPlayList = rightPanel.querySelector('.video-pod__list.section')
return isPlayList ? this.checkPlayListPlayMode(rightPanel) : this.checkPlayMode()
}
childListSubtree(rightPanel, () => check())
this.onVideoChange(() => check())
})
}
}
class BangumiLegacyAutoPlay extends LegacyAutoPlay {
async setupAutoPlay() {
const autoPlayRadio = (await select(
'.bpx-player-ctrl-setting-handoff input[type="radio"][value="0"]',
)) as HTMLInputElement
if (autoPlayRadio === null) {
const console = useScopedConsole('传统连播模式')
console.warn('未找到 autoPlayRadio')
return
}
autoPlayRadio.click()
}
}
export const component = defineComponentMetadata({
name: 'legacyAutoPlay',
displayName: '传统连播模式',
description: '模拟传统的多 P 连播策略: 仅连播视频的分 P, 最后 1P 放完禁止连播其他推荐视频.',
tags: [componentsTags.video],
urlInclude: videoUrls,
urlInclude: [...videoUrls, ...bangumiUrls],
entry: async () => {
const console = useScopedConsole('传统连播模式')
const autoPlayControls = {
// 命中时应该打开连播: 传统分 P, 合集
enable: [
':is(.base-video-sections, .base-video-sections-v1) .next-button',
':is(.multi-page, .multi-page-v1) .next-button',
'.player-auxiliary-autoplay-switch input',
'.video-pod .auto-play .switch-btn',
],
// 命中时应该关闭连播: 关联视频推荐
disable: [
':is(.recommend-list, .recommend-list-v1) .next-button',
'.recommend-list-v1 .switch-btn',
],
}
// 最后 1P 时不能开启连播
const disableConditions = [
// 传统分 P
() =>
Boolean(
dq(
':is(.multi-page, .multi-page-v1) .list-box li.on:last-child, .video-pod__list .simple-base-item.active:last-child',
),
),
// 替代分 P 的合集
// & 可分子合集的分 P 合集, 布局长得比下面那个丑一点
() =>
Boolean(
dq(
'.video-sections-item:last-child .video-episode-card:last-child .video-episode-card__info-playing',
),
),
// 可分子合集的合集
() =>
Boolean(
dq(
'.video-sections-item:last-child .video-episode-card:last-child .video-episode-card__info-title-playing',
),
),
]
await playerReady()
// 初始化状态
const rightPanel = await Promise.any([
select('.right-container-inner'),
select('.playlist-container--right'),
select('.video-pod'),
])
// 如果未找到元素,提前退出不执行后续动作
if (!rightPanel) {
console.warn('未找到 rightPanelContainer 或 playListContainer')
return
}
const checkPlayListPlayMode = async () => {
const videoSequentialNumber = dq(rightPanel, '.list-count, .video-pod__header .amt')
const sequentialNumbers = videoSequentialNumber.innerHTML
.replace(/[]/g, '')
.split('/')
.map(it => parseInt(it))
const lastVideoElement = dq(
'.action-list .action-list-item-wrap:last-child .action-list-item .actionlist-item-inner, .video-pod__list .pod-item:last-child',
)
const isSingleList =
lastVideoElement.classList.contains('singlep-list-item-inner') ||
lastVideoElement.querySelector('.single-p') !== null
const isLastVideo = (() => {
if (isSingleList) {
return (
lastVideoElement.classList.contains('siglep-active') ||
lastVideoElement.querySelector('.simple-base-item.active') !== null
)
}
return lastVideoElement.children[1].lastElementChild.classList.contains(
'multip-list-item-active',
)
})()
const shouldContinue = !(sequentialNumbers[0] >= sequentialNumbers[1] && isLastVideo)
console.log('checkPlayListPlayMode', { isLastVideo, sequentialNumbers, shouldContinue })
const app = document.getElementById('app')
const vueInstance = getVueData(app)
// 不用判断当前状态是什么,直接将将是否需要继续播放赋值 vue 实例中的 continuousPlay
vueInstance.setContinuousPlay(shouldContinue)
}
const isChecked = (container: HTMLElement) =>
Boolean(container.querySelector('.switch-button.on') || container.matches(':checked')) ||
container.classList.contains('on')
const checkPlayMode = async () => {
const element = (await select(
[...autoPlayControls.disable, ...autoPlayControls.enable].join(','),
)) as HTMLElement
if (!element) {
return
const legacyAutoPlay = (() => {
const isBangumi = bangumiUrls.some(url => matchUrlPattern(url))
if (isBangumi) {
return new BangumiLegacyAutoPlay()
}
const shouldChecked =
autoPlayControls.enable.some(selector => element.matches(selector)) &&
disableConditions.every(condition => !condition())
const checked = isChecked(element)
console.log('checkPlayMode', { shouldChecked, checked })
if (shouldChecked !== checked) {
element.click()
}
}
const checkRightPanelPlayMode = async () => {
const isPlayList = rightPanel.querySelector('.video-pod__list.section')
return isPlayList ? checkPlayListPlayMode() : checkPlayMode()
}
videoChange(async () => {
const video = (await playerAgent.query.video.element()) as HTMLVideoElement
video?.addEventListener('play', () => checkRightPanelPlayMode(), { once: true })
})
childListSubtree(rightPanel, () => checkRightPanelPlayMode())
return new VideoLegacyAutoPlay()
})()
await legacyAutoPlay.setupAutoPlay()
},
})

View File

@ -8,10 +8,16 @@ import desc from './desc.md'
import ScreenshotContainer from './VideoScreenshotContainer.vue'
export const VideoScreenshotDisabledClass = 'video-screenshot-disable'
const entry = async () => {
let screenShotsList: Vue & {
screenshots: Screenshot[]
let screenShotsList: Vue & {
screenshots: Screenshot[]
}
const exitConfirmHandler = (e: BeforeUnloadEvent) => {
if (screenShotsList?.screenshots.length > 0) {
e.preventDefault()
}
}
const entry = async () => {
addControlBarButton({
name: 'takeScreenshot',
displayName: '截图',
@ -35,6 +41,7 @@ const entry = async () => {
}
},
})
window.addEventListener('beforeunload', exitConfirmHandler)
}
export const component = defineComponentMetadata({
name: 'videoScreenshot',
@ -45,8 +52,14 @@ export const component = defineComponentMetadata({
'zh-CN': desc,
},
urlInclude: playerUrls,
reload: () => document.body.classList.remove(VideoScreenshotDisabledClass),
unload: () => document.body.classList.add(VideoScreenshotDisabledClass),
reload: () => {
document.body.classList.remove(VideoScreenshotDisabledClass)
window.addEventListener('beforeunload', exitConfirmHandler)
},
unload: () => {
document.body.classList.add(VideoScreenshotDisabledClass)
window.removeEventListener('beforeunload', exitConfirmHandler)
},
plugin: {
displayName: '视频截图 - 快捷键支持',
setup: ({ addData }) => {

View File

@ -0,0 +1,46 @@
import { PluginMetadata } from '@/plugins/plugin'
import type { CustomNavbarItemInit } from '../../../components/style/custom-navbar/custom-navbar-item'
export const plugin: PluginMetadata = {
name: 'customNavbar.items.pgc',
displayName: '自定义顶栏 - 版权内容',
description: '为自定义顶栏扩充版权内容相关的快速入口, 包括国创 / 电影 / 电视剧 /综艺 / 纪录片',
async setup({ addData }) {
addData('customNavbar.items', (items: CustomNavbarItemInit[]) => {
const pgcItems = [
{
name: 'guochuang',
displayName: '国创',
href: 'https://www.bilibili.com/guochuang/',
},
{
name: 'movie',
displayName: '电影',
href: 'https://www.bilibili.com/movie/',
},
{
name: 'tv',
displayName: '电视剧',
href: 'https://www.bilibili.com/tv/',
},
{
name: 'variety',
displayName: '综艺',
href: 'https://www.bilibili.com/variety/',
},
{
name: 'documentary',
displayName: '纪录片',
href: 'https://www.bilibili.com/documentary/',
},
]
items.push(
...pgcItems.map(item => ({
...item,
content: item.displayName,
active: document.URL.startsWith(item.href),
})),
)
})
},
}

View File

@ -7,14 +7,14 @@ import { DownloadVideoOutput } from '../../../../components/video/download/types
export const plugin: PluginMetadata = {
name: 'downloadVideo.outputs.idm',
displayName: '下载视频 - IDM 输出支持',
description:
'为下载视频增加 IDM 输出支持,建议配合 [ef2.exe](https://github.com/MotooriKashin/ef2) 以简化操作、保留文件名.',
description: '为下载视频增加 IDM 输出支持.',
setup: ({ addData }) => {
addData('downloadVideo.outputs', (outputs: DownloadVideoOutput[]) => {
outputs.push({
name: 'idm',
displayName: 'IDM',
description: '使用 IDM 的 .ef2 格式导出, 可以在 IDM 中使用导入开始下载.',
description:
'使用 IDM 的 .ef2 格式导出, 可以在 IDM 中使用导入开始下载, 建议配合 [ef2.exe](https://github.com/MotooriKashin/ef2) 以简化操作、保留文件名.',
runAction: async action => {
const { infos } = action
const referer = document.URL.replace(window.location.search, '')

View File

@ -1,5 +1,5 @@
{
"version": "2.9.5",
"version": "2.9.6",
"author": "Grant Howard, Coulomb-G",
"copyright": "[year], Grant Howard (https://github.com/the1812) & Coulomb-G (https://github.com/Coulomb-G)",
"license": "MIT",

View File

@ -1,7 +1,7 @@
<template>
<div
tabindex="0"
class="be-launch-bar-action-item suggest-item"
class="be-launch-bar-action-item be-launch-bar-suggest-item"
:title="action.displayName || action.name"
:data-indexer="action.indexer"
@click.self="performAction($event)"
@ -10,27 +10,35 @@
@keydown.up.prevent.stop="$emit('previous-item', $event.currentTarget)"
@keydown.down.prevent.stop="$emit('next-item', $event.currentTarget)"
>
<div class="suggest-item-content">
<div v-if="action.icon" class="suggest-item-icon" @click="performAction($event)">
<div class="be-launch-bar-suggest-item-content">
<div
v-if="action.icon"
class="be-launch-bar-suggest-item-icon"
@click="performAction($event)"
>
<VIcon :icon="action.icon" :size="18" />
</div>
<div class="suggest-item-title" @click="performAction($event)">
<div class="be-launch-bar-suggest-item-title" @click="performAction($event)">
<component
:is="action.content"
v-if="action.content"
class="suggest-item-name"
class="be-launch-bar-suggest-item-name"
:name="action.name"
></component>
<div v-else class="suggest-item-name">
<div v-else class="be-launch-bar-suggest-item-name">
{{ action.displayName || action.name }}
</div>
<div v-if="action.description" class="suggest-item-description" :title="action.description">
<div
v-if="action.description"
class="be-launch-bar-suggest-item-description"
:title="action.description"
>
{{ action.description }}
</div>
</div>
<div
v-if="action.deleteAction"
class="suggest-item-delete"
class="be-launch-bar-suggest-item-delete"
title="删除此项"
@click="performDelete($event)"
>
@ -72,7 +80,7 @@ export default Vue.extend({
<style lang="scss">
@import 'common';
.suggest-item {
.be-launch-bar-suggest-item {
outline: none !important;
padding: 6px 6px 6px 10px;
cursor: pointer;

View File

@ -21,7 +21,7 @@
</div>
<div ref="list" class="launch-bar-suggest-list">
<div v-if="isHistory" class="launch-bar-history-list">
<div v-if="actions.length === 0" class="history-empty suggest-item disabled" tabindex="0">
<div v-if="actions.length === 0" class="history-empty be-launch-bar-suggest-item disabled" tabindex="0">
暂无搜索历史
</div>
<ActionItem
@ -30,7 +30,7 @@
:action="a"
@previous-item="previousItem()"
@next-item="nextItem()"
@delete-item="onDeleteItem()"
@delete-item="onDeleteItem(index)"
@action="
index === actions.length - 1 && onClearHistory()
onAction(a)
@ -41,20 +41,20 @@
<VEmpty
v-if="actions.length === 0 && noActions"
tabindex="0"
class="suggest-item disabled"
class="be-launch-bar-suggest-item disabled"
></VEmpty>
<VLoading
v-if="actions.length === 0 && !noActions"
tabindex="0"
class="suggest-item disabled"
class="be-launch-bar-suggest-item disabled"
></VLoading>
<ActionItem
v-for="a of actions"
v-for="(a, index) of actions"
:key="a.key"
:action="a"
@previous-item="previousItem()"
@next-item="nextItem()"
@delete-item="onDeleteItem()"
@delete-item="onDeleteItem(index)"
@action="onAction(a)"
/>
</div>
@ -234,7 +234,8 @@ export default Vue.extend({
this.focusTarget.next()
e.preventDefault()
},
handleIndexUpdate() {
async handleIndexUpdate() {
await this.$nextTick()
if (!this.focusTarget.hasFocus) {
this.focusInput()
return
@ -248,7 +249,8 @@ export default Vue.extend({
this.focusTarget.next()
},
search,
onDeleteItem() {
onDeleteItem(index: number) {
this.focusTarget.setFocus(index)
this.focusTarget.previous()
this.getActions()
},
@ -263,7 +265,7 @@ export default Vue.extend({
this.$refs.input.focus()
},
focusSuggestItem(nth: number) {
this.$refs.list.querySelector(`.suggest-item:nth-child(${nth})`)?.focus()
this.$refs.list.querySelector(`.be-launch-bar-suggest-item:nth-child(${nth})`)?.focus()
},
},
})

View File

@ -26,6 +26,9 @@ export class FocusTarget extends EventTarget {
return this.itemIndex > -1
}
setFocus(index: number) {
this.index = index
}
reset(length: number, index = this.index) {
this.itemLength = length
this.index = index

View File

@ -1875,8 +1875,32 @@
{
"name": "新歌热榜",
"report": "musicplus",
"url": "//www.bilibili.com/v/musicplus/",
"url": "//music.bilibili.com/pc/music-center/",
"icon": "musicplus"
},
{
"name": "综艺",
"report": "variety",
"url": "//www.bilibili.com/variety/",
"icon": "zongyi"
},
{
"name": "VLOG",
"report": "vlog",
"url": "//www.bilibili.com/v/life/daily/?tag=530003",
"icon": "VLOG"
},
{
"name": "搞笑",
"report": "funny",
"url": "//www.bilibili.com/v/life/funny/",
"icon": "gaoxiao"
},
{
"name": "单机游戏",
"report": "stand_alone",
"url": "//www.bilibili.com/v/game/stand_alone/",
"icon": "danjiyouxi"
}
]
]