diff --git a/README.md b/README.md index 57666bc..19f082a 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,9 @@ Better GitHub is a Chrome extension that enhances the GitHub experience. Inspire **Open source and zero-tracking** — no analytics, no telemetry; all your data stays in your browser. (Some features require a GitHub token with the `repo` scope.) - +| Without Better GitHub | With Better GitHub | +| :---: | :---: | +| GitHub without Better GitHub | GitHub with Better GitHub | ### Features Overview @@ -20,6 +22,8 @@ Every feature can be individually toggled on or off in the settings. > **Note:** Because GitHub frequently A/B tests features and offers extra toggles under Feature Preview, features that are hard to maintain or change often may not be added — the goal is to keep the project at a manageable complexity. +Please feel free to [file an issue](https://github.com/rrbe/better-github/issues) to share your suggestions and bug reports. + ## Feature Details ### Home diff --git a/README_CN.md b/README_CN.md index 5af35f8..948b7c0 100644 --- a/README_CN.md +++ b/README_CN.md @@ -8,7 +8,9 @@ Better GitHub 是一个增强 GitHub 使用体验的 Chrome 插件,灵感源 **开源且零追踪**——无数据分析,无遥测,所有数据均保留在您的浏览器中。(部分功能需要具有 `repo` 权限的 GitHub Token。) - +| 未使用 Better GitHub | 使用 Better GitHub | +| :----------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------: | +| 未使用 Better GitHub 的 GitHub 页面 | 使用 Better GitHub 后的 GitHub 页面 | ### 功能概览 @@ -20,6 +22,8 @@ Better GitHub 是一个增强 GitHub 使用体验的 Chrome 插件,灵感源 > **注意:** 由于 GitHub 经常灰度测试功能,并且在 Feature Preview 中还有一些额外的功能开关,一些难以维护或经常变动的功能可能就不会加了,尽量控制项目在一个可维护的复杂度。 +欢迎随时[提交 Issue](https://github.com/rrbe/better-github/issues),分享你的建议或报告 Bug。 + ## 功能详情 ### 首页 diff --git a/docs/screenshots/main_screenshot.png b/docs/screenshots/main_screenshot.png deleted file mode 100644 index 89f4472..0000000 Binary files a/docs/screenshots/main_screenshot.png and /dev/null differ diff --git a/docs/screenshots/main_screenshot_with_better_github.png b/docs/screenshots/main_screenshot_with_better_github.png new file mode 100644 index 0000000..39e047a Binary files /dev/null and b/docs/screenshots/main_screenshot_with_better_github.png differ diff --git a/docs/screenshots/main_screenshot_without_better_github.png b/docs/screenshots/main_screenshot_without_better_github.png new file mode 100644 index 0000000..91895e6 Binary files /dev/null and b/docs/screenshots/main_screenshot_without_better_github.png differ diff --git a/src/features/pr-branch-names.ts b/src/features/pr-branch-names.ts index 0a93423..f1fcc86 100644 --- a/src/features/pr-branch-names.ts +++ b/src/features/pr-branch-names.ts @@ -1,6 +1,6 @@ import { isPRListPage, getRepoInfo, getPRListParams } from "../lib/page-detect"; import { fetchPRBranches } from "../lib/github-api"; -import { getOrCreateInfoRow } from "../lib/info-row"; +import { insertInfoRowItem } from "../lib/info-row"; import { clearSkeletons } from "../lib/info-row-skeleton"; import { t } from "../lib/i18n"; @@ -69,16 +69,13 @@ export async function injectPRBranchNames(): Promise { if (row.querySelector(`.${BADGE_CLASS}`)) continue; - const infoRow = getOrCreateInfoRow(row); - if (!infoRow) continue; - const badge = document.createElement("span"); badge.className = BADGE_CLASS; badge.textContent = branchName; badge.dataset.branch = branchName; badge.title = t("branchCopyTitle"); - infoRow.appendChild(badge); + insertInfoRowItem(row, "branch", badge); } } finally { clearSkeletons("branch"); diff --git a/src/features/pr-conflict-indicator.ts b/src/features/pr-conflict-indicator.ts index 184c448..7d699c2 100644 --- a/src/features/pr-conflict-indicator.ts +++ b/src/features/pr-conflict-indicator.ts @@ -1,6 +1,6 @@ import { isPRListPage, getRepoInfo } from "../lib/page-detect"; import { fetchPRConflictStatuses } from "../lib/github-api"; -import { getOrCreateInfoRow } from "../lib/info-row"; +import { insertInfoRowItem } from "../lib/info-row"; import { t } from "../lib/i18n"; const INDICATOR_CLASS = "better-github-conflict-indicator"; @@ -52,15 +52,12 @@ async function checkRows( continue; } - const infoRow = getOrCreateInfoRow(row); - if (!infoRow) continue; - const indicator = document.createElement("span"); indicator.className = INDICATOR_CLASS; indicator.textContent = t("prConflicts"); indicator.title = t("prConflictsTitle"); indicator.setAttribute("role", "status"); - infoRow.appendChild(indicator); + insertInfoRowItem(row, "conflict", indicator); } } diff --git a/src/features/pr-diff-stats.ts b/src/features/pr-diff-stats.ts index 7fa734c..6181f75 100644 --- a/src/features/pr-diff-stats.ts +++ b/src/features/pr-diff-stats.ts @@ -1,6 +1,6 @@ import { isPRListPage, getRepoInfo } from "../lib/page-detect"; import { fetchPRDiffStats } from "../lib/github-api"; -import { getOrCreateInfoRow } from "../lib/info-row"; +import { insertInfoRowItem } from "../lib/info-row"; import { buildDiffStatsBadge } from "../lib/diff-stats-badge"; import { clearSkeletons } from "../lib/info-row-skeleton"; @@ -38,10 +38,7 @@ export async function injectPRDiffStats(): Promise { if (row.querySelector(`.${BADGE_CLASS}`)) continue; - const infoRow = getOrCreateInfoRow(row); - if (!infoRow) continue; - - infoRow.appendChild(buildDiffStatsBadge(stat, BADGE_CLASS)); + insertInfoRowItem(row, "diff", buildDiffStatsBadge(stat, BADGE_CLASS)); } } finally { clearSkeletons("prDiff"); diff --git a/src/features/pr-label-position.ts b/src/features/pr-label-position.ts index 99bd3d9..b020a30 100644 --- a/src/features/pr-label-position.ts +++ b/src/features/pr-label-position.ts @@ -1,5 +1,5 @@ import { isIssueOrPRListPage } from "../lib/page-detect"; -import { getOrCreateInfoRow } from "../lib/info-row"; +import { insertInfoRowItem } from "../lib/info-row"; const LABEL_WRAPPER_CLASS = "better-github-label-prefix"; const HIDDEN_ORIGINAL_CLASS = "better-github-labels-hidden"; @@ -12,9 +12,6 @@ function processTrailingBadges(container: HTMLElement): void { const labels = container.querySelectorAll("a"); if (labels.length === 0) return; - const infoRow = getOrCreateInfoRow(row); - if (!infoRow) return; - const wrapper = document.createElement("span"); wrapper.className = LABEL_WRAPPER_CLASS; @@ -22,8 +19,8 @@ function processTrailingBadges(container: HTMLElement): void { wrapper.appendChild(label.cloneNode(true) as HTMLElement); } + if (!insertInfoRowItem(row, "labels", wrapper)) return; container.classList.add(HIDDEN_ORIGINAL_CLASS); - infoRow.insertBefore(wrapper, infoRow.firstChild); } /** Process old Turbo DOM rows (PR list). */ @@ -36,18 +33,15 @@ function processOldRows(): void { const labels = row.querySelectorAll("a.IssueLabel"); if (labels.length === 0) continue; - const infoRow = getOrCreateInfoRow(row); - if (!infoRow) continue; - const wrapper = document.createElement("span"); wrapper.className = LABEL_WRAPPER_CLASS; for (const label of labels) { wrapper.appendChild(label.cloneNode(true) as HTMLElement); - label.classList.add(HIDDEN_ORIGINAL_CLASS); } - infoRow.insertBefore(wrapper, infoRow.firstChild); + if (!insertInfoRowItem(row, "labels", wrapper)) continue; + for (const label of labels) label.classList.add(HIDDEN_ORIGINAL_CLASS); } } diff --git a/src/features/pr-review-status.ts b/src/features/pr-review-status.ts index 5e4816c..767ec93 100644 --- a/src/features/pr-review-status.ts +++ b/src/features/pr-review-status.ts @@ -1,7 +1,7 @@ import { isPRListPage, getRepoInfo } from "../lib/page-detect"; import { fetchPRReviewStatuses, fetchReviewThreadDetails } from "../lib/github-api"; import type { ReviewThreadDetail } from "../lib/messages"; -import { getOrCreateInfoRow } from "../lib/info-row"; +import { insertInfoRowItem } from "../lib/info-row"; // Aliased to `i18n` because this module already uses `t` as a thread loop var. import { t as i18n } from "../lib/i18n"; @@ -276,9 +276,6 @@ export async function injectPRReviewStatus(): Promise { if (row.querySelector(`.${STATUS_CLASS}`)) continue; - const infoRow = getOrCreateInfoRow(row); - if (!infoRow) continue; - const badge = document.createElement("span"); badge.className = STATUS_CLASS; @@ -298,6 +295,6 @@ export async function injectPRReviewStatus(): Promise { setupPopover(badge, info.owner, info.repo, prNumber); } - infoRow.appendChild(badge); + insertInfoRowItem(row, "review", badge); } } diff --git a/src/lib/info-row-skeleton.test.ts b/src/lib/info-row-skeleton.test.ts index d90b9e0..b8ecb2e 100644 --- a/src/lib/info-row-skeleton.test.ts +++ b/src/lib/info-row-skeleton.test.ts @@ -31,6 +31,11 @@ describe("reserveInfoRowSkeletons", () => { expect(document.querySelectorAll(".bg-skeleton-pill--branch")).toHaveLength(1); expect(document.querySelectorAll(".bg-skeleton-pill--pr-diff")).toHaveLength(1); + expect( + [...document.querySelector(".better-github-info-row")!.children].map( + (el) => (el as HTMLElement).dataset.bgInfoRowItem, + ), + ).toEqual(["branch", "diff"]); clearSkeletons("branch"); expect(document.querySelector(".bg-skeleton-pill--branch")).toBeNull(); diff --git a/src/lib/info-row-skeleton.ts b/src/lib/info-row-skeleton.ts index d042a98..41a066a 100644 --- a/src/lib/info-row-skeleton.ts +++ b/src/lib/info-row-skeleton.ts @@ -1,6 +1,6 @@ import { isPRListPage, isCommitsListPage, getRepoInfo } from "./page-detect"; import { collectCommitRows, MAIN_CONTENT_INNER_SELECTOR } from "./commit-dom"; -import { getOrCreateInfoRow } from "./info-row"; +import { insertInfoRowItem } from "./info-row"; export type SkeletonKind = "branch" | "prDiff" | "commitDiff"; @@ -61,11 +61,8 @@ function reservePRListSkeletons(flags: SkeletonFlags): void { const needDiff = wantDiff && !present.has(prDiff.real) && !present.has(prDiff.skeleton); if (!needBranch && !needDiff) continue; - const infoRow = getOrCreateInfoRow(row); - if (!infoRow) continue; - - if (needBranch) infoRow.appendChild(buildPill(branch.skeleton)); - if (needDiff) infoRow.appendChild(buildPill(prDiff.skeleton)); + if (needBranch) insertInfoRowItem(row, "branch", buildPill(branch.skeleton)); + if (needDiff) insertInfoRowItem(row, "diff", buildPill(prDiff.skeleton)); } } diff --git a/src/lib/info-row.test.ts b/src/lib/info-row.test.ts index 17f153c..7e70f18 100644 --- a/src/lib/info-row.test.ts +++ b/src/lib/info-row.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from "vitest"; -import { getOrCreateInfoRow, INFO_ROW_CLASS } from "./info-row"; +import { getOrCreateInfoRow, INFO_ROW_CLASS, insertInfoRowItem } from "./info-row"; describe("getOrCreateInfoRow", () => { beforeEach(() => { @@ -62,4 +62,49 @@ describe("getOrCreateInfoRow", () => { expect(second).toBe(first); expect(row.querySelectorAll(`.${INFO_ROW_CLASS}`)).toHaveLength(1); }); + + it("keeps items ordered and replaces skeleton slots in place", () => { + document.body.innerHTML = ` +
  • + Title +
  • + `; + const row = document.getElementById("issue_5")!; + const item = (text: string) => + Object.assign(document.createElement("span"), { textContent: text }); + + insertInfoRowItem(row, "review", item("review")); + insertInfoRowItem(row, "diff", item("diff skeleton")); + insertInfoRowItem(row, "conflict", item("conflict")); + insertInfoRowItem(row, "branch", item("branch skeleton")); + + const infoRow = row.querySelector(`.${INFO_ROW_CLASS}`)!; + expect([...infoRow.children].map((el) => el.textContent)).toEqual([ + "branch skeleton", + "diff skeleton", + "conflict", + "review", + ]); + + insertInfoRowItem(row, "labels", item("labels")); + + expect([...infoRow.children].map((el) => el.textContent)).toEqual([ + "branch skeleton", + "diff skeleton", + "labels", + "conflict", + "review", + ]); + + insertInfoRowItem(row, "diff", item("diff")); + insertInfoRowItem(row, "branch", item("branch")); + + expect([...infoRow.children].map((el) => el.textContent)).toEqual([ + "branch", + "diff", + "labels", + "conflict", + "review", + ]); + }); }); diff --git a/src/lib/info-row.ts b/src/lib/info-row.ts index c5773bf..fdea140 100644 --- a/src/lib/info-row.ts +++ b/src/lib/info-row.ts @@ -8,6 +8,10 @@ export const INFO_ROW_CLASS = "better-github-info-row"; +export type InfoRowItemKind = "branch" | "diff" | "labels" | "conflict" | "review"; + +const INFO_ROW_ITEM_ORDER: InfoRowItemKind[] = ["branch", "diff", "labels", "conflict", "review"]; + export function getOrCreateInfoRow(row: Element): HTMLElement | null { const existing = row.querySelector(`.${INFO_ROW_CLASS}`); if (existing) return existing; @@ -42,3 +46,25 @@ export function getOrCreateInfoRow(row: Element): HTMLElement | null { return null; } + +export function insertInfoRowItem(row: Element, kind: InfoRowItemKind, item: HTMLElement): boolean { + const infoRow = getOrCreateInfoRow(row); + if (!infoRow) return false; + + item.dataset.bgInfoRowItem = kind; + + const current = infoRow.querySelector(`:scope > [data-bg-info-row-item="${kind}"]`); + if (current) { + current.replaceWith(item); + return true; + } + + const itemIndex = INFO_ROW_ITEM_ORDER.indexOf(kind); + const next = [...infoRow.children].find((child) => { + const childKind = (child as HTMLElement).dataset.bgInfoRowItem as InfoRowItemKind | undefined; + const childIndex = childKind ? INFO_ROW_ITEM_ORDER.indexOf(childKind) : -1; + return childIndex === -1 || childIndex > itemIndex; + }); + infoRow.insertBefore(item, next || null); + return true; +}