diff --git a/source/Background/index.ts b/source/Background/index.ts index 0e40e96..5606482 100644 --- a/source/Background/index.ts +++ b/source/Background/index.ts @@ -1,12 +1,72 @@ +/** + * Background Script (Service Worker in Chrome MV3) + * + * This script runs in the background and acts as a central hub for + * communication between different parts of the extension. + * + * Communication Flow: + * ┌─────────────────────────────────────────────────────────────────────┐ + * │ BACKGROUND SCRIPT │ + * │ │ + * │ Content Script ──PAGE_VISITED──► Background │ + * │ (page loaded) │ │ + * │ ▼ │ + * │ Increment visitCount │ + * │ in browser.storage │ + * │ │ + * │ Popup ──GET_VISIT_COUNT──► Background │ + * │ │ │ + * │ ▼ │ + * │ Read visitCount │ + * │ from storage │ + * │ │ │ + * │ Popup ◄──VISIT_COUNT_RESPONSE──┘ │ + * └─────────────────────────────────────────────────────────────────────┘ + * + * Message Types: + * - PAGE_VISITED (incoming from content): A page was visited + * - GET_VISIT_COUNT (incoming from popup): Request for total visit count + * - VISIT_COUNT_RESPONSE (outgoing to popup): Response with visit count + */ + import browser from 'webextension-polyfill'; -import {ExtensionMessage} from '../types/messages'; +import {ExtensionMessage, VisitCountResponseMessage} from '../types/messages'; +import {getStorage, setStorage} from '../utils/storage'; browser.runtime.onInstalled.addListener((): void => { - console.log('extension installed'); + console.log('Extension installed'); }); // Listen for messages from popup or content scripts -browser.runtime.onMessage.addListener((message: unknown): void => { - const msg = message as ExtensionMessage; - console.log('Background received message:', msg.type); -}); +browser.runtime.onMessage.addListener( + ( + message: unknown + ): Promise | undefined => { + const msg = message as ExtensionMessage; + + // Content script notifies us when a page is visited + if (msg.type === 'PAGE_VISITED') { + console.log('Page visited:', msg.data.title, '-', msg.data.url); + console.log( + ` Words: ${msg.data.wordCount}, Links: ${msg.data.linkCount}, Images: ${msg.data.imageCount}` + ); + + // Increment visit count + getStorage(['visitCount']).then(({visitCount}) => { + setStorage({visitCount: visitCount + 1}); + }); + + return undefined; + } + + // Popup requests the visit count + if (msg.type === 'GET_VISIT_COUNT') { + return getStorage(['visitCount']).then(({visitCount}) => ({ + type: 'VISIT_COUNT_RESPONSE', + count: visitCount, + })); + } + + return undefined; + } +); diff --git a/source/ContentScript/index.ts b/source/ContentScript/index.ts index 46457d2..a49fd0f 100644 --- a/source/ContentScript/index.ts +++ b/source/ContentScript/index.ts @@ -1,16 +1,62 @@ +/** + * Content Script + * + * This script is injected into every web page that matches the patterns + * defined in manifest.json's content_scripts section. + * + * Communication Flow: + * ┌─────────────────────────────────────────────────────────────────────┐ + * │ CONTENT SCRIPT │ + * │ │ + * │ 1. Page loads → Collects page stats → Sends PAGE_VISITED to │ + * │ background script │ + * │ │ + * │ 2. Popup requests GET_PAGE_INFO → Content script responds with │ + * │ PAGE_INFO_RESPONSE containing current page stats │ + * └─────────────────────────────────────────────────────────────────────┘ + * + * Message Types: + * - PAGE_VISITED (outgoing to background): Notify that a page was loaded + * - GET_PAGE_INFO (incoming from popup): Request for current page stats + * - PAGE_INFO_RESPONSE (outgoing to popup): Response with page stats + */ + import browser from 'webextension-polyfill'; -import {ExtensionMessage, PongMessage} from '../types/messages'; +import { + ExtensionMessage, + PageInfo, + PageInfoResponseMessage, +} from '../types/messages'; import {getStorage} from '../utils/storage'; +// Collect page information (word count, links, images) +function getPageInfo(): PageInfo { + const bodyText = document.body?.innerText || ''; + const wordCount = bodyText + .split(/\s+/) + .filter((word) => word.length > 0).length; + const linkCount = document.querySelectorAll('a').length; + const imageCount = document.querySelectorAll('img').length; + + return { + url: window.location.href, + title: document.title, + wordCount, + linkCount, + imageCount, + timestamp: Date.now(), + }; +} + // Listen for messages from popup or background browser.runtime.onMessage.addListener( - (message: unknown): Promise | undefined => { + (message: unknown): Promise | undefined => { const msg = message as ExtensionMessage; - if (msg.type === 'PING') { + if (msg.type === 'GET_PAGE_INFO') { return Promise.resolve({ - type: 'PONG', - timestamp: Date.now(), + type: 'PAGE_INFO_RESPONSE', + data: getPageInfo(), }); } @@ -18,6 +64,27 @@ browser.runtime.onMessage.addListener( } ); +// Notify background script when page loads +function notifyPageVisit(): void { + const pageInfo = getPageInfo(); + + browser.runtime + .sendMessage({ + type: 'PAGE_VISITED', + data: pageInfo, + }) + .catch(() => { + // Background script might not be ready yet, ignore error + }); +} + +// Wait for page to fully load before collecting info +if (document.readyState === 'complete') { + notifyPageVisit(); +} else { + window.addEventListener('load', notifyPageVisit); +} + // Log when content script loads (if logging is enabled) getStorage(['enableLogging']).then(({enableLogging}) => { if (enableLogging) { diff --git a/source/Popup/Popup.module.scss b/source/Popup/Popup.module.scss index 9bba6a7..46db1d4 100644 --- a/source/Popup/Popup.module.scss +++ b/source/Popup/Popup.module.scss @@ -42,3 +42,69 @@ .tabCard { margin-bottom: 14px; } + +.statsCard { + background: white; + border-radius: 12px; + padding: 16px; + margin-bottom: 14px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); +} + +.statsTitle { + font-size: 13px; + font-weight: variables.$semibold; + color: variables.$skyBlue; + margin-bottom: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.statsGrid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; +} + +.statItem { + display: flex; + flex-direction: column; + align-items: center; + padding: 8px; + background: variables.$greyWhite; + border-radius: 8px; +} + +.statValue { + font-size: 20px; + font-weight: variables.$bold; + color: variables.$primary; +} + +.statLabel { + font-size: 11px; + color: variables.$skyBlue; + margin-top: 4px; +} + +.visitCard { + display: flex; + justify-content: space-between; + align-items: center; + background: white; + border-radius: 12px; + padding: 14px 16px; + margin-bottom: 14px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); +} + +.visitLabel { + font-size: 13px; + color: variables.$skyBlue; +} + +.visitCount { + font-size: 18px; + font-weight: variables.$bold; + color: variables.$primary; +} diff --git a/source/Popup/Popup.tsx b/source/Popup/Popup.tsx index 1f57cbd..ae1c24e 100644 --- a/source/Popup/Popup.tsx +++ b/source/Popup/Popup.tsx @@ -1,7 +1,40 @@ +/** + * Popup Component + * + * This is the main UI that appears when the user clicks the extension icon. + * It communicates with both the content script and background script. + * + * Communication Flow: + * ┌─────────────────────────────────────────────────────────────────────┐ + * │ POPUP │ + * │ │ + * │ On mount: │ + * │ │ + * │ 1. Popup ──GET_PAGE_INFO──► Content Script (via browser.tabs) │ + * │ Popup ◄──PAGE_INFO_RESPONSE── Content Script │ + * │ → Displays word count, link count, image count │ + * │ │ + * │ 2. Popup ──GET_VISIT_COUNT──► Background Script (via runtime) │ + * │ Popup ◄──VISIT_COUNT_RESPONSE── Background Script │ + * │ → Displays total pages tracked │ + * │ │ + * │ 3. Popup ──► browser.storage.local │ + * │ → Reads username for greeting │ + * └─────────────────────────────────────────────────────────────────────┘ + * + * Note: browser.tabs.sendMessage() sends to content script in specific tab + * browser.runtime.sendMessage() sends to background script + */ + import {useEffect, useState} from 'react'; import type {FC} from 'react'; import browser, {Tabs} from 'webextension-polyfill'; import {getStorage} from '../utils/storage'; +import { + PageInfo, + PageInfoResponseMessage, + VisitCountResponseMessage, +} from '../types/messages'; import {TabInfo} from './components/TabInfo/TabInfo'; import {FooterActions} from './components/FooterActions/FooterActions'; import styles from './Popup.module.scss'; @@ -18,9 +51,12 @@ interface TabData { const Popup: FC = () => { const [tabInfo, setTabInfo] = useState(null); + const [pageInfo, setPageInfo] = useState(null); + const [visitCount, setVisitCount] = useState(0); const [username, setUsername] = useState(''); useEffect(() => { + // Get current tab info browser.tabs.query({active: true, currentWindow: true}).then((tabs) => { const tab = tabs[0]; if (tab) { @@ -29,9 +65,38 @@ const Popup: FC = () => { url: tab.url || 'Unknown', favIconUrl: tab.favIconUrl, }); + + // Request page info from content script + if (tab.id) { + browser.tabs + .sendMessage(tab.id, {type: 'GET_PAGE_INFO'}) + .then((response: unknown) => { + const res = response as PageInfoResponseMessage; + if (res?.data) { + setPageInfo(res.data); + } + }) + .catch(() => { + // Content script might not be injected on this page + }); + } } }); + // Get visit count from background script + browser.runtime + .sendMessage({type: 'GET_VISIT_COUNT'}) + .then((response: unknown) => { + const res = response as VisitCountResponseMessage; + if (res?.count !== undefined) { + setVisitCount(res.count); + } + }) + .catch(() => { + // Background script might not be ready + }); + + // Get username from storage getStorage(['username']).then(({username: storedUsername}) => { setUsername(storedUsername); }); @@ -66,6 +131,33 @@ const Popup: FC = () => { )} + {/* Page Stats from Content Script */} + {pageInfo && ( +
+

