mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2024-09-20 04:53:43 +02:00
Load existing chapters
This commit is contained in:
parent
cf3b3c5c48
commit
2ebc5489cd
7 changed files with 90 additions and 28 deletions
|
@ -6,6 +6,7 @@ import Utils from "../utils";
|
|||
import SubmissionNoticeComponent from "./SubmissionNoticeComponent";
|
||||
import { RectangleTooltip } from "../render/RectangleTooltip";
|
||||
import SelectorComponent, { SelectorOption } from "./SelectorComponent";
|
||||
import { GenericUtils } from "../utils/genericUtils";
|
||||
|
||||
|
||||
const utils = new Utils();
|
||||
|
@ -289,8 +290,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||
const sponsorTimeEdits = this.state.sponsorTimeEdits;
|
||||
|
||||
// check if change is small engough to show tooltip
|
||||
const before = utils.getFormattedTimeToSeconds(sponsorTimeEdits[index]);
|
||||
const after = utils.getFormattedTimeToSeconds(targetValue);
|
||||
const before = GenericUtils.getFormattedTimeToSeconds(sponsorTimeEdits[index]);
|
||||
const after = GenericUtils.getFormattedTimeToSeconds(targetValue);
|
||||
const difference = Math.abs(before - after);
|
||||
if (0 < difference && difference < 0.5) this.showScrollToEditToolTip();
|
||||
|
||||
|
@ -313,7 +314,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||
}
|
||||
|
||||
const sponsorTimeEdits = this.state.sponsorTimeEdits;
|
||||
let timeAsNumber = utils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[index]);
|
||||
let timeAsNumber = GenericUtils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[index]);
|
||||
if (timeAsNumber !== null && e.deltaY != 0) {
|
||||
if (e.deltaY < 0) {
|
||||
timeAsNumber += step;
|
||||
|
@ -530,8 +531,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||
const sponsorTimesSubmitting = this.props.contentContainer().sponsorTimesSubmitting;
|
||||
|
||||
if (this.state.editing) {
|
||||
const startTime = utils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[0]);
|
||||
const endTime = utils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[1]);
|
||||
const startTime = GenericUtils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[0]);
|
||||
const endTime = GenericUtils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[1]);
|
||||
|
||||
// Change segment time only if the format was correct
|
||||
if (startTime !== null && endTime !== null) {
|
||||
|
|
|
@ -15,7 +15,7 @@ import { Message, MessageResponse, VoteResponse } from "./messageTypes";
|
|||
import * as Chat from "./js-components/chat";
|
||||
import { SkipButtonControlBar } from "./js-components/skipButtonControlBar";
|
||||
import { getStartTimeFromUrl } from "./utils/urlParser";
|
||||
import { findValidElement, getControls, getHashParams, isVisible } from "./utils/pageUtils";
|
||||
import { findValidElement, getControls, getExistingChapters, getHashParams, isVisible } from "./utils/pageUtils";
|
||||
import { isSafari, keybindEquals } from "./utils/configUtils";
|
||||
import { CategoryPill } from "./render/CategoryPill";
|
||||
import { AnimationUtils } from "./utils/animationUtils";
|
||||
|
@ -806,6 +806,17 @@ async function sponsorsLookup(id: string, keepOldSubmissions = true) {
|
|||
//otherwise the listener can handle it
|
||||
updatePreviewBar();
|
||||
}
|
||||
|
||||
// Add existing chapters if we can
|
||||
if (utils.chaptersEnabled()) {
|
||||
GenericUtils.wait(() => getExistingChapters(sponsorVideoID, video.duration),
|
||||
5000, 100, (c) => c?.length > 0).then((chapters) => {
|
||||
if (chapters?.length > 0) {
|
||||
sponsorTimes = sponsorTimes.concat(...chapters);
|
||||
updatePreviewBar();
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (response?.status === 404) {
|
||||
retryFetch();
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import Config from "../config";
|
|||
import { ActionType, Category, SegmentContainer, SponsorTime } from "../types";
|
||||
import Utils from "../utils";
|
||||
import { partition } from "../utils/arrayUtils";
|
||||
import { GenericUtils } from "../utils/genericUtils";
|
||||
const utils = new Utils();
|
||||
|
||||
const TOOLTIP_VISIBLE_CLASS = 'sponsorCategoryTooltipVisible';
|
||||
|
@ -111,7 +112,7 @@ class PreviewBar {
|
|||
const tooltipText = tooltipTextElement.textContent;
|
||||
if (tooltipText === null || tooltipText.length === 0) continue;
|
||||
|
||||
timeInSeconds = utils.getFormattedTimeToSeconds(tooltipText);
|
||||
timeInSeconds = GenericUtils.getFormattedTimeToSeconds(tooltipText);
|
||||
|
||||
if (timeInSeconds !== null) break;
|
||||
}
|
||||
|
@ -389,6 +390,8 @@ class PreviewBar {
|
|||
const chapterBar = document.querySelector(".ytp-chapters-container:not(.sponsorBlockChapterBar)") as HTMLElement;
|
||||
if (!progressBar || !chapterBar) return;
|
||||
|
||||
// todo: Can this same mutation observer be reused to import chapters
|
||||
// maybe not, looks like it has to be imported from the skipping to chapters page
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
const changes: Record<string, HTMLElement> = {};
|
||||
for (const mutation of mutations) {
|
||||
|
|
|
@ -71,7 +71,8 @@ export type Category = string & { __categoryBrand: unknown };
|
|||
|
||||
export enum SponsorSourceType {
|
||||
Server = undefined,
|
||||
Local = 1
|
||||
Local = 1,
|
||||
YouTube = 2
|
||||
}
|
||||
|
||||
export interface SegmentContainer {
|
||||
|
|
20
src/utils.ts
20
src/utils.ts
|
@ -29,7 +29,7 @@ export default class Utils {
|
|||
this.backgroundScriptContainer = backgroundScriptContainer;
|
||||
}
|
||||
|
||||
async wait<T>(condition: () => T | false, timeout = 5000, check = 100): Promise<T> {
|
||||
async wait<T>(condition: () => T, timeout = 5000, check = 100): Promise<T> {
|
||||
return GenericUtils.wait(condition, timeout, check);
|
||||
}
|
||||
|
||||
|
@ -442,20 +442,6 @@ export default class Utils {
|
|||
return formatted;
|
||||
}
|
||||
|
||||
getFormattedTimeToSeconds(formatted: string): number | null {
|
||||
const fragments = /^(?:(?:(\d+):)?(\d+):)?(\d*(?:[.,]\d+)?)$/.exec(formatted);
|
||||
|
||||
if (fragments === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hours = fragments[1] ? parseInt(fragments[1]) : 0;
|
||||
const minutes = fragments[2] ? parseInt(fragments[2] || '0') : 0;
|
||||
const seconds = fragments[3] ? parseFloat(fragments[3].replace(',', '.')) : 0;
|
||||
|
||||
return hours * 3600 + minutes * 60 + seconds;
|
||||
}
|
||||
|
||||
shortCategoryName(categoryName: string): string {
|
||||
return chrome.i18n.getMessage("category_" + categoryName + "_short") || chrome.i18n.getMessage("category_" + categoryName);
|
||||
}
|
||||
|
@ -529,4 +515,8 @@ export default class Utils {
|
|||
|
||||
Config.forceLocalUpdate("downvotedSegments");
|
||||
}
|
||||
|
||||
chaptersEnabled(): boolean {
|
||||
return Config.config.renderAsChapters && !!this.getCategorySelection("chapter");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/** Function that can be used to wait for a condition before returning. */
|
||||
async function wait<T>(condition: () => T | false, timeout = 5000, check = 100): Promise<T> {
|
||||
async function wait<T>(condition: () => T, timeout = 5000, check = 100, predicate?: (obj: T) => boolean): Promise<T> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
clearInterval(interval);
|
||||
|
@ -8,7 +8,7 @@ async function wait<T>(condition: () => T | false, timeout = 5000, check = 100):
|
|||
|
||||
const intervalCheck = () => {
|
||||
const result = condition();
|
||||
if (result) {
|
||||
if (predicate ? predicate(result) : result) {
|
||||
resolve(result);
|
||||
clearInterval(interval);
|
||||
}
|
||||
|
@ -21,6 +21,20 @@ async function wait<T>(condition: () => T | false, timeout = 5000, check = 100):
|
|||
});
|
||||
}
|
||||
|
||||
function getFormattedTimeToSeconds(formatted: string): number | null {
|
||||
const fragments = /^(?:(?:(\d+):)?(\d+):)?(\d*(?:[.,]\d+)?)$/.exec(formatted);
|
||||
|
||||
if (fragments === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hours = fragments[1] ? parseInt(fragments[1]) : 0;
|
||||
const minutes = fragments[2] ? parseInt(fragments[2] || '0') : 0;
|
||||
const seconds = fragments[3] ? parseFloat(fragments[3].replace(',', '.')) : 0;
|
||||
|
||||
return hours * 3600 + minutes * 60 + seconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the error message in a nice string
|
||||
*
|
||||
|
@ -64,10 +78,11 @@ function hexToRgb(hex: string): {r: number, g: number, b: number} {
|
|||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : null;
|
||||
}
|
||||
}
|
||||
|
||||
export const GenericUtils = {
|
||||
wait,
|
||||
getFormattedTimeToSeconds,
|
||||
getErrorMessage,
|
||||
getLuminance
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
export function getControls(): HTMLElement | false {
|
||||
import { ActionType, Category, SponsorSourceType, SponsorTime, VideoID } from "../types";
|
||||
import { GenericUtils } from "./genericUtils";
|
||||
|
||||
export function getControls(): HTMLElement {
|
||||
const controlsSelectors = [
|
||||
// YouTube
|
||||
".ytp-right-controls",
|
||||
|
@ -16,7 +19,7 @@ export function getControls(): HTMLElement | false {
|
|||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isVisible(element: HTMLElement): boolean {
|
||||
|
@ -62,3 +65,41 @@ export function getHashParams(): Record<string, unknown> {
|
|||
|
||||
return {};
|
||||
}
|
||||
|
||||
export function getExistingChapters(currentVideoID: VideoID, duration: number): SponsorTime[] {
|
||||
const chaptersBox = document.querySelector("ytd-macro-markers-list-renderer");
|
||||
|
||||
const chapters: SponsorTime[] = [];
|
||||
if (chaptersBox) {
|
||||
let lastSegment: SponsorTime = null;
|
||||
const links = chaptersBox.querySelectorAll("ytd-macro-markers-list-item-renderer > a");
|
||||
for (const link of links) {
|
||||
const timeElement = link.querySelector("#time") as HTMLElement;
|
||||
const description = link.querySelector("#details h4") as HTMLElement;
|
||||
if (timeElement && description?.innerText?.length > 0 && link.getAttribute("href")?.includes(currentVideoID)) {
|
||||
const time = GenericUtils.getFormattedTimeToSeconds(timeElement.innerText);
|
||||
|
||||
if (lastSegment) {
|
||||
lastSegment.segment[1] = time;
|
||||
chapters.push(lastSegment);
|
||||
}
|
||||
|
||||
lastSegment = {
|
||||
segment: [time, null],
|
||||
category: "chapter" as Category,
|
||||
actionType: ActionType.Chapter,
|
||||
description: description.innerText,
|
||||
source: SponsorSourceType.YouTube,
|
||||
UUID: null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (lastSegment) {
|
||||
lastSegment.segment[1] = duration;
|
||||
chapters.push(lastSegment);
|
||||
}
|
||||
}
|
||||
|
||||
return chapters;
|
||||
}
|
Loading…
Reference in a new issue