Skip to content

Commit

Permalink
feat: Playlist length bar #542
Browse files Browse the repository at this point in the history
  • Loading branch information
VampireChicken12 committed Jul 29, 2024
1 parent e166355 commit 8ed563f
Show file tree
Hide file tree
Showing 22 changed files with 735 additions and 6 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions public/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@
"decreaseLimit": "Can't decrease further ({{SPEED}})",
"increaseLimit": "Can't increase further ({{SPEED}})"
},
"playlistLength": {
"title": "Total length may not be accurate if some videos are hidden or if you haven't loaded enough videos to get the full length."
},
"screenshotButton": {
"button": {
"label": "Screenshot"
Expand Down Expand Up @@ -398,6 +401,19 @@
},
"title": "Playback speed settings"
},
"playlistLength": {
"enable": {
"label": "Display playlist length information",
"title": "Shows the total length of the playlist, how much has been watched, and how much remains."
},
"title": "Playlist length settings",
"wayToGetLength": {
"select": {
"label": "Method to get playlist length",
"title": "The way to get playlist length information (API method will fallback to HTML if an error occurs)"
}
}
},
"screenshotButton": {
"enable": {
"label": "Screenshot button",
Expand Down Expand Up @@ -502,6 +518,14 @@
},
"title": "Volume boost settings"
},
"youtubeDataApiV3Key": {
"getApiKeyLinkText": "You can get one from here",
"input": {
"label": "API Key",
"title": "Enter your Youtube Data API V3 key."
},
"title": "YouTube API V3 Key"
},
"youtubeDeepDark": {
"author": "Author",
"co-authors": "Co-authors",
Expand Down
21 changes: 21 additions & 0 deletions public/locales/en-US.json.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ interface EnUS {
decreaseLimit: "Can't decrease further ({{SPEED}})";
increaseLimit: "Can't increase further ({{SPEED}})";
};
playlistLength: {
title: "Total length may not be accurate if some videos are hidden or if you haven't loaded enough videos to get the full length.";
};
screenshotButton: {
button: { label: "Screenshot" };
copiedToClipboard: "Screenshot copied to clipboard";
Expand Down Expand Up @@ -299,6 +302,19 @@ interface EnUS {
select: { label: "Player speed"; title: "The speed to set the video to" };
title: "Playback speed settings";
};
playlistLength: {
enable: {
label: "Display playlist length information";
title: "Shows the total length of the playlist, how much has been watched, and how much remains.";
};
title: "Playlist length settings";
wayToGetLength: {
select: {
label: "Method to get playlist length";
title: "The way to get playlist length information (API method will fallback to HTML if an error occurs)";
};
};
};
screenshotButton: {
enable: {
label: "Screenshot button";
Expand Down Expand Up @@ -371,6 +387,11 @@ interface EnUS {
};
title: "Volume boost settings";
};
youtubeDataApiV3Key: {
getApiKeyLinkText: "You can get one from here";
input: { label: "API Key"; title: "Enter your Youtube Data API V3 key." };
title: "YouTube API V3 Key";
};
youtubeDeepDark: {
author: "Author";
"co-authors": "Co-authors";
Expand Down
59 changes: 59 additions & 0 deletions src/components/Inputs/TextInput/TextInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { Nullable } from "@/src/types";
import type { ChangeEvent } from "react";

import { cn, debounce } from "@/src/utils/utilities";
import React, { useCallback, useRef, useState } from "react";
import { IoMdEye, IoMdEyeOff } from "react-icons/io";

export type TextInputProps = {
className?: string;
id: string;
input_type: "password" | "text";
label: string;
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
title: string;
value: string;
};

const TextInput: React.FC<TextInputProps> = ({ className, id, input_type, label, onChange, title, value }) => {
const [showPassword, setShowPassword] = useState(false);
const debouncedOnChange = useCallback(debounce(onChange, 300), []);
const inputRef = useRef<Nullable<HTMLInputElement>>(null);
const handleInputWrapperClick = () => {
inputRef.current?.focus();
};
// FIXME: cursor not being restored to position it was in when value is saved
return (
<div aria-valuetext={value} className={cn("relative flex flex-row items-center justify-between gap-4", className)} id={id} title={title}>
<label htmlFor={id}>{label}</label>
<div
className="flex w-40 items-center justify-between rounded-md border border-gray-300 bg-white p-2 text-black dark:multi-['border-gray-700;bg-[#23272a];text-white']"
onClick={handleInputWrapperClick}
>
{input_type === "password" && (
<button
className="text-black hover:text-black dark:text-white dark:hover:text-white"
onClick={() => setShowPassword(!showPassword)}
type="button"
>
{showPassword ?
<IoMdEye size={18} />
: <IoMdEyeOff size={18} />}
</button>
)}
<input
className="!m-0 h-fit w-[118px] bg-transparent !p-0 !text-sm focus:outline-none"
id={id}
onChange={({ target: { value } }) => {
debouncedOnChange({ currentTarget: { value } });
}}
ref={inputRef}
type={showPassword && input_type === "password" ? "text" : input_type}
value={value}
/>
</div>
</div>
);
};

export default TextInput;
3 changes: 3 additions & 0 deletions src/components/Inputs/TextInput/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import TextInput from "./TextInput";

export { TextInput };
3 changes: 2 additions & 1 deletion src/components/Inputs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ import { ColorPicker } from "./ColorPicker";
import { NumberInput } from "./Number";
import { Select } from "./Select";
import { Slider } from "./Slider";
export { CSSEditor, Checkbox, ColorPicker, NumberInput, Select, type SelectOption, Slider };
import { TextInput } from "./TextInput";
export { CSSEditor, Checkbox, ColorPicker, NumberInput, Select, type SelectOption, Slider, TextInput };
49 changes: 49 additions & 0 deletions src/components/Settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,16 @@ export default function Settings() {
value
};
});
const playlistLengthGetMethodOptions: SelectOption<"playlist_length_get_method">[] = [
{
label: "API",
value: "api"
},
{
label: "HTML",
value: "html"
}
];
const settingsImportChange: ChangeEventHandler<HTMLInputElement> = (event): void => {
void (async () => {
const { target } = event;
Expand Down Expand Up @@ -1191,6 +1201,45 @@ export default function Settings() {
value={settings.custom_css_code}
/>
</SettingSection>
<SettingSection title={t("settings.sections.playlistLength.title")}>
<SettingTitle />
<Setting
checked={settings.enable_playlist_length?.toString() === "true"}
id="enable_playlist_length"
label={t("settings.sections.playlistLength.enable.label")}
onChange={setCheckboxOption("enable_playlist_length")}
title={t("settings.sections.playlistLength.enable.title")}
type="checkbox"
/>
<Setting
disabled={settings.enable_playlist_length?.toString() !== "true"}
id="playlist_length_get_method"
label={t("settings.sections.playlistLength.wayToGetLength.select.label")}
onChange={setValueOption("playlist_length_get_method")}
options={playlistLengthGetMethodOptions}
selectedOption={getSelectedOption("playlist_length_get_method")}
title={t("settings.sections.playlistLength.wayToGetLength.select.title")}
type="select"
/>
</SettingSection>
<SettingSection title={t("settings.sections.youtubeDataApiV3Key.title")}>
<SettingTitle />
<Setting
id="youtube_data_api_v3_key"
input_type="password"
label={t("settings.sections.youtubeDataApiV3Key.input.label")}
onChange={setValueOption("youtube_data_api_v3_key")}
title={t("settings.sections.youtubeDataApiV3Key.input.title")}
type="text-input"
value={settings.youtube_data_api_v3_key}
/>
<fieldset className={cn("flex flex-row gap-1")}>
<Link className="ml-2" href="https://developers.google.com/youtube/v3/getting-started" target="_blank">
{t("settings.sections.youtubeDataApiV3Key.getApiKeyLinkText")}
</Link>
</fieldset>
</SettingSection>

<div className="sticky bottom-0 left-0 z-10 flex justify-between gap-1 bg-[#f5f5f5] p-2 dark:bg-[#181a1b]">
<input
className="danger p-2 text-sm sm:text-base md:text-lg dark:hover:bg-[rgba(24,26,27,0.5)]"
Expand Down
8 changes: 7 additions & 1 deletion src/components/Settings/components/Setting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import type { ColorPickerProps } from "../../Inputs/ColorPicker/ColorPicker";
import type { NumberInputProps } from "../../Inputs/Number/Number";
import type { SelectProps } from "../../Inputs/Select/Select";
import type { SliderProps } from "../../Inputs/Slider/Slider";
import type { TextInputProps } from "../../Inputs/TextInput/TextInput";

import { CSSEditor, Checkbox, ColorPicker, NumberInput, Select, Slider } from "../../Inputs";
import { CSSEditor, Checkbox, ColorPicker, NumberInput, Select, Slider, TextInput } from "../../Inputs";

type SettingInputProps<ID extends configurationId> = {
id: ID;
Expand All @@ -23,6 +24,7 @@ type SettingInputProps<ID extends configurationId> = {
| ({ type: "number" } & NumberInputProps)
| ({ type: "select" } & SelectProps<ID>)
| ({ type: "slider" } & SliderProps)
| ({ type: "text-input" } & TextInputProps)
);
function SettingInput<ID extends configurationId>(settingProps: SettingInputProps<ID>) {
const { type } = settingProps;
Expand Down Expand Up @@ -75,6 +77,10 @@ function SettingInput<ID extends configurationId>(settingProps: SettingInputProp
const { className, disabled, id, label, onChange, title, value } = settingProps;
return <ColorPicker className={className} disabled={disabled} id={id} label={label} onChange={onChange} title={title} value={value} />;
}
case "text-input": {
const { className, id, input_type, label, onChange, title, value } = settingProps;
return <TextInput className={className} id={id} input_type={input_type} label={label} onChange={onChange} title={title} value={value} />;
}
}
}
export default function Setting<ID extends configurationId>(settingProps: SettingInputProps<ID>) {
Expand Down
43 changes: 43 additions & 0 deletions src/features/playlistLength/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Nullable } from "@/src/types";

import eventManager from "@/src/utils/EventManager";
import { YouTube_Enhancer_Public_Youtube_Data_API_V3_Key } from "@/src/utils/constants";
import { isWatchPage, waitForAllElements, waitForSpecificMessage } from "@/src/utils/utilities";

import { headerSelector, initializePlaylistLength, playlistItemsSelector } from "./utils";
let documentObserver: Nullable<MutationObserver> = null;
export async function enablePlaylistLength() {
const IsWatchPage = isWatchPage();
const {
data: {
options: { enable_playlist_length, playlist_length_get_method: playlistLengthGetMethod, youtube_data_api_v3_key }
}
} = await waitForSpecificMessage("options", "request_data", "content");
if (!enable_playlist_length) return;
const urlContainsListParameter = window.location.href.includes("list=");
if (!urlContainsListParameter) return;
await waitForAllElements([headerSelector(), playlistItemsSelector()]);
const apiKey = youtube_data_api_v3_key === "" ? YouTube_Enhancer_Public_Youtube_Data_API_V3_Key : youtube_data_api_v3_key;
const pageType = IsWatchPage ? "watch" : "playlist";
try {
documentObserver = await initializePlaylistLength({
apiKey,
pageType,
playlistLengthGetMethod
});
} catch (error) {
documentObserver?.disconnect();
documentObserver = null;
documentObserver = await initializePlaylistLength({
apiKey,
pageType,
playlistLengthGetMethod: "html"
});
}
}
// FIXME: mix playlist type not falling back to get mode html when get mode is api
export function disablePlaylistLength() {
eventManager.removeEventListeners("playlistLength");
if (documentObserver) documentObserver.disconnect();
document.querySelector("#yte-playlist-length-ui")?.remove();
}
Loading

0 comments on commit 8ed563f

Please sign in to comment.