From 9b4aa7d0571b530dbf608e7bb037a2c770e193c3 Mon Sep 17 00:00:00 2001 From: zubyj Date: Sun, 6 Apr 2025 21:25:16 -0700 Subject: [PATCH 01/23] css update: use simple animation, colors, etc --- src/content-script/update-description-tab.ts | 174 ++++---- src/content-script/update-solutions-tab.ts | 399 ++++++++++++------- src/problems-by-company/company.css | 164 ++++++-- src/problems-by-company/company.ts | 3 +- 4 files changed, 490 insertions(+), 250 deletions(-) diff --git a/src/content-script/update-description-tab.ts b/src/content-script/update-description-tab.ts index 4e0c11d..fab22fd 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,52 +97,47 @@ 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); } - } + }); }); } @@ -184,65 +181,66 @@ 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, - }); - }; + const problem = result.leetcodeProblems.questions.find((p: Problem) => p.title === problemTitle); + if (!problem?.companies?.length) return; - button.style.display = 'flex'; - button.style.alignItems = 'center'; - button.style.justifyContent = 'center'; + const topCompanies = problem.companies.slice(0, 5); + topCompanies.forEach((company: { name: string; }) => { + const button = document.createElement('button'); + button.onclick = () => { + chrome.runtime.sendMessage({ + action: 'openCompanyPage', + company: company.name, + }); + }; - 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.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'; - 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; + 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)'}`; - chrome.storage.local.get(['isDarkTheme'], (result) => { - const isDark = result.isDarkTheme; - applyButtonTheme(button, isDark); + 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)'; }); - - const companyName = document.createTextNode(`${company.name}`); - button.appendChild(companyName); - companyTagContainer.appendChild(button); }); - } + + 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; } diff --git a/src/content-script/update-solutions-tab.ts b/src/content-script/update-solutions-tab.ts index 8b12c97..04018b5 100644 --- a/src/content-script/update-solutions-tab.ts +++ b/src/content-script/update-solutions-tab.ts @@ -4,19 +4,31 @@ const VIDEO_ASPECT_RATIO = 56.25; // 16:9 aspect ratio 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'; + 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)'}`; + - }) + // on hover just make the background a few shades darker or lighter + button.addEventListener('mouseenter', () => { + button.style.backgroundColor = isDark ? '#424242' : '#e6e6e6'; + }); + button.addEventListener('mouseleave', () => { + 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'; + return button; } @@ -26,77 +38,113 @@ 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; - const controlsContainer = createStyledElement('div', { display: 'flex', justifyContent: 'center', 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; + // channel element is white on dark mode and black on light mode + 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 +153,86 @@ function updateVideo(iframe: HTMLIFrameElement, videoUrl: string) { } function createCodeContainer() { - // Create an HTML element to hold the code 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.borderRadius = '8px'; + codeElement.style.padding = '16px'; + codeElement.style.marginTop = '24px'; codeElement.style.width = '95%'; - codeElement.style.fontSize = '12px'; + codeElement.style.fontSize = '14px'; codeElement.style.marginLeft = '2.5%'; - codeElement.style.padding = '10px'; - codeElement.style.maxHeight = '400px'; + codeElement.style.maxHeight = '500px'; codeElement.style.overflowY = 'auto'; + 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'; + }); + return codeElement; } 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'; - - let navContainer = document.getElementsByClassName('nav-container')[0] as HTMLDivElement; - if (navContainer) navContainer.style.display = 'flex'; - - let videoContainer = document.querySelector('div.video-container') as HTMLDivElement; - if (videoContainer) { - videoContainer.style.paddingBottom = '0%'; - videoContainer.style.display = 'none'; - } + const elements = [ + '.code-container', + '.language-buttons-container', + '.video-container' + ].map(selector => document.querySelector(selector) as HTMLElement); + + elements.forEach(element => { + if (element) { + if (element.classList.contains('video-container')) { + element.style.paddingBottom = '0'; + } + element.style.display = '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', () => { + const buttons = [ + { text: 'Discussion', show: true }, + { text: 'Video', show: problem.videos?.length > 0 }, + { text: 'Code', show: problem.languages?.length > 0 } + ]; + + const activeButton = buttons[0]; + buttons.forEach(({ text, show }) => { + if (!show) return; + + const button = createStyledButton(text, text === activeButton.text); + button.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'; - }); - 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'; + if (text === 'Video') { + const videoContainer = document.querySelector('.video-container') as HTMLElement; + if (videoContainer) { + videoContainer.style.display = 'flex'; + videoContainer.style.paddingBottom = `${VIDEO_ASPECT_RATIO}%`; + } + } else if (text === 'Code') { + const elements = ['.code-container', '.language-buttons-container'] + .map(selector => document.querySelector(selector) as HTMLElement); + elements.forEach(el => el && (el.style.display = 'flex')); + } }); - navContainer.append(codeButton); - } + navContainer.append(button); + }); return navContainer; } @@ -253,37 +293,39 @@ 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)'}`; }); - // 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 +334,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 +351,6 @@ function createLanguageButtons(problem: any) { container.append(langButton); }); return container; - } function addCopyIconToElement(element: HTMLElement) { @@ -311,20 +360,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,6 +395,77 @@ function addCopyIconToElement(element: HTMLElement) { element.insertBefore(icon, element.firstChild); } +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() { + chrome.storage.onChanged.addListener((changes) => { + if (changes.isDarkTheme) { + const isDark = changes.isDarkTheme.newValue; + const elements = [ + '.code-container', + '.video-container', + '.language-buttons-container' + ].map(selector => document.querySelector(selector) as HTMLElement); + + elements.forEach(element => { + if (element) { + updateThemeForElement(element, isDark); + } + }); + } + }); +} + chrome.runtime.onMessage.addListener((request) => { // get discussion tab so we can insert the content before it if (request.action === 'updateSolutions') { @@ -385,6 +509,9 @@ chrome.runtime.onMessage.addListener((request) => { languageButtonsContainer.style.display = 'none'; if (searchBar) searchBar.insertBefore(languageButtonsContainer, searchBar.children[1]); // Or choose a different position } + + // Add theme change listener after creating containers + setupThemeChangeListener(); }); } }); \ No newline at end of file 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); From a66d66f40ade960abb497b774dce0c7f852001d9 Mon Sep 17 00:00:00 2001 From: zubyj Date: Sun, 6 Apr 2025 21:39:59 -0700 Subject: [PATCH 02/23] fix company tags and video/code containers not updating theme until page refresh --- src/content-script/update-description-tab.ts | 81 +++++++++++++++++--- src/content-script/update-solutions-tab.ts | 77 ++++++++++++++++--- 2 files changed, 135 insertions(+), 23 deletions(-) diff --git a/src/content-script/update-description-tab.ts b/src/content-script/update-description-tab.ts index fab22fd..b3bb3d1 100644 --- a/src/content-script/update-description-tab.ts +++ b/src/content-script/update-description-tab.ts @@ -195,6 +195,7 @@ function loadCompanyTags(problemTitle: string, companyTagContainer: HTMLElement) 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', @@ -214,18 +215,7 @@ function loadCompanyTags(problemTitle: string, companyTagContainer: HTMLElement) chrome.storage.local.get(['isDarkTheme'], (result) => { const isDark = result.isDarkTheme; - 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', () => { - 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)'; - }); + updateCompanyTagStyle(button, isDark); }); const icon = document.createElement('img'); @@ -244,6 +234,69 @@ function loadCompanyTags(problemTitle: string, companyTagContainer: HTMLElement) 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 @@ -252,6 +305,9 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 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; @@ -262,3 +318,4 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { // Return true to indicate we will send a response asynchronously (needed for sendResponse) return true; }); + diff --git a/src/content-script/update-solutions-tab.ts b/src/content-script/update-solutions-tab.ts index 04018b5..ff7bc8b 100644 --- a/src/content-script/update-solutions-tab.ts +++ b/src/content-script/update-solutions-tab.ts @@ -448,20 +448,75 @@ function updateThemeForElement(element: HTMLElement, isDark: boolean) { } function setupThemeChangeListener() { + // Listen for our extension's theme changes chrome.storage.onChanged.addListener((changes) => { if (changes.isDarkTheme) { const isDark = changes.isDarkTheme.newValue; - const elements = [ - '.code-container', - '.video-container', - '.language-buttons-container' - ].map(selector => document.querySelector(selector) as HTMLElement); - - elements.forEach(element => { - if (element) { - updateThemeForElement(element, isDark); - } - }); + updateAllElements(isDark); + } + }); + + // 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); + } + }); + } + }); + }); + + // Start observing the body element for class changes + observer.observe(document.body, { + attributes: true, + attributeFilter: ['class'] + }); +} + +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 { + updateThemeForElement(element, isDark); + } } }); } From c8f3eaf716ca05c0d7116694767869a888cf852d Mon Sep 17 00:00:00 2001 From: zubyj Date: Sun, 6 Apr 2025 22:06:13 -0700 Subject: [PATCH 03/23] fix company tags not adding sometimes --- src/content-script/update-description-tab.ts | 113 ++++++++++++++++--- 1 file changed, 99 insertions(+), 14 deletions(-) diff --git a/src/content-script/update-description-tab.ts b/src/content-script/update-description-tab.ts index b3bb3d1..4c7ecac 100644 --- a/src/content-script/update-description-tab.ts +++ b/src/content-script/update-description-tab.ts @@ -154,25 +154,72 @@ function showCompanyTags(problemTitle: string) { return; } - if (companyTagContainer) { - while (companyTagContainer.firstChild) { - companyTagContainer.firstChild.remove(); - } - } else { - companyTagContainer = document.createElement('div'); - companyTagContainer.id = 'companyTagContainer'; - companyTagContainer.style.display = 'flex'; - companyTagContainer.style.flexDirection = 'row'; - companyTagContainer.style.marginBottom = '20px'; - companyTagContainer.style.gap = '5px'; + // Try to find the description element with retries + const maxRetries = 10; + const baseDelay = 300; + let retryCount = 0; + const tryInsertCompanyTags = () => { const description = document.getElementsByClassName('elfjS')[0]; - if (description) { + + 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; + } + + 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) => { + 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; + } + + insertCompanyTags(description); + }; + + const insertCompanyTags = (description: Element) => { + // Check if container already exists + if (companyTagContainer) { + // Clear existing tags + while (companyTagContainer.firstChild) { + companyTagContainer.firstChild.remove(); + } + } else { + // Create new container + companyTagContainer = document.createElement('div'); + companyTagContainer.id = 'companyTagContainer'; + companyTagContainer.style.display = 'flex'; + companyTagContainer.style.flexDirection = 'row'; + companyTagContainer.style.marginBottom = '20px'; + companyTagContainer.style.gap = '5px'; + description.insertBefore(companyTagContainer, description.firstChild); } - } - loadCompanyTags(problemTitle, companyTagContainer); + // Load and inject company tags + loadCompanyTags(problemTitle, companyTagContainer); + }; + + // Start the retry process + tryInsertCompanyTags(); }); } @@ -319,3 +366,41 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { return true; }); +// Self-initialization function that runs when the content script loads +function initializeContentScript() { + // Wait for the DOM to be fully loaded + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', onDOMReady); + } else { + onDOMReady(); + } + + function onDOMReady() { + // Check if we're on a LeetCode problem page + const isProblemPage = /^https:\/\/leetcode\.com\/problems\/.*\/?/.test(window.location.href); + + if (isProblemPage) { + console.log('LeetCode problem page detected, initializing content script...'); + + // Extract the problem title from the page title + const pageTitle = document.title; + const problemTitle = pageTitle.split('-')[0].trim(); + + // Detect theme on first load of a problem page + detectAndSyncTheme(); + showExamples(); + showCompanyTags(problemTitle); + showDifficulty(); + showRating(problemTitle); + + // Add theme change listener after creating company tags + setupDescriptionThemeListener(); + + console.log('Content script initialized for problem:', problemTitle); + } + } +} + +// Run the initialization +initializeContentScript(); + From 827ddefc4dd9f1ba6bdcb2448526c8eb4d7bdfc1 Mon Sep 17 00:00:00 2001 From: zubyj Date: Sun, 6 Apr 2025 23:49:55 -0700 Subject: [PATCH 04/23] add hover effect to code language selector --- src/content-script/update-solutions-tab.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/content-script/update-solutions-tab.ts b/src/content-script/update-solutions-tab.ts index ff7bc8b..2af42c3 100644 --- a/src/content-script/update-solutions-tab.ts +++ b/src/content-script/update-solutions-tab.ts @@ -316,6 +316,14 @@ function createLanguageButtons(problem: any) { 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'; + }); }); const langIcon = document.createElement('img'); From a2ee98e0547b592d9564d4b839207cafb43b1baa Mon Sep 17 00:00:00 2001 From: zubyj Date: Mon, 7 Apr 2025 12:18:38 -0700 Subject: [PATCH 05/23] fix company tags getting added multiple times --- src/content-script/update-description-tab.ts | 14 ++++++++++++++ src/popup/settings.ts | 3 +-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/content-script/update-description-tab.ts b/src/content-script/update-description-tab.ts index 4c7ecac..7a88a9c 100644 --- a/src/content-script/update-description-tab.ts +++ b/src/content-script/update-description-tab.ts @@ -147,11 +147,25 @@ function showCompanyTags(problemTitle: string) { const showCompanyTags = result.showCompanyTags; let companyTagContainer = document.getElementById('companyTagContainer'); + // First handle visibility if (!showCompanyTags) { if (companyTagContainer) { companyTagContainer.style.display = 'none'; } return; + } else if (companyTagContainer && companyTagContainer instanceof HTMLElement) { + companyTagContainer.style.display = 'flex'; + // If container exists and is visible, just update styles + chrome.storage.local.get(['isDarkTheme'], (result) => { + const isDark = result.isDarkTheme; + const tags = companyTagContainer.querySelectorAll('.company-tag'); + tags.forEach(tag => { + if (tag instanceof HTMLElement) { + updateCompanyTagStyle(tag, isDark); + } + }); + }); + return; } // Try to find the description element with retries 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' }); } }); From 1086f58e78f92d9e4c27c1e20e7da365056725f7 Mon Sep 17 00:00:00 2001 From: zubyj Date: Mon, 7 Apr 2025 12:23:40 -0700 Subject: [PATCH 06/23] fix: when switching yt video, nav buttons readded into page --- src/content-script/update-solutions-tab.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/content-script/update-solutions-tab.ts b/src/content-script/update-solutions-tab.ts index 2af42c3..52cf8ba 100644 --- a/src/content-script/update-solutions-tab.ts +++ b/src/content-script/update-solutions-tab.ts @@ -4,7 +4,7 @@ const VIDEO_ASPECT_RATIO = 56.25; // 16:9 aspect ratio function createStyledButton(text: string, isActive: boolean = false): HTMLButtonElement { const button = document.createElement('button'); button.textContent = text; - + chrome.storage.local.get(['isDarkTheme'], (result) => { const isDark = result.isDarkTheme; button.style.backgroundColor = isDark ? '#373737' : '#f3f4f5'; @@ -543,16 +543,13 @@ chrome.runtime.onMessage.addListener((request) => { return; } - // Check if the nav container already exists before adding + // Only create nav container if it doesn't exist or if this is not a video update let existingNavContainer = document.querySelector('.nav-container'); - if (existingNavContainer) { - existingNavContainer.remove(); + if (!existingNavContainer) { + const newNavContainer = createNavContainer(problem); + searchBar?.insertBefore(newNavContainer, searchBar.firstChild); } - // Create a new nav container (ensure that the 'createNavContainer' function is defined correctly and accessible) - const newNavContainer = createNavContainer(problem); - searchBar?.insertBefore(newNavContainer, searchBar.firstChild) - // Check if the video container already exists before adding if (!document.querySelector('.video-container') && problem.videos.length > 0) { let videoContainer = createVideoContainer(problem); From e4cc30fb40b3dac1721c7868c5aafeaf0d427d79 Mon Sep 17 00:00:00 2001 From: zubyj Date: Mon, 7 Apr 2025 12:46:28 -0700 Subject: [PATCH 07/23] fix: dynamically opening problem not updatint company tags, video, code on page --- manifest.json | 3 +- src/background/background.ts | 121 ++++++--------------- src/content-script/update-solutions-tab.ts | 13 +++ 3 files changed, 47 insertions(+), 90 deletions(-) diff --git a/manifest.json b/manifest.json index 2fa4754..8740f4d 100644 --- a/manifest.json +++ b/manifest.json @@ -14,7 +14,8 @@ }, "permissions": [ "storage", - "tabs" + "tabs", + "webNavigation" ], "host_permissions": [ "https://api.leetcodeapp.com/*" diff --git a/src/background/background.ts b/src/background/background.ts index 3609b78..6168871 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -1,105 +1,48 @@ -// Helper function to get or create user ID -function getRandomToken(): string { - const randomPool = new Uint8Array(32); - crypto.getRandomValues(randomPool); - return Array.from(randomPool) - .map(b => b.toString(16).padStart(2, '0')) - .join(''); -} - -// Load problem data & default settings on install -chrome.runtime.onInstalled.addListener(() => { - // Generate and store user ID if it doesn't exist - chrome.storage.sync.get('userId', function (items) { - if (!items.userId) { - const userId = getRandomToken(); - chrome.storage.sync.set({ userId: userId }); - } - }); - - // Load JSON file of problem data into storage - const leetcodeProblems = chrome.runtime.getURL('src/assets/data/problem_data.json'); - fetch(leetcodeProblems) - .then((response) => response.json()) - .then((data) => { - chrome.storage.local.set({ leetcodeProblems: data }); - }) - .catch((error) => { - console.error(error); - }); - - // Load problems by company JSON file into storage - const companyProblems = chrome.runtime.getURL('src/assets/data/problems_by_company.json'); - fetch(companyProblems) - .then((response) => response.json()) - .then((data) => { - chrome.storage.local.set({ companyProblems: data }); - }) - .catch((error) => { - console.error(error); - }); - - // Load default settings - chrome.storage.local.set({ fontSize: 12 }); // Default to small display size - chrome.storage.local.set({ showExamples: true }); - chrome.storage.local.set({ showDifficulty: true }); - chrome.storage.local.set({ showRating: true }); - chrome.storage.local.set({ showCompanyTags: true }); - // Set default theme to auto mode and default to dark - chrome.storage.local.set({ isDarkTheme: true }); // Default to dark theme - chrome.storage.local.set({ themeMode: 'auto' }); -}); - -chrome.runtime.onMessage.addListener((request) => { - if (request.action === 'openCompanyPage') { - chrome.storage.local.set({ clickedCompany: request.company }); - chrome.tabs.create({ - url: chrome.runtime.getURL('src/problems-by-company/company.html'), - active: true, - }, function (tab) { - // Remove the listener once the tab is loaded - const listener = function (tabId: number, changedProps: any) { - if (tabId == tab.id && changedProps.status == 'complete') { - chrome.tabs.sendMessage(tabId, request); - chrome.tabs.onUpdated.removeListener(listener); - } - }; - // Attach the listener - chrome.tabs.onUpdated.addListener(listener); - }); - } -}); - chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { if (changeInfo.status === 'complete' && tab.url) { const url = tab.url; let problemUrl = /^https:\/\/leetcode\.com\/problems\/.*\/?/; if (url.match(problemUrl)) { - chrome.storage.local.get(['currentLeetCodeProblemTitle', 'descriptionTabUpdated', 'solutionsTabUpdated'], (result) => { + chrome.storage.local.get(['currentLeetCodeProblemTitle'], (result) => { let lastTitle = result.currentLeetCodeProblemTitle || ''; - let descriptionTabUpdated = result.descriptionTabUpdated || false; - let solutionsTabUpdated = result.solutionsTabUpdated || false; + + // If the title has changed, we need to update both tabs if (tab.title !== lastTitle) { chrome.storage.local.set({ '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' }); - } - - 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' }); + // Force update both tabs when problem changes + chrome.tabs.sendMessage(tabId, { + action: 'updateDescription', + title: tab.title || 'title', + forceUpdate: true + }); + chrome.tabs.sendMessage(tabId, { + action: 'updateSolutions', + title: tab.title || 'title', + forceUpdate: true + }); + } else { + // If we're on the same problem but URL changed, update appropriate tab + let descriptionUrl = /^https:\/\/leetcode\.com\/problems\/.*\/(description\/)?/; + let solutionsUrl = /^https:\/\/leetcode\.com\/problems\/.*\/solutions\/?/; + + if (url.match(descriptionUrl)) { + chrome.tabs.sendMessage(tabId, { + action: 'updateDescription', + title: tab.title || 'title' + }); + } + + if (url.match(solutionsUrl)) { + chrome.tabs.sendMessage(tabId, { + action: 'updateSolutions', + title: tab.title || 'title' + }); + } } }); } diff --git a/src/content-script/update-solutions-tab.ts b/src/content-script/update-solutions-tab.ts index 52cf8ba..4a4c3f5 100644 --- a/src/content-script/update-solutions-tab.ts +++ b/src/content-script/update-solutions-tab.ts @@ -543,6 +543,19 @@ chrome.runtime.onMessage.addListener((request) => { return; } + // If forceUpdate is true or this is not a video update, remove existing containers + if (request.forceUpdate) { + const existingContainers = [ + '.nav-container', + '.video-container', + '.code-container', + '.language-buttons-container' + ].forEach(selector => { + const element = document.querySelector(selector); + if (element) element.remove(); + }); + } + // Only create nav container if it doesn't exist or if this is not a video update let existingNavContainer = document.querySelector('.nav-container'); if (!existingNavContainer) { From 49570830037ab246efec15d430c9870ef1c9e11a Mon Sep 17 00:00:00 2001 From: zubyj Date: Mon, 7 Apr 2025 12:54:25 -0700 Subject: [PATCH 08/23] fix company tags getting added multiple times --- src/content-script/update-description-tab.ts | 56 ++++++++++++-------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/src/content-script/update-description-tab.ts b/src/content-script/update-description-tab.ts index 7a88a9c..72e4ba1 100644 --- a/src/content-script/update-description-tab.ts +++ b/src/content-script/update-description-tab.ts @@ -158,12 +158,14 @@ function showCompanyTags(problemTitle: string) { // If container exists and is visible, just update styles chrome.storage.local.get(['isDarkTheme'], (result) => { const isDark = result.isDarkTheme; - const tags = companyTagContainer.querySelectorAll('.company-tag'); - tags.forEach(tag => { - if (tag instanceof HTMLElement) { - updateCompanyTagStyle(tag, isDark); - } - }); + const tags = companyTagContainer?.querySelectorAll('.company-tag'); + if (tags) { + tags.forEach(tag => { + if (tag instanceof HTMLElement) { + updateCompanyTagStyle(tag, isDark); + } + }); + } }); return; } @@ -174,6 +176,11 @@ function showCompanyTags(problemTitle: string) { let retryCount = 0; const tryInsertCompanyTags = () => { + // First check if container already exists to prevent duplicates + if (document.getElementById('companyTagContainer')) { + return; + } + const description = document.getElementsByClassName('elfjS')[0]; if (!description && retryCount < maxRetries) { @@ -190,6 +197,12 @@ function showCompanyTags(problemTitle: string) { // 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 @@ -210,26 +223,23 @@ function showCompanyTags(problemTitle: string) { }; const insertCompanyTags = (description: Element) => { - // Check if container already exists - if (companyTagContainer) { - // Clear existing tags - while (companyTagContainer.firstChild) { - companyTagContainer.firstChild.remove(); - } - } else { - // Create new container - companyTagContainer = document.createElement('div'); - companyTagContainer.id = 'companyTagContainer'; - companyTagContainer.style.display = 'flex'; - companyTagContainer.style.flexDirection = 'row'; - companyTagContainer.style.marginBottom = '20px'; - companyTagContainer.style.gap = '5px'; - - description.insertBefore(companyTagContainer, description.firstChild); + // 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, companyTagContainer); + loadCompanyTags(problemTitle, newCompanyTagContainer); }; // Start the retry process From 3f47d9071540f4808a38aff06354a96eee8dc484 Mon Sep 17 00:00:00 2001 From: zubyj Date: Mon, 7 Apr 2025 14:37:05 -0700 Subject: [PATCH 09/23] fix company tags not opening top problems page --- src/background/background.ts | 124 ++++++++++++++----- src/content-script/update-description-tab.ts | 4 +- 2 files changed, 92 insertions(+), 36 deletions(-) diff --git a/src/background/background.ts b/src/background/background.ts index 6168871..369327c 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -1,51 +1,107 @@ +// Helper function to get or create user ID +function getRandomToken(): string { + const randomPool = new Uint8Array(32); + crypto.getRandomValues(randomPool); + return Array.from(randomPool) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); +} + +// Load problem data & default settings on install +chrome.runtime.onInstalled.addListener(() => { + // Generate and store user ID if it doesn't exist + chrome.storage.sync.get('userId', function (items) { + if (!items.userId) { + const userId = getRandomToken(); + chrome.storage.sync.set({ userId: userId }); + } + }); + + // Load JSON file of problem data into storage + const leetcodeProblems = chrome.runtime.getURL('src/assets/data/problem_data.json'); + fetch(leetcodeProblems) + .then((response) => response.json()) + .then((data) => { + chrome.storage.local.set({ leetcodeProblems: data }); + }) + .catch((error) => { + console.error(error); + }); + + // Load problems by company JSON file into storage + const companyProblems = chrome.runtime.getURL('src/assets/data/problems_by_company.json'); + fetch(companyProblems) + .then((response) => response.json()) + .then((data) => { + chrome.storage.local.set({ companyProblems: data }); + }) + .catch((error) => { + console.error(error); + }); + + // Load default settings + chrome.storage.local.set({ fontSize: 12 }); // Default to small display size + chrome.storage.local.set({ showExamples: true }); + chrome.storage.local.set({ showDifficulty: true }); + chrome.storage.local.set({ showRating: true }); + chrome.storage.local.set({ showCompanyTags: true }); + // Set default theme to auto mode and default to dark + chrome.storage.local.set({ isDarkTheme: true }); // Default to dark theme + chrome.storage.local.set({ themeMode: 'auto' }); +}); + +chrome.runtime.onMessage.addListener((request) => { + if (request.action === 'openCompanyPage') { + chrome.storage.local.set({ clickedCompany: request.company }); + chrome.tabs.create({ + url: chrome.runtime.getURL('src/problems-by-company/company.html'), + active: true, + }, function (tab) { + // Remove the listener once the tab is loaded + const listener = function (tabId: number, changedProps: any) { + if (tabId == tab.id && changedProps.status == 'complete') { + chrome.tabs.sendMessage(tabId, request); + chrome.tabs.onUpdated.removeListener(listener); + } + }; + // Attach the listener + chrome.tabs.onUpdated.addListener(listener); + }); + } +}); + chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { if (changeInfo.status === 'complete' && tab.url) { const url = tab.url; let problemUrl = /^https:\/\/leetcode\.com\/problems\/.*\/?/; if (url.match(problemUrl)) { - chrome.storage.local.get(['currentLeetCodeProblemTitle'], (result) => { + chrome.storage.local.get(['currentLeetCodeProblemTitle', 'descriptionTabUpdated', 'solutionsTabUpdated'], (result) => { let lastTitle = result.currentLeetCodeProblemTitle || ''; - - // If the title has changed, we need to update both tabs + let descriptionTabUpdated = result.descriptionTabUpdated || false; + let solutionsTabUpdated = result.solutionsTabUpdated || false; if (tab.title !== lastTitle) { chrome.storage.local.set({ 'currentLeetCodeProblemTitle': tab.title, 'descriptionTabUpdated': false, 'solutionsTabUpdated': false }); + // If the title has changed, we reset both flags + descriptionTabUpdated = false; + solutionsTabUpdated = false; + } - // Force update both tabs when problem changes - chrome.tabs.sendMessage(tabId, { - action: 'updateDescription', - title: tab.title || 'title', - forceUpdate: true - }); - chrome.tabs.sendMessage(tabId, { - action: 'updateSolutions', - title: tab.title || 'title', - forceUpdate: true - }); - } else { - // If we're on the same problem but URL changed, update appropriate tab - let descriptionUrl = /^https:\/\/leetcode\.com\/problems\/.*\/(description\/)?/; - let solutionsUrl = /^https:\/\/leetcode\.com\/problems\/.*\/solutions\/?/; - - if (url.match(descriptionUrl)) { - chrome.tabs.sendMessage(tabId, { - action: 'updateDescription', - title: tab.title || 'title' - }); - } - - if (url.match(solutionsUrl)) { - chrome.tabs.sendMessage(tabId, { - action: 'updateSolutions', - title: tab.title || 'title' - }); - } + 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' }); + } + + 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' }); } }); } } -}); - +}); \ 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 72e4ba1..4514245 100644 --- a/src/content-script/update-description-tab.ts +++ b/src/content-script/update-description-tab.ts @@ -269,8 +269,7 @@ function loadCompanyTags(problemTitle: string, companyTagContainer: HTMLElement) button.classList.add('company-tag'); button.onclick = () => { chrome.runtime.sendMessage({ - action: 'openCompanyPage', - company: company.name, + action: 'openCompanyPage', company: company.name, }); }; @@ -283,6 +282,7 @@ function loadCompanyTags(problemTitle: string, companyTagContainer: HTMLElement) button.style.letterSpacing = '.5px'; button.style.transition = 'all 0.2s ease'; button.style.cursor = 'pointer'; + chrome.storage.local.get(['isDarkTheme'], (result) => { const isDark = result.isDarkTheme; From c3800a65602518f3c155ff894b75dad893b4f205 Mon Sep 17 00:00:00 2001 From: zubyj Date: Mon, 7 Apr 2025 22:41:54 -0700 Subject: [PATCH 10/23] rm moving popup buttons on hover --- src/popup/popup.css | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/popup/popup.css b/src/popup/popup.css index a624024..5a4086b 100644 --- a/src/popup/popup.css +++ b/src/popup/popup.css @@ -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); } From a85d4eb3719a2a39a4e06239f6d6af3ae28f3fc7 Mon Sep 17 00:00:00 2001 From: zubyj Date: Mon, 7 Apr 2025 22:55:33 -0700 Subject: [PATCH 11/23] on popup nav btn hover, slightly change bg color --- src/popup/popup.css | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/popup/popup.css b/src/popup/popup.css index 5a4086b..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 */ @@ -115,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*/ From 21bd1b283e220d10683c714ad1ffe1bece43fc77 Mon Sep 17 00:00:00 2001 From: zubyj Date: Tue, 8 Apr 2025 22:37:03 -0700 Subject: [PATCH 12/23] fix dynamic routing, but refreshing removes solution tab update --- src/background/background.ts | 25 +++++++--- src/content-script/update-description-tab.ts | 7 +-- src/content-script/update-solutions-tab.ts | 49 +++++++++----------- 3 files changed, 44 insertions(+), 37 deletions(-) diff --git a/src/background/background.ts b/src/background/background.ts index 369327c..6906ed3 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -71,33 +71,44 @@ chrome.runtime.onMessage.addListener((request) => { }); 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\/.*\/?/; + + // Only proceed if this is a leetcode problem page if (url.match(problemUrl)) { - chrome.storage.local.get(['currentLeetCodeProblemTitle', 'descriptionTabUpdated', 'solutionsTabUpdated'], (result) => { + // Extract the problem path from the URL + const problemPath = url.match(/\/problems\/([^/]+)/)?.[1]; + + chrome.storage.local.get(['currentLeetCodeProblem', 'currentLeetCodeProblemTitle', 'descriptionTabUpdated', 'solutionsTabUpdated'], (result) => { + let lastProblem = result.currentLeetCodeProblem || ''; let lastTitle = result.currentLeetCodeProblemTitle || ''; let descriptionTabUpdated = result.descriptionTabUpdated || false; let solutionsTabUpdated = result.solutionsTabUpdated || false; - if (tab.title !== lastTitle) { + + // Only reset if we've switched to a different problem + if (problemPath && problemPath !== lastProblem) { + console.log('Problem changed from', lastProblem, 'to', problemPath); 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 the description tab has not been updated and the url matches the description page, we update the flag + 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' }); } - let solutionsUrl = /^https:\/\/leetcode\.com\/problems\/.*\/solutions\/?/; - if (url.match(solutionsUrl)) { + // If the solutions tab has not been updated and the url matches the solutions page, we update the flag + let solutionsUrl = /^https:\/\/leetcode\.com\/problems\/.*\/solutions\/?$/; + if (!solutionsTabUpdated && url.match(solutionsUrl)) { chrome.storage.local.set({ 'solutionsTabUpdated': true }); chrome.tabs.sendMessage(tabId, { action: 'updateSolutions', title: tab.title || 'title' }); } diff --git a/src/content-script/update-description-tab.ts b/src/content-script/update-description-tab.ts index 4514245..773a970 100644 --- a/src/content-script/update-description-tab.ts +++ b/src/content-script/update-description-tab.ts @@ -371,6 +371,7 @@ function setupDescriptionThemeListener() { 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()); @@ -400,8 +401,8 @@ function initializeContentScript() { } function onDOMReady() { - // Check if we're on a LeetCode problem page - const isProblemPage = /^https:\/\/leetcode\.com\/problems\/.*\/?/.test(window.location.href); + // Check if we're on a LeetCode problem's description page + const isProblemPage = /^https:\/\/leetcode\.com\/problems\/.*\/description\/?/.test(window.location.href); if (isProblemPage) { console.log('LeetCode problem page detected, initializing content script...'); @@ -420,7 +421,7 @@ function initializeContentScript() { // Add theme change listener after creating company tags setupDescriptionThemeListener(); - console.log('Content script initialized for problem:', problemTitle); + console.log('Description tab content script initialized for problem:', problemTitle); } } } diff --git a/src/content-script/update-solutions-tab.ts b/src/content-script/update-solutions-tab.ts index 4a4c3f5..ceefec3 100644 --- a/src/content-script/update-solutions-tab.ts +++ b/src/content-script/update-solutions-tab.ts @@ -538,49 +538,44 @@ chrome.runtime.onMessage.addListener((request) => { 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) { + if (!problem?.videos && !problem?.languages) return; + if (problem.videos?.length == 0 && problem.languages?.length == 0) { return; } - // If forceUpdate is true or this is not a video update, remove existing containers - if (request.forceUpdate) { - const existingContainers = [ - '.nav-container', - '.video-container', - '.code-container', - '.language-buttons-container' - ].forEach(selector => { - const element = document.querySelector(selector); - if (element) element.remove(); - }); - } + // Always remove existing containers when updating solutions + const existingContainers = [ + '.nav-container', + '.video-container', + '.code-container', + '.language-buttons-container' + ].forEach(selector => { + const element = document.querySelector(selector); + if (element) element.remove(); + }); - // Only create nav container if it doesn't exist or if this is not a video update - let existingNavContainer = document.querySelector('.nav-container'); - if (!existingNavContainer) { - const newNavContainer = createNavContainer(problem); - searchBar?.insertBefore(newNavContainer, searchBar.firstChild); - } + // Create new nav container + const newNavContainer = createNavContainer(problem); + searchBar?.insertBefore(newNavContainer, searchBar.firstChild); - // Check if the video container already exists before adding - if (!document.querySelector('.video-container') && problem.videos.length > 0) { + // Add video container if videos exist + if (problem.videos?.length > 0) { let videoContainer = createVideoContainer(problem); if (searchBar) searchBar.insertBefore(videoContainer, searchBar.children[1]); } - // Check if the code container already exists before adding - if (!document.querySelector('.code-container') && problem.languages.length > 0) { + // Add code container if languages exist + if (problem.languages?.length > 0) { let codeContainer = createCodeContainer(); if (searchBar) searchBar.insertBefore(codeContainer, searchBar.children[1]); } - // Check if the language buttons container already exists before adding - if (!document.querySelector('.language-buttons-container')) { + // Add language buttons container if languages exist + if (problem.languages?.length > 0) { 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) searchBar.insertBefore(languageButtonsContainer, searchBar.children[1]); } // Add theme change listener after creating containers From b5dc3d3228ee2e8ac9a9511f0f0408bf3b1463b8 Mon Sep 17 00:00:00 2001 From: zubyj Date: Tue, 8 Apr 2025 22:40:21 -0700 Subject: [PATCH 13/23] fix refreshing not updating solutions tab --- src/background/background.ts | 68 +++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/src/background/background.ts b/src/background/background.ts index 6906ed3..f3a1385 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -75,44 +75,48 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { const url = tab.url; let problemUrl = /^https:\/\/leetcode\.com\/problems\/.*\/?/; - // Only proceed if this is a leetcode problem page + // Check if this is a leetcode problem page if (url.match(problemUrl)) { // Extract the problem path from the URL const problemPath = url.match(/\/problems\/([^/]+)/)?.[1]; - chrome.storage.local.get(['currentLeetCodeProblem', 'currentLeetCodeProblemTitle', 'descriptionTabUpdated', 'solutionsTabUpdated'], (result) => { - let lastProblem = result.currentLeetCodeProblem || ''; - let lastTitle = result.currentLeetCodeProblemTitle || ''; - let descriptionTabUpdated = result.descriptionTabUpdated || false; - let solutionsTabUpdated = result.solutionsTabUpdated || false; - - // Only reset if we've switched to a different problem - if (problemPath && problemPath !== lastProblem) { - console.log('Problem changed from', lastProblem, 'to', problemPath); - chrome.storage.local.set({ - 'currentLeetCodeProblem': problemPath, - 'currentLeetCodeProblemTitle': tab.title, - 'descriptionTabUpdated': false, - 'solutionsTabUpdated': false - }); - descriptionTabUpdated = false; - solutionsTabUpdated = false; - } + // Handle both page refresh (complete status) and dynamic updates + if (changeInfo.status === 'complete' || changeInfo.url) { + console.log('Tab updated or refreshed:', url); + chrome.storage.local.get(['currentLeetCodeProblem', 'currentLeetCodeProblemTitle', 'descriptionTabUpdated', 'solutionsTabUpdated'], (result) => { + let lastProblem = result.currentLeetCodeProblem || ''; + let lastTitle = result.currentLeetCodeProblemTitle || ''; + let descriptionTabUpdated = result.descriptionTabUpdated || false; + let solutionsTabUpdated = result.solutionsTabUpdated || false; + + // Reset flags on problem change or page refresh + if (problemPath && (problemPath !== lastProblem || changeInfo.status === 'complete')) { + console.log('Problem changed or page refreshed:', problemPath); + chrome.storage.local.set({ + 'currentLeetCodeProblem': problemPath, + 'currentLeetCodeProblemTitle': tab.title, + 'descriptionTabUpdated': false, + 'solutionsTabUpdated': false + }); + descriptionTabUpdated = false; + solutionsTabUpdated = false; + } - // If the description tab has not been updated and the url matches the description page, we update the flag - 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' }); - } + // If the description tab has not been updated and the url matches the description page, we update the flag + 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' }); + } - // If the solutions tab has not been updated and the url matches the solutions page, we update the flag - let solutionsUrl = /^https:\/\/leetcode\.com\/problems\/.*\/solutions\/?$/; - if (!solutionsTabUpdated && url.match(solutionsUrl)) { - chrome.storage.local.set({ 'solutionsTabUpdated': true }); - chrome.tabs.sendMessage(tabId, { action: 'updateSolutions', title: tab.title || 'title' }); - } - }); + // If the solutions tab has not been updated and the url matches the solutions page, we update the flag + let solutionsUrl = /^https:\/\/leetcode\.com\/problems\/.*\/solutions\/?$/; + if (!solutionsTabUpdated && url.match(solutionsUrl)) { + chrome.storage.local.set({ 'solutionsTabUpdated': true }); + chrome.tabs.sendMessage(tabId, { action: 'updateSolutions', title: tab.title || 'title' }); + } + }); + } } } }); \ No newline at end of file From a4c9d0c1f9e8f841e8a885eeb952a234b4f4dacc Mon Sep 17 00:00:00 2001 From: zubyj Date: Tue, 8 Apr 2025 23:02:20 -0700 Subject: [PATCH 14/23] fix page update spamming --- src/background/background.ts | 42 +++++++++++---- src/content-script/themeDetector.js | 82 ++++++++++++----------------- 2 files changed, 64 insertions(+), 60 deletions(-) diff --git a/src/background/background.ts b/src/background/background.ts index f3a1385..af07d9e 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -70,6 +70,12 @@ chrome.runtime.onMessage.addListener((request) => { } }); +// Keep track of the last state to avoid duplicate updates +let lastState = { + problemPath: '', + view: '' // 'problem' or 'solutions' +}; + chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { if (tab.url) { const url = tab.url; @@ -80,9 +86,25 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { // Extract the problem path from the URL const problemPath = url.match(/\/problems\/([^/]+)/)?.[1]; - // Handle both page refresh (complete status) and dynamic updates - if (changeInfo.status === 'complete' || changeInfo.url) { - console.log('Tab updated or refreshed:', url); + // Determine the current view - now only distinguishing between problem view and solutions + let currentView = url.includes('/solutions') ? 'solutions' : 'problem'; + + // Handle page refreshes and initial loads + const isPageRefresh = changeInfo.status === 'complete'; + const isProblemChange = problemPath !== lastState.problemPath; + const isViewChange = currentView !== lastState.view; + + if (isProblemChange || isViewChange || isPageRefresh) { + console.log(`State change detected - ${ + isProblemChange ? 'New Problem' : + isViewChange ? 'View Changed' : + 'Page Refresh' + }`); + + // Update last state + lastState.problemPath = problemPath || ''; + lastState.view = currentView; + chrome.storage.local.get(['currentLeetCodeProblem', 'currentLeetCodeProblemTitle', 'descriptionTabUpdated', 'solutionsTabUpdated'], (result) => { let lastProblem = result.currentLeetCodeProblem || ''; let lastTitle = result.currentLeetCodeProblemTitle || ''; @@ -90,8 +112,8 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { let solutionsTabUpdated = result.solutionsTabUpdated || false; // Reset flags on problem change or page refresh - if (problemPath && (problemPath !== lastProblem || changeInfo.status === 'complete')) { - console.log('Problem changed or page refreshed:', problemPath); + if (isProblemChange || isPageRefresh) { + console.log('Updating problem state:', problemPath); chrome.storage.local.set({ 'currentLeetCodeProblem': problemPath, 'currentLeetCodeProblemTitle': tab.title, @@ -102,16 +124,14 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { solutionsTabUpdated = false; } - // If the description tab has not been updated and the url matches the description page, we update the flag - let descriptionUrl = /^https:\/\/leetcode\.com\/problems\/.*\/(description\/)?$/; - if (!descriptionTabUpdated && url.match(descriptionUrl)) { + // Only update description if we're in problem view and not updated + if (!descriptionTabUpdated && currentView === 'problem') { chrome.storage.local.set({ 'descriptionTabUpdated': true }); chrome.tabs.sendMessage(tabId, { action: 'updateDescription', title: tab.title || 'title' }); } - // If the solutions tab has not been updated and the url matches the solutions page, we update the flag - let solutionsUrl = /^https:\/\/leetcode\.com\/problems\/.*\/solutions\/?$/; - if (!solutionsTabUpdated && url.match(solutionsUrl)) { + // Only update solutions if we're in solutions view and not updated + if (!solutionsTabUpdated && currentView === 'solutions') { chrome.storage.local.set({ 'solutionsTabUpdated': true }); chrome.tabs.sendMessage(tabId, { action: 'updateSolutions', title: tab.title || 'title' }); } 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 From e5c7dd857ce2208909523a8774abf32e81cbfb08 Mon Sep 17 00:00:00 2001 From: zubyj Date: Wed, 9 Apr 2025 22:33:14 -0700 Subject: [PATCH 15/23] fix switching videos causing container to reset --- src/background/background.ts | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/background/background.ts b/src/background/background.ts index af07d9e..dcb7630 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -73,7 +73,8 @@ chrome.runtime.onMessage.addListener((request) => { // Keep track of the last state to avoid duplicate updates let lastState = { problemPath: '', - view: '' // 'problem' or 'solutions' + view: '', // 'problem' or 'solutions' + lastPathname: '' // Track full pathname to detect real navigation }; chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { @@ -85,25 +86,33 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { if (url.match(problemUrl)) { // 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'; - // Handle page refreshes and initial loads - const isPageRefresh = changeInfo.status === 'complete'; + // Only trigger updates on actual page loads or problem changes + const isPageLoad = changeInfo.status === 'complete' && !changeInfo.url; const isProblemChange = problemPath !== lastState.problemPath; const isViewChange = currentView !== lastState.view; + const isRealNavigation = pathname !== lastState.lastPathname && + !pathname.includes('playground') && + !pathname.includes('editor') && + !pathname.includes('interpret-solution') && + !pathname.includes('submissions'); - if (isProblemChange || isViewChange || isPageRefresh) { + // Only update if there's a real navigation or problem change + if ((isProblemChange || (isViewChange && isRealNavigation) || isPageLoad) && problemPath) { console.log(`State change detected - ${ isProblemChange ? 'New Problem' : isViewChange ? 'View Changed' : - 'Page Refresh' + 'Page Load' }`); // Update last state - lastState.problemPath = problemPath || ''; + lastState.problemPath = problemPath; lastState.view = currentView; + lastState.lastPathname = pathname; chrome.storage.local.get(['currentLeetCodeProblem', 'currentLeetCodeProblemTitle', 'descriptionTabUpdated', 'solutionsTabUpdated'], (result) => { let lastProblem = result.currentLeetCodeProblem || ''; @@ -111,8 +120,8 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { let descriptionTabUpdated = result.descriptionTabUpdated || false; let solutionsTabUpdated = result.solutionsTabUpdated || false; - // Reset flags on problem change or page refresh - if (isProblemChange || isPageRefresh) { + // Reset flags on problem change or page load + if (isProblemChange || (isPageLoad && !descriptionTabUpdated && !solutionsTabUpdated)) { console.log('Updating problem state:', problemPath); chrome.storage.local.set({ 'currentLeetCodeProblem': problemPath, From cfb77ae1f89f92c9f14f68a83b5db19938297d51 Mon Sep 17 00:00:00 2001 From: zubyj Date: Wed, 9 Apr 2025 23:10:53 -0700 Subject: [PATCH 16/23] fix prev/next triggering page refresh --- src/background/background.ts | 76 +++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 27 deletions(-) diff --git a/src/background/background.ts b/src/background/background.ts index dcb7630..a6fa58c 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -74,7 +74,9 @@ chrome.runtime.onMessage.addListener((request) => { let lastState = { problemPath: '', view: '', // 'problem' or 'solutions' - lastPathname: '' // Track full pathname to detect real navigation + 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) => { @@ -92,20 +94,43 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { let currentView = url.includes('/solutions') ? 'solutions' : 'problem'; // Only trigger updates on actual page loads or problem changes - const isPageLoad = changeInfo.status === 'complete' && !changeInfo.url; + const isPageLoad = changeInfo.status === 'complete'; const isProblemChange = problemPath !== lastState.problemPath; const isViewChange = currentView !== lastState.view; - const isRealNavigation = pathname !== lastState.lastPathname && - !pathname.includes('playground') && - !pathname.includes('editor') && - !pathname.includes('interpret-solution') && - !pathname.includes('submissions'); + + // 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')); - // Only update if there's a real navigation or problem change - if ((isProblemChange || (isViewChange && isRealNavigation) || isPageLoad) && problemPath) { + // 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' }`); @@ -113,25 +138,22 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { lastState.problemPath = problemPath; lastState.view = currentView; lastState.lastPathname = pathname; + lastState.lastUpdateTime = Date.now(); - chrome.storage.local.get(['currentLeetCodeProblem', 'currentLeetCodeProblemTitle', 'descriptionTabUpdated', 'solutionsTabUpdated'], (result) => { - let lastProblem = result.currentLeetCodeProblem || ''; - let lastTitle = result.currentLeetCodeProblemTitle || ''; + // 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 + }); + } + + // Get current state + chrome.storage.local.get(['descriptionTabUpdated', 'solutionsTabUpdated'], (result) => { let descriptionTabUpdated = result.descriptionTabUpdated || false; let solutionsTabUpdated = result.solutionsTabUpdated || false; - - // Reset flags on problem change or page load - if (isProblemChange || (isPageLoad && !descriptionTabUpdated && !solutionsTabUpdated)) { - console.log('Updating problem state:', problemPath); - chrome.storage.local.set({ - 'currentLeetCodeProblem': problemPath, - 'currentLeetCodeProblemTitle': tab.title, - 'descriptionTabUpdated': false, - 'solutionsTabUpdated': false - }); - descriptionTabUpdated = false; - solutionsTabUpdated = false; - } // Only update description if we're in problem view and not updated if (!descriptionTabUpdated && currentView === 'problem') { @@ -139,8 +161,8 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { chrome.tabs.sendMessage(tabId, { action: 'updateDescription', title: tab.title || 'title' }); } - // Only update solutions if we're in solutions view and not updated - if (!solutionsTabUpdated && currentView === 'solutions') { + // Only update solutions if we're in solutions view and not updated or if view just changed to solutions + if ((!solutionsTabUpdated || (isViewChange && currentView === 'solutions')) && currentView === 'solutions') { chrome.storage.local.set({ 'solutionsTabUpdated': true }); chrome.tabs.sendMessage(tabId, { action: 'updateSolutions', title: tab.title || 'title' }); } From b7c3f278b8610929caa998f5d598d954d87d8d44 Mon Sep 17 00:00:00 2001 From: zubyj Date: Fri, 11 Apr 2025 00:29:35 -0700 Subject: [PATCH 17/23] cleaner navbar implementation --- src/content-script/update-solutions-tab.ts | 174 +++++++++++++-------- 1 file changed, 107 insertions(+), 67 deletions(-) diff --git a/src/content-script/update-solutions-tab.ts b/src/content-script/update-solutions-tab.ts index ceefec3..a8bc8c5 100644 --- a/src/content-script/update-solutions-tab.ts +++ b/src/content-script/update-solutions-tab.ts @@ -1,9 +1,24 @@ 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.classList.add('nav-button'); + if (isActive) button.classList.add('active'); chrome.storage.local.get(['isDarkTheme'], (result) => { const isDark = result.isDarkTheme; @@ -11,13 +26,15 @@ function createStyledButton(text: string, isActive: boolean = false): HTMLButton button.style.color = isDark ? '#fff' : '#1a1a1a'; button.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 button.addEventListener('mouseenter', () => { - button.style.backgroundColor = isDark ? '#424242' : '#e6e6e6'; + if (!button.classList.contains('active')) { + button.style.backgroundColor = isDark ? '#424242' : '#e6e6e6'; + } }); button.addEventListener('mouseleave', () => { - button.style.backgroundColor = isDark ? '#373737' : '#f3f4f5'; + if (!button.classList.contains('active')) { + button.style.backgroundColor = isDark ? '#373737' : '#f3f4f5'; + } }); }); @@ -28,6 +45,7 @@ function createStyledButton(text: string, isActive: boolean = false): HTMLButton button.style.fontSize = '11px'; button.style.transition = 'all 0.2s ease'; button.style.letterSpacing = '0.5px'; + button.style.cursor = 'pointer'; return button; } @@ -47,7 +65,7 @@ function createVideoContainer(problem: any) { maxWidth: '800px', margin: '0 auto', }); - container.classList.add('video-container'); + container.classList.add('video-container', 'content-section'); const controlsContainer = createStyledElement('div', { display: 'flex', @@ -101,7 +119,6 @@ function createVideoContainer(problem: any) { chrome.storage.local.get(['isDarkTheme'], (result) => { const isDark = result.isDarkTheme; - // channel element is white on dark mode and black on light mode channelElement.style.color = isDark ? '#fff' : '#1a1a1a'; }); @@ -153,17 +170,26 @@ function updateVideo(iframe: HTMLIFrameElement, videoUrl: string) { } function createCodeContainer() { + 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.display = 'block'; codeElement.style.borderRadius = '8px'; codeElement.style.padding = '16px'; codeElement.style.marginTop = '24px'; - codeElement.style.width = '95%'; + codeElement.style.width = '100%'; codeElement.style.fontSize = '14px'; - codeElement.style.marginLeft = '2.5%'; codeElement.style.maxHeight = '500px'; codeElement.style.overflowY = 'auto'; + codeElement.style.boxSizing = 'border-box'; codeElement.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)'; chrome.storage.local.get(['isDarkTheme'], (result) => { @@ -173,24 +199,52 @@ function createCodeContainer() { codeElement.style.color = isDark ? '#fff' : '#1a1a1a'; }); - return codeElement; + container.appendChild(codeElement); + return container; } -function hideContent() { - const elements = [ - '.code-container', - '.language-buttons-container', - '.video-container' - ].map(selector => document.querySelector(selector) as HTMLElement); +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'; + }); - elements.forEach(element => { - if (element) { - if (element.classList.contains('video-container')) { - element.style.paddingBottom = '0'; + // 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}%`; } - element.style.display = 'none'; + break; + case 'Code': + const codeSection = document.querySelector('.code-section') as HTMLElement; + const languageButtons = document.querySelector('.language-buttons-container') as HTMLElement; + if (codeSection) codeSection.style.display = 'block'; + 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'); } }); + + // 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) { @@ -212,24 +266,12 @@ function createNavContainer(problem: any) { { text: 'Code', show: problem.languages?.length > 0 } ]; - const activeButton = buttons[0]; - buttons.forEach(({ text, show }) => { + buttons.forEach(({ text, show }, index) => { if (!show) return; - const button = createStyledButton(text, text === activeButton.text); + const button = createStyledButton(text, index === 0); button.addEventListener('click', () => { - hideContent(); - if (text === 'Video') { - const videoContainer = document.querySelector('.video-container') as HTMLElement; - if (videoContainer) { - videoContainer.style.display = 'flex'; - videoContainer.style.paddingBottom = `${VIDEO_ASPECT_RATIO}%`; - } - } else if (text === 'Code') { - const elements = ['.code-container', '.language-buttons-container'] - .map(selector => document.querySelector(selector) as HTMLElement); - elements.forEach(el => el && (el.style.display = 'flex')); - } + showContent(text as 'Discussion' | 'Video' | 'Code'); }); navContainer.append(button); }); @@ -530,55 +572,53 @@ function updateAllElements(isDark: boolean) { } 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 searchBar = document.querySelectorAll('input.block')[0]?.parentElement?.parentElement?.parentElement; + if (!searchBar) return; + 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 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; - } + if (problem.videos?.length === 0 && problem.languages?.length === 0) return; - // Always remove existing containers when updating solutions - const existingContainers = [ - '.nav-container', - '.video-container', - '.code-container', - '.language-buttons-container' - ].forEach(selector => { - const element = document.querySelector(selector); - if (element) element.remove(); - }); + // Remove any existing containers + const existingWrapper = document.querySelector('.leetcode-explained-wrapper'); + if (existingWrapper) existingWrapper.remove(); - // Create new nav container - const newNavContainer = createNavContainer(problem); - searchBar?.insertBefore(newNavContainer, searchBar.firstChild); + // Create wrapper for all our custom content + const wrapper = createCustomContentWrapper(); + + // Create and add nav container + const navContainer = createNavContainer(problem); + wrapper.appendChild(navContainer); // Add video container if videos exist if (problem.videos?.length > 0) { - let videoContainer = createVideoContainer(problem); - if (searchBar) searchBar.insertBefore(videoContainer, searchBar.children[1]); - } - - // Add code container if languages exist - if (problem.languages?.length > 0) { - let codeContainer = createCodeContainer(); - if (searchBar) searchBar.insertBefore(codeContainer, searchBar.children[1]); + const videoContainer = createVideoContainer(problem); + wrapper.appendChild(videoContainer); } - // Add language buttons container if languages exist + // Add code container and language buttons if languages exist if (problem.languages?.length > 0) { - let languageButtonsContainer = createLanguageButtons(problem); + const codeContainer = createCodeContainer(); + const languageButtonsContainer = createLanguageButtons(problem); languageButtonsContainer.classList.add('language-buttons-container'); languageButtonsContainer.style.display = 'none'; - if (searchBar) searchBar.insertBefore(languageButtonsContainer, searchBar.children[1]); + + wrapper.appendChild(languageButtonsContainer); + wrapper.appendChild(codeContainer); } - // Add theme change listener after creating containers + // 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(); }); } From edbaff28cfa9f45199d4d138c2e798af27713308 Mon Sep 17 00:00:00 2001 From: zubyj Date: Fri, 11 Apr 2025 00:42:50 -0700 Subject: [PATCH 18/23] omg i mighta fixed the stupid bugs on this tab --- src/content-script/update-solutions-tab.ts | 160 +++++++++++++++------ 1 file changed, 119 insertions(+), 41 deletions(-) diff --git a/src/content-script/update-solutions-tab.ts b/src/content-script/update-solutions-tab.ts index a8bc8c5..0503712 100644 --- a/src/content-script/update-solutions-tab.ts +++ b/src/content-script/update-solutions-tab.ts @@ -571,55 +571,133 @@ function updateAllElements(isDark: boolean) { }); } -chrome.runtime.onMessage.addListener((request) => { - if (request.action === 'updateSolutions') { - chrome.storage.local.get(['leetcodeProblems'], (result) => { +// Function to update the solutions tab content +function updateSolutionsTab(title: string) { + 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) return; - - 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, don't do anything - if (!problem?.videos && !problem?.languages) return; - if (problem.videos?.length === 0 && problem.languages?.length === 0) return; - - // Remove any existing containers - const existingWrapper = document.querySelector('.leetcode-explained-wrapper'); - if (existingWrapper) existingWrapper.remove(); - - // Create wrapper for all our custom content - const wrapper = createCustomContentWrapper(); - // 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); + 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; } - // 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'; + 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 + insertContent(searchBar, title, result); + } + }); + + // Start observing the document with the configured parameters + observer.observe(document.body, { + childList: true, + subtree: true + }); - wrapper.appendChild(languageButtonsContainer); - wrapper.appendChild(codeContainer); + return; } - // Insert the wrapper at the top of the solutions tab - searchBar.insertBefore(wrapper, searchBar.firstChild); + insertContent(searchBar, title, result); + }; - // Show discussion by default - showContent('Discussion'); + tryInsertContent(); + }); +} - // Set up theme change listener - setupThemeChangeListener(); - }); +// 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; + + // Remove any existing containers + const existingWrapper = document.querySelector('.leetcode-explained-wrapper'); + if (existingWrapper) existingWrapper.remove(); + + // Create wrapper for all our custom content + const wrapper = createCustomContentWrapper(); + + // 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 initializeContentScript() { + // Wait for the DOM to be fully loaded + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', onDOMReady); + } else { + onDOMReady(); + } + + function onDOMReady() { + // Check if we're on a LeetCode problem's solutions page + const isSolutionsPage = /^https:\/\/leetcode\.com\/problems\/.*\/solutions\/?/.test(window.location.href); + + if (isSolutionsPage) { + console.log('LeetCode solutions page detected, initializing content script...'); + + // Extract the problem title from the page title + const pageTitle = document.title; + + // Update the solutions tab + updateSolutionsTab(pageTitle); + + console.log('Solutions tab content script initialized for problem:', pageTitle); + } + } +} + +// Run the initialization +initializeContentScript(); + +chrome.runtime.onMessage.addListener((request) => { + if (request.action === 'updateSolutions') { + updateSolutionsTab(request.title); } }); \ No newline at end of file From af563b25ef6b6c4f4993bc2d03ef8dc55a2dc518 Mon Sep 17 00:00:00 2001 From: zubyj Date: Fri, 11 Apr 2025 00:52:25 -0700 Subject: [PATCH 19/23] fix solutions tab elements getting readded when switching tabs --- src/content-script/update-solutions-tab.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/content-script/update-solutions-tab.ts b/src/content-script/update-solutions-tab.ts index 0503712..ad3f1bc 100644 --- a/src/content-script/update-solutions-tab.ts +++ b/src/content-script/update-solutions-tab.ts @@ -573,6 +573,10 @@ function updateAllElements(isDark: boolean) { // 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; + chrome.storage.local.get(['leetcodeProblems'], (result) => { // Try to find the search bar with retries const maxRetries = 10; @@ -580,6 +584,13 @@ function updateSolutionsTab(title: string) { let retryCount = 0; const tryInsertContent = () => { + // First check if we already have a wrapper to prevent duplicates + const existingWrapper = document.querySelector('.leetcode-explained-wrapper'); + if (existingWrapper) { + console.log('Content already exists, skipping insertion'); + return; + } + const searchBar = document.querySelectorAll('input.block')[0]?.parentElement?.parentElement?.parentElement; if (!searchBar && retryCount < maxRetries) { @@ -596,6 +607,12 @@ function updateSolutionsTab(title: string) { // If still not found, set up a MutationObserver to watch for DOM changes const observer = new MutationObserver((mutations, obs) => { + // Check again for existing wrapper before proceeding + if (document.querySelector('.leetcode-explained-wrapper')) { + obs.disconnect(); + return; + } + const searchBar = document.querySelectorAll('input.block')[0]?.parentElement?.parentElement?.parentElement; if (searchBar) { obs.disconnect(); // Stop observing once we find the element From 60a9a01b09fe335760eb6793cda6454f1a8d8a47 Mon Sep 17 00:00:00 2001 From: zubyj Date: Fri, 11 Apr 2025 01:01:27 -0700 Subject: [PATCH 20/23] fix dynamic routing not resetting solutions tab elements --- src/content-script/update-solutions-tab.ts | 43 ++++++++++++++-------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/src/content-script/update-solutions-tab.ts b/src/content-script/update-solutions-tab.ts index ad3f1bc..b68eabc 100644 --- a/src/content-script/update-solutions-tab.ts +++ b/src/content-script/update-solutions-tab.ts @@ -210,6 +210,12 @@ function showContent(type: 'Discussion' | 'Video' | 'Code') { (section as HTMLElement).style.display = 'none'; }); + // Get the language buttons container + const languageButtons = document.querySelector('.language-buttons-container') as HTMLElement; + if (languageButtons) { + languageButtons.style.display = 'none'; // Hide by default + } + // Show the selected content switch (type) { case 'Video': @@ -221,9 +227,13 @@ function showContent(type: 'Discussion' | 'Video' | 'Code') { break; case 'Code': const codeSection = document.querySelector('.code-section') as HTMLElement; - const languageButtons = document.querySelector('.language-buttons-container') as HTMLElement; - if (codeSection) codeSection.style.display = 'block'; - if (languageButtons) languageButtons.style.display = 'flex'; + 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 @@ -584,11 +594,21 @@ function updateSolutionsTab(title: string) { let retryCount = 0; const tryInsertContent = () => { - // First check if we already have a wrapper to prevent duplicates + // Check if we already have a wrapper and if it's for the same problem const existingWrapper = document.querySelector('.leetcode-explained-wrapper'); if (existingWrapper) { - console.log('Content already exists, skipping insertion'); - return; + const currentTitle = document.title.split('-')[0].trim(); + const wrapperTitle = existingWrapper.getAttribute('data-problem-title'); + + // If it's the same problem, preserve the state + if (wrapperTitle === currentTitle) { + console.log('Content for same problem exists, preserving state'); + return; + } + + // If it's a different problem, remove the old wrapper + console.log('Different problem detected, updating content'); + existingWrapper.remove(); } const searchBar = document.querySelectorAll('input.block')[0]?.parentElement?.parentElement?.parentElement; @@ -607,12 +627,6 @@ function updateSolutionsTab(title: string) { // If still not found, set up a MutationObserver to watch for DOM changes const observer = new MutationObserver((mutations, obs) => { - // Check again for existing wrapper before proceeding - if (document.querySelector('.leetcode-explained-wrapper')) { - obs.disconnect(); - return; - } - const searchBar = document.querySelectorAll('input.block')[0]?.parentElement?.parentElement?.parentElement; if (searchBar) { obs.disconnect(); // Stop observing once we find the element @@ -645,12 +659,9 @@ function insertContent(searchBar: Element, title: string, result: any) { if (!problem?.videos && !problem?.languages) return; if (problem.videos?.length === 0 && problem.languages?.length === 0) return; - // Remove any existing containers - const existingWrapper = document.querySelector('.leetcode-explained-wrapper'); - if (existingWrapper) existingWrapper.remove(); - // 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); From bd02e02c488f6162b505fdac0b6cf9e54b8adb47 Mon Sep 17 00:00:00 2001 From: zubyj Date: Fri, 11 Apr 2025 14:36:45 -0700 Subject: [PATCH 21/23] fix solution to description tab not showing company tags --- src/background/background.ts | 8 +- src/content-script/update-description-tab.ts | 119 +++++++++---------- 2 files changed, 58 insertions(+), 69 deletions(-) diff --git a/src/background/background.ts b/src/background/background.ts index a6fa58c..e80fa9a 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -155,14 +155,14 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { let descriptionTabUpdated = result.descriptionTabUpdated || false; let solutionsTabUpdated = result.solutionsTabUpdated || false; - // Only update description if we're in problem view and not updated - if (!descriptionTabUpdated && currentView === 'problem') { + // 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' }); } - // Only update solutions if we're in solutions view and not updated or if view just changed to solutions - if ((!solutionsTabUpdated || (isViewChange && currentView === 'solutions')) && currentView === 'solutions') { + // 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' }); } diff --git a/src/content-script/update-description-tab.ts b/src/content-script/update-description-tab.ts index 773a970..1941226 100644 --- a/src/content-script/update-description-tab.ts +++ b/src/content-script/update-description-tab.ts @@ -144,29 +144,7 @@ function showRating(problemTitle: string) { // 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'); - - // First handle visibility - if (!showCompanyTags) { - if (companyTagContainer) { - companyTagContainer.style.display = 'none'; - } - return; - } else if (companyTagContainer && companyTagContainer instanceof HTMLElement) { - companyTagContainer.style.display = 'flex'; - // If container exists and is visible, just update styles - chrome.storage.local.get(['isDarkTheme'], (result) => { - const isDark = result.isDarkTheme; - const tags = companyTagContainer?.querySelectorAll('.company-tag'); - if (tags) { - tags.forEach(tag => { - if (tag instanceof HTMLElement) { - updateCompanyTagStyle(tag, isDark); - } - }); - } - }); + if (!result.showCompanyTags) { return; } @@ -175,6 +153,26 @@ function showCompanyTags(problemTitle: string) { 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')) { @@ -219,30 +217,11 @@ function showCompanyTags(problemTitle: string) { return; } + // If we found the description element, insert the company tags insertCompanyTags(description); }; - 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); - }; - - // Start the retry process + // Start the process tryInsertCompanyTags(); }); } @@ -401,31 +380,41 @@ function initializeContentScript() { } function onDOMReady() { - // Check if we're on a LeetCode problem's description page - const isProblemPage = /^https:\/\/leetcode\.com\/problems\/.*\/description\/?/.test(window.location.href); + // Set up theme detection and synchronization + setupDescriptionThemeListener(); - if (isProblemPage) { - console.log('LeetCode problem page detected, initializing content script...'); - - // Extract the problem title from the page title - const pageTitle = document.title; - const problemTitle = pageTitle.split('-')[0].trim(); - - // Detect theme on first load of a problem page - detectAndSyncTheme(); - showExamples(); - showCompanyTags(problemTitle); - showDifficulty(); - showRating(problemTitle); - - // Add theme change listener after creating company tags - setupDescriptionThemeListener(); - - console.log('Description tab content script initialized for problem:', problemTitle); + // 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 }); } } } -// Run the initialization +// Initialize the content script initializeContentScript(); From a19b620139eeaf9f2a9a84a1331a844ce4e8170d Mon Sep 17 00:00:00 2001 From: zubyj Date: Fri, 11 Apr 2025 14:46:01 -0700 Subject: [PATCH 22/23] fix video controls not changing color automatically when theme changed in settings --- src/content-script/update-description-tab.ts | 4 +- src/content-script/update-solutions-tab.ts | 42 ++++++++++++-------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/content-script/update-description-tab.ts b/src/content-script/update-description-tab.ts index 1941226..22a68cf 100644 --- a/src/content-script/update-description-tab.ts +++ b/src/content-script/update-description-tab.ts @@ -371,7 +371,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { }); // Self-initialization function that runs when the content script loads -function initializeContentScript() { +function initializeDescriptionTab() { // Wait for the DOM to be fully loaded if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', onDOMReady); @@ -416,5 +416,5 @@ function initializeContentScript() { } // Initialize the content script -initializeContentScript(); +initializeDescriptionTab(); diff --git a/src/content-script/update-solutions-tab.ts b/src/content-script/update-solutions-tab.ts index b68eabc..9f8a2c4 100644 --- a/src/content-script/update-solutions-tab.ts +++ b/src/content-script/update-solutions-tab.ts @@ -574,6 +574,18 @@ function updateAllElements(isDark: boolean) { 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); } @@ -695,7 +707,7 @@ function insertContent(searchBar: Element, title: string, result: any) { } // Self-initialization function that runs when the content script loads -function initializeContentScript() { +function initializeSolutionsTab() { // Wait for the DOM to be fully loaded if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', onDOMReady); @@ -704,25 +716,23 @@ function initializeContentScript() { } function onDOMReady() { - // Check if we're on a LeetCode problem's solutions page - const isSolutionsPage = /^https:\/\/leetcode\.com\/problems\/.*\/solutions\/?/.test(window.location.href); + // Get the problem title from the page + const problemTitle = document.title.replace(' - LeetCode', ''); - if (isSolutionsPage) { - console.log('LeetCode solutions page detected, initializing content script...'); - - // Extract the problem title from the page title - const pageTitle = document.title; - - // Update the solutions tab - updateSolutionsTab(pageTitle); - - console.log('Solutions tab content script initialized for problem:', pageTitle); - } + // Find the search bar to insert our content before it + const searchBar = document.querySelector('div[class*="search"]'); + if (!searchBar) return; + + // Load problem data and insert content + chrome.storage.local.get(['leetcodeProblems'], (result) => { + if (!result.leetcodeProblems) return; + insertContent(searchBar, problemTitle, result); + }); } } -// Run the initialization -initializeContentScript(); +// Initialize the content script +initializeSolutionsTab(); chrome.runtime.onMessage.addListener((request) => { if (request.action === 'updateSolutions') { From 3e8c970cc6123a6f7f0d81c4c7d297ede5bed593 Mon Sep 17 00:00:00 2001 From: zubyj Date: Fri, 11 Apr 2025 15:23:23 -0700 Subject: [PATCH 23/23] preserve state when swtiching tabs on same page --- manifest.json | 3 +- src/content-script/update-solutions-tab.ts | 99 ++++++++++++++-------- 2 files changed, 65 insertions(+), 37 deletions(-) diff --git a/manifest.json b/manifest.json index 8740f4d..c841405 100644 --- a/manifest.json +++ b/manifest.json @@ -34,7 +34,8 @@ ], "matches": [ "https://leetcode.com/problems/*" - ] + ], + "run_at": "document_end" } ], "web_accessible_resources": [ diff --git a/src/content-script/update-solutions-tab.ts b/src/content-script/update-solutions-tab.ts index 9f8a2c4..27badbc 100644 --- a/src/content-script/update-solutions-tab.ts +++ b/src/content-script/update-solutions-tab.ts @@ -599,6 +599,22 @@ function updateSolutionsTab(title: string) { 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; @@ -606,23 +622,6 @@ function updateSolutionsTab(title: string) { let retryCount = 0; const tryInsertContent = () => { - // Check if we already have a wrapper and if it's for the same problem - const existingWrapper = document.querySelector('.leetcode-explained-wrapper'); - if (existingWrapper) { - const currentTitle = document.title.split('-')[0].trim(); - const wrapperTitle = existingWrapper.getAttribute('data-problem-title'); - - // If it's the same problem, preserve the state - if (wrapperTitle === currentTitle) { - console.log('Content for same problem exists, preserving state'); - return; - } - - // If it's a different problem, remove the old wrapper - console.log('Different problem detected, updating content'); - existingWrapper.remove(); - } - const searchBar = document.querySelectorAll('input.block')[0]?.parentElement?.parentElement?.parentElement; if (!searchBar && retryCount < maxRetries) { @@ -642,7 +641,11 @@ function updateSolutionsTab(title: string) { const searchBar = document.querySelectorAll('input.block')[0]?.parentElement?.parentElement?.parentElement; if (searchBar) { obs.disconnect(); // Stop observing once we find the element - insertContent(searchBar, title, result); + // 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); + } } }); @@ -655,7 +658,11 @@ function updateSolutionsTab(title: string) { return; } - insertContent(searchBar, title, result); + // 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(); @@ -708,32 +715,52 @@ function insertContent(searchBar: Element, title: string, result: any) { // Self-initialization function that runs when the content script loads function initializeSolutionsTab() { - // Wait for the DOM to be fully loaded - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', onDOMReady); - } else { - onDOMReady(); - } - - function onDOMReady() { + // Function to initialize content + const initialize = () => { // Get the problem title from the page const problemTitle = document.title.replace(' - LeetCode', ''); - // Find the search bar to insert our content before it - const searchBar = document.querySelector('div[class*="search"]'); - if (!searchBar) return; - - // Load problem data and insert content - chrome.storage.local.get(['leetcodeProblems'], (result) => { - if (!result.leetcodeProblems) return; - insertContent(searchBar, problemTitle, result); - }); + // 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);