Skip to content

Commit

Permalink
Merge pull request #53 from VampireChicken12/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
VampireChicken12 authored Oct 9, 2023
2 parents 5890c88 + 4a8d66c commit 1f91238
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 15 deletions.
1 change: 1 addition & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add i18n support
9 changes: 9 additions & 0 deletions src/components/Settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,15 @@ export default function Settings({
onChange={setCheckboxOption("enable_video_history")}
/>
</div>
<div className="mx-2 mb-1" title="Shows the remaining time of the video you're watching">
<Checkbox
id="enable_remaining_time"
title="Shows the remaining time of the video you're watching"
label="Enable remaining time"
checked={settings.enable_remaining_time.toString() === "true"}
onChange={setCheckboxOption("enable_remaining_time")}
/>
</div>
</fieldset>
<fieldset className="mx-1">
<legend className="mb-1 text-lg sm:text-xl md:text-2xl">Scroll wheel volume control settings</legend>
Expand Down
1 change: 0 additions & 1 deletion src/features/maximizePlayerButton/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ export async function addMaximizePlayerButton(): Promise<void> {
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);
Expand Down
67 changes: 67 additions & 0 deletions src/features/remainingTime/index.ts
Original file line number Diff line number Diff line change
@@ -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");
}
43 changes: 43 additions & 0 deletions src/features/remainingTime/utils.ts
Original file line number Diff line number Diff line change
@@ -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);
}
1 change: 0 additions & 1 deletion src/features/scrollWheelVolumeControl/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
14 changes: 13 additions & 1 deletion src/pages/content/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -74,6 +74,7 @@ window.onload = function () {
adjustVolumeOnScrollWheel();
setupVideoHistory();
promptUserToResumeVideo();
setupRemainingTime();
};
document.addEventListener("yt-player-updated", enableFeatures);
/**
Expand Down Expand Up @@ -175,6 +176,17 @@ window.onload = function () {
}
break;
}
case "remainingTimeChange": {
const {
data: { remainingTimeEnabled }
} = message;
if (remainingTimeEnabled) {
setupRemainingTime();
} else {
removeRemainingTimeDisplay();
}
break;
}
default: {
return;
}
Expand Down
5 changes: 5 additions & 0 deletions src/pages/inject/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
13 changes: 9 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -55,7 +56,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";
Expand Down Expand Up @@ -90,10 +91,11 @@ 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 }>;
remainingTimeChange: DataResponseMessage<"remainingTimeChange", { remainingTimeEnabled: boolean }>;
};
export type FilterMessagesBySource<T extends Messages, S extends MessageSource> = {
[K in keyof T]: Extract<T[K], { source: S }>;
Expand All @@ -119,10 +121,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<T> = {
type TypeToZod<T> = {
[K in keyof T]: T[K] extends string | number | boolean | null | undefined
? undefined extends T[K]
? z.ZodOptional<z.ZodType<Exclude<T[K], undefined>>>
: z.ZodType<T[K]>
: z.ZodObject<TypeToZod<T[K]>>;
};
export type ConfigurationToZodSchema<T> = z.ZodObject<{
[K in keyof T]: T[K] extends object ? z.ZodObject<TypeToZod<T[K]>> : z.ZodType<T[K]>;
}>;
3 changes: 2 additions & 1 deletion src/utils/EventManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type FeatureName = "videoHistory" | "screenshotButton" | "maximizePlayerButton" | "scrollWheelVolumeControl";
export type FeatureName = "videoHistory" | "screenshotButton" | "maximizePlayerButton" | "scrollWheelVolumeControl" | "remainingTime";
type EventCallback<K extends keyof HTMLElementEventMap> = (event: HTMLElementEventMap[K]) => void;

export interface EventListenerInfo<K extends keyof ElementEventMap> {
Expand Down Expand Up @@ -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
Expand Down
14 changes: 7 additions & 7 deletions src/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import z from "zod";
import type { TypeToZod, configuration } from "../types";
import type { ConfigurationToZodSchema, configuration } from "../types";
import {
screenshotFormat,
screenshotType,
Expand All @@ -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
Expand All @@ -35,7 +36,7 @@ export const defaultConfiguration = {
player_speed: 1
} satisfies configuration;

export const configurationSchemaProperties = {
export const configurationSchema: ConfigurationToZodSchema<configuration> = z.object({
enable_scroll_wheel_volume_control: z.boolean(),
enable_remember_last_volume: z.boolean(),
enable_automatically_set_quality: z.boolean(),
Expand All @@ -44,6 +45,7 @@ export const configurationSchemaProperties = {
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),
Expand All @@ -55,8 +57,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<configuration>;
export const configurationSchema = z.object(configurationSchemaProperties);
player_speed: z.number().min(0.25).max(4.0).step(0.25),
remembered_volume: z.number().optional()
});

0 comments on commit 1f91238

Please sign in to comment.