diff --git a/src/lib/ActionElement.ts b/src/lib/ActionElement.ts index 85bde7a..56ab985 100644 --- a/src/lib/ActionElement.ts +++ b/src/lib/ActionElement.ts @@ -1,7 +1,7 @@ import { createAutoplaySwitch } from "./Autoplay"; import { setPlaybackRate } from "./PlaybackRate"; -import { CYCLABLE_PLAYBACK_RATES } from "./declarations"; -import { StateObject, BooleanDictionary, PolyDictionary } from "./definitions"; +import { CYCLABLE_PLAYBACK_RATES, INJECTION_MARKER } from "./declarations"; +import { BooleanDictionary, PolyDictionary, StateObject } from "./definitions"; import { getActionElement, getCurrentId, getTitle, getVideo } from "./getters"; import { wheel } from "./utils"; @@ -15,8 +15,10 @@ export function populateActionElement( const actionElement = getActionElement(); const ytShorts = getVideo(); - if (!actionElement) throw new Error("Action element not found"); - if (!ytShorts) throw new Error("Video not found"); + if (!actionElement) return; // throw new Error("Action element not found"); + if (!ytShorts) return; // throw new Error("Video not found"); + + actionElement.setAttribute(INJECTION_MARKER, ""); // ? set marker for injection checks // adsu- idk how any of this works so im just going to leave it be const betterYTContainer = document.createElement("div"); diff --git a/src/lib/Events.ts b/src/lib/Events.ts index 0bd2b20..ab3aa0f 100644 --- a/src/lib/Events.ts +++ b/src/lib/Events.ts @@ -5,8 +5,10 @@ import { getVideo } from "./getters"; /** * Handle adding event listeners to existing elements. * For example, adds a double click event to the player to enter fullscreen mode + * + * Note that duplicate event listeners are ignored, so need need to check */ -export function injectEventsToExistingElements(options: PolyDictionary) { +export function injectEvents(options: PolyDictionary) { const player: HTMLElement | null = getVideo(); // double click the player to enter fullscreen mode diff --git a/src/lib/Info.ts b/src/lib/Info.ts index 604eb02..f73e02d 100644 --- a/src/lib/Info.ts +++ b/src/lib/Info.ts @@ -1,33 +1,40 @@ import { isVideoPlaying } from "./VideoState"; +import { INJECTION_MARKER } from "./declarations"; import { BooleanDictionary } from "./definitions"; import { getCurrentId, + getInfoElement, getOverlayElement, getUploadDate, getViews, } from "./getters"; export function setInfo(features: BooleanDictionary) { - if (!isVideoPlaying()) throw new Error("Video not playing"); + if (!isVideoPlaying()) return; // throw new Error("Video not playing"); - const addInfo = () => { - const info = []; - if (features["viewCounter"]) { - const views = getViews().replace(/(\r\n|\n|\r)/gm, ""); - if (views) info.push(views); - } - if (features["uploadDate"]) { - const uploadDate = getUploadDate().replace(/(\r\n|\n|\r)/gm, ""); - if (uploadDate) info.push(uploadDate); - } + const overlayElement = getOverlayElement(); + const h5 = document.createElement("h5"); + h5.id = `bys-ytViews${getCurrentId()}`; + h5.setAttribute(INJECTION_MARKER, ""); // ? for injection checks + overlayElement.querySelector("reel-player-header-renderer h2")?.prepend(h5); - const overlayElement = getOverlayElement(); - const h5 = document.createElement("h5"); - h5.id = `ytViews${getCurrentId()}`; - h5.innerText = info.join(" | "); - overlayElement.querySelector("reel-player-header-renderer h2")?.prepend(h5); - clearInterval(views_interval); - }; + updateInfo(features); +} + +export function updateInfo(features: BooleanDictionary) { + const element = getInfoElement(); + if (element === null) return; + + const info = []; + + if (features["viewCounter"]) { + const views = getViews().replace(/(\r\n|\n|\r)/gm, ""); + if (views) info.push(views); + } + if (features["uploadDate"]) { + const uploadDate = getUploadDate().replace(/(\r\n|\n|\r)/gm, ""); + if (uploadDate) info.push(uploadDate); + } - const views_interval = setInterval(addInfo, 100); + element.innerText = info.join(" | "); } diff --git a/src/lib/InjectionHandling.ts b/src/lib/InjectionHandling.ts new file mode 100644 index 0000000..3d3e7a1 --- /dev/null +++ b/src/lib/InjectionHandling.ts @@ -0,0 +1,77 @@ +import { populateActionElement } from "./ActionElement"; +import { setInfo } from "./Info"; +import { InjectionItemsEnum } from "./definitions"; +import { modifyProgressBar } from "./ProgressBar"; +import { setVolumeSlider } from "./VolumeSlider"; +import { INJECTION_MARKER } from "./declarations"; +import { BooleanDictionary, PolyDictionary, StateObject } from "./definitions"; +import { + getActionElement, + getCurrentId, + getInfoElement, + getProgressBarList, + getVolumeContainer, +} from "./getters"; + +export function injectItems( + state: StateObject, + settings: PolyDictionary, + options: PolyDictionary, + features: BooleanDictionary, +) { + state.lastTime = -1; + const id = getCurrentId(); + if (id === null) return; + + const items = Object.values(InjectionItemsEnum); + + items.map((item) => + injectIfNotPresent(item, state, settings, options, features), + ); +} + +/** + * Returns true if the element has an injection marker (this should mean the item was injected) + * @param element The element that has the marker, generally on something with a getter function (like, say, getVideo()) + */ +export function checkForInjectionMarker(element: Element | HTMLElement | null) { + return element !== null && element.hasAttribute(INJECTION_MARKER); +} + +/** + * Switch case, checks if the given item was injected or not + * @param item + */ +function injectIfNotPresent( + item: string, + state: StateObject, + settings: PolyDictionary, + options: PolyDictionary, + features: BooleanDictionary, +) { + switch (item) { + case InjectionItemsEnum.ACTION_ELEMENT: + if (!checkForInjectionMarker(getActionElement())) + populateActionElement(state, settings, features); + break; + + case InjectionItemsEnum.PROGRESS_BAR: + if (!checkForInjectionMarker(getProgressBarList())) + modifyProgressBar(features["progressBar"]); + break; + + case InjectionItemsEnum.VOLUME_SLIDER: + if (!checkForInjectionMarker(getVolumeContainer())) + setVolumeSlider( + state, + settings, + features["showVolumeHorizontally"], + features["volumeSlider"], + ); + break; + + case InjectionItemsEnum.INFO: + if (!checkForInjectionMarker(getInfoElement())) setInfo(features); + break; + } +} diff --git a/src/lib/InjectionState.ts b/src/lib/InjectionState.ts deleted file mode 100644 index 55484d7..0000000 --- a/src/lib/InjectionState.ts +++ /dev/null @@ -1,127 +0,0 @@ -// todo: change values out for localised strings -export enum InjectionItemsEnum { - EXISTING_EVENTS = "Events", - ACTION_ELEMENT = "Action Elements", - PROGRESS_BAR = "Progress Bar", - VOLUME_SLIDER = "Volume Slider", - INFO = "Info", -} - -export enum InjectionStateEnum { - FAILED = 0, - NEW, - SUCCESS, -} - -const MAX_NUMBER_OF_ATTEMPTS = 3; - -export class InjectionStateUnit { - name: InjectionItemsEnum; - state: InjectionStateEnum; - callback: () => void; - attempts: number; - - constructor(name: InjectionItemsEnum, callback: () => void) { - this.name = name; - this.callback = callback; - this.state = InjectionStateEnum.NEW; - this.attempts = 0; - } - - logError(err: Error) { - console.group("%cBYS Injection Error", "color: #ff7161;"); - console.log( - `%cError while injecting "${this.name}"\n\nInjection was attempted ${this.attempts} times.\n\nMessage: "${err.message}"`, - "color: #ff7161;", - ); - console.groupEnd(); - } - - inject() { - if (this.attempts > MAX_NUMBER_OF_ATTEMPTS) return; - - let state = InjectionStateEnum.SUCCESS; - this.attempts++; - - //prettier-ignore - try { - this.callback(); - } - catch (err) { - if (this.attempts === MAX_NUMBER_OF_ATTEMPTS) - { - this.logError(err as Error); - } - - state = InjectionStateEnum.FAILED; - } - finally { - this.state = state; - // eslint-disable-next-line no-unsafe-finally - return this.state === InjectionStateEnum.SUCCESS; - } - } -} - -export class InjectionState { - units: InjectionStateUnit[]; - id: number; - injectionSucceeded: boolean; - - constructor(id: number, ...units: InjectionStateUnit[]) { - this.units = units; - this.id = id; - this.injectionSucceeded = false; - } - - injectRemainingItems() { - if (this.injectionSucceeded) return; - - this.injectionSucceeded = true; - - for (const unit of this.units) { - if (unit.state !== InjectionStateEnum.SUCCESS) { - // eslint-disable-next-line prettier/prettier - this.injectionSucceeded = ( unit.inject() && this.injectionSucceeded ) as boolean ; - } - } - } - - /** - * Adds {@link InjectionStateUnit} to the list of injection candidates. - * This is specifically for items we know will fail if injected before the video starts, - * as otherwise we could just pass them into the constructor. - * - * This method will handle duplicate checks too. - * - * @param _unit The injection unit. - */ - addUnit(_unit: InjectionStateUnit) { - if (this.hasUnit(_unit.name)) return; - this.units.push(_unit); - this.injectionSucceeded = false; - } - - hasUnit(unitName: InjectionItemsEnum) { - for (const unit of this.units) { - if (unit.name === unitName) { - return true; - } - } - - return false; - } -} - -/** - * Assumes set items are objects that have an `id` prop - * @returns {@link InjectionState}, or {null} if unfound - */ -export function findInjectionStateInSet(id: number, set: Set) { - //prettier-ignore - for ( const item of set ) - if ( item.id === id ) - return item; - - return null; -} diff --git a/src/lib/InjectionSuccess.ts b/src/lib/InjectionSuccess.ts deleted file mode 100644 index 121f5b8..0000000 --- a/src/lib/InjectionSuccess.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { populateActionElement } from "./ActionElement"; -import { modifyProgressBar } from "./ProgressBar"; -import { setInfo } from "./Info"; -import { setVolumeSlider } from "./VolumeSlider"; -import { BooleanDictionary, PolyDictionary, StateObject } from "./definitions"; -import { getCurrentId } from "./getters"; -import { injectEventsToExistingElements } from "./Events"; -import { - InjectionItemsEnum, - InjectionState, - InjectionStateUnit, - findInjectionStateInSet, -} from "./InjectionState"; - -export function injectItems( - state: StateObject, - settings: PolyDictionary, - options: PolyDictionary, - features: BooleanDictionary, -) { - state.lastTime = -1; - const id = getCurrentId(); - if (id === null) return; - - // eslint-disable-next-line prettier/prettier - let injectionState = findInjectionStateInSet(id, state.injectedItems as Set); - let isNewState = false; - if (injectionState === null) { - // eslint-disable-next-line prettier/prettier - injectionState = createNewInjectionState(id, state, settings, options, features); - isNewState = true; - } - - injectionState.injectRemainingItems(); - - // eslint-disable-next-line prettier/prettier - if (isNewState) { - (state.injectedItems as Set)?.add(injectionState); - } -} - -function createNewInjectionState( - id: number, - state: StateObject, - settings: { [x: string]: string | number | boolean }, - options: PolyDictionary, - features: BooleanDictionary, -) { - // eslint-disable-next-line prettier/prettier - return new InjectionState( - id, - new InjectionStateUnit(InjectionItemsEnum.ACTION_ELEMENT, () => { - populateActionElement(state, settings, features); - }), - new InjectionStateUnit(InjectionItemsEnum.EXISTING_EVENTS, () => { - injectEventsToExistingElements(options); - }), - new InjectionStateUnit(InjectionItemsEnum.PROGRESS_BAR, () => { - modifyProgressBar(features["progressBar"]); - }), - new InjectionStateUnit(InjectionItemsEnum.VOLUME_SLIDER, () => { - setVolumeSlider( - state, - settings, - options["showVolumeHorizontally"] as boolean, - features["volumeSlider"], - ); - }), - ); -} - -export function injectInfoElement( - state: StateObject, - features: BooleanDictionary, -) { - const id = getCurrentId(); - if (id === null) return; - - const injectionState = findInjectionStateInSet( - id, - state.injectedItems as Set, - ); - - if (injectionState === null) return; - - injectionState.addUnit( - new InjectionStateUnit(InjectionItemsEnum.INFO, () => { - setInfo(features); - }), - ); -} diff --git a/src/lib/ProgressBar.ts b/src/lib/ProgressBar.ts index f5894f9..86745fb 100644 --- a/src/lib/ProgressBar.ts +++ b/src/lib/ProgressBar.ts @@ -1,3 +1,4 @@ +import { INJECTION_MARKER } from "./declarations"; import { getOverlayElement, getProgressBarList, getVideo } from "./getters"; import { render } from "./utils"; @@ -7,14 +8,16 @@ export function modifyProgressBar(enabled: boolean) { const overlayElement = getOverlayElement(); const ytShorts = getVideo(); - if (!overlayElement) throw new Error("Overlay element not found"); - if (ytShorts === null) throw new Error("Video not found"); + if (!overlayElement) return; // throw new Error("Overlay element not found"); + if (ytShorts === null) return; // throw new Error("Video not found"); //[id="0"] > div.overlay.style-scope.ytd-reel-video-renderer > ytd-reel-player-overlay-renderer > #overlay const progressBar = getProgressBarList() as HTMLElement; // ? the progressbar itself const pbBackground = progressBar.children[0] as HTMLElement; // ? the grey background of the bar const pbForeground = progressBar.children[1] as HTMLElement; // ? The red part of the progress bar + progressBar.setAttribute(INJECTION_MARKER, ""); // ? for injection checks + const subBox = overlayElement.children[1] as HTMLElement; const tooltip = render( `
`, @@ -52,7 +55,7 @@ function addListeners({ tooltip, }: ListenerProps) { const ytShorts = getVideo(); - if (ytShorts === null) throw new Error(); + if (ytShorts === null) return; // throw new Error(); progressBar.addEventListener("mouseover", () => { pbBackground.classList.add("betterYT-progress-bar-hover"); diff --git a/src/lib/VolumeSlider.ts b/src/lib/VolumeSlider.ts index d5e8a8e..91656d8 100644 --- a/src/lib/VolumeSlider.ts +++ b/src/lib/VolumeSlider.ts @@ -1,5 +1,5 @@ import { saveSettingsToStorage } from "./SaveToStorage"; -import { VOLUME_INCREMENT_AMOUNT } from "./declarations"; +import { INJECTION_MARKER, VOLUME_INCREMENT_AMOUNT } from "./declarations"; import { PolyDictionary, StateObject } from "./definitions"; import { getCurrentId, @@ -62,9 +62,10 @@ export function setVolumeSlider( if (!enabled) return; const id = getCurrentId(); - if (id === null) throw new Error("ID not found"); + if (id === null) return; // throw new Error("ID not found"); const volumeContainer = getVolumeContainer(); + volumeContainer.setAttribute(INJECTION_MARKER, ""); // ? for injections // const slider = document.createElement("input") const slider = render(` (), - actualVolume: null, skippedId: null, @@ -299,3 +296,5 @@ export const DISABLED_BIND_STRING = local("disabled"); export const VOLUME_INCREMENT_AMOUNT = 0.025; export const CYCLABLE_PLAYBACK_RATES = [0.5, 1, 1.5, 2]; + +export const INJECTION_MARKER = "bys-injection-marker"; diff --git a/src/lib/definitions.ts b/src/lib/definitions.ts index 1228917..0b80445 100644 --- a/src/lib/definitions.ts +++ b/src/lib/definitions.ts @@ -1,5 +1,3 @@ -import { InjectionState } from "./InjectionState"; - export type NumberDictionary = { [key: string]: number; }; @@ -25,7 +23,7 @@ export type IconDictionary = { }; export interface StateObject { - [key: string]: string | boolean | number | Set | null; + [key: string]: string | boolean | number | null; } export interface OptionsDictionary { @@ -50,3 +48,10 @@ export enum ChangedObjectStateEnum { SETTINGS, FEATURES, } + +export enum InjectionItemsEnum { + ACTION_ELEMENT = "Action Elements", + PROGRESS_BAR = "Progress Bar", + VOLUME_SLIDER = "Volume Slider", + INFO = "Info", +} diff --git a/src/lib/getters.ts b/src/lib/getters.ts index 41cf558..38400ff 100644 --- a/src/lib/getters.ts +++ b/src/lib/getters.ts @@ -92,6 +92,10 @@ export function getBackButton() { ) as HTMLElement; } +export function getInfoElement() { + return document.getElementById(`bys-ytViews${getCurrentId()}`) as HTMLElement; +} + export function getShortsContainer() { return document.getElementById("page-manager"); } diff --git a/src/main.ts b/src/main.ts index 53dba48..1afbf29 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,13 +1,15 @@ -import { getCurrentId, getVideo } from "./lib/getters"; -import { handleSkipShortsWithLowLikes } from "./lib/SkipShortsWithLowLikes"; -import { injectInfoElement, injectItems } from "./lib/InjectionSuccess"; -import { hasVideoEnded, isVideoPlaying } from "./lib/VideoState"; -import { handleAutoplay, handleEnableAutoplay } from "./lib/Autoplay"; +import { features, options, settings, state } from "./content"; import { handleAutomaticallyOpenComments } from "./lib/AutomaticallyOpenComments"; -import { handleProgressBarNotAppearing } from "./lib/ProgressBar"; +import { handleAutoplay, handleEnableAutoplay } from "./lib/Autoplay"; +import { injectEvents } from "./lib/Events"; import { handleHideShortsOverlay } from "./lib/HideShortsOverlay"; +import { updateInfo } from "./lib/Info"; +import { injectItems } from "./lib/InjectionHandling"; import { setTimer } from "./lib/PlaybackRate"; -import { state, features, options, settings } from "./content"; +import { handleProgressBarNotAppearing } from "./lib/ProgressBar"; +import { handleSkipShortsWithLowLikes } from "./lib/SkipShortsWithLowLikes"; +import { hasVideoEnded, isVideoPlaying } from "./lib/VideoState"; +import { getCurrentId, getVideo } from "./lib/getters"; export function main() { if (window.location.toString().indexOf("youtube.com/shorts/") < 0) return; @@ -25,7 +27,7 @@ export function main() { if (isVideoPlaying()) { handleSkipShortsWithLowLikes(state, options); handleAutomaticallyOpenComments(state, options); // dev note: the implementation of this feature is a good starting point to figure out how to format your own - injectInfoElement(state, features); + updateInfo(features); } if (hasVideoEnded()) { handleAutoplay(state, settings, features["autoplay"]); @@ -33,6 +35,7 @@ export function main() { setTimer(state, features["timer"]); injectItems(state, settings, options, features); + injectEvents(options); handleProgressBarNotAppearing(); handleEnableAutoplay(); handleHideShortsOverlay(options);