diff --git a/README.md b/README.md index 19f082a..8ab9ae4 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Better GitHub is a Chrome extension that enhances the GitHub experience. Inspire - **Contributor profile insights** — quickly tell whether a contributor is just an "AI-coding account that spams low-quality commits." - **Better PR and issue lists** — sort by most recently updated, with branch names, conflict warnings, review status, and diff stats all at a glance. -- **Repository extras** — a dedicated Releases tab, hover previews for watcher/fork/star counts, plus commit tags and diff stats. +- **Repository extras** — release asset download counts, a dedicated Releases tab, hover previews for watcher/fork/star counts, plus commit tags and diff stats. Every feature can be individually toggled on or off in the settings. @@ -118,6 +118,8 @@ Please feel free to [file an issue](https://github.com/rrbe/better-github/issues ### Repository +- **Release Asset Downloads** — Show the download count for every release asset, including zero-download assets that GitHub may leave blank. GitHub's native counts are still rolling out; disable this feature in the extension options if both counts appear. + - **Releases Tab** — Add a Releases tab to the repository navigation bar for quick access.
@@ -141,10 +143,6 @@ Please feel free to [file an issue](https://github.com/rrbe/better-github/issues All features except Recent Commit Message Color can be individually toggled on/off in the extension options. -## Removed Features - -- **Release Asset Downloads** — Removed after GitHub updated the Releases page to show release asset download counts natively. Discovered on 2026-07-01. - ## Build from Source 1. Clone the repo and build: diff --git a/README_CN.md b/README_CN.md index 948b7c0..17531d4 100644 --- a/README_CN.md +++ b/README_CN.md @@ -16,7 +16,7 @@ Better GitHub 是一个增强 GitHub 使用体验的 Chrome 插件,灵感源 - **Contributor Profile 增强** -- 帮你快速识别一个贡献者是不是“只会乱提交代码的纯 AI Coding 用户” - **更好的 PR(拉取请求)与 Issue(问题)列表** —— 支持按最近更新排序,分支名称、冲突警告、评审状态及代码差异统计(diff stats)一目了然。 -- **仓库额外扩展** —— 提供专门的 Releases 标签页、对关注者/分叉/点赞数(watchers/forks/stars)悬停预览,以及提交标签(commit tags)和代码差异统计。 +- **仓库额外扩展** —— 展示 Release 资源下载量,提供专门的 Releases 标签页、对关注者/分叉/点赞数(watchers/forks/stars)悬停预览,以及提交标签(commit tags)和代码差异统计。 所有功能均可在设置中单独开启或关闭。 @@ -118,6 +118,8 @@ Better GitHub 是一个增强 GitHub 使用体验的 Chrome 插件,灵感源 ### 仓库页面 +- **Release 资源下载量** — 显示每个 Release 资源的下载次数,包括 GitHub 可能留空的零下载资源。GitHub 原生下载量仍在灰度中;如果页面同时出现两份下载量,可在扩展选项中关闭此功能。 + - **Releases Tab** — 在仓库导航栏添加 Releases 标签页,快速访问。
@@ -141,10 +143,6 @@ Better GitHub 是一个增强 GitHub 使用体验的 Chrome 插件,灵感源 除"最近提交 Commit Message 颜色"外,所有功能均可在扩展选项中单独开关。 -## 已删除功能 - -- **Release 资源下载量** — 已删除。2026 年 7 月 1 日发现 GitHub 官方更新了 Release 页面,已原生支持显示 release asset 下载量。 - ## 自行打包安装 1. 克隆仓库并构建: diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 76f3e3d..f9d7912 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -113,6 +113,10 @@ "featReleaseTabDesc": { "message": "Add a Releases tab to the repository navigation bar for quick access." }, + "featReleaseDownloadsName": { "message": "Release Asset Downloads" }, + "featReleaseDownloadsDesc": { + "message": "Show download counts for every release asset. GitHub may also show native counts during rollout; disable this feature if they overlap." + }, "featWatchForkStarName": { "message": "Watch/Fork/Star Popup" }, "featWatchForkStarDesc": { "message": "Hover over Watch, Fork, or Star counts to preview the list in a popup." @@ -159,6 +163,10 @@ "message": "Tag: $name$", "placeholders": { "name": { "content": "$1", "example": "v1.0.0" } } }, + "assetDownloadsTitle": { + "message": "Downloaded $count$ times", + "placeholders": { "count": { "content": "$1", "example": "1,234" } } + }, "copied": { "message": "Copied!" }, "branchCopyTitle": { "message": "Click to copy branch name" }, diff --git a/src/_locales/zh_CN/messages.json b/src/_locales/zh_CN/messages.json index 45359d7..9253f52 100644 --- a/src/_locales/zh_CN/messages.json +++ b/src/_locales/zh_CN/messages.json @@ -113,6 +113,10 @@ "featReleaseTabDesc": { "message": "在仓库导航栏添加一个 Releases 标签页,方便快速访问。" }, + "featReleaseDownloadsName": { "message": "Release 资源下载量" }, + "featReleaseDownloadsDesc": { + "message": "显示每个 Release 资源的下载次数。GitHub 灰度期间可能同时显示原生下载量,如有重复可关闭此功能。" + }, "featWatchForkStarName": { "message": "Watch/Fork/Star 弹窗" }, "featWatchForkStarDesc": { "message": "悬停在 Watch、Fork 或 Star 数字上,即可在弹窗中预览列表。" @@ -159,6 +163,10 @@ "message": "Tag: $name$", "placeholders": { "name": { "content": "$1", "example": "v1.0.0" } } }, + "assetDownloadsTitle": { + "message": "已下载 $count$ 次", + "placeholders": { "count": { "content": "$1", "example": "1,234" } } + }, "copied": { "message": "已复制!" }, "branchCopyTitle": { "message": "点击复制分支名" }, diff --git a/src/_locales/zh_TW/messages.json b/src/_locales/zh_TW/messages.json index 022e0b8..ead5497 100644 --- a/src/_locales/zh_TW/messages.json +++ b/src/_locales/zh_TW/messages.json @@ -113,6 +113,10 @@ "featReleaseTabDesc": { "message": "在儲存庫導覽列新增一個 Releases 分頁,方便快速存取。" }, + "featReleaseDownloadsName": { "message": "Release 資源下載次數" }, + "featReleaseDownloadsDesc": { + "message": "顯示每個 Release 資源的下載次數。GitHub 灰度期間可能同時顯示原生下載次數,如有重複可關閉此功能。" + }, "featWatchForkStarName": { "message": "Watch/Fork/Star 彈出視窗" }, "featWatchForkStarDesc": { "message": "將游標停在 Watch、Fork 或 Star 數字上,即可在彈出視窗中預覽清單。" @@ -159,6 +163,10 @@ "message": "Tag: $name$", "placeholders": { "name": { "content": "$1", "example": "v1.0.0" } } }, + "assetDownloadsTitle": { + "message": "已下載 $count$ 次", + "placeholders": { "count": { "content": "$1", "example": "1,234" } } + }, "copied": { "message": "已複製!" }, "branchCopyTitle": { "message": "點擊複製分支名稱" }, diff --git a/src/content.ts b/src/content.ts index 442eda2..e9d7151 100644 --- a/src/content.ts +++ b/src/content.ts @@ -8,6 +8,10 @@ import { import { injectPRReviewStatus } from "./features/pr-review-status"; import { injectPRDiffStats } from "./features/pr-diff-stats"; import { injectReleasesTab } from "./features/release-tab"; +import { + cleanupReleaseAssetDownloads, + injectReleaseAssetDownloads, +} from "./features/release-asset-downloads"; import { injectPRLabelPosition, cleanupPRLabelPosition } from "./features/pr-label-position"; import { injectFileAgeColor } from "./features/file-age-color"; import { injectPRApproveNow } from "./features/pr-approve-now"; @@ -30,6 +34,7 @@ const FEATURE_KEYS = [ "feature-pr-review-status", "feature-pr-diff-stats", "feature-release-tab", + "feature-release-downloads-count", "feature-pr-label-position", "feature-pr-approve-now", "feature-default-sort", @@ -50,6 +55,7 @@ const FEATURE_CLASSES: Record = { "feature-pr-review-status": ["better-github-review-status"], "feature-pr-diff-stats": ["better-github-diff-stats", "bg-skeleton-pill--pr-diff"], "feature-release-tab": ["better-github-releases-tab"], + "feature-release-downloads-count": ["better-github-asset-downloads"], "feature-pr-label-position": ["better-github-label-prefix"], "feature-pr-approve-now": ["better-github-approve-now", "better-github-approve-dialog-overlay"], "feature-default-sort": [], @@ -68,6 +74,7 @@ const FEATURE_CLEANUPS: Partial void>> = { "feature-pr-conflict-indicator": cleanupPRConflictIndicator, "feature-pr-label-position": cleanupPRLabelPosition, "feature-watch-fork-star-popup": cleanupWatchForkStarPopup, + "feature-release-downloads-count": cleanupReleaseAssetDownloads, "feature-contributor-card": cleanupContributorCard, }; @@ -120,6 +127,9 @@ function injectFeature(key: FeatureKey): void { case "feature-release-tab": injectReleasesTab(); break; + case "feature-release-downloads-count": + void injectReleaseAssetDownloads(); + break; case "feature-pr-label-position": injectPRLabelPosition(); break; diff --git a/src/features/release-asset-downloads.test.ts b/src/features/release-asset-downloads.test.ts new file mode 100644 index 0000000..3fabfa1 --- /dev/null +++ b/src/features/release-asset-downloads.test.ts @@ -0,0 +1,125 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { setUrl } from "../test-utils/url"; +import { fetchReleaseDownloads } from "../lib/github-api"; +import { + cleanupReleaseAssetDownloads, + getDownloadHeat, + injectReleaseAssetDownloads, + parseAssetHref, +} from "./release-asset-downloads"; + +vi.mock("../lib/github-api"); + +const BADGE = ".better-github-asset-downloads"; + +function assetRow(tag: string, name: string): string { + return ` +
  • + +
    1.9 KB
    +
  • + `; +} + +describe("release asset downloads", () => { + beforeEach(() => { + vi.clearAllMocks(); + setUrl("https://github.com/owner/repo/releases"); + document.body.innerHTML = ""; + }); + + afterEach(() => { + cleanupReleaseAssetDownloads(); + vi.restoreAllMocks(); + }); + + it("parses encoded asset URLs and rejects non-assets", () => { + expect( + parseAssetHref( + "/owner/repo/releases/download/release%2F1.0/my%20app.zip?x=1", + "owner", + "repo", + ), + ).toEqual({ tag: "release/1.0", name: "my app.zip" }); + expect(parseAssetHref("/owner/repo/archive/v1.zip", "owner", "repo")).toBeNull(); + }); + + it("maps download volume onto a monotonic 1..10 heat scale", () => { + expect(getDownloadHeat(0)).toBe(1); + expect(getDownloadHeat(100)).toBe(2); + expect(getDownloadHeat(1_000)).toBe(4); + expect(getDownloadHeat(1_000_000)).toBe(10); + }); + + it("shows a zero count in the asset-name column", async () => { + document.body.innerHTML = `
      ${assetRow("v1.0.0", "app.zip")}
    `; + vi.mocked(fetchReleaseDownloads).mockResolvedValue([ + { tag: "v1.0.0", name: "app.zip", downloadCount: 0 }, + ]); + + await injectReleaseAssetDownloads(); + + const badge = document.querySelector(BADGE)!; + expect(badge.textContent).toBe("0"); + expect(badge.title).toBe("Downloaded 0 times"); + expect(badge.getAttribute("href")).toBe("/owner/repo/releases/download/v1.0.0/app.zip"); + expect(badge.parentElement?.lastElementChild).toBe(badge); + }); + + it("groups visible assets by release tag", async () => { + document.body.innerHTML = `
      ${assetRow("v2.0.0", "a.zip")}${assetRow("v2.0.0", "b.zip")}${assetRow("v1.0.0", "c.zip")}
    `; + vi.mocked(fetchReleaseDownloads).mockImplementation((_owner, _repo, tag) => + Promise.resolve( + tag === "v2.0.0" + ? [ + { tag, name: "a.zip", downloadCount: 11 }, + { tag, name: "b.zip", downloadCount: 22 }, + ] + : [{ tag, name: "c.zip", downloadCount: 33 }], + ), + ); + + await injectReleaseAssetDownloads(); + + expect(fetchReleaseDownloads).toHaveBeenCalledTimes(2); + expect(Array.from(document.querySelectorAll(BADGE), (badge) => badge.textContent)).toEqual([ + "11", + "22", + "33", + ]); + }); + + it("processes assets that GitHub renders later", async () => { + document.body.innerHTML = `
      `; + vi.mocked(fetchReleaseDownloads).mockResolvedValue([ + { tag: "v3.0.0", name: "late.zip", downloadCount: 42 }, + ]); + + await injectReleaseAssetDownloads(); + document.getElementById("list")!.innerHTML = assetRow("v3.0.0", "late.zip"); + + await vi.waitFor(() => expect(document.querySelector(BADGE)?.textContent).toBe("42")); + expect(fetchReleaseDownloads).toHaveBeenCalledWith("owner", "repo", "v3.0.0"); + }); + + it("does not inject after the feature is disabled during a fetch", async () => { + document.body.innerHTML = `
        ${assetRow("v1.0.0", "app.zip")}
      `; + let resolveDownloads!: (downloads: Awaited>) => void; + vi.mocked(fetchReleaseDownloads).mockReturnValue( + new Promise((resolve) => { + resolveDownloads = resolve; + }), + ); + + const injection = injectReleaseAssetDownloads(); + cleanupReleaseAssetDownloads(); + resolveDownloads([{ tag: "v1.0.0", name: "app.zip", downloadCount: 7 }]); + await injection; + + expect(document.querySelector(BADGE)).toBeNull(); + }); +}); diff --git a/src/features/release-asset-downloads.ts b/src/features/release-asset-downloads.ts new file mode 100644 index 0000000..cc241f8 --- /dev/null +++ b/src/features/release-asset-downloads.ts @@ -0,0 +1,167 @@ +import { isReleasesPage, getRepoInfo, type RepoInfo } from "../lib/page-detect"; +import { fetchReleaseDownloads } from "../lib/github-api"; +import { escapeHtml } from "../lib/utils"; +import { t } from "../lib/i18n"; + +const BADGE_CLASS = "better-github-asset-downloads"; + +// Octicon download-16 — matches GitHub's own icon set. +const DOWNLOAD_ICON = ``; + +interface ParsedAsset { + tag: string; + name: string; +} + +export function parseAssetHref( + href: string | null, + owner: string, + repo: string, +): ParsedAsset | null { + if (!href) return null; + const path = href.split(/[?#]/)[0]; + + const marker = "/releases/download/"; + const ownerPrefix = `/${owner}/${repo}${marker}`; + let rest: string; + if (path.startsWith(ownerPrefix)) { + rest = path.slice(ownerPrefix.length); + } else { + const idx = path.indexOf(marker); + if (idx === -1) return null; + rest = path.slice(idx + marker.length); + } + + const segments = rest.split("/"); + if (segments.length < 2) return null; + const nameSeg = segments.pop(); + if (!nameSeg) return null; + + const decode = (value: string): string => { + try { + return decodeURIComponent(value); + } catch { + return value; + } + }; + + return { tag: decode(segments.join("/")), name: decode(nameSeg) }; +} + +function findNameColumn(link: Element, row: Element): HTMLElement | null { + for (const child of Array.from(row.children)) { + if (child.contains(link)) return child as HTMLElement; + } + return null; +} + +const DOWNLOAD_HEAT_THRESHOLDS = [100, 300, 1000, 3000, 10000, 30000, 100000, 300000, 1000000]; + +export function getDownloadHeat(count: number): number { + let level = 1; + for (const threshold of DOWNLOAD_HEAT_THRESHOLDS) { + if (count >= threshold) level++; + } + return level; +} + +function buildBadge(count: number, href: string): HTMLAnchorElement { + const formatted = count.toLocaleString(); + const badge = document.createElement("a"); + badge.className = BADGE_CLASS; + badge.href = href; + badge.rel = "nofollow"; + badge.setAttribute("data-turbo", "false"); + badge.dataset.betterGithubDlHeat = String(getDownloadHeat(count)); + badge.title = t("assetDownloadsTitle", formatted); + badge.innerHTML = `${escapeHtml(formatted)}${DOWNLOAD_ICON}`; + return badge; +} + +interface PendingAsset { + link: HTMLAnchorElement; + name: string; + href: string; +} + +let observer: MutationObserver | null = null; +let rafId: number | null = null; +let runId = 0; + +async function injectTag( + info: RepoInfo, + tag: string, + items: PendingAsset[], + activeRun: number, +): Promise { + const downloads = await fetchReleaseDownloads(info.owner, info.repo, tag); + if (activeRun !== runId || downloads.length === 0) return; + + const counts = new Map(); + for (const download of downloads) counts.set(download.name, download.downloadCount); + + for (const { link, name, href } of items) { + const count = counts.get(name); + if (count === undefined) continue; + + const row = link.closest("li"); + if (!row || row.querySelector(`.${BADGE_CLASS}`)) continue; + + const nameColumn = findNameColumn(link, row); + if (nameColumn) nameColumn.appendChild(buildBadge(count, href)); + } +} + +async function processVisibleAssets(info: RepoInfo, activeRun: number): Promise { + const links = document.querySelectorAll( + `a[href*="/releases/download/"]:not(.${BADGE_CLASS}):not([data-bg-dl-seen])`, + ); + if (links.length === 0) return; + + const byTag = new Map(); + for (const link of links) { + link.dataset.bgDlSeen = "1"; + const href = link.getAttribute("href"); + const parsed = parseAssetHref(href, info.owner, info.repo); + if (!href || !parsed) continue; + const item: PendingAsset = { link, name: parsed.name, href }; + const group = byTag.get(parsed.tag); + if (group) group.push(item); + else byTag.set(parsed.tag, [item]); + } + + await Promise.all([...byTag].map(([tag, items]) => injectTag(info, tag, items, activeRun))); +} + +export async function injectReleaseAssetDownloads(): Promise { + cleanupReleaseAssetDownloads(); + const activeRun = runId; + if (!isReleasesPage()) return; + + const info = getRepoInfo(); + if (!info) return; + + observer = new MutationObserver(() => { + if (rafId !== null) return; + rafId = requestAnimationFrame(() => { + rafId = null; + void processVisibleAssets(info, activeRun); + }); + }); + observer.observe(document.body, { childList: true, subtree: true }); + + await processVisibleAssets(info, activeRun); +} + +export function cleanupReleaseAssetDownloads(): void { + runId++; + observer?.disconnect(); + observer = null; + if (rafId !== null) { + cancelAnimationFrame(rafId); + rafId = null; + } + document.querySelectorAll("[data-bg-dl-seen]").forEach((element) => { + delete element.dataset.bgDlSeen; + }); +} diff --git a/src/lib/github-api.ts b/src/lib/github-api.ts index 4e88bb9..cde55bb 100644 --- a/src/lib/github-api.ts +++ b/src/lib/github-api.ts @@ -12,6 +12,7 @@ import type { WatcherInfo, SocialList, ForkInfo, + ReleaseAssetDownload, ContributorInfo, } from "./messages"; @@ -52,6 +53,7 @@ export type { WatcherInfo, SocialList, ForkInfo, + ReleaseAssetDownload, ContributorInfo, }; @@ -265,3 +267,21 @@ export async function fetchForks(owner: string, repo: string): Promise { + try { + return await sendMessage({ + type: "FETCH_RELEASE_DOWNLOADS", + owner, + repo, + tag, + }); + } catch (err) { + console.error("[Better GitHub] Failed to fetch release downloads:", err); + return []; + } +} diff --git a/src/lib/messages.ts b/src/lib/messages.ts index c3799d9..2fdc8f5 100644 --- a/src/lib/messages.ts +++ b/src/lib/messages.ts @@ -76,6 +76,12 @@ export interface ForkInfo { stargazersCount: number; } +export interface ReleaseAssetDownload { + tag: string; + name: string; + downloadCount: number; +} + /** Objective facts about a GitHub account, for the contributor profile card. * Pure-fact only — no scoring. See docs/contributor-profile-card.md. */ export interface ContributorInfo { @@ -114,6 +120,7 @@ export type ServiceWorkerRequest = | { type: "FETCH_STARGAZERS"; owner: string; repo: string } | { type: "FETCH_WATCHERS"; owner: string; repo: string } | { type: "FETCH_FORKS"; owner: string; repo: string } + | { type: "FETCH_RELEASE_DOWNLOADS"; owner: string; repo: string; tag: string } | { type: "FETCH_CONTRIBUTOR_INFO"; login: string; owner?: string; repo?: string }; export type ServiceWorkerResponse = { ok: true; data: T } | { ok: false; error: string }; diff --git a/src/options.test.ts b/src/options.test.ts index 2248a0c..b6a80db 100644 --- a/src/options.test.ts +++ b/src/options.test.ts @@ -6,6 +6,7 @@ const FEATURE_KEYS = [ "feature-pr-review-status", "feature-pr-diff-stats", "feature-release-tab", + "feature-release-downloads-count", "feature-pr-label-position", "feature-pr-approve-now", "feature-default-sort", diff --git a/src/options.ts b/src/options.ts index 10cf08c..fe20b8a 100644 --- a/src/options.ts +++ b/src/options.ts @@ -30,6 +30,7 @@ const FEATURE_KEYS = [ "feature-pr-review-status", "feature-pr-diff-stats", "feature-release-tab", + "feature-release-downloads-count", "feature-pr-label-position", "feature-pr-approve-now", "feature-default-sort", diff --git a/src/service-worker.test.ts b/src/service-worker.test.ts index 632b48d..849df6d 100644 --- a/src/service-worker.test.ts +++ b/src/service-worker.test.ts @@ -575,6 +575,37 @@ describe("service worker", () => { expect(vi.mocked(fetch).mock.calls[0][0]).toContain("/repos/owner/repo/forks"); }); + it("fetches a release by tag and keeps zero-download assets", async () => { + const state = await loadWorker("token"); + vi.mocked(fetch).mockResolvedValue( + jsonResponse({ + tag_name: "release/1.0", + assets: [ + { name: "app.zip", download_count: 42 }, + { name: "empty.zip", download_count: 0 }, + ], + }), + ); + + const response = await sendMessage(state.messageListeners[0], { + type: "FETCH_RELEASE_DOWNLOADS", + owner: "owner", + repo: "repo", + tag: "release/1.0", + }); + + expect(response).toEqual({ + ok: true, + data: [ + { tag: "release/1.0", name: "app.zip", downloadCount: 42 }, + { tag: "release/1.0", name: "empty.zip", downloadCount: 0 }, + ], + }); + expect(vi.mocked(fetch).mock.calls[0][0]).toBe( + "https://api.github.com/repos/owner/repo/releases/tags/release%2F1.0", + ); + }); + it("reads tag commit OIDs from the GraphQL refs response", async () => { const state = await loadWorker("token"); vi.mocked(fetch).mockResolvedValue( diff --git a/src/service-worker.ts b/src/service-worker.ts index 16b85d0..abdd9b6 100644 --- a/src/service-worker.ts +++ b/src/service-worker.ts @@ -13,6 +13,7 @@ import type { WatcherInfo, SocialList, ForkInfo, + ReleaseAssetDownload, ContributorInfo, } from "./lib/messages"; @@ -672,6 +673,39 @@ async function fetchForks(owner: string, repo: string): Promise { }); } +interface RawRelease { + tag_name: string; + assets?: Array<{ name: string; download_count: number }>; +} + +async function fetchReleaseDownloads( + owner: string, + repo: string, + tag: string, +): Promise { + const cacheKey = `cache:releasedl:${owner}/${repo}:tag:${tag}`; + return cachedFetch(cacheKey, async () => { + const headers = restHeaders(await getToken(), "application/vnd.github+json"); + const url = `https://api.github.com/repos/${owner}/${repo}/releases/tags/${encodeURIComponent(tag)}`; + const response = await fetch(url, { headers }); + + if (!response.ok) { + console.error( + `[Better GitHub] Release downloads API error: ${response.status} ${response.statusText}`, + ); + return []; + } + + const release: RawRelease = await response.json(); + if (!release.tag_name || !Array.isArray(release.assets)) return []; + return release.assets.map((asset) => ({ + tag: release.tag_name, + name: asset.name, + downloadCount: asset.download_count, + })); + }); +} + // --- Contributor profile card --- // // Objective facts about an account, fetched lazily on hover (one user at a @@ -854,6 +888,11 @@ async function handleMessage( return { ok: true, data: await fetchWatchers(request.owner, request.repo) }; case "FETCH_FORKS": return { ok: true, data: await fetchForks(request.owner, request.repo) }; + case "FETCH_RELEASE_DOWNLOADS": + return { + ok: true, + data: await fetchReleaseDownloads(request.owner, request.repo, request.tag), + }; case "FETCH_CONTRIBUTOR_INFO": return { ok: true, diff --git a/src/styles/content.css b/src/styles/content.css index c77b994..2842d2a 100644 --- a/src/styles/content.css +++ b/src/styles/content.css @@ -261,6 +261,64 @@ a.better-github-review-popover-item:hover { color: var(--fgColor-muted, var(--color-fg-muted, #656d76)); } +/* Release Asset Downloads */ +.better-github-asset-downloads { + display: inline-flex; + align-items: center; + justify-content: flex-end; + gap: 4px; + margin-left: auto; + padding: 0 16px; + flex-shrink: 0; + white-space: nowrap; + font-size: 12px; + font-weight: 500; + line-height: 18px; + text-align: right; + text-decoration: none; + color: color-mix( + in srgb, + var(--better-github-heat-color) var(--better-github-download-heat, 0%), + var(--fgColor-muted, var(--color-fg-muted, #656d76)) + ) !important; +} + +.better-github-asset-downloads:hover { + text-decoration: underline; +} + +.better-github-asset-downloads svg { + flex-shrink: 0; +} + +.better-github-asset-downloads[data-better-github-dl-heat="2"] { + --better-github-download-heat: 12%; +} +.better-github-asset-downloads[data-better-github-dl-heat="3"] { + --better-github-download-heat: 22%; +} +.better-github-asset-downloads[data-better-github-dl-heat="4"] { + --better-github-download-heat: 33%; +} +.better-github-asset-downloads[data-better-github-dl-heat="5"] { + --better-github-download-heat: 44%; +} +.better-github-asset-downloads[data-better-github-dl-heat="6"] { + --better-github-download-heat: 56%; +} +.better-github-asset-downloads[data-better-github-dl-heat="7"] { + --better-github-download-heat: 67%; +} +.better-github-asset-downloads[data-better-github-dl-heat="8"] { + --better-github-download-heat: 78%; +} +.better-github-asset-downloads[data-better-github-dl-heat="9"] { + --better-github-download-heat: 89%; +} +.better-github-asset-downloads[data-better-github-dl-heat="10"] { + --better-github-download-heat: 100%; +} + /* File Age Color — heat map for recent commits */ :root { --better-github-heat-color: #c24e00; diff --git a/static/options.html b/static/options.html index c9d34f9..e2b0547 100644 --- a/static/options.html +++ b/static/options.html @@ -723,6 +723,21 @@

      Settings

      +
    • +
      +
      + Release Asset Downloads +
      +
      + Show download counts for every release asset. GitHub may also show native counts + during rollout; disable this feature if they overlap. +
      +
      + +
    • Watch/Fork/Star Popup