diff --git a/README.md b/README.md index 8480aee..d0d95fd 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ To use Keyword Highlighter, simply click on the extension icon in the top right- Keyword Highlighter uses storage to store user settings and preferences, making it easy to access them every time the extension is used. This feature provides a more personalized and seamless experience for the user. +![](/docs/chrome-web-store/Screenshot_2.png) + ## Contributions If you'd like to contribute to Keyword Highlighter, please feel free to submit a pull request. We welcome any feedback or suggestions to improve the functionality of the extension. diff --git a/docs/chrome-web-store/Screenshot_1.png b/docs/chrome-web-store/Screenshot_1.png index 390a913..2f1cde5 100644 Binary files a/docs/chrome-web-store/Screenshot_1.png and b/docs/chrome-web-store/Screenshot_1.png differ diff --git a/docs/chrome-web-store/Screenshot_2.png b/docs/chrome-web-store/Screenshot_2.png new file mode 100644 index 0000000..c8cd1da Binary files /dev/null and b/docs/chrome-web-store/Screenshot_2.png differ diff --git a/manifest.ts b/manifest.ts index 78b1231..7f12b9f 100644 --- a/manifest.ts +++ b/manifest.ts @@ -14,7 +14,6 @@ const manifest: chrome.runtime.ManifestV3 = { type: "module", }, action: { - default_popup: "src/pages/popup/index.html", default_icon: "icon-34.png", }, icons: { diff --git a/package.json b/package.json index 4e3d6cf..76965f4 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,11 @@ }, "type": "module", "dependencies": { + "@emotion/react": "^11.11.0", + "@emotion/styled": "^11.11.0", + "@fontsource/roboto": "^4.5.8", + "@mui/icons-material": "^5.11.16", + "@mui/material": "^5.12.3", "lodash.debounce": "^4.0.8", "lodash.throttle": "^4.1.1", "react": "18.2.0", diff --git a/public/manifest.json b/public/manifest.json index 7716c32..f00bdee 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -9,26 +9,17 @@ "type": "module" }, "action": { - "default_popup": "src/pages/popup/index.html", "default_icon": "icon-34.png" }, "icons": { "128": "icon-128.png" }, - "permissions": [ - "storage" - ], + "permissions": ["storage"], "content_scripts": [ { - "matches": [ - "https://www.linkedin.com/*" - ], - "js": [ - "src/pages/content/index.js" - ], - "css": [ - "assets/css/contentStyle16834373313.chunk.css" - ] + "matches": ["https://www.linkedin.com/*"], + "js": ["src/pages/content/index.js"], + "css": ["assets/css/contentStyle16836018082.chunk.css"] } ], "web_accessible_resources": [ @@ -39,9 +30,7 @@ "icon-128.png", "icon-34.png" ], - "matches": [ - "*://*/*" - ] + "matches": ["*://*/*"] } ] -} \ No newline at end of file +} diff --git a/src/pages/background/index.ts b/src/pages/background/index.ts index 305028c..2919b17 100644 --- a/src/pages/background/index.ts +++ b/src/pages/background/index.ts @@ -1,5 +1,11 @@ import reloadOnUpdate from "virtual:reload-on-update-in-background-script"; +chrome.action.setPopup({ popup: '' }); + +chrome.action.onClicked.addListener(() => { + chrome.tabs.create({ url: 'src/pages/options/index.html' }); +}); + reloadOnUpdate("pages/background"); /** diff --git a/src/pages/content/components/App.tsx b/src/pages/content/components/App.tsx index f320648..ac0b8ae 100644 --- a/src/pages/content/components/App.tsx +++ b/src/pages/content/components/App.tsx @@ -1,31 +1,35 @@ import { useEffect, useState } from "react"; -import { get } from "@services/storage/storageService"; -import { - SETTINGS_KEYWORDS, - OBSERVABLE_DEBOUNCE_TIME_MS, -} from "@services/constants"; +import { OBSERVABLE_DEBOUNCE_TIME_MS } from "@services/constants"; +import { getRules } from "@src/services/storage/optionsService"; +import { IKeywordRule } from "@src/pages/options/components/KeywordRule"; import Highlighter from "./Highlighter"; export default function App() { - const [keywords, setKeywords] = useState(""); + const [rules, setRules] = useState([]); + + const map = (rule: IKeywordRule) => { + return { + className: rule.id, + keywords: rule.keywords, + styles: rule.cssStyles, + }; + }; useEffect(() => { - get(SETTINGS_KEYWORDS).then((result) => { - const keywords = result?.[SETTINGS_KEYWORDS] || null; - if (keywords) { - setKeywords(keywords); - } + getRules().then((rules) => { + setRules(rules.map(map)); }); }, []); - if (!keywords) { - return; - } - return ( - + <> + {rules.map((rule) => ( + + ))} + ); } diff --git a/src/pages/content/components/Highlighter.tsx b/src/pages/content/components/Highlighter.tsx index b465199..ab24d9d 100644 --- a/src/pages/content/components/Highlighter.tsx +++ b/src/pages/content/components/Highlighter.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo } from "react"; +import React, { useCallback, useMemo } from "react"; import { findAndWrap } from "@src/lib/findAndWrapHTMLNodes"; import useMutationObservable, { DEFAULT_OPTIONS, @@ -7,19 +7,21 @@ import useMutationObservable, { interface HighlighterProps { debounceTimeMs: number; keywords: string; + highlightedClassName: string; } export default function Highlighter({ debounceTimeMs, keywords, + highlightedClassName, }: HighlighterProps) { const onMutation = useCallback(() => { findAndWrap(document.body, { findTextRegExp: new RegExp(keywords.replaceAll(",", "|"), "gi"), wrapWithTag: "span", - wrapWithClassName: "highlighted", + wrapWithClassName: highlightedClassName, wrapIf: (node: Text) => { - return !node.parentElement.classList.contains("highlighted"); + return !node.parentElement.classList.contains(highlightedClassName); }, }); }, []); diff --git a/src/pages/content/components/index.tsx b/src/pages/content/components/index.tsx index b281ab4..2824e9f 100644 --- a/src/pages/content/components/index.tsx +++ b/src/pages/content/components/index.tsx @@ -2,12 +2,9 @@ import { createRoot } from "react-dom/client"; import DebugPopup from "@src/pages/content/components/DebugPopup"; import App from "@src/pages/content/components/App"; import { get } from "@services/storage/storageService"; -import { - SETTINGS_DEBUG, - SETTINGS_HIGHLIGHTED_STYLES, - SETTINGS_HIGHLIGHTED_STYLES_DEFAULT_VALUE, -} from "@services/constants"; +import { SETTINGS_DEBUG, SETTINGS_KEYWORD_RULES } from "@services/constants"; import refreshOnUpdate from "virtual:reload-on-update-in-view"; +import { getRules } from "@src/services/storage/optionsService"; refreshOnUpdate("pages/content"); @@ -36,20 +33,16 @@ const initApp = () => { createRoot(root).render(); }; -const addStyleBlock = (styles: string) => { +const addStyleBlock = (className: string, styles: string) => { var sheet = document.createElement("style"); - sheet.innerHTML = `.highlighted { ${styles} }`; + sheet.innerHTML = `.${className} { ${styles} }`; document.body.appendChild(sheet); }; const initStyles = () => { - const key = SETTINGS_HIGHLIGHTED_STYLES, - defaultValue = SETTINGS_HIGHLIGHTED_STYLES_DEFAULT_VALUE; - - get(key).then((result) => { - const highlightedStyles = result?.[key] || defaultValue; - if (highlightedStyles) { - addStyleBlock(highlightedStyles); + getRules().then((styles) => { + for (const style of styles) { + addStyleBlock(style.id, style.cssStyles); } }); }; diff --git a/src/pages/newtab/Newtab.css b/src/pages/newtab/Newtab.css deleted file mode 100644 index 74b5e05..0000000 --- a/src/pages/newtab/Newtab.css +++ /dev/null @@ -1,38 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/src/pages/newtab/Newtab.scss b/src/pages/newtab/Newtab.scss deleted file mode 100644 index 8960c7b..0000000 --- a/src/pages/newtab/Newtab.scss +++ /dev/null @@ -1,10 +0,0 @@ -$myColor: red; - -h1, -h2, -h3, -h4, -h5, -h6 { - color: $myColor; -} diff --git a/src/pages/newtab/Newtab.tsx b/src/pages/newtab/Newtab.tsx deleted file mode 100644 index b63a57f..0000000 --- a/src/pages/newtab/Newtab.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from "react"; -import logo from "@assets/img/logo.svg"; -import "@pages/newtab/Newtab.css"; -import "@pages/newtab/Newtab.scss"; - -const Newtab = () => { - return ( -
-
- logo -

