diff --git a/maze-utils b/maze-utils index 5d5e0b096b..0142824108 160000 --- a/maze-utils +++ b/maze-utils @@ -1 +1 @@ -Subproject commit 5d5e0b096b67f138122d0c6142505210bfe52120 +Subproject commit 014282410890159651909953aa42fef37ce4b1c0 diff --git a/public/_locales b/public/_locales index 63fe3f4bbc..ed0fe943e3 160000 --- a/public/_locales +++ b/public/_locales @@ -1 +1 @@ -Subproject commit 63fe3f4bbcebeeb9a4fc15f401f35f11a20c79d0 +Subproject commit ed0fe943e3186ed8b84c030ba865e06638ce734e diff --git a/public/options/options.css b/public/options/options.css index b68ce3dd21..fcec837a86 100644 --- a/public/options/options.css +++ b/public/options/options.css @@ -603,6 +603,22 @@ svg { border-radius: 5px; } +.speedOption { + background-color: #c00000; + color: white; + + box-sizing: content-box; + border: none; + font-size: 14px; + border-radius: 5px; + padding: 5px; +} + +.skipOption { + display: flex; + gap: 5px; +} + .categoryColorTextBox { width: 60px; @@ -731,4 +747,4 @@ svg { .dearrow-link:hover .close-button { opacity: 1; -} \ No newline at end of file +} diff --git a/src/components/options/CategorySkipOptionsComponent.tsx b/src/components/options/CategorySkipOptionsComponent.tsx index d865abb2bd..5b2898246c 100644 --- a/src/components/options/CategorySkipOptionsComponent.tsx +++ b/src/components/options/CategorySkipOptionsComponent.tsx @@ -2,12 +2,12 @@ import * as React from "react"; import Config from "../../config" import * as CompileConfig from "../../../config.json"; -import { Category, CategorySkipOption } from "../../types"; +import { Category, CategorySelection, CategorySkipOption } from "../../types"; import { getCategorySuffix } from "../../utils/categoryUtils"; import ToggleOptionComponent from "./ToggleOptionComponent"; -export interface CategorySkipOptionsProps { +export interface CategorySkipOptionsProps { category: Category; defaultColor?: string; defaultPreviewColor?: string; @@ -17,6 +17,8 @@ export interface CategorySkipOptionsProps { export interface CategorySkipOptionsState { color: string; previewColor: string; + speed: number; + option: CategorySkipOption; } export interface ToggleOption { @@ -27,36 +29,51 @@ export interface ToggleOption { class CategorySkipOptionsComponent extends React.Component { setBarColorTimeout: NodeJS.Timeout; + categorySelection: CategorySelection; constructor(props: CategorySkipOptionsProps) { super(props); + this.categorySelection = this.getCategorySelection(); + // Setup state this.state = { color: props.defaultColor || Config.config.barTypes[this.props.category]?.color, - previewColor: props.defaultPreviewColor || Config.config.barTypes["preview-" + this.props.category]?.color + previewColor: props.defaultPreviewColor || Config.config.barTypes["preview-" + this.props.category]?.color, + speed: this.categorySelection.speed, + option: this.categorySelection.option, }; } + getCategorySelection() { + let categorySelection = Config.config.categorySelections + .find(categorySelection => categorySelection.name === this.props.category) + if (!categorySelection) { + categorySelection = { + name: this.props.category, + option: CategorySkipOption.Disabled, + speed: 2, + } + Config.config.categorySelections.push(categorySelection); + } + return categorySelection; + } + render(): React.ReactElement { let defaultOption = "disable"; - // Set the default opton properly - for (const categorySelection of Config.config.categorySelections) { - if (categorySelection.name === this.props.category) { - switch (categorySelection.option) { - case CategorySkipOption.ShowOverlay: - defaultOption = "showOverlay"; - break; - case CategorySkipOption.ManualSkip: - defaultOption = "manualSkip"; - break; - case CategorySkipOption.AutoSkip: - defaultOption = "autoSkip"; - break; - } - + switch (this.categorySelection.option) { + case CategorySkipOption.ShowOverlay: + defaultOption = "showOverlay"; + break; + case CategorySkipOption.ManualSkip: + defaultOption = "manualSkip"; + break; + case CategorySkipOption.AutoSkip: + defaultOption = "autoSkip"; + break; + case CategorySkipOption.FastForward: + defaultOption = "fastForward"; break; - } } return ( @@ -76,6 +93,16 @@ class CategorySkipOptionsComponent extends React.Component {this.getCategorySkipOptions()} + {this.props.category !== "chapter" && @@ -113,7 +140,7 @@ class CategorySkipOptionsComponent extends React.Component - + {this.getExtraOptionComponents(this.props.category)} @@ -127,6 +154,7 @@ class CategorySkipOptionsComponent extends React.Component categorySelection.name !== this.props.category); + this.setState({ option: CategorySkipOption.Disabled }); return; case "showOverlay": option = CategorySkipOption.ShowOverlay; @@ -146,32 +174,44 @@ class CategorySkipOptionsComponent extends React.Component selection.name === this.props.category); - if (existingSelection) { - existingSelection.option = option; - } else { - Config.config.categorySelections.push({ - name: this.props.category, - option: option - }); + break; } + this.setState({ option }); + + this.categorySelection = this.getCategorySelection(); + this.categorySelection.option = option; + + Config.forceSyncUpdate("categorySelections"); + } + + forwardSpeedEntered(event: React.ChangeEvent): void { + const speedRaw = event.target.value; + const speed = +parseFloat(speedRaw || '1').toFixed(2); + if (speed < 0.1 || speed > 10) return; + + this.setState({ speed }); + + this.categorySelection = this.getCategorySelection(); + this.categorySelection.speed = speed; + Config.forceSyncUpdate("categorySelections"); } getCategorySkipOptions(): JSX.Element[] { const elements: JSX.Element[] = []; - let optionNames = ["disable", "showOverlay", "manualSkip", "autoSkip"]; + let optionNames = ["disable", "showOverlay", "manualSkip", "autoSkip", "fastForward"]; if (this.props.category === "chapter") optionNames = ["disable", "showOverlay"] else if (this.props.category === "exclusive_access") optionNames = ["disable", "showOverlay"]; for (const optionName of optionNames) { elements.push( ); @@ -210,8 +250,8 @@ class CategorySkipOptionsComponent extends React.Component - @@ -253,4 +293,4 @@ class CategorySkipOptionsComponent extends React.Component s.name === "chapter")) { config.categorySelections.push({ name: "chapter" as Category, - option: CategorySkipOption.ShowOverlay + option: CategorySkipOption.ShowOverlay, + speed: 2, }); - - config.categorySelections = config.categorySelections; } } @@ -272,6 +272,13 @@ function migrateOldSyncFormats(config: SBConfig) { if (config["lastIsVipUpdate"]) { chrome.storage.sync.remove("lastIsVipUpdate"); } + + if (!config["fastForwardCategorySkipUpdate"]) { + config["fastForwardCategorySkipUpdate"] = true; + for (const selection of config.categorySelections) { + selection.speed = 2; + } + } } const syncDefaults = { @@ -365,16 +372,20 @@ const syncDefaults = { categorySelections: [{ name: "sponsor" as Category, - option: CategorySkipOption.AutoSkip + option: CategorySkipOption.AutoSkip, + speed: 2, }, { name: "poi_highlight" as Category, - option: CategorySkipOption.ManualSkip + option: CategorySkipOption.ManualSkip, + speed: 2, }, { name: "exclusive_access" as Category, - option: CategorySkipOption.ShowOverlay + option: CategorySkipOption.ShowOverlay, + speed: 2, }, { name: "chapter" as Category, - option: CategorySkipOption.ShowOverlay + option: CategorySkipOption.ShowOverlay, + speed: 2, }], payments: { @@ -478,7 +489,7 @@ const syncDefaults = { opacity: "0" }, } -}; +} satisfies SBConfig; const localDefaults = { downvotedSegments: {}, @@ -486,7 +497,7 @@ const localDefaults = { alreadyInstalled: false, unsubmittedSegments: {} -}; +} satisfies SBStorage; const Config = new ConfigClass(syncDefaults, localDefaults, migrateOldSyncFormats); export default Config; @@ -511,4 +522,4 @@ export function generateDebugDetails(): string { output.config.whitelistedChannels = output.config.whitelistedChannels.length; return JSON.stringify(output, null, 4); -} \ No newline at end of file +} diff --git a/src/content.ts b/src/content.ts index 83e80dd073..868863a699 100644 --- a/src/content.ts +++ b/src/content.ts @@ -35,7 +35,7 @@ import { ChapterVote } from "./render/ChapterVote"; import { openWarningDialog } from "./utils/warnings"; import { extensionUserAgent, isFirefoxOrSafari, waitFor } from "../maze-utils/src"; import { getErrorMessage, getFormattedTime } from "../maze-utils/src/formating"; -import { getChannelIDInfo, getVideo, getIsAdPlaying, getIsLivePremiere, setIsAdPlaying, checkVideoIDChange, getVideoID, getYouTubeVideoID, setupVideoModule, checkIfNewVideoID, isOnInvidious, isOnMobileYouTube, isOnYouTubeMusic, isOnYTTV, getLastNonInlineVideoID, triggerVideoIDChange, triggerVideoElementChange, getIsInline, getCurrentTime, setCurrentTime, getVideoDuration, verifyCurrentTime, waitForVideo } from "../maze-utils/src/video"; +import { getChannelIDInfo, getVideo, getIsAdPlaying, getIsLivePremiere, setIsAdPlaying, checkVideoIDChange, getVideoID, getYouTubeVideoID, setupVideoModule, checkIfNewVideoID, isOnInvidious, isOnMobileYouTube, isOnYouTubeMusic, isOnYTTV, getLastNonInlineVideoID, triggerVideoIDChange, triggerVideoElementChange, getIsInline, getCurrentTime, setCurrentTime, setSpeed, getVideoDuration, verifyCurrentTime, waitForVideo } from "../maze-utils/src/video"; import { Keybind, StorageChangesObject, isSafari, keybindEquals, keybindToString } from "../maze-utils/src/config"; import { findValidElement } from "../maze-utils/src/dom" import { getHash, HashedValue } from "../maze-utils/src/hash"; @@ -1732,7 +1732,8 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u if (Config.config.disableSkipping) return; // There will only be one submission if it is manual skip - const autoSkip: boolean = forceAutoSkip || shouldAutoSkip(skippingSegments[0]); + const autoSkip = forceAutoSkip || shouldAutoSkip(skippingSegments[0]); + const fastForward = shouldFastForward(skippingSegments[0]); const isSubmittingSegment = sponsorTimesSubmitting.some((time) => time.segment === skippingSegments[0].segment); if ((autoSkip || isSubmittingSegment) @@ -1789,6 +1790,10 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u }) } + if (fastForward) { + setupFastForward(skippingSegments[0]); + } + if (!autoSkip && skippingSegments.length === 1 && skippingSegments[0].actionType === ActionType.Poi) { @@ -1925,6 +1930,16 @@ function createButton(baseID: string, title: string, callback: () => void, image return newButton; } +function shouldFastForward(segment: SponsorTime): boolean { + const canSkipNonMusic = !Config.config.skipNonMusicOnlyOnYoutubeMusic || isOnYouTubeMusic(); + if (segment.category === "music_offtopic" && !canSkipNonMusic) { + return false; + } + + return (!Config.config.manualSkipOnFullVideo || !sponsorTimes?.some((s) => s.category === segment.category && s.actionType === ActionType.Full)) + && utils.getCategorySelection(segment.category)?.option === CategorySkipOption.FastForward; +} + function shouldAutoSkip(segment: SponsorTime): boolean { const canSkipNonMusic = !Config.config.skipNonMusicOnlyOnYoutubeMusic || isOnYouTubeMusic(); if (segment.category === "music_offtopic" && !canSkipNonMusic) { @@ -2867,3 +2882,25 @@ function checkForMiniplayerPlaying() { } } } + +let fastForwardInterval: NodeJS.Timeout = null; +let savedVideoSpeed = 1; + +function fastForwardUpdate(startTime: number, endTime: number) { + const currentTime = getCurrentTime(); + if (currentTime < startTime - 1 || currentTime >= endTime) { + clearInterval(fastForwardInterval); + fastForwardInterval = null; + setSpeed(savedVideoSpeed); + } +} + +function setupFastForward(segment: SponsorTime) { + if (fastForwardInterval) return; + const newSpeed = utils.getCategorySelection(segment.category)!.speed; + savedVideoSpeed = getVideo().playbackRate; + setSpeed(newSpeed); + const startTime = segment.actionType === ActionType.Poi || segment.segment.length !== 2 ? 0 : segment.segment[0]; + const endTime = segment.segment[segment.segment.length - 1]; + fastForwardInterval = setInterval(() => fastForwardUpdate(startTime, endTime), 400); +} diff --git a/src/types.ts b/src/types.ts index d994ba36cb..55eef62459 100644 --- a/src/types.ts +++ b/src/types.ts @@ -33,12 +33,14 @@ export enum CategorySkipOption { Disabled = -1, ShowOverlay, ManualSkip, - AutoSkip + AutoSkip, + FastForward, } export interface CategorySelection { name: Category; option: CategorySkipOption; + speed: number; } export enum SponsorHideType { @@ -225,4 +227,4 @@ export enum NoticeVisibilityMode { MiniForAll = 2, FadedForAutoSkip = 3, FadedForAll = 4 -} \ No newline at end of file +} diff --git a/src/utils.ts b/src/utils.ts index 5d6178bb7b..e470029065 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -217,7 +217,7 @@ export default class Utils { return selection; } } - return { name: category, option: CategorySkipOption.Disabled} as CategorySelection; + return { name: category, option: CategorySkipOption.Disabled, speed: 2 } as CategorySelection; } /**