Skip to content
Open
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
6 changes: 6 additions & 0 deletions dist/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
},
"default_popup": "popup.html"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["js/contentScript.js"]
}
],
"background": {
"service_worker": "js/backgroundPage.js"
},
Expand Down
1 change: 1 addition & 0 deletions dist/popup.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Chrome Extension (built with TypeScript + React)</title>
<script src="js/popup.js"></script>
</head>
Expand Down
110 changes: 41 additions & 69 deletions src/popup/component.tsx
Original file line number Diff line number Diff line change
@@ -1,74 +1,46 @@
import React from "react";
import { Hello } from "@src/components/hello";
import browser, { Tabs } from "webextension-polyfill";
import { Scroller } from "@src/components/scroller";

// // // //

// Scripts to execute in current tab
const scrollToTopPosition = 0;
const scrollToBottomPosition = 9999999;

function scrollWindow(position: number) {
window.scroll(0, position);
}

/**
* Executes a string of Javascript on the current tab
* @param code The string of code to execute on the current tab
*/
function executeScript(position: number): void {
// Query for the active tab in the current window
browser.tabs
.query({ active: true, currentWindow: true })
.then((tabs: Tabs.Tab[]) => {
// Pulls current tab from browser.tabs.query response
const currentTab: Tabs.Tab | number = tabs[0];

// Short circuits function execution is current tab isn't found
if (!currentTab) {
return;
}
const currentTabId: number = currentTab.id as number;

// Executes the script in the current tab
browser.scripting
.executeScript({
target: {
tabId: currentTabId,
},
func: scrollWindow,
args: [position],
})
.then(() => {
console.log("Done Scrolling");
});
import React, { useState } from "react";
import "../css/app.css";

const Component = () => {
const [pageText, setPageText] = useState("");

const getTextAndSpeak = () => {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (!tabs[0]?.id) return;

chrome.tabs.sendMessage(
tabs[0].id,
{ type: "GET_PAGE_TEXT" },
(response) => {
if (response?.text) {
setPageText(response.text);

const utterance = new SpeechSynthesisUtterance(
response.text,
);
utterance.lang = "ko-KR";
speechSynthesis.speak(utterance);
} else {
alert("페이지의 텍스트를 가져오지 못했어요.");
}
},
);
});
Comment on lines +11 to 28
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for chrome.runtime.lastError

When using callback-based Chrome API methods, you should check for chrome.runtime.lastError to handle potential errors from the messaging system.

chrome.tabs.sendMessage(
    tabs[0].id,
    { type: "GET_PAGE_TEXT" },
    (response) => {
+       if (chrome.runtime.lastError) {
+           alert(`오류: ${chrome.runtime.lastError.message}`);
+           return;
+       }
        if (response?.text) {
            setPageText(response.text);

            const utterance = new SpeechSynthesisUtterance(
                response.text,
            );
            utterance.lang = "ko-KR";
            speechSynthesis.speak(utterance);
        } else {
            alert("페이지의 텍스트를 가져오지 못했어요.");
        }
    },
);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
chrome.tabs.sendMessage(
tabs[0].id,
{ type: "GET_PAGE_TEXT" },
(response) => {
if (response?.text) {
setPageText(response.text);
const utterance = new SpeechSynthesisUtterance(
response.text,
);
utterance.lang = "ko-KR";
speechSynthesis.speak(utterance);
} else {
alert("페이지의 텍스트를 가져오지 못했어요.");
}
},
);
});
chrome.tabs.sendMessage(
tabs[0].id,
{ type: "GET_PAGE_TEXT" },
(response) => {
if (chrome.runtime.lastError) {
alert(`오류: ${chrome.runtime.lastError.message}`);
return;
}
if (response?.text) {
setPageText(response.text);
const utterance = new SpeechSynthesisUtterance(
response.text,
);
utterance.lang = "ko-KR";
speechSynthesis.speak(utterance);
} else {
alert("페이지의 텍스트를 가져오지 못했어요.");
}
},
);

}

// // // //
};

export function Popup() {
// Sends the `popupMounted` event
React.useEffect(() => {
browser.runtime.sendMessage({ popupMounted: true });
}, []);

// Renders the component tree
return (
<div>
<div className="mx-4 my-4 bg-black">
<Hello />
<hr />
<Scroller
onClickScrollTop={() => {
executeScript(scrollToTopPosition);
}}
onClickScrollBottom={() => {
executeScript(scrollToBottomPosition);
}}
/>
</div>
<div className="w-64 p-4 bg-white rounded-lg shadow-lg">
<h1 className="text-xl font-semibold mb-2 text-gray-800">
VOIM tts test
</h1>
<button
onClick={getTextAndSpeak}
className="w-full mt-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded text-sm"
>
읽어줘!
</button>
</div>
);
}
};

export default Component;
Comment on lines +1 to +46
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Functional implementation, but lacks user experience considerations

The implementation is functional but has a few issues that should be addressed:

  1. No cleanup mechanism for speech synthesis when the popup closes
  2. No way to stop reading once started
  3. Limited error handling
  4. Hardcoded Korean language without user options
  5. No loading/in-progress indicators

Consider enhancing the user experience with these improvements:

import React, { useState, useEffect } from "react";
import "../css/app.css";

