mirror of
https://github.com/gearheaded/obsidian-github-contributions.git
synced 2026-07-22 07:43:36 +00:00
Add scan depth, add demo mode, fix commit wording
This commit is contained in:
parent
2c3dcad68f
commit
117e271b71
3 changed files with 154 additions and 23 deletions
BIN
files.zip
BIN
files.zip
Binary file not shown.
83
main.js
83
main.js
|
|
@ -37,6 +37,8 @@ var DEFAULT_SETTINGS = {
|
|||
githubUsername: "",
|
||||
dataSource: "github",
|
||||
localRepoRoot: "",
|
||||
scanDepth: 2,
|
||||
demoMode: false,
|
||||
sidebarSide: "right",
|
||||
selectedYear: new Date().getFullYear(),
|
||||
sizePreset: "medium",
|
||||
|
|
@ -109,7 +111,7 @@ function runGit(args, cwd) {
|
|||
return "";
|
||||
}
|
||||
}
|
||||
async function discoverRepos(rootPath) {
|
||||
async function discoverRepos(rootPath, maxDepth = 2) {
|
||||
if (!isDesktop)
|
||||
return [];
|
||||
const repos = [];
|
||||
|
|
@ -117,7 +119,7 @@ async function discoverRepos(rootPath) {
|
|||
const fs = window.require("fs");
|
||||
const path = window.require("path");
|
||||
const scanDir = (dir, depth) => {
|
||||
if (depth > 2)
|
||||
if (maxDepth !== 0 && depth > maxDepth)
|
||||
return;
|
||||
let entries;
|
||||
try {
|
||||
|
|
@ -296,6 +298,40 @@ function daysSinceLastCommit(days) {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
function buildDemoData(year) {
|
||||
const days = /* @__PURE__ */ new Map();
|
||||
const isLeap = year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
|
||||
const daysInYear = isLeap ? 366 : 365;
|
||||
let seed = 42;
|
||||
const rand = () => {
|
||||
seed = seed * 1664525 + 1013904223 & 4294967295;
|
||||
return (seed >>> 0) / 4294967295;
|
||||
};
|
||||
const demoRepos = ["my-project", "obsidian-plugin", "dotfiles"];
|
||||
for (let i = 0; i < daysInYear; i++) {
|
||||
const d = new Date(year, 0, i + 1);
|
||||
const dateStr = `${year}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||||
const r = rand();
|
||||
const recencyBoost = i / daysInYear;
|
||||
const active = r < 0.45 + recencyBoost * 0.3;
|
||||
if (!active)
|
||||
continue;
|
||||
const count = Math.floor(rand() * 8) + 1;
|
||||
const repos = {};
|
||||
let remaining = count;
|
||||
for (const repo of demoRepos) {
|
||||
if (remaining <= 0)
|
||||
break;
|
||||
const n = Math.min(remaining, Math.floor(rand() * 4) + 1);
|
||||
if (rand() > 0.4) {
|
||||
repos[repo] = n;
|
||||
remaining -= n;
|
||||
}
|
||||
}
|
||||
days.set(dateStr, { date: dateStr, count, githubCount: count, localCount: 0, repos });
|
||||
}
|
||||
return days;
|
||||
}
|
||||
function mostRecentRepo(repos) {
|
||||
var _a, _b;
|
||||
if (!repos.length)
|
||||
|
|
@ -383,13 +419,23 @@ var ContributionsView = class extends import_obsidian.ItemView {
|
|||
try {
|
||||
let repos = [];
|
||||
if (needsLocal && s.localRepoRoot) {
|
||||
repos = await discoverRepos(s.localRepoRoot);
|
||||
repos = await discoverRepos(s.localRepoRoot, s.scanDepth);
|
||||
}
|
||||
let days;
|
||||
let totalGH = 0, totalLocal = 0;
|
||||
if (s.demoMode) {
|
||||
days = buildDemoData(this.displayYear);
|
||||
totalGH = [...days.values()].reduce((a, d) => a + d.githubCount, 0);
|
||||
} else {
|
||||
const result = await buildDayMap(s, this.displayYear, repos);
|
||||
days = result.days;
|
||||
totalGH = result.totalGH;
|
||||
totalLocal = result.totalLocal;
|
||||
}
|
||||
const { days, totalGH, totalLocal } = await buildDayMap(s, this.displayYear, repos);
|
||||
container.empty();
|
||||
const streaks = calculateStreaks(days, this.displayYear);
|
||||
const sinceCommit = daysSinceLastCommit(days);
|
||||
const recentRepo = mostRecentRepo(repos);
|
||||
const recentRepo = s.demoMode ? "my-project" : mostRecentRepo(repos);
|
||||
this.renderHeader(container);
|
||||
this.renderStats(container, { totalGH, totalLocal, streaks, sinceCommit, recentRepo });
|
||||
const style = (_a = this.plugin.settings.statsStyle) != null ? _a : "default";
|
||||
|
|
@ -475,8 +521,10 @@ var ContributionsView = class extends import_obsidian.ItemView {
|
|||
["\u{1F525}", info.streaks.current + "d", "Current streak"],
|
||||
["\u2B50", info.streaks.longest + "d", "Best streak"]
|
||||
];
|
||||
if (showLocal && info.sinceCommit !== null)
|
||||
items.push(["\u23F1", info.sinceCommit + "d", "Days since last commit"]);
|
||||
if (showLocal && info.sinceCommit !== null) {
|
||||
const sinceLabel = info.sinceCommit === 0 ? "committed today" : info.sinceCommit + "d ago";
|
||||
items.push(["\u23F1", info.sinceCommit === 0 ? "today" : info.sinceCommit + "d", sinceLabel]);
|
||||
}
|
||||
if (info.recentRepo)
|
||||
items.push(["\u{1F4C1}", info.recentRepo, info.recentRepo]);
|
||||
for (const [icon, val, title] of items) {
|
||||
|
|
@ -490,7 +538,7 @@ var ContributionsView = class extends import_obsidian.ItemView {
|
|||
this.pill(stats, info.streaks.current + "d", "streak");
|
||||
this.pill(stats, info.streaks.longest + "d", "best");
|
||||
if (showLocal && info.sinceCommit !== null)
|
||||
this.pill(stats, info.sinceCommit + "d", "since commit");
|
||||
this.pill(stats, info.sinceCommit === 0 ? "today" : info.sinceCommit + "d ago", info.sinceCommit === 0 ? "committed" : "since commit");
|
||||
if (info.recentRepo) {
|
||||
const maxLen = 16;
|
||||
const name = info.recentRepo.length > maxLen ? info.recentRepo.slice(0, maxLen - 1) + "\u2026" : info.recentRepo;
|
||||
|
|
@ -506,8 +554,11 @@ var ContributionsView = class extends import_obsidian.ItemView {
|
|||
["\u{1F525}", info.streaks.current + "d", "streak"],
|
||||
["\u2B50", info.streaks.longest + "d", "best"]
|
||||
];
|
||||
if (showLocal && info.sinceCommit !== null)
|
||||
items.push(["\u23F1", info.sinceCommit + "d", "since commit"]);
|
||||
if (showLocal && info.sinceCommit !== null) {
|
||||
const val = info.sinceCommit === 0 ? "today" : info.sinceCommit + "d ago";
|
||||
const lbl = info.sinceCommit === 0 ? "committed today" : "since commit";
|
||||
items.push(["\u23F1", val, lbl]);
|
||||
}
|
||||
for (const [icon, val, label] of items) {
|
||||
const row = list.createDiv({ cls: "gh-stats-list-row" });
|
||||
row.createEl("span", { cls: "gh-stats-list-icon", text: icon });
|
||||
|
|
@ -723,10 +774,16 @@ var GitHubContributionsSettingTab = class extends import_obsidian.PluginSettingT
|
|||
if (!isDesktop) {
|
||||
containerEl.createEl("p", { cls: "gh-settings-notice", text: "\u26A0 Local git scanning is not available on mobile." });
|
||||
} else {
|
||||
new import_obsidian.Setting(containerEl).setName("Local repo root").setDesc("Folder to scan for git repositories (searches 2 levels deep)").addText((t) => t.setPlaceholder("C:\\Users\\Peter\\Projects").setValue(this.plugin.settings.localRepoRoot).onChange(async (v) => {
|
||||
new import_obsidian.Setting(containerEl).setName("Local repo root").setDesc("Folder to scan for git repositories").addText((t) => t.setPlaceholder("C:\\Users\\Peter\\Projects").setValue(this.plugin.settings.localRepoRoot).onChange(async (v) => {
|
||||
this.plugin.settings.localRepoRoot = v.trim();
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
new import_obsidian.Setting(containerEl).setName("Scan depth").setDesc("How many folder levels deep to search for git repos").addDropdown(
|
||||
(d) => d.addOption("2", "2 levels").addOption("3", "3 levels").addOption("4", "4 levels").addOption("5", "5 levels").addOption("0", "Unlimited (slow on large drives)").setValue(String(this.plugin.settings.scanDepth)).onChange(async (v) => {
|
||||
this.plugin.settings.scanDepth = parseInt(v);
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
containerEl.createEl("h3", { text: "Display" });
|
||||
|
|
@ -761,6 +818,10 @@ var GitHubContributionsSettingTab = class extends import_obsidian.PluginSettingT
|
|||
await this.plugin.saveSettings();
|
||||
}
|
||||
}));
|
||||
new import_obsidian.Setting(containerEl).setName("Demo mode").setDesc("Show fake contribution data \u2014 useful for screenshots or testing").addToggle((t) => t.setValue(this.plugin.settings.demoMode).onChange(async (v) => {
|
||||
this.plugin.settings.demoMode = v;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
containerEl.createEl("h3", { text: "Daily Notes" });
|
||||
new import_obsidian.Setting(containerEl).setName("Daily notes folder").setDesc("Leave blank for vault root").addText((t) => t.setPlaceholder("Daily Notes/").setValue(this.plugin.settings.dailyNoteFolder).onChange(async (v) => {
|
||||
this.plugin.settings.dailyNoteFolder = v;
|
||||
|
|
|
|||
94
src/main.ts
94
src/main.ts
|
|
@ -47,6 +47,7 @@ interface GitHubContributionsSettings {
|
|||
// Data sources
|
||||
dataSource: DataSource;
|
||||
localRepoRoot: string;
|
||||
scanDepth: number; // 2-5, or 0 = unlimited
|
||||
// Display
|
||||
sidebarSide: "left" | "right";
|
||||
selectedYear: number;
|
||||
|
|
@ -54,6 +55,7 @@ interface GitHubContributionsSettings {
|
|||
defaultView: ViewMode;
|
||||
palette: Palette;
|
||||
statsStyle: StatsStyle;
|
||||
demoMode: boolean;
|
||||
// Daily notes
|
||||
dailyNoteFolder: string;
|
||||
dailyNoteDateFormat: string;
|
||||
|
|
@ -64,6 +66,8 @@ const DEFAULT_SETTINGS: GitHubContributionsSettings = {
|
|||
githubUsername: "",
|
||||
dataSource: "github",
|
||||
localRepoRoot: "",
|
||||
scanDepth: 2,
|
||||
demoMode: false,
|
||||
sidebarSide: "right",
|
||||
selectedYear: new Date().getFullYear(),
|
||||
sizePreset: "medium",
|
||||
|
|
@ -161,10 +165,11 @@ function isGitRepo(path: string): boolean {
|
|||
return result.trim() === "true";
|
||||
}
|
||||
|
||||
async function discoverRepos(rootPath: string): Promise<LocalRepo[]> {
|
||||
async function discoverRepos(rootPath: string, maxDepth = 2): Promise<LocalRepo[]> {
|
||||
if (!isDesktop) return [];
|
||||
const repos: LocalRepo[] = [];
|
||||
// Use find-like approach: list subdirectories up to 2 levels deep that contain .git
|
||||
// Use find-like approach: list subdirectories up to maxDepth levels deep that contain .git
|
||||
// maxDepth === 0 means unlimited
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const fs = (window as any).require("fs");
|
||||
|
|
@ -172,7 +177,7 @@ async function discoverRepos(rootPath: string): Promise<LocalRepo[]> {
|
|||
const path = (window as any).require("path");
|
||||
|
||||
const scanDir = (dir: string, depth: number) => {
|
||||
if (depth > 2) return;
|
||||
if (maxDepth !== 0 && depth > maxDepth) return;
|
||||
let entries: string[];
|
||||
try { entries = fs.readdirSync(dir); } catch { return; }
|
||||
// Check if this dir itself is a repo
|
||||
|
|
@ -352,6 +357,37 @@ function daysSinceLastCommit(days: Map<string, DayData>): number | null {
|
|||
return null;
|
||||
}
|
||||
|
||||
// ── Demo mode ────────────────────────────────────────────────────────────────
|
||||
function buildDemoData(year: number): Map<string, DayData> {
|
||||
const days = new Map<string, DayData>();
|
||||
const isLeap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
||||
const daysInYear = isLeap ? 366 : 365;
|
||||
// Seeded pseudo-random for reproducible demo
|
||||
let seed = 42;
|
||||
const rand = () => { seed = (seed * 1664525 + 1013904223) & 0xffffffff; return (seed >>> 0) / 0xffffffff; };
|
||||
const demoRepos = ["my-project", "obsidian-plugin", "dotfiles"];
|
||||
|
||||
for (let i = 0; i < daysInYear; i++) {
|
||||
const d = new Date(year, 0, i + 1);
|
||||
const dateStr = `${year}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`;
|
||||
const r = rand();
|
||||
// ~60% chance of activity, weighted toward recent months
|
||||
const recencyBoost = i / daysInYear;
|
||||
const active = r < (0.45 + recencyBoost * 0.3);
|
||||
if (!active) continue;
|
||||
const count = Math.floor(rand() * 8) + 1;
|
||||
const repos: Record<string, number> = {};
|
||||
let remaining = count;
|
||||
for (const repo of demoRepos) {
|
||||
if (remaining <= 0) break;
|
||||
const n = Math.min(remaining, Math.floor(rand() * 4) + 1);
|
||||
if (rand() > 0.4) { repos[repo] = n; remaining -= n; }
|
||||
}
|
||||
days.set(dateStr, { date: dateStr, count, githubCount: count, localCount: 0, repos });
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
function mostRecentRepo(repos: LocalRepo[]): string | null {
|
||||
if (!repos.length) return null;
|
||||
const sorted = [...repos].filter(r => r.lastCommit).sort((a,b) => (b.lastCommit ?? "").localeCompare(a.lastCommit ?? ""));
|
||||
|
|
@ -445,15 +481,25 @@ export class ContributionsView extends ItemView {
|
|||
// Discover repos if needed
|
||||
let repos: LocalRepo[] = [];
|
||||
if (needsLocal && s.localRepoRoot) {
|
||||
repos = await discoverRepos(s.localRepoRoot);
|
||||
repos = await discoverRepos(s.localRepoRoot, s.scanDepth);
|
||||
}
|
||||
|
||||
let days: Map<string, DayData>;
|
||||
let totalGH = 0, totalLocal = 0;
|
||||
|
||||
if (s.demoMode) {
|
||||
days = buildDemoData(this.displayYear);
|
||||
totalGH = [...days.values()].reduce((a, d) => a + d.githubCount, 0);
|
||||
} else {
|
||||
const result = await buildDayMap(s, this.displayYear, repos);
|
||||
days = result.days; totalGH = result.totalGH; totalLocal = result.totalLocal;
|
||||
}
|
||||
|
||||
const { days, totalGH, totalLocal } = await buildDayMap(s, this.displayYear, repos);
|
||||
container.empty();
|
||||
|
||||
const streaks = calculateStreaks(days, this.displayYear);
|
||||
const sinceCommit = daysSinceLastCommit(days);
|
||||
const recentRepo = mostRecentRepo(repos);
|
||||
const recentRepo = s.demoMode ? "my-project" : mostRecentRepo(repos);
|
||||
|
||||
this.renderHeader(container);
|
||||
this.renderStats(container, { totalGH, totalLocal, streaks, sinceCommit, recentRepo });
|
||||
|
|
@ -541,8 +587,10 @@ export class ContributionsView extends ItemView {
|
|||
["🔥", info.streaks.current + "d", "Current streak"],
|
||||
["⭐", info.streaks.longest + "d", "Best streak"],
|
||||
];
|
||||
if (showLocal && info.sinceCommit !== null)
|
||||
items.push(["⏱", info.sinceCommit + "d", "Days since last commit"]);
|
||||
if (showLocal && info.sinceCommit !== null) {
|
||||
const sinceLabel = info.sinceCommit === 0 ? "committed today" : info.sinceCommit + "d ago";
|
||||
items.push(["⏱", info.sinceCommit === 0 ? "today" : info.sinceCommit + "d", sinceLabel]);
|
||||
}
|
||||
if (info.recentRepo)
|
||||
items.push(["📁", info.recentRepo, info.recentRepo]);
|
||||
for (const [icon, val, title] of items) {
|
||||
|
|
@ -556,7 +604,7 @@ export class ContributionsView extends ItemView {
|
|||
this.pill(stats, info.streaks.current + "d", "streak");
|
||||
this.pill(stats, info.streaks.longest + "d", "best");
|
||||
if (showLocal && info.sinceCommit !== null)
|
||||
this.pill(stats, info.sinceCommit + "d", "since commit");
|
||||
this.pill(stats, info.sinceCommit === 0 ? "today" : info.sinceCommit + "d ago", info.sinceCommit === 0 ? "committed" : "since commit");
|
||||
if (info.recentRepo) {
|
||||
const maxLen = 16;
|
||||
const name = info.recentRepo.length > maxLen ? info.recentRepo.slice(0, maxLen-1) + "\u2026" : info.recentRepo;
|
||||
|
|
@ -573,8 +621,11 @@ export class ContributionsView extends ItemView {
|
|||
["🔥", info.streaks.current + "d", "streak"],
|
||||
["⭐", info.streaks.longest + "d", "best"],
|
||||
];
|
||||
if (showLocal && info.sinceCommit !== null)
|
||||
items.push(["⏱", info.sinceCommit + "d", "since commit"]);
|
||||
if (showLocal && info.sinceCommit !== null) {
|
||||
const val = info.sinceCommit === 0 ? "today" : info.sinceCommit + "d ago";
|
||||
const lbl = info.sinceCommit === 0 ? "committed today" : "since commit";
|
||||
items.push(["⏱", val, lbl]);
|
||||
}
|
||||
for (const [icon, val, label] of items) {
|
||||
const row = list.createDiv({ cls: "gh-stats-list-row" });
|
||||
row.createEl("span", { cls: "gh-stats-list-icon", text: icon });
|
||||
|
|
@ -825,9 +876,22 @@ class GitHubContributionsSettingTab extends PluginSettingTab {
|
|||
} else {
|
||||
new Setting(containerEl)
|
||||
.setName("Local repo root")
|
||||
.setDesc("Folder to scan for git repositories (searches 2 levels deep)")
|
||||
.setDesc("Folder to scan for git repositories")
|
||||
.addText(t => t.setPlaceholder("C:\\Users\\Peter\\Projects").setValue(this.plugin.settings.localRepoRoot)
|
||||
.onChange(async v => { this.plugin.settings.localRepoRoot = v.trim(); await this.plugin.saveSettings(); }));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Scan depth")
|
||||
.setDesc("How many folder levels deep to search for git repos")
|
||||
.addDropdown(d => d
|
||||
.addOption("2", "2 levels")
|
||||
.addOption("3", "3 levels")
|
||||
.addOption("4", "4 levels")
|
||||
.addOption("5", "5 levels")
|
||||
.addOption("0", "Unlimited (slow on large drives)")
|
||||
.setValue(String(this.plugin.settings.scanDepth))
|
||||
.onChange(async v => { this.plugin.settings.scanDepth = parseInt(v); await this.plugin.saveSettings(); })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -893,6 +957,12 @@ class GitHubContributionsSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
// ── Daily Notes
|
||||
new Setting(containerEl)
|
||||
.setName("Demo mode")
|
||||
.setDesc("Show fake contribution data — useful for screenshots or testing")
|
||||
.addToggle(t => t.setValue(this.plugin.settings.demoMode)
|
||||
.onChange(async v => { this.plugin.settings.demoMode = v; await this.plugin.saveSettings(); }));
|
||||
|
||||
containerEl.createEl("h3", { text: "Daily Notes" });
|
||||
|
||||
new Setting(containerEl)
|
||||
|
|
|
|||
Loading…
Reference in a new issue