- Edit src/pages/newtab/Newtab.tsx and save to reload. -

- - Learn React! - -
The color of this paragraph is defined using SASS.
-
-
- ); -}; - -export default Newtab; diff --git a/src/pages/newtab/index.css b/src/pages/newtab/index.css deleted file mode 100644 index ec2585e..0000000 --- a/src/pages/newtab/index.css +++ /dev/null @@ -1,13 +0,0 @@ -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; -} diff --git a/src/pages/newtab/index.html b/src/pages/newtab/index.html deleted file mode 100644 index 0d5481b..0000000 --- a/src/pages/newtab/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - New tab - - - -
- - - diff --git a/src/pages/newtab/index.tsx b/src/pages/newtab/index.tsx deleted file mode 100644 index 950551b..0000000 --- a/src/pages/newtab/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from "react"; -import { createRoot } from "react-dom/client"; -import Newtab from "@pages/newtab/Newtab"; -import "@pages/newtab/index.css"; -import refreshOnUpdate from "virtual:reload-on-update-in-view"; - -refreshOnUpdate("pages/newtab"); - -function init() { - const appContainer = document.querySelector("#app-container"); - if (!appContainer) { - throw new Error("Can not find #app-container"); - } - const root = createRoot(appContainer); - root.render(); -} - -init(); diff --git a/src/pages/options/Options.tsx b/src/pages/options/Options.tsx index 28d8fe9..400421e 100644 --- a/src/pages/options/Options.tsx +++ b/src/pages/options/Options.tsx @@ -1,8 +1,174 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; +import CssBaseline from "@mui/material/CssBaseline"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Container from "@mui/material/Container"; +import Paper from "@mui/material/Paper"; +import Grid from "@mui/material/Grid"; +import Typography from "@mui/material/Typography"; +import TextField from "@mui/material/TextField"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import Checkbox from "@mui/material/Checkbox"; +import Divider from "@mui/material/Divider"; +import { createTheme, ThemeProvider } from "@mui/material/styles"; +import { get, set } from "@services/storage/storageService"; +import { uid } from "@services/uidService"; import "@pages/options/Options.css"; +import { + SETTINGS_DEBUG, + SETTINGS_KEYWORD_RULES, + TEXT_OPTIONS, + TEXT_OPTIONS_DISPLAY_DEBUG, + TEXT_OPTIONS_SAVED_SUCCESSFULLY, +} from "@src/services/constants"; +import Header from "./components/Header"; +import KeywordRule, { IKeywordRule } from "./components/KeywordRule"; -const Options: React.FC = () => { - return
Options
; -}; +const theme = createTheme(); -export default Options; +export default function Options() { + const [displayDebug, setDisplayDebug] = useState(false); + const [rules, setRules] = useState([]); + + useEffect(() => { + initForm(); + }, []); + + const initForm = () => { + get([SETTINGS_DEBUG, SETTINGS_KEYWORD_RULES]).then((result) => { + setDisplayDebug(result?.[SETTINGS_DEBUG] || false); + setRules(result?.[SETTINGS_KEYWORD_RULES] || [createRule()]); + }); + }; + + const handleDisplayDebugChange = ( + event: React.ChangeEvent + ) => { + setDisplayDebug(event.target.checked); + }; + + const handleAddRule = () => { + const newRule = createRule(); + setRules([...rules, newRule]); + }; + + const createRule = () => { + return { + id: uid(), + keywords: null, + cssStyles: null, + enabledOn: null, + }; + }; + + const handleRuleChange = (rule: IKeywordRule) => { + const newRules = [...rules]; + const index = newRules.findIndex((item) => item.id === rule.id); + newRules[index] = { ...newRules[index], ...rule }; + setRules(newRules); + }; + + const handleDeleteRule = (rule: IKeywordRule) => { + const newRules = rules.filter((item) => item.id !== rule.id); + if (!newRules.length) { + newRules.push(createRule()); + } + setRules(newRules); + }; + + const handleApply = async () => { + await set({ + [SETTINGS_DEBUG]: displayDebug, + [SETTINGS_KEYWORD_RULES]: rules, + }); + alert(TEXT_OPTIONS_SAVED_SUCCESSFULLY); + }; + + const handleCancel = () => initForm(); + + const handleReset = () => { + const newRules = [createRule()]; + setRules(newRules); + setDisplayDebug(false); + }; + + return ( + + +
+ + + + {TEXT_OPTIONS} + + + + {rules.map((rule) => ( + + ))} + + + + + + + + + + } + label={TEXT_OPTIONS_DISPLAY_DEBUG} + /> + + + + + + + + + + + + ); +} diff --git a/src/pages/options/components/Header.tsx b/src/pages/options/components/Header.tsx new file mode 100644 index 0000000..be26108 --- /dev/null +++ b/src/pages/options/components/Header.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import AppBar from "@mui/material/AppBar"; +import Box from "@mui/material/Box"; +import Toolbar from "@mui/material/Toolbar"; +import Typography from "@mui/material/Typography"; +import { TEXT_EXTENSION_TITLE } from "@src/services/constants"; + +export default function Header() { + return ( + `1px solid ${t.palette.divider}`, + }} + > + + + {TEXT_EXTENSION_TITLE} + + + + ); +} diff --git a/src/pages/options/components/KeywordRule.tsx b/src/pages/options/components/KeywordRule.tsx new file mode 100644 index 0000000..85f1153 --- /dev/null +++ b/src/pages/options/components/KeywordRule.tsx @@ -0,0 +1,111 @@ +import React from "react"; +import Grid from "@mui/material/Grid"; +import TextField from "@mui/material/TextField"; +import Divider from "@mui/material/Divider"; +import IconButton from "@mui/material/IconButton"; +import DeleteIcon from "@mui/icons-material/Delete"; +import Button from "@mui/material/Button"; + +export interface IKeywordRule { + id: string; + keywords: string; + cssStyles: string; + enabledOn: string; +} + +export type KeywordRuleProps = IKeywordRule & { + onRuleChange: (rule: IKeywordRule) => void; + onDeleteRule: (rule: IKeywordRule) => void; +}; + +export default function KeywordRule(props: KeywordRuleProps) { + const { keywords, cssStyles, enabledOn, onRuleChange, onDeleteRule } = props; + + const handleKeywordsChange: React.ChangeEventHandler = ( + event + ) => { + onRuleChange({ + ...props, + keywords: event.target.value, + }); + }; + + const handleCssStylesChange: React.ChangeEventHandler = ( + event + ) => { + onRuleChange({ + ...props, + cssStyles: event.target.value, + }); + }; + + const handleEnabledOnChange: React.ChangeEventHandler = ( + event + ) => { + onRuleChange({ + ...props, + enabledOn: event.target.value, + }); + }; + + const handleDeleteClick = () => { + onDeleteRule({ + ...props, + }); + }; + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/pages/popup/Popup.css b/src/pages/popup/Popup.css deleted file mode 100644 index c92d90a..0000000 --- a/src/pages/popup/Popup.css +++ /dev/null @@ -1,44 +0,0 @@ -.App { - position: absolute; - top: 0px; - bottom: 0px; - left: 0px; - right: 0px; - height: 100%; - padding: 10px; - background-color: #282c34; -} - -.App-logo { - height: 30vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - height: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/src/pages/popup/Popup.tsx b/src/pages/popup/Popup.tsx deleted file mode 100644 index f3e85b8..0000000 --- a/src/pages/popup/Popup.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { ChangeEvent } from "react"; -import "@pages/popup/Popup.css"; -import { - SETTINGS_DEBUG, - SETTINGS_KEYWORDS, - SETTINGS_HIGHLIGHTED_STYLES, - SETTINGS_HIGHLIGHTED_STYLES_DEFAULT_VALUE, -} from "@services/constants"; -import useSettingsHook from "./hooks/useSettingsHook"; - -const Popup = () => { - const [debug, setDebug] = useSettingsHook(SETTINGS_DEBUG); - const [keywords, setKeywords] = useSettingsHook(SETTINGS_KEYWORDS); - const [highlightedStyles, setHighlightedStyles] = useSettingsHook( - SETTINGS_HIGHLIGHTED_STYLES - ); - - const handleDebugChange = (e: ChangeEvent) => { - setDebug(e.target.checked); - }; - - const handleKeywordsChange = (e: ChangeEvent) => { - setKeywords(e.target.value); - }; - - const handleHighlightedStylesChange = ( - e: ChangeEvent - ) => { - setHighlightedStyles(e.target.value); - }; - - return ( -
-
-
- Settings - -
-
- - -
-
- -
-
- -
-