const Component = () => {
    const [pageText, setPageText] = useState("");
+   const [isLoading, setIsLoading] = useState(false);
+   const [isSpeaking, setIsSpeaking] = useState(false);

+   useEffect(() => {
+       return () => {
+           // Cleanup speech synthesis when component unmounts
+           speechSynthesis.cancel();
+       };
+   }, []);

    const getTextAndSpeak = () => {
+       setIsLoading(true);
        chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
            if (!tabs[0]?.id) return;

            chrome.tabs.sendMessage(
                tabs[0].id,
                { type: "GET_PAGE_TEXT" },
                (response) => {
+                   setIsLoading(false);
                    if (response?.text) {
                        setPageText(response.text);

                        const utterance = new SpeechSynthesisUtterance(
                            response.text,
                        );
                        utterance.lang = "ko-KR";
+                       setIsSpeaking(true);
+                       utterance.onend = () => setIsSpeaking(false);
+                       utterance.onerror = () => {
+                           setIsSpeaking(false);
+                           alert("읽기 중 오류가 발생했습니다.");
+                       };
                        speechSynthesis.speak(utterance);
                    } else {
                        alert("페이지의 텍스트를 가져오지 못했어요.");
                    }
                },
            );
        });
    };

+   const stopSpeaking = () => {
+       speechSynthesis.cancel();
+       setIsSpeaking(false);
+   };

    return (
        <div className="w-64 p-4 bg-white rounded-lg shadow-lg">
            <h1 className="text-xl font-semibold mb-2 text-gray-800">
                VOIM tts test
            </h1>
            <button
                onClick={getTextAndSpeak}
+               disabled={isLoading || isSpeaking}
                className="w-full mt-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded text-sm"
            >
-               읽어줘!
+               {isLoading ? "로딩 중..." : isSpeaking ? "읽는 중..." : "읽어줘!"}
            </button>
+           {isSpeaking && (
+               <button
+                   onClick={stopSpeaking}
+                   className="w-full mt-2 py-2 bg-red-500 hover:bg-red-600 text-white rounded text-sm"
+               >
+                   중지
+               </button>
+           )}
        </div>
    );
};

export default Component;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import React, { useState } from "react";
import "../css/app.css";
const Component = () => {
const [pageText, setPageText] = useState("");
const getTextAndSpeak = () => {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (!tabs[0]?.id) return;
chrome.tabs.sendMessage(
tabs[0].id,
{ type: "GET_PAGE_TEXT" },
(response) => {
if (response?.text) {
setPageText(response.text);
const utterance = new SpeechSynthesisUtterance(
response.text,
);
utterance.lang = "ko-KR";
speechSynthesis.speak(utterance);
} else {
alert("페이지의 텍스트를 가져오지 못했어요.");
}
},
);
});
}
// // // //
};
export function Popup() {
// Sends the `popupMounted` event
React.useEffect(() => {
browser.runtime.sendMessage({ popupMounted: true });
}, []);
// Renders the component tree
return (
<div>
<div className="mx-4 my-4 bg-black">
<Hello />
<hr />
<Scroller
onClickScrollTop={() => {
executeScript(scrollToTopPosition);
}}
onClickScrollBottom={() => {
executeScript(scrollToBottomPosition);
}}
/>
</div>
<div className="w-64 p-4 bg-white rounded-lg shadow-lg">
<h1 className="text-xl font-semibold mb-2 text-gray-800">
VOIM tts test
</h1>
<button
onClick={getTextAndSpeak}
className="w-full mt-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded text-sm"
>
읽어줘!
</button>
</div>
);
}
};
export default Component;
import React, { useState, useEffect } from "react";
import "../css/app.css";
const Component = () => {
const [pageText, setPageText] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [isSpeaking, setIsSpeaking] = useState(false);
useEffect(() => {
return () => {
// Cleanup speech synthesis when component unmounts
speechSynthesis.cancel();
};
}, []);
const getTextAndSpeak = () => {
setIsLoading(true);
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (!tabs[0]?.id) return;
chrome.tabs.sendMessage(
tabs[0].id,
{ type: "GET_PAGE_TEXT" },
(response) => {
setIsLoading(false);
if (response?.text) {
setPageText(response.text);
const utterance = new SpeechSynthesisUtterance(
response.text,
);
utterance.lang = "ko-KR";
setIsSpeaking(true);
utterance.onend = () => setIsSpeaking(false);
utterance.onerror = () => {
setIsSpeaking(false);
alert("읽기 중 오류가 발생했습니다.");
};
speechSynthesis.speak(utterance);
} else {
alert("페이지의 텍스트를 가져오지 못했어요.");
}
},
);
});
};
const stopSpeaking = () => {
speechSynthesis.cancel();
setIsSpeaking(false);
};
return (
<div className="w-64 p-4 bg-white rounded-lg shadow-lg">
<h1 className="text-xl font-semibold mb-2 text-gray-800">
VOIM tts test
</h1>
<button
onClick={getTextAndSpeak}
disabled={isLoading || isSpeaking}
className="w-full mt-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded text-sm"
>
{isLoading ? "로딩 중..." : isSpeaking ? "읽는 중..." : "읽어줘!"}
</button>
{isSpeaking && (
<button
onClick={stopSpeaking}
className="w-full mt-2 py-2 bg-red-500 hover:bg-red-600 text-white rounded text-sm"
>
중지
</button>
)}
</div>
);
};
export default Component;

6 changes: 2 additions & 4 deletions src/popup/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import * as React from "react";
import { createRoot } from "react-dom/client";
import browser from "webextension-polyfill";
import { Popup } from "./component";
import Component from "./component";
import "../css/app.css";

// // // //

browser.tabs
.query({ active: true, currentWindow: true })
.then(() => {
Expand All @@ -21,7 +19,7 @@ browser.tabs
const container = document.getElementById("popup");
if (container) {
const root = createRoot(container);
root.render(<Popup />);
root.render(<Component />);
} else {
console.error("popup이라는 id를 가진 요소가 존재하지 않아요!");
}
Expand Down