Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev #53

Merged
merged 6 commits into from
Oct 9, 2023
Merged

Dev #53

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
});
Loading