diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 00000000..7cd8ee0d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,36 @@ +--- +name: 问题反馈 +about: 提交您在使用插件中遇到的 Bug 或错误 +title: "[BUG] " +labels: bug +assignees: '' + +--- + +## 问题描述 +简要描述您遇到的 Bug 或错误: + +## 复现步骤 +请提供详细的复现步骤: + +1. 打开网址: +2. +3. + +## 预期表现 +描述您期望的正确表现: + +## 实际表现 +描述目前实际出现的错误或不符合预期的行为: + +## 屏幕截图或录像 +如果可能,请附上相关截图或视频,以便更好地说明问题: + +## 运行环境 +- 操作系统(OS): +- 浏览器: +- 浏览器版本: +- 插件版本: + +## 其他信息 +补充任何相关信息或日志: diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 00000000..d7fa297d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,20 @@ +--- +name: 新功能建议 +about: 提交您对插件的新功能或改进的建议 +title: "[Feature] " +labels: "新功能 Feature" +assignees: '' + +--- + +## 功能描述 +简要描述您希望添加的新功能或改进: + +## 使用场景 +请描述此功能的使用场景或带来的价值: + +## 您的建议 +如果可以,请提供您对功能实现的详细建议或方案: + +## 其他信息 +补充任何相关的上下文信息或参考资料: diff --git a/manifest/manifest.json b/manifest/manifest.json index 7b855a2d..76d54bf3 100644 --- a/manifest/manifest.json +++ b/manifest/manifest.json @@ -1,7 +1,7 @@ { "name": "__MSG_fullName__", "short_name": "__MSG_shortName__", - "version": "0.5.4", + "version": "0.5.7", "default_locale": "zh_CN", "description": "__MSG_Description__", "icons": { diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index 425205b7..372cea92 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -596,7 +596,7 @@ "message": "跳过后的视频仍然流畅,就好像跳过的片段不存在一样" }, "category_sponsor": { - "message": "赞助广告" + "message": "广告" }, "category_sponsor_description": { "message": "付费推广、付费推荐和直接广告。不是自我推广或免费提及他们喜欢的商品/创作者/网站/产品。" @@ -623,13 +623,13 @@ "message": "不是公司设计的产品和周边" }, "category_exclusive_access": { - "message": "品牌合作" + "message": "柔性推广/品牌合作" }, "category_exclusive_access_description": { "message": "仅用于对整个视频进行标记。适用于展示UP主免费或获得补贴后使用的产品、服务或场地的视频。" }, "category_exclusive_access_pill": { - "message": "此视频展示了UP主免费或获得后补贴使用的产品、服务或场地", + "message": "此视频展示了UP主免费或获得后补贴使用的产品或者服务,包括软广、产品推广或者品牌推广", "description": "此类别的简短描述" }, "category_exclusive_access_guideline1": { diff --git a/public/_locales/zh_CN/messages.json b/public/_locales/zh_CN/messages.json index d5c2dc5e..e16bf1a5 100644 --- a/public/_locales/zh_CN/messages.json +++ b/public/_locales/zh_CN/messages.json @@ -596,7 +596,7 @@ "message": "跳过后的视频仍然流畅,就好像跳过的片段不存在一样" }, "category_sponsor": { - "message": "赞助广告" + "message": "广告" }, "category_sponsor_description": { "message": "付费推广、付费推荐和直接广告。不是自我推广或免费提及他们喜欢的商品/创作者/网站/产品。" @@ -623,13 +623,13 @@ "message": "不是公司设计的产品和周边" }, "category_exclusive_access": { - "message": "品牌合作" + "message": "柔性推广/品牌合作" }, "category_exclusive_access_description": { "message": "仅用于对整个视频进行标记。适用于展示UP主免费或获得补贴后使用的产品、服务或场地的视频。" }, "category_exclusive_access_pill": { - "message": "此视频展示了UP主免费或获得后补贴使用的产品、服务或场地", + "message": "此视频展示了UP主免费或获得后补贴使用的产品或者服务,包括软广、产品推广或者品牌推广", "description": "此类别的简短描述" }, "category_exclusive_access_guideline1": { diff --git a/public/_locales/zh_TW/messages.json b/public/_locales/zh_TW/messages.json index 477e6d29..66cd5196 100644 --- a/public/_locales/zh_TW/messages.json +++ b/public/_locales/zh_TW/messages.json @@ -596,7 +596,7 @@ "message": "跳過後的影片仍然流暢,就好像跳過的片段不存在一樣" }, "category_sponsor": { - "message": "贊助廣告" + "message": "廣告" }, "category_sponsor_description": { "message": "付費推廣、付費推薦和直接廣告。不是自我推廣或免費提及他們喜歡的商品/創作者/網站/產品。" @@ -623,10 +623,10 @@ "message": "不是公司設計的產品和周邊" }, "category_exclusive_access": { - "message": "品牌合作" + "message": "柔性推廣/品牌合作" }, "category_exclusive_access_description": { - "message": "僅用於標記整個影片。適用於影片展示創作者免費或獲得補助使用的產品、服務或場地。" + "message": "僅用於標記整個影片。適用於影片展示創作者免費或獲得補助使用的產品或者服務,包括軟廣、產品推廣或者品牌推廣。" }, "category_exclusive_access_pill": { "message": "此影片展示了創作者免費或獲得補助使用的產品、服務或場地", diff --git a/public/content.css b/public/content.css index 01bf18f2..5fa9f062 100644 --- a/public/content.css +++ b/public/content.css @@ -33,7 +33,7 @@ width: 100%; height: 100%; z-index: 1; - + transition: transform 0.1s cubic-bezier(0, 0, 0.2, 1); } @@ -103,28 +103,27 @@ .autoHiding { overflow: visible !important; + transition: transform 0.2s cubic-bezier(0.215, 0.61, 0.355, 1), width 0.2s cubic-bezier(0.215, 0.61, 0.355, 1), + opacity 0.2s cubic-bezier(0.19, 1, 0.22, 1) !important; } .autoHiding:not(.sbhidden) { transform: translateX(0%); - transition: transform 0.2s cubic-bezier(0.215, 0.61, 0.355, 1), width 0.2s cubic-bezier(0.215, 0.61, 0.355, 1), opacity 0.2s cubic-bezier(0.19, 1, 0.22, 1) !important; } .autoHiding.sbhidden { transform: translateX(100%); - transition: transform 0.2s cubic-bezier(0.215, 0.61, 0.355, 1), width 0.2s cubic-bezier(0.215, 0.61, 0.355, 1), opacity 0.2s cubic-bezier(0.19, 1, 0.22, 1) !important; - /* width: 0px !important; */ opacity: 0; } -@media (max-width: 1260px){ - .autoHiding.sbhidden{ +@media (max-width: 1260px) { + .autoHiding.sbhidden { width: 0px !important; } } .autoHiding.sbhidden.autoHideLeft { - transform: translateX(-100%) scale(0); + transform: translateX(-100%); } .sponsorSkipObject { @@ -655,14 +654,6 @@ input::-webkit-inner-spin-button { display: none !important; } -#sbSkipIconControlBarImage { - height: 60%; - top: 0px; - bottom: 0px; - display: block; - margin: auto; -} - .sponsorBlockTooltip { position: absolute; background-color: rgba(28, 28, 28, 0.7); diff --git a/public/options/options.css b/public/options/options.css index a55b043d..c5fe7b61 100644 --- a/public/options/options.css +++ b/public/options/options.css @@ -98,7 +98,7 @@ body { #navigation { display: flex; flex-direction: column; - gap: 25px; + gap: 20px; } .tab-heading { @@ -655,7 +655,7 @@ svg { } /* Top bar navigation for smaller screens */ -@media only screen and (max-height: 750px), only screen and (max-width: 1200px) { +@media only screen and (max-height: 765px), only screen and (max-width: 1200px) { #options-container { flex-direction: column; } diff --git a/src/content.ts b/src/content.ts index 50e09460..37e42719 100644 --- a/src/content.ts +++ b/src/content.ts @@ -1948,11 +1948,13 @@ function skipToTime({ v, skipTime, skippingSegments, openNotice, forceAutoSkip, } if (!autoSkip && skippingSegments.length === 1 && skippingSegments[0].actionType === ActionType.Poi) { - skipButtonControlBar.enable(skippingSegments[0]); - if (Config.config.skipKeybind == null) skipButtonControlBar.setShowKeybindHint(false); + waitFor(() => skipButtonControlBar).then(() => { + skipButtonControlBar.enable(skippingSegments[0]); + if (Config.config.skipKeybind == null) skipButtonControlBar.setShowKeybindHint(false); - activeSkipKeybindElement?.setShowKeybindHint(false); - activeSkipKeybindElement = skipButtonControlBar; + activeSkipKeybindElement?.setShowKeybindHint(false); + activeSkipKeybindElement = skipButtonControlBar; + }); } else { if (openNotice) { //send out the message saying that a sponsor message was skipped diff --git a/src/document.ts b/src/document.ts index 5e64e7ae..1220bfd9 100644 --- a/src/document.ts +++ b/src/document.ts @@ -1,5 +1,9 @@ +import { getFrameRate, PlayInfo } from "./document/frameRateUtils"; +import { DataCache } from "./utils/cache"; import { InjectedScriptMessageSend, sourceId } from "./utils/injectedScriptMessageUtils"; +const playInfoCache = new DataCache(() => []); + const sendMessageToContent = (messageData: InjectedScriptMessageSend, payload): void => { window.postMessage( { @@ -11,26 +15,32 @@ const sendMessageToContent = (messageData: InjectedScriptMessageSend, payload): "/" ); }; -let frameRate: number = 30; -(function () { +function overwriteFetch() { const originalFetch = window.fetch; window.fetch = async function (input, init) { - const url = typeof input === 'string' ? input : (input as Request).url; - + const urlStr = typeof input === "string" ? input : (input as Request).url; const response = await originalFetch(input, init); - if (url.includes('/player/wbi/playurl') && url.includes(window.__INITIAL_STATE__.cid.toString())) { - response.clone().json().then(data => { - frameRate = data.data.dash.video - .filter((v) => v.id === data.data.quality && v.codecid === data.data.video_codecid)[0]?.frameRate; - }).catch(() => { - frameRate = 30; - }); - } + response + .clone() + .json() + .then((res) => { + const url = new URL(urlStr); + if (url.pathname.includes("/player/wbi/playurl")) { + const cid = url.searchParams.get("cid"); + if (!playInfoCache.getFromCache(cid) && res?.data?.dash?.video) { + playInfoCache.set( + cid, + res.data.dash.video.map((v) => ({ id: v.id, frameRate: parseFloat(v.frameRate) })) + ); + } + } + }) + .catch(() => {}); return response; }; -})(); +} function windowMessageListener(message: MessageEvent) { const data: InjectedScriptMessageSend = message.data; @@ -41,7 +51,7 @@ function windowMessageListener(message: MessageEvent) { if (data.type === "getBvID") { sendMessageToContent(data, window?.__INITIAL_STATE__?.bvid); } else if (data.type === "getFrameRate") { - sendMessageToContent(data, frameRate); + sendMessageToContent(data, getFrameRate(playInfoCache)); } else if (data.type === "getChannelID") { sendMessageToContent(data, window?.__INITIAL_STATE__?.upData?.mid); } else if (data.type === "getDescription") { @@ -52,6 +62,7 @@ function windowMessageListener(message: MessageEvent) { function init(): void { window.addEventListener("message", windowMessageListener); + overwriteFetch(); } init(); diff --git a/src/document/frameRateUtils.ts b/src/document/frameRateUtils.ts new file mode 100644 index 00000000..18efedf7 --- /dev/null +++ b/src/document/frameRateUtils.ts @@ -0,0 +1,32 @@ +import { DataCache } from "../utils/cache"; + +export interface PlayInfo { + id: number; + frameRate: number; +} + +export function getPlayInfo(playInfoCache: DataCache): PlayInfo[] { + if (window.__playinfo__?.data?.dash?.video) { + return window.__playinfo__.data.dash.video.map((v) => ({ id: v.id, frameRate: parseFloat(v.frameRate) })); + } else if (playInfoCache.getFromCache(window?.__INITIAL_STATE__?.cid.toString())) { + return playInfoCache.getFromCache(window.__INITIAL_STATE__.cid.toString()); + } + return []; +} + +export function getFrameRate(playInfoCache: DataCache) { + let currentQuality = null; + try { + currentQuality ||= JSON.parse(window?.localStorage?.bpx_player_profile)?.media?.quality; + } catch (e) { + console.debug("Failed to get current quality", e); + } + currentQuality = currentQuality ?? window?.__playinfo__?.data?.quality; + + const possibleQuality = getPlayInfo(playInfoCache).filter((v) => v.id === currentQuality && !!v.frameRate); + + if (possibleQuality.length > 0) { + return possibleQuality[0].frameRate; + } + return 30; +} diff --git a/src/globals.d.ts b/src/globals.d.ts index f19d69ae..07f54bc5 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -2,6 +2,13 @@ import SBObject from "./config"; declare global { interface Window { SB: typeof SBObject; - __INITIAL_STATE__?: { bvid: string; aid: number; cid: number; upData: { mid: string }; videoData: { desc: string }}; + __INITIAL_STATE__?: { + bvid: string; + aid: number; + cid: number; + upData: { mid: string }; + videoData: { desc: string }; + }; + __playinfo__?: { data: { quality: number; dash: { video: { id: number; frameRate: string }[] } } }; } } diff --git a/src/js-components/skipButtonControlBar.ts b/src/js-components/skipButtonControlBar.ts index 88352c58..03d04817 100644 --- a/src/js-components/skipButtonControlBar.ts +++ b/src/js-components/skipButtonControlBar.ts @@ -1,8 +1,10 @@ import Config from "../config"; +import { keybindToString } from "../config/config"; +import { getPageLoaded } from "../content"; import { SegmentUUID, SponsorTime } from "../types"; -import { getSkippingText } from "../utils/categoryUtils"; import { AnimationUtils } from "../utils/animationUtils"; -import { keybindToString } from "../config/config"; +import { getSkippingText } from "../utils/categoryUtils"; +import { waitFor } from "../utils/index"; export interface SkipButtonControlBarProps { skip: (segment: SponsorTime) => void; @@ -11,9 +13,10 @@ export interface SkipButtonControlBarProps { export class SkipButtonControlBar { container: HTMLElement; + skipButton: HTMLButtonElement; skipIcon: HTMLImageElement; - textContainer: HTMLElement; - chapterText: HTMLElement; + // textContainer: HTMLElement; + // chapterText: HTMLElement; segment: SponsorTime; showKeybindHint = true; @@ -32,15 +35,23 @@ export class SkipButtonControlBar { this.container.classList.add("skipButtonControlBarContainer"); this.container.classList.add("sbhidden"); + this.skipButton = document.createElement("button"); + this.skipButton.classList.add("bpx-player-ctrl-btn", "playerButton"); + this.skipButton.id = "sbSkipIconControlBarButton"; + this.skipButton.draggable = false; + this.skipIcon = document.createElement("img"); this.skipIcon.src = chrome.runtime.getURL("icons/skipIcon.svg"); - this.skipIcon.classList.add("ytp-button"); + this.skipIcon.classList.add("bpx-player-ctrl-btn-icon", "playerButtonImage"); this.skipIcon.id = "sbSkipIconControlBarImage"; + this.skipIcon.draggable = false; + + this.skipButton.appendChild(this.skipIcon); - this.textContainer = document.createElement("div"); + // this.textContainer = document.createElement("div"); - this.container.appendChild(this.skipIcon); - this.container.appendChild(this.textContainer); + this.container.appendChild(this.skipButton); + // this.container.appendChild(this.textContainer); this.container.addEventListener("click", () => this.toggleSkip()); this.container.addEventListener("mouseenter", () => { this.stopTimer(); @@ -60,18 +71,19 @@ export class SkipButtonControlBar { return this.container; } - attachToPage(): void { + async attachToPage(): Promise { + await waitFor(getPageLoaded, 10000, 10); const mountingContainer = this.getMountingContainer(); - this.chapterText = document.querySelector(".ytp-chapter-container"); + // this.chapterText = document.querySelector(".ytp-chapter-container"); if (mountingContainer && !mountingContainer.contains(this.container)) { - mountingContainer.insertBefore(this.container, this.chapterText); - AnimationUtils.setupAutoHideAnimation(this.skipIcon, mountingContainer, false, false); + mountingContainer.append(this.container); + AnimationUtils.setupAutoHideAnimation(this.skipButton, mountingContainer, false, false); } } private getMountingContainer(): HTMLElement { - return document.querySelector(".ytp-left-controls"); + return document.querySelector(".bpx-player-control-bottom-left"); } enable(segment: SponsorTime, duration?: number): void { @@ -81,18 +93,18 @@ export class SkipButtonControlBar { this.refreshText(); this.container?.classList?.remove("textDisabled"); - this.textContainer?.classList?.remove("sbhidden"); - AnimationUtils.disableAutoHideAnimation(this.skipIcon); + // this.textContainer?.classList?.remove("sbhidden"); + AnimationUtils.disableAutoHideAnimation(this.skipButton); this.startTimer(); } refreshText(): void { if (this.segment) { - this.chapterText?.classList?.add("sbhidden"); + // this.chapterText?.classList?.add("sbhidden"); this.container.classList.remove("sbhidden"); - this.textContainer.innerText = this.getTitle(); - this.skipIcon.setAttribute("title", this.getTitle()); + // this.textContainer.innerText = this.getTitle(); + this.skipButton.setAttribute("title", this.getTitle()); } } @@ -117,8 +129,8 @@ export class SkipButtonControlBar { disable(): void { this.container.classList.add("sbhidden"); - this.chapterText?.classList?.remove("sbhidden"); - this.getChapterPrefix()?.classList?.remove("sbhidden"); + // this.chapterText?.classList?.remove("sbhidden"); + // this.getChapterPrefix()?.classList?.remove("sbhidden"); this.enabled = false; } @@ -141,12 +153,12 @@ export class SkipButtonControlBar { } this.container.classList.add("textDisabled"); - this.textContainer?.classList?.add("sbhidden"); - this.chapterText?.classList?.remove("sbhidden"); + // this.textContainer?.classList?.add("sbhidden"); + // this.chapterText?.classList?.remove("sbhidden"); this.getChapterPrefix()?.classList?.add("sbhidden"); - AnimationUtils.enableAutoHideAnimation(this.skipIcon); + AnimationUtils.enableAutoHideAnimation(this.skipButton); } private getTitle(): string { diff --git a/src/utils/cache.ts b/src/utils/cache.ts index ea2c1075..d4c851e3 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -7,7 +7,7 @@ export class DataCache { private init: () => V; private cacheLimit: number; - constructor(init: () => V, cacheLimit = 2000) { + constructor(init?: () => V, cacheLimit = 2000) { this.cache = {}; this.init = init; this.cacheLimit = cacheLimit; @@ -17,17 +17,17 @@ export class DataCache { return this.cache[key]; } + public set(key: T, value: V): void { + this.cache[key] = { + ...value, + lastUsed: Date.now(), + }; + this.gc(); + } + public setupCache(key: T): V & CacheRecord { - if (!this.cache[key]) { - this.cache[key] = { - ...this.init(), - lastUsed: Date.now(), - }; - - if (Object.keys(this.cache).length > this.cacheLimit) { - const oldest = Object.entries(this.cache).reduce((a, b) => (a[1].lastUsed < b[1].lastUsed ? a : b)); - delete this.cache[oldest[0]]; - } + if (!this.cache[key] && this.init) { + this.set(key, this.init()); } return this.cache[key]; @@ -38,4 +38,11 @@ export class DataCache { return !!this.cache[key]; } + + private gc(): void { + if (Object.keys(this.cache).length > this.cacheLimit) { + const oldest = Object.entries(this.cache).reduce((a, b) => (a[1].lastUsed < b[1].lastUsed ? a : b)); + delete this.cache[oldest[0]]; + } + } }