const getPosition = element => { let x = 0; let y = 0; while (element && !isNaN(element.offsetLeft) && !isNaN(element.offsetTop)) { x += element.offsetLeft - element.scrollLeft; y += element.offsetTop - element.scrollTop; element = element.offsetParent; } return { x: x, y: y, }; }; const secondsToTime = sec => { sec = Math.abs(sec); const hours = Math.floor(sec / 3600); const minutes = Math.floor((sec - hours * 3600) / 60); const seconds = sec - hours * 3600 - minutes * 60; let result = fixed(seconds) + "秒"; if (minutes > 0) { result = minutes + "分" + result; } if (hours > 0) { result = hours + "时" + result; } return result; }; const secondsToHms = sec => { sec = Math.abs(sec); const hours = Math.floor(sec / 3600); const minutes = Math.floor((sec - hours * 3600) / 60); const seconds = sec - hours * 3600 - minutes * 60; let result = (seconds < 10 ? "0" : "") + fixed(seconds); result = (minutes < 10 ? "0" : "") + minutes + ":" + result; result = (hours < 10 ? "0" : "") + hours + ":" + result; return result; }; class Swiper { constructor(element) { this.action = new SwipeAction(element); this.onTouchStart = null; this.onTouchEnd = null; this.direction = null; element.addEventListener("touchstart", e => { this.xDown = e.touches[0].clientX; this.yDown = e.touches[0].clientY; if (this.onTouchStart) { this.onTouchStart(e); } }); element.addEventListener("touchmove", e => { if (!this.xDown || !this.yDown) { return; } const xUp = e.touches[0].clientX; const yUp = e.touches[0].clientY; const elementPosition = getPosition(element); const position = { x: (e.touches[0].pageX - elementPosition.x) / element.clientWidth, y: (e.touches[0].pageY - elementPosition.y) / element.clientHeight, width: element.clientWidth, height: element.clientHeight, }; const xDiff = this.xDown - xUp; const yDiff = this.yDown - yUp; if (!this.direction) { let direction = ""; if (Math.abs(xDiff) > Math.abs(yDiff)) { direction = "horizontal"; } else { direction = "vertical"; } this.direction = direction; e.preventDefault(); } else { if (this.direction === "vertical") { this.action.startAction(this.direction, yDiff, position); } else if (this.direction === "horizontal") { this.action.startAction(this.direction, -xDiff, position); } e.preventDefault(); } }); element.addEventListener("touchend", e => { this.xDown = null; this.yDown = null; this.direction = null; if (this.onTouchEnd) { this.onTouchEnd(e); } }); } } class SwipeAction { constructor(element) { this.lowSpeedForward = null; this.lowSpeedBackward = null; this.mediumSpeedForward = null; this.mediumSpeedBackward = null; this.highSpeedForward = null; this.highSpeedBackward = null; this.lowVolumeUp = null; this.lowVolumeDown = null; this.mediumVolumeUp = null; this.mediumVolumeDown = null; this.highVolumeUp = null; this.highVolumeDown = null; this.speedCancel = null; this.volumeCancel = null; this.minSwipeDistance = 20; this.onActionStart = null; this.onActionEnd = null; this.element = element; this.touchStart = false; this.startPosition = null; this.lastAction = null; element.addEventListener("touchstart", e => { this.touchStart = true; }); element.addEventListener("touchend", e => { this.startPosition = null; this.onActionEnd && this.onActionEnd(this.lastAction); this.lastAction = null; }); } startAction(direction, distance, position) { if (this.touchStart) { this.onActionStart && this.onActionStart(direction); this.startPosition = position; this.touchStart = false; } if (direction === "vertical") { if (Math.abs(distance) < this.minSwipeDistance) { this.volumeCancel && this.volumeCancel(); this.lastAction = null; } else { let volumeFactor = 0; let upHandler = undefined; let downHandler = undefined; if (this.startPosition.x < 1 / 3) { volumeFactor = 0.4; upHandler = this.lowVolumeUp; downHandler = this.lowVolumeDown; } else if (this.startPosition.x >= 1 / 3 && this.startPosition.x <= 2 / 3) { volumeFactor = 1; upHandler = this.mediumVolumeUp; downHandler = this.mediumVolumeDown; } else { volumeFactor = 2; upHandler = this.highVolumeUp; downHandler = this.highVolumeDown; } if (distance > 0) { const volumeChange = Math.round( volumeFactor * 100 * (distance - this.minSwipeDistance) / (1.5 * position.height) ); upHandler && upHandler(volumeChange); this.lastAction = { type: "volume", volume: volumeChange, }; } else { const volumeChange = Math.round( volumeFactor * 100 * (distance + this.minSwipeDistance) / (1.5 * position.height) ); downHandler && downHandler(volumeChange); this.lastAction = { type: "volume", volume: volumeChange, }; } } } else if (direction === "horizontal") { if (position.y < 1 / 3 && (position.x < 0.1 || position.x > 0.9) || Math.abs(distance) < this.minSwipeDistance) { this.speedCancel && this.speedCancel(); this.lastAction = null; } else { let speedFactor = 0; let forwardHandler = undefined; let backwardHandler = undefined; if (this.startPosition.y < 1 / 3) { speedFactor = 0.05; forwardHandler = this.lowSpeedForward; backwardHandler = this.lowSpeedBackward; } else if (this.startPosition.y >= 1 / 3 && this.startPosition.y <= 2 / 3) { speedFactor = 0.2; forwardHandler = this.mediumSpeedForward; backwardHandler = this.mediumSpeedBackward; } else { speedFactor = 1; forwardHandler = this.highSpeedForward; backwardHandler = this.highSpeedBackward; } if (distance > 0) { const seconds = (distance - this.minSwipeDistance) * speedFactor; forwardHandler && forwardHandler(seconds); this.lastAction = { type: "playback", seconds: seconds, }; } else { const seconds = (distance + this.minSwipeDistance) * speedFactor; backwardHandler && backwardHandler(seconds); this.lastAction = { type: "playback", seconds: seconds, }; } } } } } class VideoShot { constructor() { this.aid = unsafeWindow.aid; this.cid = unsafeWindow.cid; this.cidData = null; this.supportWebp = VideoShot.supportWebp; } getVideoshot(currentTime, done) { if (!(this.aid && this.cid)) { return; } if (!this.cidData) { Ajax.getText(`https://api.bilibili.com/x/player/videoshot?aid=${this.aid}&cid=${this.cid}&index=1`).then(response => { this.cidData = JSON.parse(response).data; this.getVideoshot(currentTime, done); }); } else { const data = this.cidData; const indexData = data.index; let shotIndex = 0; for (let index = 0; index < indexData.length - 2; index++) { if (currentTime >= indexData[index] && currentTime < indexData[index + 1]) { shotIndex = index; break; } } let imageData = data.image; if (this.supportWebp) { imageData = imageData.map(url => url.replace(".jpg", ".jpg@.webp")); } const xLength = parseInt(data.pv_x_len) || 10; const yLength = parseInt(data.pv_y_len) || 10; const xSize = parseInt(data.pv_x_size) || 160; const ySize = parseInt(data.pv_y_size) || 90; const x = -(shotIndex % 100 % xLength) * xSize; const y = -Math.floor(shotIndex % 100 / yLength) * ySize; done({ width: xSize, height: ySize, backgroundImage: `url(${imageData[Math.floor(shotIndex / 100)]})`, backgroundPosition: `${x}px ${y}px`, }); } } static get supportWebp() { try { const canvas = document.createElement("canvas"); if (canvas.getContext && canvas.getContext("2d")) try { return canvas.toDataURL("image/webp").indexOf("data:image/webp") === 0; } catch (ex) { return false; } else return false; } catch (ex) { return false; } } } function unbindIconsClick(icons) { icons.unbind("click"); } function setupTouchPlayer(player) { if ($(".touch-video-box").length !== 0) { return; } $(".bilibili-player-video-subtitle").before(/*html*/`
`); const video = $("video"); const videoDuration = video.prop("duration"); const swiper = new Swiper(player.get(0)); const text = document.getElementsByClassName("touch-video-info")[0]; const box = document.getElementsByClassName("touch-video-box")[0]; let originalVolume = Math.round(video.prop("volume") * 100); const setVolume = volume => { volume /= 100; if (volume < 0) { volume = 0; } else if (volume > 1) { volume = 1; } video.prop("volume", volume); const playerSettings = JSON.parse(localStorage.bilibili_player_settings); playerSettings.video_status.volume = volume; localStorage.bilibili_player_settings = JSON.stringify(playerSettings); $(".bilibili-player-video-volume-num").text(Math.round(volume * 100)); $(".bui-thumb").css("transform", `translateY(-${48 * volume}px)`); $(".bui-track-vertical .bui-bar").css("transform", `scaleY(${volume})`); if (volume === 0) { $(".bilibili-player-video-btn-volume").addClass("video-state-volume-min"); $(".bilibili-player-video-btn-volume").removeClass("video-state-volume-max"); video.prop("muted", true); } else if (volume === 1) { $(".bilibili-player-video-btn-volume").removeClass("video-state-volume-min"); $(".bilibili-player-video-btn-volume").addClass("video-state-volume-max"); video.prop("muted", false); } else { $(".bilibili-player-video-btn-volume").removeClass("video-state-volume-min"); $(".bilibili-player-video-btn-volume").removeClass("video-state-volume-max"); video.prop("muted", false); } }; if (settings.touchVideoPlayerAnimation) { box.classList.add("animation"); } swiper.action.onActionStart = _ => { box.classList.add("adjust-opened"); text.innerHTML = ""; originalVolume = Math.round(video.prop("volume") * 100); }; const videoshot = new VideoShot(); const speedChange = speed => { return sec => { const current = video.prop("currentTime"); const currentPercent = fixed(100 * current / videoDuration); let finalTime = current + sec; let finalPercent = fixed(100 * finalTime / videoDuration); let change = sec; if (finalTime > videoDuration) { finalTime = videoDuration; finalPercent = 100; change = videoDuration - current; } else if (finalTime < 0) { finalTime = 0; finalPercent = 0; change = current; } const result = `${secondsToHms(current)} (${currentPercent}%)
👇
${secondsToHms(finalTime)} (${finalPercent}%)`; const html = /*html*/`
${speed}速
进度: ${sec > 0 ? "+" : "-"}${secondsToTime(change)}
${result}
`; text.innerHTML = html; videoshot.getVideoshot(finalTime, style => $(".videoshot").css(style)); $(".touch-progress").css("transform", `scaleX(${finalPercent / 100})`); }; }; swiper.action.lowSpeedBackward = speedChange("低"); swiper.action.lowSpeedForward = speedChange("低"); swiper.action.mediumSpeedBackward = speedChange("中"); swiper.action.mediumSpeedForward = speedChange("中"); swiper.action.highSpeedBackward = speedChange("高"); swiper.action.highSpeedForward = speedChange("高"); const volumeChange = speed => { return volume => { let finalVolume = originalVolume + volume; let change = Math.abs(volume); if (finalVolume > 100) { finalVolume = 100; change = 100 - originalVolume; } else if (finalVolume < 0) { finalVolume = 0; change = originalVolume; } const result = `${originalVolume} 👉 ${finalVolume}`; setVolume(finalVolume); const html = /*html*/`
${speed}速
音量: ${volume > 0 ? "+" : "-"}${change}
${result}
`; text.innerHTML = html; $(".touch-progress").css("transform", `scaleX(${finalVolume / 100})`); }; }; swiper.action.lowVolumeUp = volumeChange("低"); swiper.action.lowVolumeDown = volumeChange("低"); swiper.action.mediumVolumeUp = volumeChange("中"); swiper.action.mediumVolumeDown = volumeChange("中"); swiper.action.highVolumeUp = volumeChange("高"); swiper.action.highVolumeDown = volumeChange("高"); swiper.action.speedCancel = () => { text.innerHTML = `松开手指,取消进退`; $(".touch-progress").css("transform", "scaleX(0)"); }; swiper.action.volumeCancel = () => { text.innerHTML = `松开手指,取消调整`; $(".touch-progress").css("transform", "scaleX(0)"); setVolume(originalVolume); }; if (!unsafeWindow.TOUCH_PLAYER_DEBUG) { swiper.action.onActionEnd = action => { text.innerHTML = ""; if (action) { if (action.type === "playback") { let time = video.prop("currentTime"); time += action.seconds; if (time < 0) { time = 0; } else if (time > videoDuration) { time = videoDuration; } video.prop("currentTime", time); } } box.classList.remove("adjust-opened"); }; } } function overrideClickHandler(playerArea) { if (!playerArea.hasClass("disable-original-hover")) { playerArea.addClass("disable-original-hover"); const video = unsafeWindow.$(".bilibili-player-video"); const hoverClassName = "touch-video-control-show"; const originalClickHandler = video.data("events").click[0].handler; const doubleClick = new DoubleClickEvent( e => originalClickHandler(e), () => playerArea.toggleClass(hoverClassName), ); video.unbind("click"); doubleClick.bind(video[0]); } } const main = () => { SpinQuery.any( () => $(".bilibili-player-iconfont,.bilibili-player-video-quality-menu"), unbindIconsClick ); SpinQuery.condition( () => $(".bilibili-player-video"), it => it.length > 0 && $("video").length > 0 && $("video").prop("duration"), setupTouchPlayer ); if (settings.touchVideoPlayerDoubleTapControl) { new SpinQuery( () => $(".bilibili-player-area"), it => it.length > 0 && unsafeWindow.$ && unsafeWindow.$(".bilibili-player-video").data("events"), overrideClickHandler ).start(); } }; if (Observer.videoChange) { Observer.videoChange(main); } else { Observer.childList("#bofqi", () => main()); } resources.applyStyle("touchPlayerStyle");