diff --git a/src/helpers/get-github-repo-info.ts b/src/helpers/get-github-repo-info.ts index 5bb982c2..bfca8259 100644 --- a/src/helpers/get-github-repo-info.ts +++ b/src/helpers/get-github-repo-info.ts @@ -5,6 +5,28 @@ import * as pageDetect from 'github-url-detection'; import elementReady from 'element-ready'; import { getPlatform } from './get-platform'; +const repoSidebarSectionSelectors = [ + '.Layout-sidebar .BorderGrid-cell > .hide-sm.hide-md', + '.Layout-sidebar .BorderGrid-cell .hide-sm.hide-md', + '.BorderGrid-cell > .hide-sm.hide-md', + '.BorderGrid-cell .hide-sm.hide-md', +]; + +const repoSidebarMarkerSelector = [ + 'a[href$="/stargazers"]', + 'a[href$="/watchers"]', + 'a[href$="/forks"]', + 'a[href$="/activity"]', + 'a[href*="/custom-properties"]', + 'a[href="#readme-ov-file"]', + '.topic-tag.topic-tag-link', +].join(', '); + +const pickFirstVisible = (elements: JQuery) => { + const $visibleElements = elements.filter(':visible'); + return ($visibleElements.length > 0 ? $visibleElements : elements).first(); +}; + export function getRepoName() { const repoNameByUrl = getRepoNameByUrl(); const repoNameByPage = getRepoNameByPage(); @@ -36,6 +58,27 @@ export function hasRepoContainerHeader() { return headerElement && !headerElement.attr('hidden'); } +export function getRepoSidebarSection() { + const $sections = $(repoSidebarSectionSelectors.join(',')) + .filter((_, element) => !element.closest('details-dialog, template')) + .filter((_, element) => $(element).find(repoSidebarMarkerSelector).length > 0); + + return pickFirstVisible($sections); +} + +export function getRepoSidebarBorderGrid() { + const $sidebarSection = getRepoSidebarSection(); + if ($sidebarSection.length > 0) { + return $sidebarSection.closest('.BorderGrid').first(); + } + + const $borderGrids = $('.Layout-sidebar .BorderGrid, .BorderGrid') + .filter((_, element) => !element.closest('details-dialog, template')) + .filter((_, element) => $(element).find(repoSidebarMarkerSelector).length > 0); + + return pickFirstVisible($borderGrids); +} + export async function isRepoRoot() { return pageDetect.isRepoRoot(); } diff --git a/src/pages/ContentScripts/features/developer-networks/view.tsx b/src/pages/ContentScripts/features/developer-networks/view.tsx index 8b9243ef..395fe0bf 100644 --- a/src/pages/ContentScripts/features/developer-networks/view.tsx +++ b/src/pages/ContentScripts/features/developer-networks/view.tsx @@ -74,7 +74,7 @@ const View = ({ userName }: Props): JSX.Element => { > { > { > { + const $tabs = $(insightsTabSelectors).filter((_, element) => !element.closest('template')); + const $visibleTabs = $tabs.filter(':visible'); + + return ($visibleTabs.length > 0 ? $visibleTabs : $tabs).first(); +}; + +const getRepositoryNavigation = () => { + const $navigations = $('nav[aria-label="Repository"]').filter((_, element) => !element.closest('template')); + const $visibleNavigations = $navigations.filter(':visible'); + + return ($visibleNavigations.length > 0 ? $visibleNavigations : $navigations).first(); +}; + +const waitForMeasuredRepositoryNavigation = async (): Promise => { + const repositoryNavigation = (await elementReady('nav[aria-label="Repository"]', { + waitForChildren: false, + })) as HTMLElement | null; + + if (!repositoryNavigation) { + return null; + } + + if (repositoryNavigation.dataset.overflowMeasured === 'true') { + return repositoryNavigation; + } + + await new Promise((resolve) => { + let observer: MutationObserver; + const timeout = window.setTimeout(() => { + observer.disconnect(); + resolve(); + }, 1500); + + observer = new MutationObserver(() => { + if (repositoryNavigation.dataset.overflowMeasured === 'true') { + window.clearTimeout(timeout); + observer.disconnect(); + resolve(); + } + }); + + observer.observe(repositoryNavigation, { + attributes: true, + attributeFilter: ['data-overflow-measured'], + }); + }); + + return repositoryNavigation; +}; const addPerceptorTab = async (): Promise => { // the creation of the Perceptor tab is based on the Insights tab - const insightsTab = await elementReady('a.UnderlineNav-item[id="insights-tab"]', { waitForChildren: false }); + await waitForMeasuredRepositoryNavigation(); + await elementReady(insightsTabSelectors, { waitForChildren: false }); + const $insightsTab = getInsightsTab(); + const insightsTab = $insightsTab[0] as HTMLAnchorElement | undefined; if (!insightsTab) { - // if the selector failed to find the Insights tab return false; } - const perceptorTab = insightsTab.cloneNode(true) as HTMLAnchorElement; + + $(`#${featureId}`).closest('li').remove(); + + const insightTabListItem = insightsTab.closest('li'); + const perceptorTabListItem = + (insightTabListItem?.cloneNode(true) as HTMLElement | null) ?? document.createElement('li'); + const perceptorTab = (perceptorTabListItem.querySelector('a') ?? insightsTab.cloneNode(true)) as HTMLAnchorElement; + if (!perceptorTabListItem.contains(perceptorTab)) { + perceptorTabListItem.appendChild(perceptorTab); + } + delete perceptorTab.dataset.selectedLinks; + delete perceptorTab.dataset.reactNav; + delete perceptorTab.dataset.reactNavAnchor; + delete perceptorTab.dataset.hotkey; perceptorTab.removeAttribute('aria-current'); perceptorTab.classList.remove('selected'); - const perceptorHref = `${insightsTab.href}?redirect=perceptor`; + const perceptorUrl = new URL(insightsTab.href); + perceptorUrl.searchParams.set('redirect', 'perceptor'); + const perceptorHref = perceptorUrl.toString(); perceptorTab.href = perceptorHref; perceptorTab.id = featureId; - perceptorTab.setAttribute('data-tab-item', featureId); + perceptorTab.setAttribute('data-tab-item', 'perceptor'); perceptorTab.setAttribute( 'data-analytics-event', `{"category":"Underline navbar","action":"Click tab","label":"Perceptor","target":"UNDERLINE_NAV.TAB"}` @@ -33,51 +109,26 @@ const addPerceptorTab = async (): Promise => { perceptorTitle.text('Perceptor').attr('data-content', 'Perceptor'); // slot for any future counter function - const perceptorCounter = $('[class=Counter]', perceptorTab); + const perceptorCounter = $('[class=Counter], [data-component="counter"]', perceptorTab); perceptorCounter.attr('id', `${featureId}-count`); // replace with the perceptor Icon $('svg.octicon', perceptorTab).html(iconSvgPath); // add the Perceptor tab to the tabs list - if (!insightsTab.parentElement) { - return false; - } - const tabContainer = document.createElement('li'); - tabContainer.appendChild(perceptorTab); - tabContainer.setAttribute('data-view-component', 'true'); - tabContainer.className = 'd-inline-flex'; - insightsTab.parentElement.after(tabContainer); - - // add to drop down menu (when the window is narrow enough some tabs are hidden into "···" menu) - const repoNavigationDropdown = await elementReady('.UnderlineNav-actions ul'); - if (!repoNavigationDropdown) { + if (!insightTabListItem?.parentElement) { return false; } - const insightsTabDataItem = $('li[data-menu-item$="insights-tab"]', repoNavigationDropdown); - const perceptorTabDataItem = insightsTabDataItem.clone(true); - perceptorTabDataItem.attr('data-menu-item', featureId); - perceptorTabDataItem.children('a').attr({ - href: perceptorHref, - }); - const perceptorSvgElement = perceptorTabDataItem - .children('a') - .find('span.ActionListItem-visual.ActionListItem-visual--leading') - .find('svg'); - perceptorSvgElement.attr('class', 'octicon octicon-perceptor'); - perceptorSvgElement.html(iconSvgPath); - const perceptorTextElement = perceptorTabDataItem.children('a').find('span.ActionListItem-label'); - perceptorTextElement.text('Perceptor'); - insightsTabDataItem.after(perceptorTabDataItem); + insightTabListItem.after(perceptorTabListItem); // Trigger a reflow to push the right-most tab into the overflow dropdown window.dispatchEvent(new Event('resize')); }; const updatePerceptorTabHighlighting = async (): Promise => { - const insightsTab = $('#insights-tab'); + const insightsTab = getInsightsTab(); const perceptorTab = $(`#${featureId}`); // no operation needed - if (!isPerceptor()) return; + if (!isPerceptor() || perceptorTab.length === 0 || insightsTab.length === 0) return; // if perceptor tab if (insightsTab.hasClass('selected')) { insightsTab.removeClass('selected'); @@ -86,6 +137,11 @@ const updatePerceptorTabHighlighting = async (): Promise => { perceptorTab.addClass('selected'); } + if (insightsTab.attr('aria-current') === 'page') { + insightsTab.removeAttr('aria-current'); + perceptorTab.attr('aria-current', 'page'); + } + const insightsTabSeletedLinks = insightsTab.attr('data-selected-links'); insightsTab.removeAttr('data-selected-links'); perceptorTab.attr('data-selected-links', 'pulse'); @@ -96,13 +152,62 @@ const updatePerceptorTabHighlighting = async (): Promise => { perceptorTab.removeAttr('data-selected-links'); }; +const syncPerceptorTab = async (): Promise => { + if (syncingPerceptorTab) { + return; + } + + syncingPerceptorTab = true; + try { + await addPerceptorTab(); + await updatePerceptorTabHighlighting(); + } finally { + syncingPerceptorTab = false; + } +}; + +const observeRepositoryNavigation = async (): Promise => { + const repositoryNavigation = await waitForMeasuredRepositoryNavigation(); + if (!repositoryNavigation || repositoryNavigation === observedRepositoryNavigation) { + return; + } + + navigationObserver?.disconnect(); + observedRepositoryNavigation = repositoryNavigation as HTMLElement; + navigationObserver = new MutationObserver(async () => { + const navigation = getRepositoryNavigation()[0]; + if (!navigation || syncingPerceptorTab) { + return; + } + + const perceptorTab = document.getElementById(featureId); + if (!perceptorTab) { + await syncPerceptorTab(); + return; + } + + if (isPerceptor() && perceptorTab.getAttribute('aria-current') !== 'page') { + await updatePerceptorTabHighlighting(); + } + }); + + navigationObserver.observe(repositoryNavigation, { + childList: true, + subtree: true, + }); +}; + const init = async (): Promise => { - await addPerceptorTab(); + await syncPerceptorTab(); + await observeRepositoryNavigation(); // TODO need a mechanism to remove extra listeners like this one // add event listener to update tab highlighting at each turbo:load event - document.addEventListener('turbo:load', async () => { - await updatePerceptorTabHighlighting(); - }); + if (!highlightingListenerAttached) { + highlightingListenerAttached = true; + document.addEventListener('turbo:load', async () => { + await syncPerceptorTab(); + }); + } }; features.add(featureId, { diff --git a/src/pages/ContentScripts/features/repo-activity-openrank-trends/index.tsx b/src/pages/ContentScripts/features/repo-activity-openrank-trends/index.tsx index 558bfe69..f3b523b6 100644 --- a/src/pages/ContentScripts/features/repo-activity-openrank-trends/index.tsx +++ b/src/pages/ContentScripts/features/repo-activity-openrank-trends/index.tsx @@ -2,7 +2,13 @@ import React from 'react'; import $ from 'jquery'; import { createRoot } from 'react-dom/client'; import features from '../../../../feature-manager'; -import { getRepoName, isPublicRepoWithMeta, isRepoRoot } from '../../../../helpers/get-github-repo-info'; +import elementReady from 'element-ready'; +import { + getRepoName, + getRepoSidebarBorderGrid, + isPublicRepoWithMeta, + isRepoRoot, +} from '../../../../helpers/get-github-repo-info'; import { getActivity, getOpenrank } from '../../../../api/repo'; import { RepoMeta, metaStore } from '../../../../api/common'; import View from './view'; @@ -24,12 +30,10 @@ const renderTo = (container: any) => { createRoot(container).render(); }; -const init = async (): Promise => { - platform = getPlatform(); - repoName = getRepoName(); - await getData(); +const createTrendRow = () => { + const existingRow = document.getElementById(featureId); + existingRow?.remove(); - // create container const newBorderGridRow = document.createElement('div'); newBorderGridRow.id = featureId; newBorderGridRow.className = 'BorderGrid-row'; @@ -37,10 +41,28 @@ const init = async (): Promise => { newBorderGridCell.className = 'BorderGrid-cell'; newBorderGridRow.appendChild(newBorderGridCell); - renderTo(newBorderGridCell); + return { row: newBorderGridRow, cell: newBorderGridCell }; +}; - const borderGridRows = $('div.Layout-sidebar').children('.BorderGrid'); - borderGridRows.append(newBorderGridRow); +const mountTrendRow = (row: HTMLElement) => { + const $borderGrid = getRepoSidebarBorderGrid(); + if ($borderGrid.length === 0) { + return false; + } + + $borderGrid.append(row); + return true; +}; + +const init = async (): Promise => { + platform = getPlatform(); + repoName = getRepoName(); + await getData(); + await elementReady('.BorderGrid, .BorderGrid-cell, .Layout-sidebar'); + + const { row, cell } = createTrendRow(); + renderTo(cell); + mountTrendRow(row); }; const restore = async () => { @@ -51,7 +73,18 @@ const restore = async () => { await getData(); } // rerender the chart or it will be empty - renderTo($(`#${featureId}`).children('.BorderGrid-cell')[0]); + await elementReady('.BorderGrid, .BorderGrid-cell, .Layout-sidebar'); + let container = $(`#${featureId}`).children('.BorderGrid-cell')[0]; + + if (!container) { + const { row, cell } = createTrendRow(); + if (!mountTrendRow(row)) { + return; + } + container = cell; + } + + renderTo(container); }; features.add(featureId, { diff --git a/src/pages/ContentScripts/features/repo-fork-tooltip/index.tsx b/src/pages/ContentScripts/features/repo-fork-tooltip/index.tsx index a40cdf32..a887dee5 100644 --- a/src/pages/ContentScripts/features/repo-fork-tooltip/index.tsx +++ b/src/pages/ContentScripts/features/repo-fork-tooltip/index.tsx @@ -2,7 +2,7 @@ import features from '../../../../feature-manager'; import View from './view'; import { NativePopover } from '../../components/NativePopover'; import elementReady from 'element-ready'; -import { getRepoName, hasRepoContainerHeader, isPublicRepoWithMeta } from '../../../../helpers/get-github-repo-info'; +import { getRepoName, isPublicRepoWithMeta } from '../../../../helpers/get-github-repo-info'; import { getForks } from '../../../../api/repo'; import { RepoMeta, metaStore } from '../../../../api/common'; @@ -16,31 +16,43 @@ let repoName: string; let forks: any; let meta: RepoMeta; let platform: string; +const forkButtonSelectors = [ + 'a[data-hydro-click*="FORK_BUTTON"]', + 'button[data-hydro-click*="FORK_BUTTON"]', + '#fork-button', + '#fork-icon-button', +]; + const getData = async () => { forks = await getForks(platform, repoName); meta = (await metaStore.get(platform, repoName)) as RepoMeta; }; +const getForkButtons = () => { + return $(forkButtonSelectors.join(',')).filter((_, element) => !element.closest('template')); +}; const init = async (): Promise => { platform = getPlatform(); repoName = getRepoName(); await getData(); - const forkButtonSelector = '#fork-button'; - await elementReady(forkButtonSelector); - const $forkButton = $(forkButtonSelector); - const placeholderElement = $('
').appendTo('body')[0]; - createRoot(placeholderElement).render( - - - - ); + await elementReady(forkButtonSelectors.join(',')); + const $forkButtons = getForkButtons(); + + $forkButtons.each(function (_index, element) { + const placeholderElement = $('
').appendTo('body')[0]; + createRoot(placeholderElement).render( + + + + ); + }); }; const restore = async () => {}; features.add(featureId, { - asLongAs: [isGithub, isPublicRepoWithMeta, hasRepoContainerHeader], + asLongAs: [isGithub, isPublicRepoWithMeta], awaitDomReady: false, init, restore, diff --git a/src/pages/ContentScripts/features/repo-header-labels/gitee-index.tsx b/src/pages/ContentScripts/features/repo-header-labels/gitee-index.tsx index a06e7926..94c67808 100644 --- a/src/pages/ContentScripts/features/repo-header-labels/gitee-index.tsx +++ b/src/pages/ContentScripts/features/repo-header-labels/gitee-index.tsx @@ -53,7 +53,7 @@ const init = async (): Promise => { await getData(); const container = document.createElement('div'); container.id = featureId; - container.className = 'inline-label-container'; + container.className = 'hypercrx-inline-label-container'; renderTo(container); await elementReady('.git-project-header-container'); $('.git-project-header-container .repository').after(container); diff --git a/src/pages/ContentScripts/features/repo-header-labels/index.tsx b/src/pages/ContentScripts/features/repo-header-labels/index.tsx index e1209d54..7c82ec94 100644 --- a/src/pages/ContentScripts/features/repo-header-labels/index.tsx +++ b/src/pages/ContentScripts/features/repo-header-labels/index.tsx @@ -5,7 +5,7 @@ import OpenrankView from './openrankView'; import ParticipantView from './participantView'; import { NativePopover } from '../../components/NativePopover'; import elementReady from 'element-ready'; -import { getRepoName, hasRepoContainerHeader, isPublicRepoWithMeta } from '../../../../helpers/get-github-repo-info'; +import { getRepoName, isPublicRepoWithMeta } from '../../../../helpers/get-github-repo-info'; import { getActivity, getOpenrank, getParticipant, getContributor } from '../../../../api/repo'; import { RepoMeta, metaStore } from '../../../../api/common'; import React from 'react'; @@ -21,6 +21,9 @@ let participant: any; let contributor: any; let meta: RepoMeta; let platform: string; +const repoTitleSelector = '#repo-title-component'; +const repoTitleFallbackSelector = '#repository-container-header .flex-auto.min-width-0.width-fit'; +const repoTitleContainerSelector = `${repoTitleSelector}, ${repoTitleFallbackSelector}`; const getData = async () => { activity = await getActivity(platform, repoName); @@ -35,6 +38,37 @@ const renderTo = (container: any) => { ); }; +const getRepoTitleContainer = () => { + const pickFirst = (selector: string) => { + const $elements = $(selector).filter((_, element) => !element.closest('template')); + const $visibleElements = $elements.filter(':visible'); + + return ($visibleElements.length > 0 ? $visibleElements : $elements).first(); + }; + + const $repoTitle = pickFirst(repoTitleSelector); + if ($repoTitle.length > 0) { + return $repoTitle; + } + + return pickFirst(repoTitleFallbackSelector); +}; +const mountHeaderLabels = (container: HTMLElement) => { + const $repoTitleContainer = getRepoTitleContainer(); + + if ($repoTitleContainer.length === 0) { + return; + } + + const $visibilityLabel = $repoTitleContainer.children('span.Label, .Label, [class*="Label--"]').last(); + + if ($visibilityLabel.length > 0) { + $visibilityLabel.after(container); + return; + } + + $repoTitleContainer.append(container); +}; const waitForElement = (selector: string) => { return new Promise((resolve) => { const observer = new MutationObserver(() => { @@ -53,9 +87,10 @@ const init = async (): Promise => { await getData(); const container = document.createElement('div'); container.id = featureId; + container.className = 'hypercrx-inline-label-container'; renderTo(container); - await elementReady('#repository-container-header'); - $('#repository-container-header').find('span.Label').after(container); + await elementReady(repoTitleContainerSelector); + mountHeaderLabels(container); await waitForElement('#activity-header-label'); await waitForElement('#OpenRank-header-label'); await waitForElement('#participant-header-label'); @@ -85,11 +120,18 @@ const restore = async () => { // Ideally, we should do nothing if the container already exists. But after a tubor // restoration visit, tooltip cannot be triggered though it exists in DOM tree. One // way to solve this is to rerender the view to the container. At least this way works. - renderTo($(`#${featureId}`)[0]); + const container = $(`#${featureId}`)[0]; + if (!container) { + return; + } + + await elementReady(repoTitleContainerSelector); + mountHeaderLabels(container); + renderTo(container); }; features.add(featureId, { - asLongAs: [isGithub, isPublicRepoWithMeta, hasRepoContainerHeader], + asLongAs: [isGithub, isPublicRepoWithMeta], awaitDomReady: false, init, restore, diff --git a/src/pages/ContentScripts/features/repo-header-labels/view.tsx b/src/pages/ContentScripts/features/repo-header-labels/view.tsx index cc301adb..aefde91c 100644 --- a/src/pages/ContentScripts/features/repo-header-labels/view.tsx +++ b/src/pages/ContentScripts/features/repo-header-labels/view.tsx @@ -38,10 +38,10 @@ const View = ({ activity, openrank, participant, contributor, meta }: Props): JS const rocketDarkLogo = chrome.runtime.getURL('rocketDarkLogo.png'); const textColor = isGithub() ? (theme === 'light' ? '#24292F' : '#C9D1D9') : '#40485B'; return ( -
+
@@ -96,7 +95,7 @@ const View = ({ activity, openrank, participant, contributor, meta }: Props): JS => { platform = getPlatform(); await getData(); - await elementReady('#issues-tab'); - const $issueTab = $('#issues-tab'); + // GitHub 新版仓库导航:使用 data-tab-item="issues" 的导航链接 + await elementReady('a[data-tab-item="issues"]'); + const $issueTab = $('a[data-tab-item="issues"]'); const placeholderElement = $('
').appendTo('body')[0]; createRoot(placeholderElement).render( diff --git a/src/pages/ContentScripts/features/repo-pr-tooltip/index.tsx b/src/pages/ContentScripts/features/repo-pr-tooltip/index.tsx index c9b8e123..a64f4977 100644 --- a/src/pages/ContentScripts/features/repo-pr-tooltip/index.tsx +++ b/src/pages/ContentScripts/features/repo-pr-tooltip/index.tsx @@ -29,6 +29,13 @@ let PRDetail: PRDetail = { }; let meta: RepoMeta; let platform: string; +const pullRequestTabSelectors = [ + 'a[data-tab-item="pull-requests"]', + '#pull-requests-tab', + 'a[href$="/pulls"][data-selected-links*="repo_pulls"]', + 'a[href$="/pulls"]', +]; + const getData = async () => { PRDetail.PROpened = await getPROpened(platform, repoName); PRDetail.PRMerged = await getPRMerged(platform, repoName); @@ -37,13 +44,19 @@ const getData = async () => { PRDetail.mergedCodeDeletion = await getMergedCodeDeletion(platform, repoName); meta = (await metaStore.get(platform, repoName)) as RepoMeta; }; +const getPullRequestTab = () => { + const $tabs = $(pullRequestTabSelectors.join(',')).filter((_, element) => !element.closest('template')); + const $visibleTabs = $tabs.filter(':visible'); + + return ($visibleTabs.length > 0 ? $visibleTabs : $tabs).first(); +}; const init = async (): Promise => { platform = getPlatform(); repoName = getRepoName(); await getData(); - await elementReady('#pull-requests-tab'); - const $prTab = $('#pull-requests-tab'); + await elementReady(pullRequestTabSelectors.join(',')); + const $prTab = getPullRequestTab(); const placeholderElement = $('
').appendTo('body')[0]; createRoot(placeholderElement).render( diff --git a/src/pages/ContentScripts/features/repo-sidebar-labels/index.tsx b/src/pages/ContentScripts/features/repo-sidebar-labels/index.tsx index f0369335..5ac8a63d 100644 --- a/src/pages/ContentScripts/features/repo-sidebar-labels/index.tsx +++ b/src/pages/ContentScripts/features/repo-sidebar-labels/index.tsx @@ -1,5 +1,6 @@ import features from '../../../../feature-manager'; -import { getRepoName, hasRepoContainerHeader, isPublicRepoWithMeta } from '../../../../helpers/get-github-repo-info'; +import elementReady from 'element-ready'; +import { getRepoName, getRepoSidebarSection, isPublicRepoWithMeta } from '../../../../helpers/get-github-repo-info'; import { Label, RepoMeta, metaStore } from '../../../../api/common'; import { createRoot } from 'react-dom/client'; import OpenDiggerLabel from './OpenDiggerLabel'; @@ -19,14 +20,34 @@ const getLabels = async (repoName: string) => { }; const renderTags = (labels: Label[]) => { - let githubTagContainer = $('.topic-tag.topic-tag-link').parent(); + const $sidebarSection = getRepoSidebarSection(); + if ($sidebarSection.length === 0) { + return; + } + + let githubTagContainer = $sidebarSection.find('.topic-tag.topic-tag-link').parent().first(); // some repositories don't have tags, create a tag container for our tags if (githubTagContainer.length === 0) { githubTagContainer = $('
'); - const githubTagContainerWrap = $('
'); + const githubTagContainerWrap = $('
'); githubTagContainerWrap.append(githubTagContainer); - const anchor = $('h3.sr-only:contains("Resources")'); - githubTagContainerWrap.insertBefore(anchor); + const resourcesHeading = $sidebarSection + .find('h3.sr-only') + .filter((_, element) => $(element).text().trim().toLowerCase() === 'resources') + .first(); + const readmeResourceRow = $sidebarSection + .find('a[href="#readme-ov-file"], a[data-analytics-event*="file:readme"]') + .first() + .closest('div'); + const insertionAnchor = resourcesHeading.length > 0 ? resourcesHeading : readmeResourceRow; + + if (insertionAnchor.length > 0) { + $('

Topics

').insertBefore(insertionAnchor); + githubTagContainerWrap.insertBefore(insertionAnchor); + } else { + $sidebarSection.append('

Topics

'); + $sidebarSection.append(githubTagContainerWrap); + } } for (const label of labels) { const id = `opendigger-label-${label.id}`; @@ -43,6 +64,7 @@ const renderTags = (labels: Label[]) => { const init = async (): Promise => { platform = getPlatform(); const repoName = getRepoName(); + await elementReady('.BorderGrid-cell, .Layout-sidebar'); const labels = await getLabels(repoName); if (labels && labels.length > 0) { renderTags(labels); @@ -50,7 +72,7 @@ const init = async (): Promise => { }; features.add(featureId, { - asLongAs: [isGithub, isPublicRepoWithMeta, hasRepoContainerHeader], + asLongAs: [isGithub, isPublicRepoWithMeta], awaitDomReady: true, init, }); diff --git a/src/pages/ContentScripts/features/repo-star-tooltip/index.tsx b/src/pages/ContentScripts/features/repo-star-tooltip/index.tsx index 6e96482e..1664717f 100644 --- a/src/pages/ContentScripts/features/repo-star-tooltip/index.tsx +++ b/src/pages/ContentScripts/features/repo-star-tooltip/index.tsx @@ -1,9 +1,8 @@ import features from '../../../../feature-manager'; import View from './view'; import { NativePopover } from '../../components/NativePopover'; -import { checkLogined } from '../../../../helpers/get-github-developer-info'; import elementReady from 'element-ready'; -import { getRepoName, hasRepoContainerHeader, isPublicRepoWithMeta } from '../../../../helpers/get-github-repo-info'; +import { getRepoName, isPublicRepoWithMeta } from '../../../../helpers/get-github-repo-info'; import { getStars } from '../../../../api/repo'; import { RepoMeta, metaStore } from '../../../../api/common'; import { createRoot } from 'react-dom/client'; @@ -16,28 +15,33 @@ let repoName: string; let stars: any; let meta: RepoMeta; let platform: string; +const starButtonSelectors = [ + 'button[data-hydro-click*="STAR_BUTTON"]', + 'button[data-hydro-click*="UNSTAR_BUTTON"]', + 'button[data-ga-click*="star button"]', + 'a[data-hydro-click*="star button"]', + 'a[data-ga-click*="star button"]', +]; + const getData = async () => { stars = await getStars(platform, repoName); meta = (await metaStore.get(platform, repoName)) as RepoMeta; }; +const getStarButtons = () => { + return $(starButtonSelectors.join(',')).filter((_, element) => !element.closest('template')); +}; const init = async (): Promise => { platform = getPlatform(); repoName = getRepoName(); await getData(); - const isLogined = checkLogined(); - const starButtonSelector = isLogined ? 'button[data-ga-click*="star button"]' : 'a[data-hydro-click*="star button"]'; - await elementReady(starButtonSelector); - //
- //
- // No matter the repo is starred or not, the two button are always there - // Select all star buttons and no more filtering + await elementReady(starButtonSelectors.join(',')); + const $starButtons = getStarButtons(); - const $starButtons = $(starButtonSelector); - $starButtons.each(function () { + $starButtons.each(function (_index, element) { const placeholderElement = $('
').appendTo('body')[0]; createRoot(placeholderElement).render( - + ); @@ -47,7 +51,7 @@ const init = async (): Promise => { const restore = async () => {}; features.add(featureId, { - asLongAs: [isGithub, isPublicRepoWithMeta, hasRepoContainerHeader], + asLongAs: [isGithub, isPublicRepoWithMeta], awaitDomReady: false, init, restore, diff --git a/src/pages/ContentScripts/index.scss b/src/pages/ContentScripts/index.scss index ac763c5c..35297ffd 100644 --- a/src/pages/ContentScripts/index.scss +++ b/src/pages/ContentScripts/index.scss @@ -142,34 +142,46 @@ .custom-tooltip .ant-tooltip-arrow::before { background-color: #ffffff !important; } -.Label, -.label { +.hypercrx-label { border: 0.0625rem solid #d1d9e0; border-radius: 624.9375rem; - display: inline-block; + display: inline-flex; + align-items: center; + gap: 0.25rem; font-size: 0.75rem; font-weight: 500; line-height: 18px; padding: 0 0.375rem; white-space: nowrap; } -.Label--secondary { - border-color: #3a3e41; - color: #393b3d; -} -.mr-1 { - margin-right: (0.25rem, 4px) !important; -} - -.v-align-middle { - vertical-align: middle !important; +.hypercrx-label--secondary { + background-color: var(--bgColor-muted, var(--color-canvas-subtle)); + border-color: var(--borderColor-default, var(--color-border-default)); + color: var(--fgColor-muted, var(--color-fg-muted)); } .unselectable { user-select: none; } -.inline-label-container { +.hypercrx-inline-label-container { display: inline-block; margin-left: 4px; + vertical-align: middle; +} + +.hypercrx-repo-header-labels { + display: inline-flex; + flex-wrap: wrap; + align-items: center; + gap: 4px; +} + +.hypercrx-repo-header-label { + cursor: default; +} + +.hypercrx-repo-header-label-icon { + display: block; + flex-shrink: 0; } .flex-items-center { diff --git a/utils/server.js b/utils/server.js index bffe8036..5e8bb735 100755 --- a/utils/server.js +++ b/utils/server.js @@ -39,7 +39,6 @@ const compiler = webpack(config); const server = new WebpackDevServer( { - https: false, hot: false, client: false, compress: false, // if set true, server-sent events will not work!