From 12b02899413f208187348b916d84ef60bfda46df Mon Sep 17 00:00:00 2001 From: VampireChicken12 Date: Mon, 9 Oct 2023 05:54:18 -0400 Subject: [PATCH 1/6] chore: remove comment I forgot to remove --- src/features/scrollWheelVolumeControl/utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/features/scrollWheelVolumeControl/utils.ts b/src/features/scrollWheelVolumeControl/utils.ts index 42048875..112c185f 100644 --- a/src/features/scrollWheelVolumeControl/utils.ts +++ b/src/features/scrollWheelVolumeControl/utils.ts @@ -258,7 +258,6 @@ export function drawVolumeDisplay({ const bottomRect = bottomElement?.getBoundingClientRect(); const paddingTop = topRect ? (isShortsPage() ? topRect.top / 2 : 0) : 0; const paddingBottom = bottomRect ? Math.round((bottomRect.bottom - bottomRect.top) / (isShortsPage() ? 1.79 : 1)) : 0; - // TODO: dynamically add padding if the volume display is under the bottom or top element switch (displayPosition) { case "top_left": case "top_right": From cedb9d3506a41d84b38ebba9d16ef42c13c71958 Mon Sep 17 00:00:00 2001 From: VampireChicken12 Date: Mon, 9 Oct 2023 11:24:46 -0400 Subject: [PATCH 2/6] fix: same event listener being added twice --- src/features/maximizePlayerButton/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/features/maximizePlayerButton/index.ts b/src/features/maximizePlayerButton/index.ts index 9624791c..e223632d 100644 --- a/src/features/maximizePlayerButton/index.ts +++ b/src/features/maximizePlayerButton/index.ts @@ -54,7 +54,6 @@ export async function addMaximizePlayerButton(): Promise { tooltip.textContent = title ?? "Maximize Player"; function mouseLeaveListener() { tooltip.remove(); - eventManager.addEventListener(maximizePlayerButton, "mouseleave", mouseLeaveListener, "maximizePlayerButton"); } eventManager.addEventListener(maximizePlayerButton, "mouseleave", mouseLeaveListener, "maximizePlayerButton"); document.body.appendChild(tooltip); From 8ecda02e01816980971ef83fb24f6835034fe708 Mon Sep 17 00:00:00 2001 From: VampireChicken12 Date: Mon, 9 Oct 2023 11:31:00 -0400 Subject: [PATCH 3/6] refactor: improve configuration zod schema --- src/types.ts | 11 +++++++---- src/utils/constants.ts | 12 +++++------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/types.ts b/src/types.ts index 6ac69c10..f58b1f4b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -27,7 +27,7 @@ export const youtubePlayerQualityLevel = [ export type YoutubePlayerQualityLevel = (typeof youtubePlayerQualityLevel)[number]; export const youtubePlayerSpeedRateExtended = [2.25, 2.5, 2.75, 3, 3.25, 3.75, 4] as const; export const youtubePlayerSpeedRate = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, ...youtubePlayerSpeedRateExtended] as const; -export type YouTubePlayerSpeedRate = (typeof youtubePlayerSpeedRate)[number] | (typeof youtubePlayerSpeedRateExtended)[number]; + export const screenshotType = ["file", "clipboard"] as const; export type ScreenshotType = (typeof screenshotType)[number]; export const screenshotFormat = ["png", "jpg", "webp"] as const; @@ -55,7 +55,7 @@ export type configuration = { volume_boost_amount: number; remembered_volume?: number; player_quality: YoutubePlayerQualityLevel; - player_speed: YouTubePlayerSpeedRate; + player_speed: number; }; export type configurationKeys = keyof configuration; export type VideoHistoryStatus = "watched" | "watching"; @@ -90,7 +90,7 @@ export type ContentSendOnlyMessageMappings = { }; export type ExtensionSendOnlyMessageMappings = { volumeBoostChange: DataResponseMessage<"volumeBoostChange", { volumeBoostEnabled: boolean; volumeBoostAmount?: number }>; - playerSpeedChange: DataResponseMessage<"playerSpeedChange", { playerSpeed?: YouTubePlayerSpeedRate; enableForcedPlaybackSpeed: boolean }>; + playerSpeedChange: DataResponseMessage<"playerSpeedChange", { playerSpeed?: number; enableForcedPlaybackSpeed: boolean }>; screenshotButtonChange: DataResponseMessage<"screenshotButtonChange", { screenshotButtonEnabled: boolean }>; maximizePlayerButtonChange: DataResponseMessage<"maximizePlayerButtonChange", { maximizePlayerButtonEnabled: boolean }>; videoHistoryChange: DataResponseMessage<"videoHistoryChange", { videoHistoryEnabled: boolean }>; @@ -119,10 +119,13 @@ export type YouTubePlayerDiv = YouTubePlayer & HTMLDivElement; export type Selector = string; export type StorageChanges = { [key: string]: chrome.storage.StorageChange }; // Taken from https://github.com/colinhacks/zod/issues/53#issuecomment-1681090113 -export type TypeToZod = { +type TypeToZod = { [K in keyof T]: T[K] extends string | number | boolean | null | undefined ? undefined extends T[K] ? z.ZodOptional>> : z.ZodType : z.ZodObject>; }; +export type ConfigurationToZodSchema = z.ZodObject<{ + [K in keyof T]: T[K] extends object ? z.ZodObject> : z.ZodType; +}>; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index f55578a4..3a1fc640 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,5 +1,5 @@ import z from "zod"; -import type { TypeToZod, configuration } from "../types"; +import type { ConfigurationToZodSchema, configuration } from "../types"; import { screenshotFormat, screenshotType, @@ -35,7 +35,7 @@ export const defaultConfiguration = { player_speed: 1 } satisfies configuration; -export const configurationSchemaProperties = { +export const configurationSchema: ConfigurationToZodSchema = z.object({ enable_scroll_wheel_volume_control: z.boolean(), enable_remember_last_volume: z.boolean(), enable_automatically_set_quality: z.boolean(), @@ -55,8 +55,6 @@ export const configurationSchemaProperties = { volume_adjustment_steps: z.number().min(1).max(100), volume_boost_amount: z.number(), player_quality: z.enum(youtubePlayerQualityLevel), - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore Figure out this error - player_speed: z.number().min(0.25).max(4.0).step(0.25) -} satisfies TypeToZod; -export const configurationSchema = z.object(configurationSchemaProperties); + player_speed: z.number().min(0.25).max(4.0).step(0.25), + remembered_volume: z.number().optional() +}); From 1578925c0bfdd249a9c1cda22aed375d5b0f801c Mon Sep 17 00:00:00 2001 From: VampireChicken12 Date: Mon, 9 Oct 2023 11:35:29 -0400 Subject: [PATCH 4/6] refactor: Remove event listener before adding --- src/utils/EventManager.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/EventManager.ts b/src/utils/EventManager.ts index a0da060a..ff7aa79a 100644 --- a/src/utils/EventManager.ts +++ b/src/utils/EventManager.ts @@ -37,6 +37,7 @@ export const eventManager: EventManager = { // Adds an event listener for the given target, eventName, and featureName addEventListener: function (target, eventName, callback, featureName) { + this.removeEventListener(target, eventName, featureName); // Get the map of target listeners for the given featureName const targetListeners = this.listeners.get(featureName) || new Map(); // Store the event listener info object in the map From 8eb062bae1e859b86ca01aa6dfc4cd1571bc137a Mon Sep 17 00:00:00 2001 From: VampireChicken12 Date: Mon, 9 Oct 2023 11:36:19 -0400 Subject: [PATCH 5/6] feat: Remaining time feature --- src/components/Settings/Settings.tsx | 9 ++++ src/features/remainingTime/index.ts | 67 ++++++++++++++++++++++++++++ src/features/remainingTime/utils.ts | 43 ++++++++++++++++++ src/pages/content/index.tsx | 14 +++++- src/pages/inject/index.tsx | 5 +++ src/types.ts | 2 + src/utils/EventManager.ts | 2 +- src/utils/constants.ts | 2 + 8 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 src/features/remainingTime/index.ts create mode 100644 src/features/remainingTime/utils.ts diff --git a/src/components/Settings/Settings.tsx b/src/components/Settings/Settings.tsx index 13de9795..7000de51 100644 --- a/src/components/Settings/Settings.tsx +++ b/src/components/Settings/Settings.tsx @@ -327,6 +327,15 @@ export default function Settings({ onChange={setCheckboxOption("enable_video_history")} /> +
+ +
Scroll wheel volume control settings diff --git a/src/features/remainingTime/index.ts b/src/features/remainingTime/index.ts new file mode 100644 index 00000000..472bfbbf --- /dev/null +++ b/src/features/remainingTime/index.ts @@ -0,0 +1,67 @@ +import { isShortsPage, isWatchPage, waitForSpecificMessage } from "@/src/utils/utilities"; +import { YouTubePlayerDiv } from "@/src/types"; +import { calculateRemainingTime } from "./utils"; +import eventManager from "@/src/utils/EventManager"; +async function playerTimeUpdateListener() { + // Get the player element + const playerContainer = isWatchPage() + ? (document.querySelector("div#movie_player") as YouTubePlayerDiv | null) + : isShortsPage() + ? (document.querySelector("div#shorts-player") as YouTubePlayerDiv | null) + : null; + + // If player element is not available, return + if (!playerContainer) return; + + // Get the video element + const videoElement = playerContainer.childNodes[0]?.childNodes[0] as HTMLVideoElement | null; + + // If video element is not available, return + if (!videoElement) return; + // Get the remaining time element + const remainingTimeElement = document.querySelector("span#ytp-time-remaining"); + if (!remainingTimeElement) return; + const remainingTime = await calculateRemainingTime({ videoElement, playerContainer }); + remainingTimeElement.textContent = remainingTime; +} +export async function setupRemainingTime() { + // Wait for the "options" message from the content script + const optionsData = await waitForSpecificMessage("options", "request_data", "content"); + if (!optionsData) return; + const { + data: { options } + } = optionsData; + // Extract the necessary properties from the options object + const { enable_remaining_time } = options; + // If remaining time option is disabled, return + if (!enable_remaining_time) return; + const timeDisplay = document.querySelector(".ytp-time-display > span:nth-of-type(2)"); + if (!timeDisplay) return; + // Get the player element + const playerContainer = isWatchPage() + ? (document.querySelector("div#movie_player") as YouTubePlayerDiv | null) + : isShortsPage() + ? (document.querySelector("div#shorts-player") as YouTubePlayerDiv | null) + : null; + // If player element is not available, return + if (!playerContainer) return; + // Get the video element + const videoElement = playerContainer.childNodes[0]?.childNodes[0] as HTMLVideoElement | null; + // If video element is not available, return + if (!videoElement) return; + const remainingTime = await calculateRemainingTime({ videoElement, playerContainer }); + const remainingTimeElementExists = document.querySelector("span#ytp-time-remaining") !== null; + const remainingTimeElement = document.querySelector("span#ytp-time-remaining") ?? document.createElement("span"); + if (!remainingTimeElementExists) { + remainingTimeElement.id = "ytp-time-remaining"; + remainingTimeElement.textContent = remainingTime; + timeDisplay.insertAdjacentElement("beforeend", remainingTimeElement); + } + eventManager.addEventListener(videoElement, "timeupdate", playerTimeUpdateListener, "remainingTime"); +} +export function removeRemainingTimeDisplay() { + const remainingTimeElement = document.querySelector("span#ytp-time-remaining"); + if (!remainingTimeElement) return; + remainingTimeElement.remove(); + eventManager.removeEventListeners("remainingTime"); +} diff --git a/src/features/remainingTime/utils.ts b/src/features/remainingTime/utils.ts new file mode 100644 index 00000000..ed700b25 --- /dev/null +++ b/src/features/remainingTime/utils.ts @@ -0,0 +1,43 @@ +import { YouTubePlayerDiv } from "@/src/types"; +function formatTime(timeInSeconds: number) { + timeInSeconds = Math.round(timeInSeconds); + const units: number[] = [ + Math.floor(timeInSeconds / (3600 * 24)), + Math.floor((timeInSeconds % (3600 * 24)) / 3600), + Math.floor((timeInSeconds % 3600) / 60), + Math.floor(timeInSeconds % 60) + ]; + + const formattedUnits: string[] = units.reduce((acc: string[], unit) => { + if (acc.length > 0) { + acc.push(unit.toString().padStart(2, "0")); + } else { + if (unit > 0) { + acc.push(unit.toString()); + } + } + + return acc; + }, []); + return ` (-${formattedUnits.length > 0 ? formattedUnits.join(":") : "0"})`; +} +export async function calculateRemainingTime({ + videoElement, + playerContainer +}: { + videoElement: HTMLVideoElement; + playerContainer: YouTubePlayerDiv; +}) { + // Get the player speed (playback rate) + const { playbackRate } = videoElement; + + // Get the current time and duration of the video + const currentTime = await playerContainer.getCurrentTime(); + const duration = await playerContainer.getDuration(); + + // Calculate the remaining time in seconds + const remainingTimeInSeconds = (duration - currentTime) / playbackRate; + + // Format the remaining time + return formatTime(remainingTimeInSeconds); +} diff --git a/src/pages/content/index.tsx b/src/pages/content/index.tsx index 062af692..981c21d7 100644 --- a/src/pages/content/index.tsx +++ b/src/pages/content/index.tsx @@ -10,7 +10,7 @@ import { addScreenshotButton, removeScreenshotButton } from "@/src/features/scre import adjustVolumeOnScrollWheel from "@/src/features/scrollWheelVolumeControl"; import { setupVideoHistory, promptUserToResumeVideo } from "@/src/features/videoHistory"; import volumeBoost from "@/src/features/volumeBoost"; -// TODO: Add remaining time feature +import { removeRemainingTimeDisplay, setupRemainingTime } from "@/src/features/remainingTime"; // TODO: Add always show progressbar feature // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -74,6 +74,7 @@ window.onload = function () { adjustVolumeOnScrollWheel(); setupVideoHistory(); promptUserToResumeVideo(); + setupRemainingTime(); }; document.addEventListener("yt-player-updated", enableFeatures); /** @@ -175,6 +176,17 @@ window.onload = function () { } break; } + case "remainingTimeChange": { + const { + data: { remainingTimeEnabled } + } = message; + if (remainingTimeEnabled) { + setupRemainingTime(); + } else { + removeRemainingTimeDisplay(); + } + break; + } default: { return; } diff --git a/src/pages/inject/index.tsx b/src/pages/inject/index.tsx index 232294ec..8868ce8d 100644 --- a/src/pages/inject/index.tsx +++ b/src/pages/inject/index.tsx @@ -177,6 +177,11 @@ const storageChangeHandler = async (changes: StorageChanges, areaName: string) = sendExtensionOnlyMessage("videoHistoryChange", { videoHistoryEnabled: castedChanges.enable_video_history.newValue }); + }, + enable_remaining_time: () => { + sendExtensionOnlyMessage("remainingTimeChange", { + remainingTimeEnabled: castedChanges.enable_remaining_time.newValue + }); } }; Object.entries( diff --git a/src/types.ts b/src/types.ts index f58b1f4b..9e2b4d25 100644 --- a/src/types.ts +++ b/src/types.ts @@ -43,6 +43,7 @@ export type configuration = { enable_screenshot_button: boolean; enable_maximize_player_button: boolean; enable_video_history: boolean; + enable_remaining_time: boolean; screenshot_save_as: ScreenshotType; screenshot_format: ScreenshotFormat; osd_display_color: OnScreenDisplayColor; @@ -94,6 +95,7 @@ export type ExtensionSendOnlyMessageMappings = { screenshotButtonChange: DataResponseMessage<"screenshotButtonChange", { screenshotButtonEnabled: boolean }>; maximizePlayerButtonChange: DataResponseMessage<"maximizePlayerButtonChange", { maximizePlayerButtonEnabled: boolean }>; videoHistoryChange: DataResponseMessage<"videoHistoryChange", { videoHistoryEnabled: boolean }>; + remainingTimeChange: DataResponseMessage<"remainingTimeChange", { remainingTimeEnabled: boolean }>; }; export type FilterMessagesBySource = { [K in keyof T]: Extract; diff --git a/src/utils/EventManager.ts b/src/utils/EventManager.ts index ff7aa79a..e9185abc 100644 --- a/src/utils/EventManager.ts +++ b/src/utils/EventManager.ts @@ -1,4 +1,4 @@ -export type FeatureName = "videoHistory" | "screenshotButton" | "maximizePlayerButton" | "scrollWheelVolumeControl"; +export type FeatureName = "videoHistory" | "screenshotButton" | "maximizePlayerButton" | "scrollWheelVolumeControl" | "remainingTime"; type EventCallback = (event: HTMLElementEventMap[K]) => void; export interface EventListenerInfo { diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 3a1fc640..0e6c6a4a 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -20,6 +20,7 @@ export const defaultConfiguration = { enable_screenshot_button: false, enable_maximize_player_button: false, enable_video_history: false, + enable_remaining_time: false, screenshot_save_as: "file", screenshot_format: "png", // Images @@ -44,6 +45,7 @@ export const configurationSchema: ConfigurationToZodSchema = z.ob enable_screenshot_button: z.boolean(), enable_maximize_player_button: z.boolean(), enable_video_history: z.boolean(), + enable_remaining_time: z.boolean(), screenshot_save_as: z.enum(screenshotType), screenshot_format: z.enum(screenshotFormat), osd_display_color: z.enum(onScreenDisplayColor), From 4a8d66c94265e7b61158f700ed060baa43f5a671 Mon Sep 17 00:00:00 2001 From: VampireChicken12 Date: Mon, 9 Oct 2023 11:37:21 -0400 Subject: [PATCH 6/6] chore: Add TODO.md --- TODO.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..1f468df4 --- /dev/null +++ b/TODO.md @@ -0,0 +1 @@ +- Add i18n support