Skip to content

Commit

Permalink
Merge pull request #46 from VampireChicken12/dev
Browse files Browse the repository at this point in the history
feat: Import/Export settings
  • Loading branch information
VampireChicken12 authored Oct 4, 2023
2 parents fb07226 + d4bc39f commit f36dee6
Show file tree
Hide file tree
Showing 10 changed files with 277 additions and 69 deletions.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib"
}
41 changes: 31 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
"type": "module",
"dependencies": {
"@formkit/auto-animate": "^0.7.0",
"get-installed-browsers": "^0.1.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"vite-plugin-css-injected-by-js": "^3.1.1",
Expand Down Expand Up @@ -52,13 +51,16 @@
"eslint-plugin-react": "^7.32.1",
"eslint-plugin-react-hooks": "^4.3.0",
"fs-extra": "^11.1.0",
"get-installed-browsers": "^0.1.7",
"nodemon": "^2.0.20",
"postcss": "^8.4.21",
"prettier": "^2.8.8",
"semantic-release": "^21.1.1",
"tailwindcss": "^3.2.4",
"ts-node": "^10.9.1",
"typescript": "^4.9.4",
"vite": "^4.0.4"
"typescript": "^5.2.2",
"vite": "^4.0.4",
"zod": "^3.22.3",
"zod-error": "^1.5.0"
}
}
}
95 changes: 89 additions & 6 deletions src/components/Settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import "@/assets/styles/tailwind.css";
import "@/components/Settings/Settings.css";

import { useNotifications } from "@/hooks";
import { configuration, configurationKeys } from "@/src/types";
import { configuration, configurationKeys, youtubePlayerSpeedRate } from "@/src/types";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import React, { ChangeEvent, Dispatch, SetStateAction, useEffect, useState } from "react";
import { Checkbox, NumberInput, Select, SelectOption } from "../Inputs";
import { settingsAreDefault } from "@/src/utils/utilities";
import { YoutubePlayerSpeedRates } from "@/src/utils/constants";
import { configurationSchema } from "@/src/utils/constants";
import { generateErrorMessage } from "zod-error";
import { formatDateForFileName } from "../../utils/utilities";

