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 |
+| :---: | :---: |
+|
|
|
### 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 |
+| :----------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------: |
+|
|
|
### 功能概览
@@ -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;
+}