diff --git a/manifest.json b/manifest.json index 2fa4754..c841405 100644 --- a/manifest.json +++ b/manifest.json @@ -14,7 +14,8 @@ }, "permissions": [ "storage", - "tabs" + "tabs", + "webNavigation" ], "host_permissions": [ "https://api.leetcodeapp.com/*" @@ -33,7 +34,8 @@ ], "matches": [ "https://leetcode.com/problems/*" - ] + ], + "run_at": "document_end" } ], "web_accessible_resources": [ diff --git a/src/background/background.ts b/src/background/background.ts index 3609b78..e80fa9a 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -70,39 +70,104 @@ chrome.runtime.onMessage.addListener((request) => { } }); +// Keep track of the last state to avoid duplicate updates +let lastState = { + problemPath: '', + view: '', // 'problem' or 'solutions' + lastPathname: '', // Track full pathname to detect real navigation + lastUrl: '', // Track full URL to detect refreshes + lastUpdateTime: 0 // Track time of last update to prevent rapid re-triggers +}; + chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { - if (changeInfo.status === 'complete' && tab.url) { + if (tab.url) { const url = tab.url; let problemUrl = /^https:\/\/leetcode\.com\/problems\/.*\/?/; + + // Check if this is a leetcode problem page if (url.match(problemUrl)) { - chrome.storage.local.get(['currentLeetCodeProblemTitle', 'descriptionTabUpdated', 'solutionsTabUpdated'], (result) => { - let lastTitle = result.currentLeetCodeProblemTitle || ''; - let descriptionTabUpdated = result.descriptionTabUpdated || false; - let solutionsTabUpdated = result.solutionsTabUpdated || false; - if (tab.title !== lastTitle) { + // Extract the problem path from the URL + const problemPath = url.match(/\/problems\/([^/]+)/)?.[1]; + const pathname = new URL(url).pathname; + + // Determine the current view - now only distinguishing between problem view and solutions + let currentView = url.includes('/solutions') ? 'solutions' : 'problem'; + + // Only trigger updates on actual page loads or problem changes + const isPageLoad = changeInfo.status === 'complete'; + const isProblemChange = problemPath !== lastState.problemPath; + const isViewChange = currentView !== lastState.view; + + // Check if this is a video navigation within solutions + const isInternalSolutionsNavigation = + currentView === 'solutions' && + lastState.view === 'solutions' && + problemPath === lastState.problemPath; + + // Detect actual page refresh vs internal navigation + const isActualRefresh = + url === lastState.lastUrl && + isPageLoad && + changeInfo.url === undefined && + !isInternalSolutionsNavigation && + Date.now() - lastState.lastUpdateTime > 1000; + + const isRealNavigation = + !isInternalSolutionsNavigation && + ((pathname !== lastState.lastPathname || isViewChange) && + !pathname.includes('playground') && + !pathname.includes('editor') && + !pathname.includes('interpret-solution') && + !pathname.includes('submissions')); + + // Update last URL and time + if (!isInternalSolutionsNavigation) { + lastState.lastUrl = url; + } + + // Only update if there's a real navigation, problem change, or actual refresh + if ((isProblemChange || (isViewChange && isRealNavigation) || isActualRefresh) && problemPath) { + console.log(`State change detected - ${ + isProblemChange ? 'New Problem' : + isViewChange ? 'View Changed' : + isActualRefresh ? 'Page Refresh' : + 'Page Load' + }`); + + // Update last state + lastState.problemPath = problemPath; + lastState.view = currentView; + lastState.lastPathname = pathname; + lastState.lastUpdateTime = Date.now(); + + // Reset flags only on problem change or actual refresh + if (isProblemChange || isActualRefresh) { chrome.storage.local.set({ + 'currentLeetCodeProblem': problemPath, 'currentLeetCodeProblemTitle': tab.title, 'descriptionTabUpdated': false, 'solutionsTabUpdated': false }); - // If the title has changed, we reset both flags - descriptionTabUpdated = false; - solutionsTabUpdated = false; } - let descriptionUrl = /^https:\/\/leetcode\.com\/problems\/.*\/(description\/)?/; - if (!descriptionTabUpdated && url.match(descriptionUrl)) { - chrome.storage.local.set({ 'descriptionTabUpdated': true }); - chrome.tabs.sendMessage(tabId, { action: 'updateDescription', title: tab.title || 'title' }); - } + // Get current state + chrome.storage.local.get(['descriptionTabUpdated', 'solutionsTabUpdated'], (result) => { + let descriptionTabUpdated = result.descriptionTabUpdated || false; + let solutionsTabUpdated = result.solutionsTabUpdated || false; - let solutionsUrl = /^https:\/\/leetcode\.com\/problems\/.*\/solutions\/?/; - if (url.match(solutionsUrl)) { - chrome.storage.local.set({ 'solutionsTabUpdated': true }); - chrome.tabs.sendMessage(tabId, { action: 'updateSolutions', title: tab.title || 'title' }); - } - }); + // Always update description tab when in problem view + if (currentView === 'problem') { + chrome.storage.local.set({ 'descriptionTabUpdated': true }); + chrome.tabs.sendMessage(tabId, { action: 'updateDescription', title: tab.title || 'title' }); + } + + // Always update solutions tab when in solutions view + if (currentView === 'solutions') { + chrome.storage.local.set({ 'solutionsTabUpdated': true }); + chrome.tabs.sendMessage(tabId, { action: 'updateSolutions', title: tab.title || 'title' }); + } + }); + } } } -}); - +}); \ No newline at end of file diff --git a/src/content-script/themeDetector.js b/src/content-script/themeDetector.js index b9ae489..1d6d8f0 100644 --- a/src/content-script/themeDetector.js +++ b/src/content-script/themeDetector.js @@ -1,26 +1,16 @@ // Listen for messages from the background script or popup chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - if (request.action === 'detectTheme') { - const theme = detectPageTheme(); - console.log(`Detecting theme: ${theme}`); - sendResponse({ theme }); + if (request.action === 'detectTheme' || request.action === 'getTheme') { + debouncedThemeDetection(sendResponse); + return true; // Keep the message channel open for asynchronous response } - if (request.action === 'getTheme') { - const theme = detectPageTheme(); - console.log(`Getting theme: ${theme}`); - sendResponse({ theme }); - } - return true; // Keep the message channel open for asynchronous response }); // Function to detect the theme of the current LeetCode page function detectPageTheme() { - console.log('Starting theme detection on leetcode page...'); - // Force a quick check to see if this is a LeetCode page const url = window.location.href; const isLeetCodePage = url.includes('leetcode.com'); - console.log('Is LeetCode page:', isLeetCodePage, url); // Method 1: Check for LeetCode's light theme indicator (most reliable) // In light mode LeetCode specifically has a white background for these elements @@ -30,11 +20,9 @@ function detectPageTheme() { if (mainContent) { const bgColor = window.getComputedStyle(mainContent).backgroundColor; - console.log('Main content background color:', bgColor); // LeetCode light mode has white or very light background if (bgColor.includes('255, 255, 255') || bgColor.includes('rgb(255, 255, 255)')) { - console.log('Theme detected from content: LIGHT (white background)'); return 'light'; } } @@ -45,13 +33,11 @@ function detectPageTheme() { // If the dark mode switcher has a sun icon, it means we're in light mode const sunIcon = darkModeSwitcher.querySelector('svg[data-icon="sun"]'); if (sunIcon) { - console.log('Theme detected from dark mode switcher: LIGHT (sun icon visible)'); return 'light'; } // If the dark mode switcher has a moon icon, it means we're in dark mode const moonIcon = darkModeSwitcher.querySelector('svg[data-icon="moon"]'); if (moonIcon) { - console.log('Theme detected from dark mode switcher: dark (moon icon visible)'); return 'dark'; } } @@ -59,20 +45,16 @@ function detectPageTheme() { // Method 3: Check HTML tag class for 'dark' or 'light' const htmlElement = document.documentElement; if (htmlElement.classList.contains('dark')) { - console.log('Theme detected from HTML class: dark'); return 'dark'; } else if (htmlElement.classList.contains('light')) { - console.log('Theme detected from HTML class: LIGHT'); return 'light'; } // Method 4: Check data-theme attribute const dataTheme = htmlElement.getAttribute('data-theme'); if (dataTheme === 'dark') { - console.log('Theme detected from data-theme: dark'); return 'dark'; } else if (dataTheme === 'light') { - console.log('Theme detected from data-theme: LIGHT'); return 'light'; } @@ -80,44 +62,19 @@ function detectPageTheme() { const header = document.querySelector('header') || document.querySelector('nav'); if (header) { const headerBgColor = window.getComputedStyle(header).backgroundColor; - console.log('Header background color:', headerBgColor); // LeetCode light mode header is usually white or very light if (headerBgColor.includes('255, 255, 255') || headerBgColor.includes('rgb(255, 255, 255)') || !isColorDark(headerBgColor)) { - console.log('Theme detected from header: LIGHT'); return 'light'; } else { - console.log('Theme detected from header: dark'); return 'dark'; } } - // Method 6: Check the code editor background (LeetCode specific) - const codeEditor = document.querySelector('.monaco-editor'); - if (codeEditor) { - const editorBgColor = window.getComputedStyle(codeEditor).backgroundColor; - console.log('Code editor background color:', editorBgColor); - if (isColorDark(editorBgColor)) { - console.log('Theme detected from code editor: dark'); - return 'dark'; - } else { - console.log('Theme detected from code editor: LIGHT'); - return 'light'; - } - } - - // Method 7: Check background color to determine if dark or light - const backgroundColor = window.getComputedStyle(document.body).backgroundColor; - console.log('Body background color:', backgroundColor); - if (isColorDark(backgroundColor)) { - console.log('Theme detected from body bg: dark'); - return 'dark'; - } else { - console.log('Theme detected from body bg: LIGHT'); - return 'light'; - } + // Default to dark if can't detect + return 'dark'; } // Helper function to determine if a color is dark based on luminance @@ -140,4 +97,31 @@ function isColorDark(color) { // Return true for dark colors (lower luminance) return luminance < 0.5; -} \ No newline at end of file +} + +// Debounce function to limit how often a function can be called +function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +} + +// Store last detected theme to prevent unnecessary updates +let lastDetectedTheme = null; + +// Debounced theme detection function +const debouncedThemeDetection = debounce((sendResponse) => { + const theme = detectPageTheme(); + if (theme !== lastDetectedTheme) { + lastDetectedTheme = theme; + if (sendResponse) { + sendResponse({ theme }); + } + } +}, 500); \ No newline at end of file diff --git a/src/content-script/update-description-tab.ts b/src/content-script/update-description-tab.ts index 4e0c11d..22a68cf 100644 --- a/src/content-script/update-description-tab.ts +++ b/src/content-script/update-description-tab.ts @@ -17,7 +17,9 @@ function showExamples() { interface Problem { title: string; rating?: string; - // Add other properties as needed + companies?: Array<{ + name: string; + }>; } // Detect LeetCode's theme and set extension theme accordingly @@ -95,87 +97,132 @@ function showDifficulty() { function showRating(problemTitle: string) { chrome.storage.local.get(['showRating'], (result) => { const showRating = result.showRating; - if (showRating) { - chrome.storage.local.get(['leetcodeProblems'], (result) => { - const problem = result.leetcodeProblems.questions.find((problem: Problem) => problem.title === problemTitle); + if (!showRating) { + const ratingElement = document.getElementById('rating'); + if (ratingElement) { + ratingElement.remove(); + } + return; + } - let ratingElement = document.getElementById('rating'); + chrome.storage.local.get(['leetcodeProblems'], (result) => { + const problem = result.leetcodeProblems.questions.find((problem: Problem) => problem.title === problemTitle); + if (!problem?.rating) return; - if (!problem || !problem.rating) { - if (ratingElement) { - ratingElement.style.display = 'none'; - ratingElement.remove(); - } - return; - } + let ratingElement = document.getElementById('rating'); + if (!ratingElement) { + ratingElement = document.createElement('div'); + ratingElement.id = 'rating'; + } - if (ratingElement) { - // update the existing rating element - ratingElement.textContent = problem.rating; - } else { - // create a new rating element - ratingElement = document.createElement('div'); - ratingElement.id = 'rating'; - ratingElement.textContent = problem.rating; - ratingElement.style.fontSize = '12px'; - ratingElement.style.backgroundColor = '#3D3D3C'; - ratingElement.style.borderRadius = '10px'; - ratingElement.style.width = '50px'; - ratingElement.style.textAlign = 'center'; - ratingElement.style.paddingTop = '2px'; - ratingElement.style.color = 'lightcyan'; - } + ratingElement.textContent = problem.rating; + ratingElement.style.fontSize = '11px'; + ratingElement.style.letterSpacing = '.5px'; + ratingElement.style.borderRadius = '6px'; + ratingElement.style.width = '60px'; + ratingElement.style.textAlign = 'center'; + ratingElement.style.padding = '4px 8px'; + ratingElement.style.transition = 'all 0.2s ease'; - const difficultyContainer = document.querySelectorAll('div.relative.inline-flex')[0] as HTMLDivElement; - if (difficultyContainer) { - // insert the rating element after the first child of the difficulty container - let parent = difficultyContainer.parentElement; - parent?.insertBefore(ratingElement, parent.firstChild); + chrome.storage.local.get(['isDarkTheme'], (result) => { + const isDark = result.isDarkTheme; + if (ratingElement) { + ratingElement.style.backgroundColor = isDark ? '#373737' : '#f3f4f5'; + ratingElement.style.color = isDark ? '#40a9ff' : '#1a1a1a'; + ratingElement.style.border = `1px solid ${isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`; } }); - } - else { - const ratingElement = document.getElementById('rating'); - if (ratingElement) { - ratingElement.style.display = 'none'; - ratingElement.remove(); + + const difficultyContainer = document.querySelectorAll('div.relative.inline-flex')[0] as HTMLDivElement; + if (difficultyContainer?.parentElement && ratingElement) { + difficultyContainer.parentElement.insertBefore(ratingElement, difficultyContainer.parentElement.firstChild); } - } + }); }); } // show the company tags if the user has enabled it in the settings function showCompanyTags(problemTitle: string) { chrome.storage.local.get(['showCompanyTags'], (result) => { - const showCompanyTags = result.showCompanyTags; - let companyTagContainer = document.getElementById('companyTagContainer'); - - if (!showCompanyTags) { - if (companyTagContainer) { - companyTagContainer.style.display = 'none'; - } + if (!result.showCompanyTags) { return; } - if (companyTagContainer) { - while (companyTagContainer.firstChild) { - companyTagContainer.firstChild.remove(); + // Try to find the description element with retries + const maxRetries = 10; + const baseDelay = 300; + let retryCount = 0; + + const insertCompanyTags = (description: Element) => { + // Double check for existing container before inserting + if (document.getElementById('companyTagContainer')) { + return; + } + + // Create new container + const newCompanyTagContainer = document.createElement('div'); + newCompanyTagContainer.id = 'companyTagContainer'; + newCompanyTagContainer.style.display = 'flex'; + newCompanyTagContainer.style.flexDirection = 'row'; + newCompanyTagContainer.style.marginBottom = '20px'; + newCompanyTagContainer.style.gap = '5px'; + + description.insertBefore(newCompanyTagContainer, description.firstChild); + + // Load and inject company tags + loadCompanyTags(problemTitle, newCompanyTagContainer); + }; + + const tryInsertCompanyTags = () => { + // First check if container already exists to prevent duplicates + if (document.getElementById('companyTagContainer')) { + return; } - } else { - companyTagContainer = document.createElement('div'); - companyTagContainer.id = 'companyTagContainer'; - companyTagContainer.style.display = 'flex'; - companyTagContainer.style.flexDirection = 'row'; - companyTagContainer.style.marginBottom = '20px'; - companyTagContainer.style.gap = '5px'; const description = document.getElementsByClassName('elfjS')[0]; - if (description) { - description.insertBefore(companyTagContainer, description.firstChild); + + if (!description && retryCount < maxRetries) { + // Use exponential backoff for retry delay + const delay = baseDelay * Math.pow(1.5, retryCount); + retryCount++; + console.log(`Attempt ${retryCount}: Waiting for description element to load... Retrying in ${delay}ms`); + setTimeout(tryInsertCompanyTags, delay); + return; } - } - loadCompanyTags(problemTitle, companyTagContainer); + if (!description) { + console.log('Failed to find description element after all retries'); + + // If still not found, set up a MutationObserver to watch for DOM changes + const observer = new MutationObserver((mutations, obs) => { + // Check if container already exists + if (document.getElementById('companyTagContainer')) { + obs.disconnect(); + return; + } + + const description = document.getElementsByClassName('elfjS')[0]; + if (description) { + obs.disconnect(); // Stop observing once we find the element + insertCompanyTags(description); + } + }); + + // Start observing the document with the configured parameters + observer.observe(document.body, { + childList: true, + subtree: true + }); + + return; + } + + // If we found the description element, insert the company tags + insertCompanyTags(description); + }; + + // Start the process + tryInsertCompanyTags(); }); } @@ -184,76 +231,134 @@ function loadCompanyTags(problemTitle: string, companyTagContainer: HTMLElement) companyTagContainer.id = 'companyTagContainer'; companyTagContainer.style.display = 'flex'; companyTagContainer.style.flexDirection = 'row'; - companyTagContainer.style.marginTop = '10px'; - companyTagContainer.style.gap = '5px'; + companyTagContainer.style.marginTop = '16px'; + companyTagContainer.style.gap = '8px'; + companyTagContainer.style.flexWrap = 'wrap'; const description = document.getElementsByClassName('elfjS')[0]; - - if (!description) { - return; - } - - interface problem { - title: string; - companies: Array<{ - name: string; - }>; - } + if (!description) return; chrome.storage.local.get(['leetcodeProblems'], (result) => { - const problem = result.leetcodeProblems.questions.find((problem: problem) => problem.title === problemTitle); - if (problem.companies && problem.companies.length > 0) { - const topCompanies = problem.companies.slice(0, 5); - // create a button for each company - topCompanies.forEach((company: { name: string; }) => { - const button = document.createElement('button'); - // opens the company page when the button is clicked - button.onclick = () => { - chrome.runtime.sendMessage({ - action: 'openCompanyPage', company: company.name, - }); - }; - - button.style.display = 'flex'; - button.style.alignItems = 'center'; - button.style.justifyContent = 'center'; - - const icon = document.createElement('img'); - icon.src = `https://logo.clearbit.com/${company.name.toLowerCase().replace(/\s/g, '')}.com`; - icon.style.height = '12px'; - icon.style.width = '12px'; - icon.style.marginRight = '5px'; - button.appendChild(icon); - - button.style.minWidth = '100px'; - button.style.height = '25px'; - button.style.padding = '1px'; - button.style.borderRadius = '10px'; - button.style.fontSize = '10px'; - - chrome.storage.local.get(['isDarkTheme'], (result) => { - const isDark = result.isDarkTheme; - applyButtonTheme(button, isDark); + const problem = result.leetcodeProblems.questions.find((p: Problem) => p.title === problemTitle); + if (!problem?.companies?.length) return; + + const topCompanies = problem.companies.slice(0, 5); + topCompanies.forEach((company: { name: string; }) => { + const button = document.createElement('button'); + button.classList.add('company-tag'); + button.onclick = () => { + chrome.runtime.sendMessage({ + action: 'openCompanyPage', company: company.name, }); + }; + + button.style.display = 'flex'; + button.style.alignItems = 'center'; + button.style.gap = '8px'; + button.style.padding = '6px 12px'; + button.style.borderRadius = '6px'; + button.style.fontSize = '11px'; + button.style.letterSpacing = '.5px'; + button.style.transition = 'all 0.2s ease'; + button.style.cursor = 'pointer'; + - const companyName = document.createTextNode(`${company.name}`); - button.appendChild(companyName); - companyTagContainer.appendChild(button); + chrome.storage.local.get(['isDarkTheme'], (result) => { + const isDark = result.isDarkTheme; + updateCompanyTagStyle(button, isDark); }); - } + + const icon = document.createElement('img'); + icon.src = `https://logo.clearbit.com/${company.name.toLowerCase().replace(/\s/g, '')}.com`; + icon.style.width = '14px'; + icon.style.height = '14px'; + button.appendChild(icon); + + const companyName = document.createTextNode(company.name); + button.appendChild(companyName); + companyTagContainer.appendChild(button); + }); }); - if (description) description.insertBefore(companyTagContainer, description.firstChild); + + description.insertBefore(companyTagContainer, description.firstChild); return companyTagContainer; } +function updateCompanyTagStyle(button: HTMLElement, isDark: boolean) { + button.style.backgroundColor = isDark ? '#373737' : '#f3f4f5'; + button.style.color = isDark ? '#fff' : '#1a1a1a'; + button.style.border = `1px solid ${isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`; + + // Remove existing listeners + const oldMouseEnter = button.onmouseenter; + const oldMouseLeave = button.onmouseleave; + if (oldMouseEnter) button.removeEventListener('mouseenter', oldMouseEnter); + if (oldMouseLeave) button.removeEventListener('mouseleave', oldMouseLeave); + + // Add new theme-aware listeners + button.addEventListener('mouseenter', () => { + button.style.backgroundColor = isDark ? '#424242' : '#e6e6e6'; + button.style.borderColor = isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + }); + button.addEventListener('mouseleave', () => { + button.style.backgroundColor = isDark ? '#373737' : '#f3f4f5'; + button.style.borderColor = isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; + }); +} + +function updateThemeForCompanyTags(isDark: boolean) { + const companyTags = document.querySelectorAll('.company-tag'); + companyTags.forEach((tag) => { + if (tag instanceof HTMLElement) { + updateCompanyTagStyle(tag, isDark); + } + }); +} + +function setupDescriptionThemeListener() { + // Listen for LeetCode's theme changes + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.target instanceof HTMLElement && mutation.target.tagName === 'BODY') { + chrome.storage.local.get(['themeMode'], (result) => { + // Only sync theme if in auto mode + if (result.themeMode === 'auto') { + const isDark = document.body.classList.contains('dark'); + // Update our extension's theme setting + chrome.storage.local.set({ isDarkTheme: isDark }); + updateThemeForCompanyTags(isDark); + } + }); + } + }); + }); + + // Start observing the body element for class changes + observer.observe(document.body, { + attributes: true, + attributeFilter: ['class'] + }); + + // Also listen for our extension's theme changes + chrome.storage.onChanged.addListener((changes) => { + if (changes.isDarkTheme) { + updateThemeForCompanyTags(changes.isDarkTheme.newValue); + } + }); +} + chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.action === 'updateDescription') { // Detect theme on first load of a problem page + console.log('Updating description tab...'); detectAndSyncTheme(); showExamples(); showCompanyTags(request.title.split('-')[0].trim()); showDifficulty(); showRating(request.title.split('-')[0].trim()); + + // Add theme change listener after creating company tags + setupDescriptionThemeListener(); } else if (request.action === 'getTheme') { // Return the current LeetCode theme const htmlElement = document.documentElement; @@ -264,3 +369,52 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { // Return true to indicate we will send a response asynchronously (needed for sendResponse) return true; }); + +// Self-initialization function that runs when the content script loads +function initializeDescriptionTab() { + // Wait for the DOM to be fully loaded + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', onDOMReady); + } else { + onDOMReady(); + } + + function onDOMReady() { + // Set up theme detection and synchronization + setupDescriptionThemeListener(); + + // Get the problem title from the page + const problemTitle = document.title.replace(' - LeetCode', ''); + + // Apply all enhancements + showDifficulty(); + showRating(problemTitle); + showCompanyTags(problemTitle); + showExamples(); + + // Set up a MutationObserver to detect tab changes + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { + // Check if we're on the description tab + const descriptionTab = document.querySelector('[data-cy="description-tab"]'); + if (descriptionTab && descriptionTab.classList.contains('active')) { + // Re-apply company tags when switching to description tab + const problemTitle = document.title.replace(' - LeetCode', ''); + showCompanyTags(problemTitle); + } + } + }); + }); + + // Start observing the tab container + const tabContainer = document.querySelector('[role="tablist"]'); + if (tabContainer) { + observer.observe(tabContainer, { childList: true, subtree: true }); + } + } +} + +// Initialize the content script +initializeDescriptionTab(); + diff --git a/src/content-script/update-solutions-tab.ts b/src/content-script/update-solutions-tab.ts index 8b12c97..27badbc 100644 --- a/src/content-script/update-solutions-tab.ts +++ b/src/content-script/update-solutions-tab.ts @@ -1,22 +1,52 @@ const VIDEO_ASPECT_RATIO = 56.25; // 16:9 aspect ratio +// Create a wrapper for all our custom content +function createCustomContentWrapper() { + const wrapper = createStyledElement('div', { + width: '100%', + maxWidth: '800px', + margin: '0 auto 32px auto', + position: 'relative', + zIndex: '1' + }); + wrapper.classList.add('leetcode-explained-wrapper'); + return wrapper; +} + // Utility function to create a styled button function createStyledButton(text: string, isActive: boolean = false): HTMLButtonElement { const button = document.createElement('button'); button.textContent = text; - button.style.border = '2px solid grey'; - button.style.width = '100px'; - button.style.padding = '3px'; - button.style.margin = '0px 20px'; - button.style.borderRadius = '5px'; - if (isActive) button.style.borderColor = 'lightgreen'; - button.style.fontSize = '12px'; + button.classList.add('nav-button'); + if (isActive) button.classList.add('active'); + chrome.storage.local.get(['isDarkTheme'], (result) => { const isDark = result.isDarkTheme; - applyButtonTheme(button, isDark); + button.style.backgroundColor = isDark ? '#373737' : '#f3f4f5'; + button.style.color = isDark ? '#fff' : '#1a1a1a'; + button.style.border = `1px solid ${isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`; - }) + button.addEventListener('mouseenter', () => { + if (!button.classList.contains('active')) { + button.style.backgroundColor = isDark ? '#424242' : '#e6e6e6'; + } + }); + button.addEventListener('mouseleave', () => { + if (!button.classList.contains('active')) { + button.style.backgroundColor = isDark ? '#373737' : '#f3f4f5'; + } + }); + }); + button.style.width = '120px'; + button.style.padding = '4px 8px'; + button.style.margin = '0 8px'; + button.style.borderRadius = '6px'; + button.style.fontSize = '11px'; + button.style.transition = 'all 0.2s ease'; + button.style.letterSpacing = '0.5px'; + button.style.cursor = 'pointer'; + return button; } @@ -26,27 +56,16 @@ function createVideoContainer(problem: any) { position: 'relative', display: 'none', justifyContent: 'center', - paddingBottom: `0px`, - marginBottom: '60px', - transition: 'padding-bottom 0.3s ease-out', + paddingBottom: `${VIDEO_ASPECT_RATIO}%`, + marginBottom: '32px', + transition: 'all 0.3s ease-out', + borderRadius: '8px', + overflow: 'hidden', + width: '100%', + maxWidth: '800px', + margin: '0 auto', }); - container.classList.add('video-container'); - - const iframe = createStyledElement('iframe', { - display: 'flex', - justifyContent: 'center', - position: 'absolute', - width: '95%', - height: '95%', - border: '1px solid grey', - paddingBottom: '20px', - marginTop: '50px', - }) as HTMLIFrameElement; - - iframe.classList.add('youtube-video'); - let src = problem.videos[0].embedded_url; - iframe.src = src; - iframe.allowFullscreen = true; + container.classList.add('video-container', 'content-section'); const controlsContainer = createStyledElement('div', { display: 'flex', @@ -54,49 +73,95 @@ function createVideoContainer(problem: any) { alignItems: 'center', position: 'absolute', width: '100%', - paddingTop: '10px', - marginBottom: '50px', + maxWidth: '800px', + padding: '16px', + marginBottom: '32px', boxSizing: 'border-box', - color: '#fff', + height: '48px', + borderRadius: '6px', + zIndex: '1', + }); + + chrome.storage.local.get(['isDarkTheme'], (result) => { + const isDark = result.isDarkTheme; + controlsContainer.style.backgroundColor = isDark ? '#373737' : '#f3f4f5'; + controlsContainer.style.color = isDark ? '#fff' : '#1a1a1a'; + controlsContainer.style.border = `1px solid ${isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`; }); const prevButton = document.createElement('button'); prevButton.textContent = '⬅️'; prevButton.style.fontSize = '20px'; + prevButton.style.padding = '8px 16px'; + prevButton.style.border = 'none'; + prevButton.style.backgroundColor = 'transparent'; + prevButton.style.transition = 'all 0.2s ease'; + prevButton.style.cursor = 'pointer'; + const nextButton = document.createElement('button'); nextButton.textContent = '➡️'; nextButton.style.fontSize = '20px'; + nextButton.style.padding = '8px 16px'; + nextButton.style.border = 'none'; + nextButton.style.backgroundColor = 'transparent'; + nextButton.style.transition = 'all 0.2s ease'; + nextButton.style.cursor = 'pointer'; const channelElement = createStyledElement('div', { - fontSize: '12px', + fontSize: '13px', + letterSpacing: '.5px', textAlign: 'center', - width: '200px', + minWidth: '200px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', }); + + chrome.storage.local.get(['isDarkTheme'], (result) => { + const isDark = result.isDarkTheme; + channelElement.style.color = isDark ? '#fff' : '#1a1a1a'; + }); + let currentVideoIndex = 0; channelElement.classList.add('channel'); channelElement.id = 'channel'; - channelElement.textContent = problem.videos[currentVideoIndex].channel;; - channelElement.style.fontWeight = '400'; - chrome.storage.local.get(['isDarkTheme'], (result) => { - channelElement.style.color = result.isDarkTheme ? 'lightcyan' : '#333'; - }) + channelElement.textContent = problem.videos[currentVideoIndex].channel; prevButton.addEventListener('click', () => { currentVideoIndex = (currentVideoIndex - 1 + problem.videos.length) % problem.videos.length; updateVideo(iframe, problem.videos[currentVideoIndex].embedded_url); - channelElement.textContent = problem.videos[currentVideoIndex].channel; // Update channel name + channelElement.textContent = problem.videos[currentVideoIndex].channel; }); nextButton.addEventListener('click', () => { currentVideoIndex = (currentVideoIndex + 1) % problem.videos.length; updateVideo(iframe, problem.videos[currentVideoIndex].embedded_url); - channelElement.textContent = problem.videos[currentVideoIndex].channel; // Update channel name + channelElement.textContent = problem.videos[currentVideoIndex].channel; }); controlsContainer.append(prevButton, channelElement, nextButton); container.append(controlsContainer); - container.append(iframe); + const iframe = createStyledElement('iframe', { + position: 'absolute', + top: '0', + left: '0', + width: '100%', + height: '100%', + borderRadius: '8px', + marginTop: '50px', + }) as HTMLIFrameElement; + + chrome.storage.local.get(['isDarkTheme'], (result) => { + const isDark = result.isDarkTheme; + iframe.style.border = `1px solid ${isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`; + }); + + iframe.classList.add('youtube-video'); + iframe.src = problem.videos[0].embedded_url; + iframe.allowFullscreen = true; + + container.append(iframe); return container; } @@ -105,94 +170,121 @@ function updateVideo(iframe: HTMLIFrameElement, videoUrl: string) { } function createCodeContainer() { - // Create an HTML element to hold the code + const container = createStyledElement('div', { + display: 'none', + width: '100%', + maxWidth: '800px', + margin: '0 auto', + position: 'relative' + }); + container.classList.add('code-section', 'content-section'); + const codeElement = document.createElement('pre'); codeElement.classList.add('code-container'); - codeElement.style.display = 'none'; - codeElement.style.border = '1px solid grey'; - codeElement.style.paddingLeft = '5px'; - codeElement.style.marginTop = '20px'; - codeElement.style.width = '95%'; - codeElement.style.fontSize = '12px'; - codeElement.style.marginLeft = '2.5%'; - codeElement.style.padding = '10px'; - codeElement.style.maxHeight = '400px'; + codeElement.style.display = 'block'; + codeElement.style.borderRadius = '8px'; + codeElement.style.padding = '16px'; + codeElement.style.marginTop = '24px'; + codeElement.style.width = '100%'; + codeElement.style.fontSize = '14px'; + codeElement.style.maxHeight = '500px'; codeElement.style.overflowY = 'auto'; - return codeElement; + codeElement.style.boxSizing = 'border-box'; + codeElement.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)'; + + chrome.storage.local.get(['isDarkTheme'], (result) => { + const isDark = result.isDarkTheme; + codeElement.style.backgroundColor = isDark ? '#2d2d2d' : '#f7f9fa'; + codeElement.style.border = `1px solid ${isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`; + codeElement.style.color = isDark ? '#fff' : '#1a1a1a'; + }); + + container.appendChild(codeElement); + return container; } -function hideContent() { - let codeContainer = document.getElementsByClassName('code-container')[0] as HTMLDivElement; - if (codeContainer) codeContainer.style.display = 'none'; - let languageButtonsContainer = document.getElementsByClassName('language-buttons-container')[0] as HTMLDivElement; - if (languageButtonsContainer) languageButtonsContainer.style.display = 'none'; +function showContent(type: 'Discussion' | 'Video' | 'Code') { + // Hide all content sections first + const contentSections = document.querySelectorAll('.content-section'); + contentSections.forEach(section => { + (section as HTMLElement).style.display = 'none'; + }); - let navContainer = document.getElementsByClassName('nav-container')[0] as HTMLDivElement; - if (navContainer) navContainer.style.display = 'flex'; + // Get the language buttons container + const languageButtons = document.querySelector('.language-buttons-container') as HTMLElement; + if (languageButtons) { + languageButtons.style.display = 'none'; // Hide by default + } - let videoContainer = document.querySelector('div.video-container') as HTMLDivElement; - if (videoContainer) { - videoContainer.style.paddingBottom = '0%'; - videoContainer.style.display = 'none'; + // Show the selected content + switch (type) { + case 'Video': + const videoContainer = document.querySelector('.video-container') as HTMLElement; + if (videoContainer) { + videoContainer.style.display = 'flex'; + videoContainer.style.paddingBottom = `${VIDEO_ASPECT_RATIO}%`; + } + break; + case 'Code': + const codeSection = document.querySelector('.code-section') as HTMLElement; + if (codeSection) { + codeSection.style.display = 'block'; + // Only show language buttons when code section is active + if (languageButtons) { + languageButtons.style.display = 'flex'; + } + } + break; + case 'Discussion': + // No need to do anything as the discussion is the default content + break; } -} + // Update button states + const buttons = document.querySelectorAll('.nav-button'); + buttons.forEach(button => { + if (button.textContent === type) { + button.classList.add('active'); + } else { + button.classList.remove('active'); + } + }); -function createNavContainer(problem: any) { + // Show/hide the discussion section + const discussionSection = document.querySelector('.discuss-markdown') as HTMLElement; + if (discussionSection) { + discussionSection.style.display = type === 'Discussion' ? 'block' : 'none'; + } +} +function createNavContainer(problem: any) { const navContainer = createStyledElement('div', { display: 'flex', justifyContent: 'center', alignItems: 'center', + gap: '8px', + padding: '16px', width: '100%', - paddingTop: '10px', - paddingBottom: '20px', - boxSizing: 'border-box', - color: '#fff', + maxWidth: '800px', + margin: '0 auto', }); navContainer.classList.add('nav-container'); - // Add discussion button - const discussionButton = createStyledButton('Discussion', true); - const codeButton = createStyledButton('Code'); - const videoButton = createStyledButton('Video'); - - discussionButton.addEventListener('click', () => { - hideContent(); - videoButton.style.borderColor = 'grey'; - discussionButton.style.borderColor = 'lightgreen'; - codeButton.style.borderColor = 'grey'; - }); - navContainer.append(discussionButton); - - if (problem.videos && problem.videos.length > 0) { - videoButton.addEventListener('click', () => { - hideContent(); - let videoContainer = document.querySelector('div.video-container') as HTMLDivElement; - videoContainer.style.paddingBottom = `${VIDEO_ASPECT_RATIO}%`; - videoContainer.style.display = 'flex'; - - videoButton.style.borderColor = 'lightgreen'; - discussionButton.style.borderColor = 'grey'; - codeButton.style.borderColor = 'grey'; + const buttons = [ + { text: 'Discussion', show: true }, + { text: 'Video', show: problem.videos?.length > 0 }, + { text: 'Code', show: problem.languages?.length > 0 } + ]; + + buttons.forEach(({ text, show }, index) => { + if (!show) return; + + const button = createStyledButton(text, index === 0); + button.addEventListener('click', () => { + showContent(text as 'Discussion' | 'Video' | 'Code'); }); - navContainer.append(videoButton); - } - if (problem.languages && problem.languages.length > 0) { - codeButton.addEventListener('click', () => { - hideContent(); - let codeContainer = document.getElementsByClassName('code-container')[0] as HTMLDivElement; - codeContainer.style.display = 'flex'; - let languageButtonsContainer = document.getElementsByClassName('language-buttons-container')[0] as HTMLDivElement; - languageButtonsContainer.classList.add('language-buttons-container'); - languageButtonsContainer.style.display = 'flex'; - - codeButton.style.borderColor = 'lightgreen'; - discussionButton.style.borderColor = 'grey'; - videoButton.style.borderColor = 'grey'; - }); - navContainer.append(codeButton); - } + navContainer.append(button); + }); return navContainer; } @@ -253,37 +345,47 @@ function createLanguageButtons(problem: any) { const container = createStyledElement('div', { paddingTop: '20px', marginLeft: '20px', + display: 'flex', + gap: '8px', + flexWrap: 'wrap', }); + container.classList.add('language-buttons-container'); - // For each language, create a button and set up its event listener problem.languages.forEach((language: string) => { - // Create the button using the utility function - const buttonLabel = (language === "cpp") ? "C++" : (language.charAt(0).toUpperCase() + language.slice(1)); const langButton = document.createElement('button'); - langButton.style.border = '1px solid grey'; - langButton.style.width = '110px'; langButton.style.display = 'flex'; - langButton.style.flexDirection = 'row'; - langButton.style.padding = '3px'; - langButton.style.margin = '0px 5px'; - langButton.addEventListener('mouseover', () => { - langButton.style.borderColor = 'lightgreen'; - }); - langButton.addEventListener('mouseout', () => { - langButton.style.borderColor = 'grey'; + langButton.style.alignItems = 'center'; + langButton.style.gap = '8px'; + langButton.style.padding = '6px 12px'; + langButton.style.borderRadius = '6px'; + langButton.style.fontSize = '11px'; + langButton.style.letterSpacing = '.5px'; + langButton.style.transition = 'all 0.2s ease'; + langButton.style.cursor = 'pointer'; + + chrome.storage.local.get(['isDarkTheme'], (result) => { + const isDark = result.isDarkTheme; + langButton.style.backgroundColor = isDark ? '#373737' : '#f3f4f5'; + langButton.style.color = isDark ? '#fff' : '#1a1a1a'; + langButton.style.border = `1px solid ${isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`; + + // on hover just make the background a few shades darker or lighter + langButton.addEventListener('mouseenter', () => { + langButton.style.backgroundColor = isDark ? '#424242' : '#e6e6e6'; + }); + langButton.addEventListener('mouseleave', () => { + langButton.style.backgroundColor = isDark ? '#373737' : '#f3f4f5'; + }); }); - // Get the icon for the language const langIcon = document.createElement('img'); langIcon.src = chrome.runtime.getURL(`src/assets/images/languages/${language}.svg`); - langIcon.style.width = '20px'; - langIcon.style.height = '20px'; - + langIcon.style.width = '14px'; + langIcon.style.height = '14px'; langButton.appendChild(langIcon); - let langName = document.createElement('span'); - langName.textContent = buttonLabel; - langName.style.fontSize = '12px'; - langName.style.paddingLeft = '15px'; + + const langName = document.createElement('span'); + langName.textContent = (language === "cpp") ? "C++" : (language.charAt(0).toUpperCase() + language.slice(1)); langButton.appendChild(langName); langButton.addEventListener('click', async () => { @@ -292,6 +394,14 @@ function createLanguageButtons(problem: any) { if (codeContainer && code) { codeContainer.style.display = 'flex'; codeContainer.textContent = code; + + chrome.storage.local.get(['isDarkTheme'], (result) => { + const isDark = result.isDarkTheme; + codeContainer.style.backgroundColor = isDark ? '#2d2d2d' : '#f7f9fa'; + codeContainer.style.color = isDark ? '#fff' : '#1a1a1a'; + codeContainer.style.border = `1px solid ${isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`; + }); + addCopyIconToElement(codeContainer); } else if (codeContainer) { codeContainer.style.display = 'flex'; @@ -301,7 +411,6 @@ function createLanguageButtons(problem: any) { container.append(langButton); }); return container; - } function addCopyIconToElement(element: HTMLElement) { @@ -311,20 +420,24 @@ function addCopyIconToElement(element: HTMLElement) { icon.style.height = '30px'; icon.style.padding = '5px'; icon.style.borderRadius = '5px'; - icon.style.border = '1px solid grey'; icon.style.cursor = 'pointer'; icon.style.marginRight = '20px'; - // on hover, change background color - icon.addEventListener('mouseover', () => { - icon.style.borderColor = 'lightgreen'; - }); - icon.addEventListener('mouseout', () => { - icon.style.borderColor = 'grey'; + icon.style.transition = 'all 0.2s ease'; + + chrome.storage.local.get(['isDarkTheme'], (result) => { + const isDark = result.isDarkTheme; + icon.style.border = `1px solid ${isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`; + + icon.addEventListener('mouseover', () => { + icon.style.borderColor = isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + }); + icon.addEventListener('mouseout', () => { + icon.style.borderColor = isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; + }); }); // On click event if you want to copy something when the icon is clicked icon.addEventListener('click', () => { - // Logic to copy whatever you want to clipboard let codeContainer = document.getElementsByClassName('code-container')[0] as HTMLDivElement; const textToCopy = codeContainer.textContent || ""; navigator.clipboard.writeText(textToCopy).then(() => { @@ -342,49 +455,314 @@ function addCopyIconToElement(element: HTMLElement) { element.insertBefore(icon, element.firstChild); } -chrome.runtime.onMessage.addListener((request) => { - // get discussion tab so we can insert the content before it - if (request.action === 'updateSolutions') { - chrome.storage.local.get(['leetcodeProblems'], (result) => { - const searchBar = document.querySelectorAll('input.block')[0].parentElement?.parentElement?.parentElement; - const title = request.title.split('-')[0].trim(); - const problem = result.leetcodeProblems.questions.find((problem: { title: string }) => problem.title === title); - - // If no solution code or videos exist, dont do anything. - if (!problem.videos && !problem.languages) return; - if (problem.videos.length == 0 && problem.languages.length == 0) { - return; +function updateThemeForElement(element: HTMLElement, isDark: boolean) { + if (!element) return; + + switch (element.className) { + case 'code-container': + element.style.backgroundColor = isDark ? '#2d2d2d' : '#f7f9fa'; + element.style.color = isDark ? '#fff' : '#1a1a1a'; + element.style.border = `1px solid ${isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`; + break; + case 'video-container': + const controls = element.querySelector('div') as HTMLElement; + if (controls) { + controls.style.backgroundColor = isDark ? '#373737' : '#f3f4f5'; + controls.style.color = isDark ? '#fff' : '#1a1a1a'; + controls.style.border = `1px solid ${isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`; + } + const channelElement = element.querySelector('#channel') as HTMLElement; + if (channelElement) { + channelElement.style.color = isDark ? '#fff' : '#1a1a1a'; } + const iframe = element.querySelector('iframe') as HTMLElement; + if (iframe) { + iframe.style.border = `1px solid ${isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`; + } + break; + case 'language-buttons-container': + const buttons = element.querySelectorAll('button'); + buttons.forEach(button => { + button.style.backgroundColor = isDark ? '#373737' : '#f3f4f5'; + button.style.color = isDark ? '#fff' : '#1a1a1a'; + button.style.border = `1px solid ${isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`; + + // Remove existing listeners + const oldMouseEnter = button.onmouseenter; + const oldMouseLeave = button.onmouseleave; + if (oldMouseEnter) button.removeEventListener('mouseenter', oldMouseEnter); + if (oldMouseLeave) button.removeEventListener('mouseleave', oldMouseLeave); + + // Add new theme-aware listeners + button.addEventListener('mouseenter', () => { + button.style.backgroundColor = isDark ? '#424242' : '#e6e6e6'; + button.style.borderColor = isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + }); + button.addEventListener('mouseleave', () => { + button.style.backgroundColor = isDark ? '#373737' : '#f3f4f5'; + button.style.borderColor = isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; + }); + }); + break; + } +} + +function setupThemeChangeListener() { + // Listen for our extension's theme changes + chrome.storage.onChanged.addListener((changes) => { + if (changes.isDarkTheme) { + const isDark = changes.isDarkTheme.newValue; + updateAllElements(isDark); + } + }); - // Check if the nav container already exists before adding - let existingNavContainer = document.querySelector('.nav-container'); - if (existingNavContainer) { - existingNavContainer.remove(); + // Listen for LeetCode's theme changes + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.target instanceof HTMLElement && mutation.target.tagName === 'BODY') { + chrome.storage.local.get(['themeMode'], (result) => { + // Only sync theme if in auto mode + if (result.themeMode === 'auto') { + const isDark = document.body.classList.contains('dark'); + // Update our extension's theme setting + chrome.storage.local.set({ isDarkTheme: isDark }); + updateAllElements(isDark); + } + }); } + }); + }); - // Create a new nav container (ensure that the 'createNavContainer' function is defined correctly and accessible) - const newNavContainer = createNavContainer(problem); - searchBar?.insertBefore(newNavContainer, searchBar.firstChild) + // Start observing the body element for class changes + observer.observe(document.body, { + attributes: true, + attributeFilter: ['class'] + }); +} - // Check if the video container already exists before adding - if (!document.querySelector('.video-container') && problem.videos.length > 0) { - let videoContainer = createVideoContainer(problem); - if (searchBar) searchBar.insertBefore(videoContainer, searchBar.children[1]); +function updateAllElements(isDark: boolean) { + const elements = [ + '.code-container', + '.video-container', + '.language-buttons-container', + '.nav-container' + ].map(selector => document.querySelector(selector) as HTMLElement); + + elements.forEach(element => { + if (element) { + if (element.classList.contains('nav-container')) { + // Update nav container buttons + const buttons = element.querySelectorAll('button'); + buttons.forEach(button => { + button.style.backgroundColor = isDark ? '#373737' : '#f3f4f5'; + button.style.color = isDark ? '#fff' : '#1a1a1a'; + button.style.border = `1px solid ${isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`; + + // Remove existing listeners + const oldMouseEnter = button.onmouseenter; + const oldMouseLeave = button.onmouseleave; + if (oldMouseEnter) button.removeEventListener('mouseenter', oldMouseEnter); + if (oldMouseLeave) button.removeEventListener('mouseleave', oldMouseLeave); + + // Add new theme-aware listeners + button.addEventListener('mouseenter', () => { + button.style.backgroundColor = isDark ? '#424242' : '#e6e6e6'; + button.style.borderColor = isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + }); + button.addEventListener('mouseleave', () => { + button.style.backgroundColor = isDark ? '#373737' : '#f3f4f5'; + button.style.borderColor = isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; + }); + }); + } else if (element.classList.contains('video-container')) { + // Update only the controls container and channel element colors + const controls = element.querySelector('div') as HTMLElement; + if (controls) { + controls.style.backgroundColor = isDark ? '#373737' : '#f3f4f5'; + controls.style.color = isDark ? '#fff' : '#1a1a1a'; + controls.style.border = `1px solid ${isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`; + } + const channelElement = element.querySelector('#channel') as HTMLElement; + if (channelElement) { + channelElement.style.color = isDark ? '#fff' : '#1a1a1a'; + } + } else { + updateThemeForElement(element, isDark); } + } + }); +} - // Check if the code container already exists before adding - if (!document.querySelector('.code-container') && problem.languages.length > 0) { - let codeContainer = createCodeContainer(); - if (searchBar) searchBar.insertBefore(codeContainer, searchBar.children[1]); +// Function to update the solutions tab content +function updateSolutionsTab(title: string) { + // Check if we're actually on the solutions tab before proceeding + const isSolutionsPage = /^https:\/\/leetcode\.com\/problems\/.*\/solutions\/?/.test(window.location.href); + if (!isSolutionsPage) return; + + // Check if we already have content for this problem + const existingWrapper = document.querySelector('.leetcode-explained-wrapper') as HTMLElement; + if (existingWrapper) { + const currentTitle = document.title.split('-')[0].trim(); + const wrapperTitle = existingWrapper.getAttribute('data-problem-title'); + + // If it's the same problem and the wrapper is in the DOM, preserve state + if (wrapperTitle === currentTitle && document.contains(existingWrapper)) { + console.log('Content exists for current problem, preserving state'); + return; + } + + // If it's a different problem or wrapper is detached, remove it + existingWrapper.remove(); + } + + chrome.storage.local.get(['leetcodeProblems'], (result) => { + // Try to find the search bar with retries + const maxRetries = 10; + const baseDelay = 300; + let retryCount = 0; + + const tryInsertContent = () => { + const searchBar = document.querySelectorAll('input.block')[0]?.parentElement?.parentElement?.parentElement; + + if (!searchBar && retryCount < maxRetries) { + // Use exponential backoff for retry delay + const delay = baseDelay * Math.pow(1.5, retryCount); + retryCount++; + console.log(`Attempt ${retryCount}: Waiting for search bar element to load... Retrying in ${delay}ms`); + setTimeout(tryInsertContent, delay); + return; } - // Check if the language buttons container already exists before adding - if (!document.querySelector('.language-buttons-container')) { - let languageButtonsContainer = createLanguageButtons(problem); - languageButtonsContainer.classList.add('language-buttons-container'); - languageButtonsContainer.style.display = 'none'; - if (searchBar) searchBar.insertBefore(languageButtonsContainer, searchBar.children[1]); // Or choose a different position + if (!searchBar) { + console.log('Failed to find search bar element after all retries'); + + // If still not found, set up a MutationObserver to watch for DOM changes + const observer = new MutationObserver((mutations, obs) => { + const searchBar = document.querySelectorAll('input.block')[0]?.parentElement?.parentElement?.parentElement; + if (searchBar) { + obs.disconnect(); // Stop observing once we find the element + // Only insert if we don't already have content for this problem + const existingWrapper = document.querySelector('.leetcode-explained-wrapper'); + if (!existingWrapper || !document.contains(existingWrapper)) { + insertContent(searchBar, title, result); + } + } + }); + + // Start observing the document with the configured parameters + observer.observe(document.body, { + childList: true, + subtree: true + }); + + return; } - }); + + // Only insert if we don't already have content for this problem + const existingWrapper = document.querySelector('.leetcode-explained-wrapper'); + if (!existingWrapper || !document.contains(existingWrapper)) { + insertContent(searchBar, title, result); + } + }; + + tryInsertContent(); + }); +} + +// Helper function to insert the content +function insertContent(searchBar: Element, title: string, result: any) { + const problemTitle = title.split('-')[0].trim(); + const problem = result.leetcodeProblems.questions.find((problem: { title: string }) => problem.title === problemTitle); + + // If no solution code or videos exist, don't do anything + if (!problem?.videos && !problem?.languages) return; + if (problem.videos?.length === 0 && problem.languages?.length === 0) return; + + // Create wrapper for all our custom content + const wrapper = createCustomContentWrapper(); + wrapper.setAttribute('data-problem-title', problemTitle); + + // Create and add nav container + const navContainer = createNavContainer(problem); + wrapper.appendChild(navContainer); + + // Add video container if videos exist + if (problem.videos?.length > 0) { + const videoContainer = createVideoContainer(problem); + wrapper.appendChild(videoContainer); + } + + // Add code container and language buttons if languages exist + if (problem.languages?.length > 0) { + const codeContainer = createCodeContainer(); + const languageButtonsContainer = createLanguageButtons(problem); + languageButtonsContainer.classList.add('language-buttons-container'); + languageButtonsContainer.style.display = 'none'; + + wrapper.appendChild(languageButtonsContainer); + wrapper.appendChild(codeContainer); + } + + // Insert the wrapper at the top of the solutions tab + searchBar.insertBefore(wrapper, searchBar.firstChild); + + // Show discussion by default + showContent('Discussion'); + + // Set up theme change listener + setupThemeChangeListener(); +} + +// Self-initialization function that runs when the content script loads +function initializeSolutionsTab() { + // Function to initialize content + const initialize = () => { + // Get the problem title from the page + const problemTitle = document.title.replace(' - LeetCode', ''); + + // Only update if we don't have content or if it's detached from DOM + const existingWrapper = document.querySelector('.leetcode-explained-wrapper'); + if (!existingWrapper || !document.contains(existingWrapper)) { + updateSolutionsTab(problemTitle); + } + }; + + // Set up page refresh detection using both URL and history state changes + let lastUrl = location.href; + let lastState = history.state; + + const observer = new MutationObserver(() => { + const currentUrl = location.href; + const currentState = history.state; + + // Check if this is a real navigation or just a tab switch + if (currentUrl !== lastUrl || JSON.stringify(currentState) !== JSON.stringify(lastState)) { + lastUrl = currentUrl; + lastState = currentState; + + if (currentUrl.includes('/solutions')) { + initialize(); + } + } + }); + + // Start observing URL changes + observer.observe(document, { subtree: true, childList: true }); + + // Initial load + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initialize); + } else { + initialize(); + } +} + +// Initialize the content script +initializeSolutionsTab(); + +// Listen for messages from background script +chrome.runtime.onMessage.addListener((request) => { + if (request.action === 'updateSolutions') { + updateSolutionsTab(request.title); } }); \ No newline at end of file diff --git a/src/popup/popup.css b/src/popup/popup.css index a624024..2a1d678 100644 --- a/src/popup/popup.css +++ b/src/popup/popup.css @@ -9,7 +9,7 @@ --border-color: black; --link-color: #303134; --text-color: black; - + --button-hover-bg: #e6e6e6; } /* Dark theme */ @@ -18,11 +18,10 @@ --border-color: #5f6368; --link-color: #8ab4f8; --button-bg-color: #303134; - --button-hover-bg: #3c4043; + --button-hover-bg: #424242; --text-color: #e8eaed; --code-bg-color: #303134; --info-message-bg: rgba(48, 49, 52, 0.5); - --button-hover-bg: black; } /* Display size variations */ @@ -78,8 +77,7 @@ body { } .material-button:hover, .code-btn:hover { - background-color: var(--button-hover-bg); - transform: translateY(-1px); + background-color: var(--button-hover-bg) !important; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } @@ -117,13 +115,12 @@ a { } .tab:hover { - color: var(--link-color); + background-color: var(--button-hover-bg) !important; } .tab.active { border-bottom: 2px solid var(--link-color); color: var(--link-color); - background: transparent; } /* Main content on popup and settings*/ diff --git a/src/popup/settings.ts b/src/popup/settings.ts index 7799c78..f3305a4 100644 --- a/src/popup/settings.ts +++ b/src/popup/settings.ts @@ -37,10 +37,9 @@ document.addEventListener('DOMContentLoaded', () => { // Apply the selected theme setTheme(selectedValue); - // Update LeetCode problem if active + // Update LeetCode problem if active - only update solutions tab chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { if (tabs[0] && tabs[0].id) { - chrome.tabs.sendMessage(tabs[0].id, { action: 'updateDescription', title: tabs[0].title || 'title' }); chrome.tabs.sendMessage(tabs[0].id, { action: 'updateSolutions', title: tabs[0].title || 'title' }); } }); diff --git a/src/problems-by-company/company.css b/src/problems-by-company/company.css index 6ce9e1d..39537cd 100644 --- a/src/problems-by-company/company.css +++ b/src/problems-by-company/company.css @@ -1,75 +1,189 @@ body { background-color: #202020; color: #f0f0f0; - font-family: 'Roboto', sans-serif; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; margin: 0; padding: 20px; display: flex; flex-direction: column; align-items: center; justify-content: center; + min-height: 100vh; } h1 { - font-size: 25px; - padding: 10px; - color: lightcyan; + font-size: 24px; + font-weight: 500; + padding: 16px; + color: #69c0ff; + margin-bottom: 24px; + text-align: center; } nav { display: flex; - justify-content: space-between; + justify-content: center; align-items: center; - margin-bottom: 20px; + gap: 12px; + margin-bottom: 32px; + flex-wrap: wrap; + width: 100%; + max-width: 800px; } - button, .row input { background-color: #373737; color: #fff; - border: none; - padding: 10px; - min-width: 400px; + border: 1px solid rgba(255, 255, 255, 0.1); + padding: 10px 16px; + min-width: 120px; max-width: 100%; - border-radius: 5px; + border-radius: 6px; + font-size: 14px; + transition: all 0.2s ease; } -button:hover, -a:hover { +nav button { + font-weight: 500; + letter-spacing: 0.8px; + text-transform: uppercase; +} + +button:hover { background-color: #424242; + border-color: rgba(255, 255, 255, 0.2); cursor: pointer; + transform: translateY(-1px); + color: #ffd700; } table { - width: 700px; - margin: auto; - border-collapse: collapse; + width: 100%; + max-width: 800px; + margin: 0 auto; + border-collapse: separate; + border-spacing: 0; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } th, td { - border: 1px solid #FFFFFF; - padding: 10px; - text-align: center; + padding: 12px 16px; + text-align: left; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); } th { background-color: #424242; - color: lightcyan; + color: #40a9ff; + font-weight: 500; + font-size: 14px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +td { + font-size: 14px; +} + +tr:last-child td { + border-bottom: none; } a { - color: lightgoldenrodyellow; + color: #40a9ff; text-decoration: none; + transition: color 0.2s ease; } a:hover { - color: lightgreen; + color: #69c0ff; + background-color: transparent; } -.header:hover { - border: 1px solid orange; +.header { + position: relative; cursor: pointer; - color: orange; + user-select: none; +} + +.header:hover { + color: #69c0ff; +} + +.row { + width: 100%; + max-width: 800px; + margin-bottom: 24px; +} + +.row input { + width: 100%; + height: 32px; + background-color: #373737; + color: #fff; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 6px; + padding: 8px 16px; + font-size: 14px; + transition: all 0.2s ease; +} + +.row input:focus { + outline: none; + border-color: #40a9ff; + box-shadow: 0 0 0 2px rgba(64, 169, 255, 0.2); +} + +/* Add smooth transitions for interactive elements */ +button, +a, +.header, +input { + transition: all 0.2s ease; +} + +/* Acceptance rate colors - smooth gradient */ +td[data-acceptance] { + position: relative; +} + +td[data-acceptance]::after { + content: attr(data-acceptance) '%'; + font-weight: 500; +} + +/* Create a smooth gradient from red to yellow to green */ +td[data-acceptance^="9"] { color: #52c41a; } +td[data-acceptance^="8"] { color: #73d13d; } +td[data-acceptance^="7"] { color: #95de64; } +td[data-acceptance^="6"] { color: #b7eb8f; } +td[data-acceptance^="5"] { color: #fadb14; } +td[data-acceptance^="4"] { color: #ffa940; } +td[data-acceptance^="3"] { color: #ff7a45; } +td[data-acceptance^="2"] { color: #ff4d4f; } +td[data-acceptance^="1"] { color: #f5222d; } +td[data-acceptance^="0"] { color: #cf1322; } + +/* Responsive design */ +@media (max-width: 768px) { + body { + padding: 16px; + } + + table { + font-size: 14px; + } + + th, + td { + padding: 8px 12px; + } + + button { + min-width: 100px; + } } \ No newline at end of file diff --git a/src/problems-by-company/company.ts b/src/problems-by-company/company.ts index 94432fa..6667979 100644 --- a/src/problems-by-company/company.ts +++ b/src/problems-by-company/company.ts @@ -143,7 +143,8 @@ function rebuildTable() { row.insertCell(2).innerHTML = `${solution.title}`; const acceptanceCell = row.insertCell(3); - acceptanceCell.innerText = solution.acceptance ? (parseFloat(solution.acceptance) * 100).toFixed(2) + '%' : 'N/A'; + const acceptanceRate = solution.acceptance ? (parseFloat(solution.acceptance) * 100).toFixed(2) : 'N/A'; + acceptanceCell.setAttribute('data-acceptance', acceptanceRate.toString()); acceptanceCell.style.fontSize = '12px'; const rankCell = row.insertCell(4);