diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..63662bfd --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules\\typescript\\lib" +} diff --git a/package-lock.json b/package-lock.json index 8ba7e49e..9831286f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,15 @@ { "name": "youtube-enhancer", - "version": "1.1.0", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "youtube-enhancer", - "version": "1.1.0", + "version": "1.3.0", "license": "MIT", "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", @@ -42,14 +41,17 @@ "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" } }, "node_modules/@alloc/quick-lru": { @@ -4345,7 +4347,8 @@ "node_modules/get-installed-browsers": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/get-installed-browsers/-/get-installed-browsers-0.1.7.tgz", - "integrity": "sha512-gxGDcxaOpA9QNk/REyILnXJVmYS9Se33HTfFN7u03Pxpkn9R/ogsYIFRwyzvc5fCoZ548RAzGk4YSF0xLM4BUw==" + "integrity": "sha512-gxGDcxaOpA9QNk/REyILnXJVmYS9Se33HTfFN7u03Pxpkn9R/ogsYIFRwyzvc5fCoZ548RAzGk4YSF0xLM4BUw==", + "dev": true }, "node_modules/get-intrinsic": { "version": "1.2.1", @@ -11840,16 +11843,16 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/uglify-js": { @@ -12309,6 +12312,24 @@ "engines": { "node": ">= 6" } + }, + "node_modules/zod": { + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", + "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-error": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/zod-error/-/zod-error-1.5.0.tgz", + "integrity": "sha512-zzopKZ/skI9iXpqCEPj+iLCKl9b88E43ehcU+sbRoHuwGd9F1IDVGQ70TyO6kmfiRL1g4IXkjsXK+g1gLYl4WQ==", + "dev": true, + "dependencies": { + "zod": "^3.20.2" + } } } } diff --git a/package.json b/package.json index b4e5449b..fb6cc31d 100755 --- a/package.json +++ b/package.json @@ -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", @@ -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" } -} \ No newline at end of file +} diff --git a/src/components/Settings/Settings.tsx b/src/components/Settings/Settings.tsx index 0ef478b9..b0701cb6 100644 --- a/src/components/Settings/Settings.tsx +++ b/src/components/Settings/Settings.tsx @@ -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, @@ -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" }, @@ -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 && (