Thank you for your interest in contributing! This guide outlines the project architecture, data schemas, coding guidelines, and workflows to help you build and test features for the extension.
The extension is built on Manifest V3 and runs in two distinct contexts (worlds) to balance features and security:
graph TD
A[Browser Toolbar Popup / popup.js] <--> |chrome.storage.local| B[Isolated Content Scripts]
B --> |DOM / events| C[Host Webpage Video Player]
E[PW Enhancements Plugin / pw-enhancements.js] --> |REST API / Proxy| F[PW Slides API]
E --> |DOM / controls| C
Files in src/core/ (e.g., speed-controller.js, question-timer.js, silence-skipper.js, study-tracker.js) execute in the browser's isolated content script sandbox.
- Capabilities: Direct access to the DOM, standard page events, and the
chrome.storageAPI. - Security: Isolated from page scripts to ensure security and prevent prototype pollution.
All extension data is stored in chrome.storage.local. Please keep the following data structures intact when updating storage logic:
- Key:
'qt_log_' + encodeURIComponent(cleanUrl)(wherecleanUrlexcludes the hash#component). - Value:
interface QuestionTimerSession { url: string; currentQuestionNum: number; elapsedTime: number; // In-progress stopwatch time (ms) isRunning: boolean; startTime: number; // Timestamp of start/resume logs: Array<{ qNum: number; durationSec: number; timeFormatted: string; targetSec: number; // 0 if benchmark disabled isOnTime: boolean; }>; isMinimized: boolean; isHidden: boolean; isClickThrough: boolean; posLeft: string | null; // CSS left position posTop: string | null; // CSS top position lastUpdated: number; // Date.now() timestamp }
- Key:
studyTrackerData - Value:
interface StudyTrackerData { dailyGoalHours: number; // e.g., 6.0 streakDays: number; // Active day streak lastActiveDate: string; // YYYY-MM-DD history: { [dateString: string]: { // YYYY-MM-DD keys realSec: number; // Actual study time elapsed coverageSec: number; // Playback rate adjusted time covered questionSec: number; // Time spent solving questions platforms: { [platformName: string]: number; // Platform-specific second tracking }; }; }; }
When an extension is updated or reloaded in Developer Mode, existing tabs run into an "Extension context invalidated" error on subsequent Chrome API calls.
- Rule: Wrap Chrome storage accesses in try/catch blocks and always check for context validity before calling storage functions. Use the helper:
function isContextValid() { try { return typeof chrome !== 'undefined' && chrome.runtime && !!chrome.runtime.id; } catch (e) { return false; } }
Content scripts must not intercept keyboard hotkeys when a user is actively typing in a text field, input, search bar, or rich-text editor (including inside Shadow DOM trees).
- Rule: Use a comprehensive
isTyping(e)guard at the beginning of all document keydown/keyup event handlers:function isTyping(e) { const active = document.activeElement; const target = e?.target; if (e && typeof e.composedPath === 'function') { const path = e.composedPath(); for (let i = 0; i < path.length; i++) { const el = path[i]; if (el && el.nodeType === 1) { const tag = el.tagName; if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tag)) return true; if (el.isContentEditable || el.getAttribute('contenteditable') === 'true') return true; const role = el.getAttribute('role'); if (['textbox', 'searchbox', 'combobox'].includes(role)) return true; } } } let cur = active; while (cur && cur.shadowRoot && cur.shadowRoot.activeElement) { cur = cur.shadowRoot.activeElement; } if (cur) { const tag = cur.tagName; if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tag)) return true; if (cur.isContentEditable || cur.getAttribute('contenteditable') === 'true') return true; const role = cur.getAttribute('role'); if (['textbox', 'searchbox', 'combobox'].includes(role)) return true; } if (target && target.nodeType === 1) { if (['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName)) return true; if (target.isContentEditable || (target.closest && target.closest('[contenteditable="true"], input, textarea, select, [role="textbox"], [role="searchbox"]'))) { return true; } } return false; }
Keep global scope clean. Every script must be encapsulated in an Immediately Invoked Function Expression (IIFE) with 'use strict'; enabled.
(function () {
'use strict';
// Module code here...
})();Many modern lecture platforms use React, Vue, or Next.js (client-side routing) which do not trigger full page load events when navigating.
- Rule: Use a periodic interval or a
MutationObserverto monitor DOM changes or URL changes to automatically re-bind event listeners or attach badges to dynamic<video>players. Seeremaining-time.jsandquestion-timer.jsfor examples.
- No External Dependencies: Write clean, vanilla JS. Avoid importing jQuery, Lodash, or large utility libraries.
- Separation of Styles: Do not use inline CSS strings inside Javascript files. Write style declarations inside
src/styles/hud.cssor scoped stylesheets and apply them via class toggles.
To add optimizations or keyboard shortcuts for a specific platform (similar to pw-enhancements.js):
- Create the Script: Create a new file under
src/plugins/your-plugin.js. - Create Scoped CSS: If layout tweaks are required, add
src/styles/your-plugin-custom.css. - Register in
manifest.json: Add a new match block undercontent_scripts:{ "matches": ["*://*.yourplatform.com/*"], "css": ["src/styles/your-plugin-custom.css"], "js": ["src/plugins/your-plugin.js"] }
- Content Scripts (HUD, Speed, Timer): Press
F12inside the lecture page and inspect the standard page Console. - Popup Dashboard: Right-click the extension icon in the toolbar, select Inspect popup, and view the developer console.
To view active storage data directly, you can run the following command in the Console of your page:
chrome.storage.local.get(null, data => console.log(data));Thank you for contributing to make video learning faster and more productive for everyone! ❤️