export default function Settings({
settings,
Expand Down Expand Up @@ -192,7 +194,7 @@ export default function Settings({
{ label: "4320p", value: "highres" },
{ label: "auto", value: "auto" }
].reverse();
const YouTubePlayerSpeedOptions: SelectOption[] = YoutubePlayerSpeedRates.map((rate) => ({ label: rate.toString(), value: rate.toString() }));
const YouTubePlayerSpeedOptions: SelectOption[] = youtubePlayerSpeedRate.map((rate) => ({ label: rate.toString(), value: rate.toString() }));
const ScreenshotFormatOptions: SelectOption[] = [
{ label: "PNG", value: "png" },
{ label: "JPEG", value: "jpeg" },
Expand All @@ -202,6 +204,66 @@ export default function Settings({
{ label: "File", value: "file" },
{ label: "Clipboard", value: "clipboard" }
];
// Import settings from a JSON file.
const importSettings = () => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".json";

input.addEventListener("change", async (event) => {
const { target } = event;
if (!target) return;
const { files } = target as HTMLInputElement;
const file = files?.[0];
if (file) {
try {
const fileContents = await file.text();
const importedSettings = JSON.parse(fileContents);
// Validate the imported settings.
const result = configurationSchema.safeParse(importedSettings);
if (!result.success) {
const { error } = result;
const errorMessage = generateErrorMessage(error.errors);
window.alert(`Error importing settings. Please check the file format.\n${errorMessage}`);
} else {
const castSettings = importedSettings as configuration;
// Set the imported settings in your state.
setSettings({ ...castSettings });
Object.assign(localStorage, castSettings);
chrome.storage.local.set(castSettings);
// Show a success notification.
addNotification("success", "Settings imported successfully");
}
} catch (error) {
// Handle any import errors.
window.alert("Error importing settings. Please check the file format.");
}
}
});

// Trigger the file input dialog.
input.click();
};

// Export settings to a JSON file.
const exportSettings = () => {
if (settings) {
const timestamp = formatDateForFileName(new Date());
const filename = `youtube_enhancer_settings_${timestamp}.json`;
const settingsJSON = JSON.stringify(settings);

const blob = new Blob([settingsJSON], { type: "application/json" });
const url = URL.createObjectURL(blob);

const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();

// Show a success notification.
addNotification("success", "Settings exported successfully");
}
};
return (
settings && (
<div className="w-fit h-fit bg-[#f5f5f5] text-black dark:bg-[#181a1b] dark:text-white">
Expand All @@ -210,7 +272,28 @@ export default function Settings({
YouTube Enhancer
<small className="light text-xs sm:text-sm md:text-base">v{chrome.runtime.getManifest().version}</small>
</h1>

<fieldset className="mx-1">
<legend className="mb-1 text-lg sm:text-xl md:text-2xl">Import/Export Settings</legend>
<div className="flex gap-1 p-2">
<input
type="button"
id="import_settings_button"
className="p-2 accent dark:hover:bg-[rgba(24,26,27,0.5)] text-sm sm:text-base md:text-lg"
value="Import Settings"
title="Import settings from a JSON file"
onClick={importSettings}
/>
<input
type="button"
id="export_settings_button"
className="p-2 accent dark:hover:bg-[rgba(24,26,27,0.5)] text-sm sm:text-base md:text-lg"
value="Export Settings"
title="Export current settings to a JSON file"
onClick={exportSettings}
style={{ marginLeft: "auto" }}
/>
</div>
</fieldset>
<fieldset className="mx-1">
<legend className="mb-1 text-lg sm:text-xl md:text-2xl">Miscellaneous settings</legend>
<div className="mx-2 mb-1" title="Remembers the last volume you set and applies it to future videos">
Expand Down Expand Up @@ -437,7 +520,7 @@ export default function Settings({
<input
type="button"
id="confirm_button"
className="p-2 danger dark:hover:bg-[rgba(24,26,27,1)] text-sm sm:text-base md:text-lg"
className="p-2 danger dark:hover:bg-[rgba(24,26,27,0.5)] text-sm sm:text-base md:text-lg"
style={{ marginLeft: "auto" }}
value="Confirm"
title="Confirm setting reset"
Expand All @@ -456,7 +539,7 @@ export default function Settings({
<input
type="button"
id="reset_button"
className="p-2 warning dark:hover:bg-[rgba(24,26,27,1)] text-sm sm:text-base md:text-lg"
className="p-2 warning dark:hover:bg-[rgba(24,26,27,0.5)] text-sm sm:text-base md:text-lg"
style={{ marginLeft: "auto" }}
value="Reset"
title="Resets all settings to their defaults, Click the confirm button to save the changes"
Expand Down
4 changes: 4 additions & 0 deletions src/components/SettingsWrapper/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Loader from "@/src/components/Loader";
import Settings from "@/src/components/Settings/Settings";
import { configuration } from "@/src/types";
import { defaultConfiguration } from "@/src/utils/constants";
import { parseStoredValue } from "@/src/utils/utilities";
import React, { useEffect, useState } from "react";

export default function SettingsWrapper(): JSX.Element {
Expand All @@ -16,6 +17,9 @@ export default function SettingsWrapper(): JSX.Element {
useEffect(() => {
const fetchSettings = () => {
chrome.storage.local.get((settings) => {
for (const [key, value] of Object.entries(settings)) {
settings[key] = parseStoredValue(value);
}
setSettings({ ...settings } as configuration);
setSelectedColor(settings.osd_display_color);
setSelectedDisplayType(settings.osd_display_type);
Expand Down
19 changes: 12 additions & 7 deletions src/features/playerQuality/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { YoutubePlayerQualityLabel, YoutubePlayerQualityLevel, YouTubePlayerDiv } from "@/src/types";
import { YoutubePlayerQualityLabels, YoutubePlayerQualityLevels } from "@/src/utils/constants";
import {
YoutubePlayerQualityLabel,
YoutubePlayerQualityLevel,
YouTubePlayerDiv,
youtubePlayerQualityLevel,
youtubePlayerQualityLabel
} from "@/src/types";
import { waitForSpecificMessage, isWatchPage, isShortsPage, chooseClosetQuality, browserColorLog } from "@/src/utils/utilities";

/**
Expand Down Expand Up @@ -48,20 +53,20 @@ export default async function setPlayerQuality(): Promise<void> {
if (!availableQualityLevels.includes(playerQuality)) {
// Convert the available quality levels to their corresponding labels
const availableResolutions = availableQualityLevels.reduce(function (array, elem) {
if (YoutubePlayerQualityLabels[YoutubePlayerQualityLevels.indexOf(elem)]) {
array.push(YoutubePlayerQualityLabels[YoutubePlayerQualityLevels.indexOf(elem)]);
if (youtubePlayerQualityLabel[youtubePlayerQualityLevel.indexOf(elem)]) {
array.push(youtubePlayerQualityLabel[youtubePlayerQualityLevel.indexOf(elem)]);
}
return array;
}, [] as YoutubePlayerQualityLabel[]);

// Choose the closest quality level based on the available resolutions
playerQuality = chooseClosetQuality(YoutubePlayerQualityLabels[YoutubePlayerQualityLevels.indexOf(playerQuality)], availableResolutions);
playerQuality = chooseClosetQuality(youtubePlayerQualityLabel[youtubePlayerQualityLevel.indexOf(playerQuality)], availableResolutions);

// If the chosen quality level is not available, return
if (!YoutubePlayerQualityLevels.at(YoutubePlayerQualityLabels.indexOf(playerQuality))) return;
if (!youtubePlayerQualityLevel.at(youtubePlayerQualityLabel.indexOf(playerQuality))) return;

// Update the playerQuality variable
playerQuality = YoutubePlayerQualityLevels.at(YoutubePlayerQualityLabels.indexOf(playerQuality)) as YoutubePlayerQualityLevel;
playerQuality = youtubePlayerQualityLevel.at(youtubePlayerQualityLabel.indexOf(playerQuality)) as YoutubePlayerQualityLevel;
}

// Log the message indicating the player quality being set
Expand Down
59 changes: 38 additions & 21 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,38 @@
import z from "zod";
import type { YouTubePlayer } from "node_modules/@types/youtube-player/dist/types";
/* eslint-disable no-mixed-spaces-and-tabs */
export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
export type DeepWriteable<T> = { -readonly [P in keyof T]: DeepWriteable<T[P]> };
export type OnScreenDisplayColor = "red" | "green" | "blue" | "yellow" | "orange" | "purple" | "pink" | "white";
export type OnScreenDisplayType = "no_display" | "text" | "line" | "round";
export type OnScreenDisplayPosition = "top_left" | "top_right" | "bottom_left" | "bottom_right" | "center";
export const onScreenDisplayColor = ["red", "green", "blue", "yellow", "orange", "purple", "pink", "white"] as const;
export type OnScreenDisplayColor = (typeof onScreenDisplayColor)[number];
export const onScreenDisplayType = ["no_display", "text", "line", "round"] as const;
export type OnScreenDisplayType = (typeof onScreenDisplayType)[number];
export const onScreenDisplayPosition = ["top_left", "top_right", "bottom_left", "bottom_right", "center"] as const;
export type OnScreenDisplayPosition = (typeof onScreenDisplayPosition)[number];
export const youtubePlayerQualityLabel = ["144p", "240p", "360p", "480p", "720p", "1080p", "1440p", "2160p", "2880p", "4320p", "auto"] as const;
export type YoutubePlayerQualityLabel = (typeof youtubePlayerQualityLabel)[number];
export const youtubePlayerQualityLevel = [
"tiny",
"small",
"medium",
"large",
"hd720",
"hd1080",
"hd1440",
"hd2160",
"hd2880",
"highres",
"auto"
] as const;
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;

export type YoutubePlayerQualityLabel = "144p" | "240p" | "360p" | "480p" | "720p" | "1080p" | "1440p" | "2160p" | "2880p" | "4320p" | "auto";
export type YoutubePlayerQualityLevel =
| "tiny"
| "small"
| "medium"
| "large"
| "hd720"
| "hd1080"
| "hd1440"
| "hd2160"
| "hd2880"
| "highres"
| "auto";
export type YouTubePlayerSpeedRateExpanded = 2.25 | 2.5 | 2.75 | 3 | 3.25 | 3.75 | 4;
export type YouTubePlayerSpeedRate = 0.25 | 0.5 | 0.75 | 1 | 1.25 | 1.5 | 1.75 | 2 | YouTubePlayerSpeedRateExpanded;
export type ScreenshotType = "file" | "clipboard";

export type ScreenshotFormat = "png" | "jpeg" | "webp";
export type ScreenshotFormat = (typeof screenshotFormat)[number];

export type configuration = {
enable_scroll_wheel_volume_control: boolean;
Expand Down Expand Up @@ -109,3 +118,11 @@ export type Messages = MessageMappings[keyof MessageMappings];
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> = {
[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]>>;
};
Loading

0 comments on commit f36dee6

Please sign in to comment.