Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.)

<img src="docs/screenshots/main_screenshot.png" width=600/>
| Without Better GitHub | With Better GitHub |
| :---: | :---: |
| <img src="docs/screenshots/main_screenshot_without_better_github.png" alt="GitHub without Better GitHub" width="600" /> | <img src="docs/screenshots/main_screenshot_with_better_github.png" alt="GitHub with Better GitHub" width="600" /> |

### Features Overview

Expand All @@ -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
Expand Down
6 changes: 5 additions & 1 deletion README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ Better GitHub 是一个增强 GitHub 使用体验的 Chrome 插件,灵感源

**开源且零追踪**——无数据分析,无遥测,所有数据均保留在您的浏览器中。(部分功能需要具有 `repo` 权限的 GitHub Token。)

<img src="docs/screenshots/main_screenshot.png" width=600/>
| 未使用 Better GitHub | 使用 Better GitHub |
| :----------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------: |
| <img src="docs/screenshots/main_screenshot_without_better_github.png" alt="未使用 Better GitHub 的 GitHub 页面" width="600" /> | <img src="docs/screenshots/main_screenshot_with_better_github.png" alt="使用 Better GitHub 后的 GitHub 页面" width="600" /> |

### 功能概览

Expand All @@ -20,6 +22,8 @@ Better GitHub 是一个增强 GitHub 使用体验的 Chrome 插件,灵感源

> **注意:** 由于 GitHub 经常灰度测试功能,并且在 Feature Preview 中还有一些额外的功能开关,一些难以维护或经常变动的功能可能就不会加了,尽量控制项目在一个可维护的复杂度。

欢迎随时[提交 Issue](https://github.com/rrbe/better-github/issues),分享你的建议或报告 Bug。

## 功能详情

### 首页
Expand Down
Binary file removed docs/screenshots/main_screenshot.png
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 2 additions & 5 deletions src/features/pr-branch-names.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -69,16 +69,13 @@ export async function injectPRBranchNames(): Promise<void> {

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");
Expand Down
7 changes: 2 additions & 5 deletions src/features/pr-conflict-indicator.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
}
}

Expand Down
7 changes: 2 additions & 5 deletions src/features/pr-diff-stats.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -38,10 +38,7 @@ export async function injectPRDiffStats(): Promise<void> {

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");
Expand Down
14 changes: 4 additions & 10 deletions src/features/pr-label-position.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -12,18 +12,15 @@ function processTrailingBadges(container: HTMLElement): void {
const labels = container.querySelectorAll<HTMLElement>("a");
if (labels.length === 0) return;

const infoRow = getOrCreateInfoRow(row);
if (!infoRow) return;

const wrapper = document.createElement("span");
wrapper.className = LABEL_WRAPPER_CLASS;

for (const label of labels) {
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). */
Expand All @@ -36,18 +33,15 @@ function processOldRows(): void {
const labels = row.querySelectorAll<HTMLElement>("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);
}
}

Expand Down
7 changes: 2 additions & 5 deletions src/features/pr-review-status.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -276,9 +276,6 @@ export async function injectPRReviewStatus(): Promise<void> {

if (row.querySelector(`.${STATUS_CLASS}`)) continue;

const infoRow = getOrCreateInfoRow(row);
if (!infoRow) continue;

const badge = document.createElement("span");
badge.className = STATUS_CLASS;

Expand All @@ -298,6 +295,6 @@ export async function injectPRReviewStatus(): Promise<void> {
setupPopover(badge, info.owner, info.repo, prNumber);
}

infoRow.appendChild(badge);
insertInfoRowItem(row, "review", badge);
}
}
5 changes: 5 additions & 0 deletions src/lib/info-row-skeleton.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
9 changes: 3 additions & 6 deletions src/lib/info-row-skeleton.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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));
}
}

Expand Down
47 changes: 46 additions & 1 deletion src/lib/info-row.test.ts
Original file line number Diff line number Diff line change
@@ -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(() => {
Expand Down Expand Up @@ -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 = `
<li id="issue_5">
<a id="issue_5_link">Title</a>
</li>
`;
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",
]);
});
});
26 changes: 26 additions & 0 deletions src/lib/info-row.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLElement>(`.${INFO_ROW_CLASS}`);
if (existing) return existing;
Expand Down Expand Up @@ -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<HTMLElement>(`: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;
}