Page Stats

+
+
+ {pageInfo.wordCount} + Words +
+
+ {pageInfo.linkCount} + Links +
+
+ {pageInfo.imageCount} + Images +
+
+
+ )} + + {/* Visit Count from Background Script */} +
+ Pages tracked: + {visitCount} +
+ => openWebPage('/Options/options.html') diff --git a/source/types/messages.ts b/source/types/messages.ts index 366a4d4..fd2df1b 100644 --- a/source/types/messages.ts +++ b/source/types/messages.ts @@ -1,10 +1,73 @@ -export interface PingMessage { - type: 'PING'; -} +/** + * Extension Message Types + * + * This file defines all message types used for communication between + * the different parts of the extension. + * + * Overall Communication Architecture: + * ┌─────────────────────────────────────────────────────────────────────────┐ + * │ │ + * │ ┌──────────────┐ PAGE_VISITED ┌──────────────────┐ │ + * │ │ │ ───────────────────► │ │ │ + * │ │ Content │ │ Background │ │ + * │ │ Script │ │ Script │ │ + * │ │ │ │ │ │ + * │ └──────────────┘ └──────────────────┘ │ + * │ ▲ ▲ │ + * │ │ GET_PAGE_INFO │ GET_VISIT_COUNT │ + * │ │ PAGE_INFO_RESPONSE │ VISIT_COUNT_RESPONSE│ + * │ │ │ │ + * │ │ ┌──────────────┐ │ │ + * │ └─────────│ │───────────────┘ │ + * │ │ Popup │ │ + * │ │ │ │ + * │ └──────────────┘ │ + * │ │ + * └─────────────────────────────────────────────────────────────────────────┘ + * + * Message Flow: + * 1. Content Script → Background: PAGE_VISITED (on page load) + * 2. Popup → Content Script: GET_PAGE_INFO / PAGE_INFO_RESPONSE + * 3. Popup → Background: GET_VISIT_COUNT / VISIT_COUNT_RESPONSE + */ -export interface PongMessage { - type: 'PONG'; +// Page info collected by content script +export interface PageInfo { + url: string; + title: string; + wordCount: number; + linkCount: number; + imageCount: number; timestamp: number; } -export type ExtensionMessage = PingMessage | PongMessage; +// Messages +export interface GetPageInfoMessage { + type: 'GET_PAGE_INFO'; +} + +export interface PageInfoResponseMessage { + type: 'PAGE_INFO_RESPONSE'; + data: PageInfo; +} + +export interface PageVisitedMessage { + type: 'PAGE_VISITED'; + data: PageInfo; +} + +export interface GetVisitCountMessage { + type: 'GET_VISIT_COUNT'; +} + +export interface VisitCountResponseMessage { + type: 'VISIT_COUNT_RESPONSE'; + count: number; +} + +export type ExtensionMessage = + | GetPageInfoMessage + | PageInfoResponseMessage + | PageVisitedMessage + | GetVisitCountMessage + | VisitCountResponseMessage; diff --git a/source/types/storage.ts b/source/types/storage.ts index beca154..a4e0bc1 100644 --- a/source/types/storage.ts +++ b/source/types/storage.ts @@ -1,9 +1,11 @@ export interface StorageSchema { username: string; enableLogging: boolean; + visitCount: number; } export const defaultStorage: StorageSchema = { username: '', enableLogging: false, + visitCount: 0, };