feat: auto-refresh panel on focus with 5-minute cooldown

This commit is contained in:
gearheaded 2026-06-21 19:56:06 -04:00
parent 74ffdc95c3
commit af4aa42472
2 changed files with 93 additions and 30 deletions

56
main.js
View file

@ -36,6 +36,7 @@ var GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
var GITHUB_USER_URL = "https://api.github.com/user";
var GITHUB_CLIENT_ID = "Ov23litfj5GbQ8mw81VV";
var MONTHS_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
var AUTO_REFRESH_COOLDOWN_MS = 5 * 60 * 1e3;
var DEFAULT_SETTINGS = {
authMethod: "oauth",
githubToken: "",
@ -471,6 +472,11 @@ var ContributionsView = class extends import_obsidian.ItemView {
this.tooltipEl = null;
this.resizeObserver = null;
this.panelWidth = 0;
this.lastFetchTime = 0;
// cachedData holds the last successfully fetched result so cooldown-skipped
// renders (e.g. refocusing the panel) can redraw from memory instead of
// re-fetching from GitHub or re-scanning the filesystem.
this.cachedData = null;
this.plugin = plugin;
this.displayYear = plugin.settings.selectedYear;
this.displayMonth = new Date().getMonth();
@ -491,11 +497,17 @@ var ContributionsView = class extends import_obsidian.ItemView {
if (Math.abs(w - this.panelWidth) > 5) {
this.panelWidth = w;
if (this.plugin.settings.sizePreset === "fit")
void this.render();
void this.render(false);
}
});
this.resizeObserver.observe(this.containerEl);
this.panelWidth = this.containerEl.clientWidth;
this.registerEvent(
this.app.workspace.on("active-leaf-change", (leaf) => {
if (leaf === this.leaf)
void this.render(false);
})
);
await this.render();
}
async onClose() {
@ -519,7 +531,10 @@ var ContributionsView = class extends import_obsidian.ItemView {
const cell = Math.max(5, Math.floor((available - gap * (cols - 1)) / cols));
return { cell, gap };
}
async render() {
// force=true bypasses the auto-refresh cooldown (used by the manual refresh
// button and the very first render). force=false is used when the panel
// regains focus - if data was fetched recently, the cached result is reused.
async render(force = true) {
var _a;
const container = this.containerEl.children[1];
container.empty();
@ -538,22 +553,33 @@ var ContributionsView = class extends import_obsidian.ItemView {
this.renderEmpty(container, "local");
return;
}
this.renderSkeleton(container);
const withinCooldown = Date.now() - this.lastFetchTime < AUTO_REFRESH_COOLDOWN_MS;
const canUseCache = !force && withinCooldown && this.cachedData;
if (!canUseCache)
this.renderSkeleton(container);
try {
let repos = [];
if (needsLocal && s.localRepoRoot) {
repos = await discoverRepos(s.localRepoRoot, s.scanDepth);
}
let repos;
let days;
let totalGH = 0, totalLocal = 0;
if (s.demoMode) {
days = buildDemoData(this.displayYear);
totalGH = [...days.values()].reduce((a, d) => a + d.githubCount, 0);
let totalGH, totalLocal;
if (canUseCache && this.cachedData) {
({ days, totalGH, totalLocal, repos } = this.cachedData);
} else {
const result = await buildDayMap(s, this.displayYear, repos);
days = result.days;
totalGH = result.totalGH;
totalLocal = result.totalLocal;
repos = [];
if (needsLocal && s.localRepoRoot) {
repos = await discoverRepos(s.localRepoRoot, s.scanDepth);
}
if (s.demoMode) {
days = buildDemoData(this.displayYear);
totalGH = [...days.values()].reduce((a, d) => a + d.githubCount, 0);
totalLocal = 0;
} else {
const result = await buildDayMap(s, this.displayYear, repos);
days = result.days;
totalGH = result.totalGH;
totalLocal = result.totalLocal;
}
this.cachedData = { days, totalGH, totalLocal, repos };
this.lastFetchTime = Date.now();
}
container.empty();
const streaks = calculateStreaks(days, this.displayYear);

View file

@ -41,6 +41,12 @@ const GITHUB_CLIENT_ID = "Ov23litfj5GbQ8mw81VV";
const MONTHS_SHORT = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
// Minimum time between automatic data refreshes when the panel regains focus.
// Prevents re-fetching from GitHub's API or re-scanning the filesystem every
// time the user simply switches back to this tab. The manual refresh button
// always bypasses this cooldown.
const AUTO_REFRESH_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
// ########################################################################
// Types
// ########################################################################
@ -709,6 +715,7 @@ export class ContributionsView extends ItemView {
tooltipEl: HTMLDivElement | null = null;
private resizeObserver: ResizeObserver | null = null;
private panelWidth = 0;
private lastFetchTime = 0;
constructor(leaf: WorkspaceLeaf, plugin: GitHubContributionsPlugin) {
super(leaf);
@ -728,11 +735,21 @@ export class ContributionsView extends ItemView {
const w = this.containerEl.clientWidth;
if (Math.abs(w - this.panelWidth) > 5) {
this.panelWidth = w;
if (this.plugin.settings.sizePreset === "fit") void this.render();
if (this.plugin.settings.sizePreset === "fit") void this.render(false);
}
});
this.resizeObserver.observe(this.containerEl);
this.panelWidth = this.containerEl.clientWidth;
// Re-render (subject to the cooldown) whenever this panel becomes the
// active leaf - e.g. the user switches back to this tab after working
// elsewhere. registerEvent() handles cleanup automatically on unload.
this.registerEvent(
this.app.workspace.on("active-leaf-change", (leaf) => {
if (leaf === this.leaf) void this.render(false);
})
);
await this.render();
}
@ -761,7 +778,15 @@ export class ContributionsView extends ItemView {
return { cell, gap };
}
private async render() {
// cachedData holds the last successfully fetched result so cooldown-skipped
// renders (e.g. refocusing the panel) can redraw from memory instead of
// re-fetching from GitHub or re-scanning the filesystem.
private cachedData: { days: Map<string, DayData>; totalGH: number; totalLocal: number; repos: LocalRepo[] } | null = null;
// force=true bypasses the auto-refresh cooldown (used by the manual refresh
// button and the very first render). force=false is used when the panel
// regains focus - if data was fetched recently, the cached result is reused.
private async render(force = true) {
const container = this.containerEl.children[1] as HTMLElement;
container.empty();
container.addClass("gh-contributions-view");
@ -781,24 +806,36 @@ export class ContributionsView extends ItemView {
this.renderEmpty(container, "local"); return;
}
this.renderSkeleton(container);
const withinCooldown = Date.now() - this.lastFetchTime < AUTO_REFRESH_COOLDOWN_MS;
const canUseCache = !force && withinCooldown && this.cachedData;
if (!canUseCache) this.renderSkeleton(container);
try {
// Discover repos if needed
let repos: LocalRepo[] = [];
if (needsLocal && s.localRepoRoot) {
repos = await discoverRepos(s.localRepoRoot, s.scanDepth);
}
let repos: LocalRepo[];
let days: Map<string, DayData>;
let totalGH = 0, totalLocal = 0;
let totalGH: number, totalLocal: number;
if (s.demoMode) {
days = buildDemoData(this.displayYear);
totalGH = [...days.values()].reduce((a, d) => a + d.githubCount, 0);
if (canUseCache && this.cachedData) {
({ days, totalGH, totalLocal, repos } = this.cachedData);
} else {
const result = await buildDayMap(s, this.displayYear, repos);
days = result.days; totalGH = result.totalGH; totalLocal = result.totalLocal;
// Discover repos if needed
repos = [];
if (needsLocal && s.localRepoRoot) {
repos = await discoverRepos(s.localRepoRoot, s.scanDepth);
}
if (s.demoMode) {
days = buildDemoData(this.displayYear);
totalGH = [...days.values()].reduce((a, d) => a + d.githubCount, 0);
totalLocal = 0;
} else {
const result = await buildDayMap(s, this.displayYear, repos);
days = result.days; totalGH = result.totalGH; totalLocal = result.totalLocal;
}
this.cachedData = { days, totalGH, totalLocal, repos };
this.lastFetchTime = Date.now();
}
container.empty();