/* THIS IS A GENERATED/BUNDLED FILE BY ESBUILD if you want to view the source, please visit the github repository of this plugin */ var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/main.ts var main_exports = {}; __export(main_exports, { ContributionsView: () => ContributionsView, default: () => GitHubContributionsPlugin }); module.exports = __toCommonJS(main_exports); var import_obsidian = require("obsidian"); var VIEW_TYPE = "github-contributions"; var GITHUB_GRAPHQL = "https://api.github.com/graphql"; var GITHUB_CLIENT_ID = "Ov23litfj5GbQ8mw81VV"; var GITHUB_DEVICE_URL = "https://github.com/login/device/code"; var GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token"; var GITHUB_USER_URL = "https://api.github.com/user"; var MONTHS_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; var DEFAULT_SETTINGS = { authMethod: "oauth", githubToken: "", githubUsername: "", dataSource: "github", localRepoRoot: "", scanDepth: 2, demoMode: false, sidebarSide: "right", selectedYear: new Date().getFullYear(), sizePreset: "medium", defaultView: "year", palette: "default", statsStyle: "default", dailyNoteFolder: "", dailyNoteDateFormat: "YYYY-MM-DD" }; var PRESET_SIZES = { "ultra-compact": { cell: 7, gap: 1 }, "compact": { cell: 9, gap: 1 }, "medium": { cell: 11, gap: 2 }, "large": { cell: 14, gap: 3 }, "fit": { cell: 0, gap: 2 } // calculated at render time }; var PALETTES = { "default": { dark: ["var(--background-modifier-border)", "#0e4429", "#006d32", "#26a641", "#39d353"], light: ["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"] }, "high-contrast": { dark: ["var(--background-modifier-border)", "#1a5e2a", "#21a045", "#32d463", "#7fffb0"], light: ["#ebedf0", "#b6f0c2", "#3dd668", "#1a9e40", "#0a5c25"] }, "colorblind": { dark: ["var(--background-modifier-border)", "#0a3a6b", "#0e6eb5", "#1ab3d8", "#57e8f5"], light: ["#edf4fb", "#b3d4f0", "#4aa8e0", "#1478c8", "#064a8a"] }, "neon": { dark: ["var(--background-modifier-border)", "#2d0a4e", "#7b00c2", "#e0008c", "#ffe600"], light: ["#f5eaff", "#c97df5", "#9400d3", "#d4006a", "#c8a800"] }, "ember": { dark: ["var(--background-modifier-border)", "#3d1f00", "#b45200", "#e8820c", "#ffc832"], light: ["#f5f0e8", "#f5d5a0", "#e8820c", "#b45200", "#7a3200"] } }; async function requestDeviceCode() { const res = await (0, import_obsidian.requestUrl)({ url: GITHUB_DEVICE_URL, method: "POST", headers: { "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ client_id: GITHUB_CLIENT_ID, scope: "read:user" }) }); if (res.status !== 200) throw new Error(`GitHub device flow error: ${res.status}`); return res.json; } async function pollForToken(deviceCode, intervalSecs, onCancel) { var _a; const delay = (ms) => new Promise((r) => setTimeout(r, ms)); const pollInterval = Math.max(intervalSecs, 5) * 1e3; while (true) { if (onCancel()) throw new Error("Cancelled"); await delay(pollInterval); if (onCancel()) throw new Error("Cancelled"); const res = await (0, import_obsidian.requestUrl)({ url: GITHUB_TOKEN_URL, method: "POST", headers: { "Accept": "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ client_id: GITHUB_CLIENT_ID, device_code: deviceCode, grant_type: "urn:ietf:params:oauth:grant-type:device_code" }) }); const data = res.json; if (data.access_token) return data.access_token; if (data.error === "authorization_pending") continue; if (data.error === "slow_down") { await delay(5e3); continue; } if (data.error === "expired_token") throw new Error("Code expired. Please try again."); if (data.error === "access_denied") throw new Error("Access denied."); throw new Error((_a = data.error_description) != null ? _a : "Unknown OAuth error"); } } async function fetchGitHubUsername(token) { const res = await (0, import_obsidian.requestUrl)({ url: GITHUB_USER_URL, headers: { Authorization: `bearer ${token}` } }); if (res.status !== 200) throw new Error("Could not fetch GitHub username"); return res.json.login; } async function fetchGitHubContributions(username, token, year) { var _a, _b, _c, _d, _e, _f; const from = `${year}-01-01T00:00:00Z`; const to = `${year}-12-31T23:59:59Z`; const query = `query($login:String!,$from:DateTime!,$to:DateTime!){user(login:$login){contributionsCollection(from:$from,to:$to){contributionCalendar{weeks{contributionDays{date contributionCount}}}}}}`; const res = await fetch(GITHUB_GRAPHQL, { method: "POST", headers: { Authorization: `bearer ${token}`, "Content-Type": "application/json" }, body: JSON.stringify({ query, variables: { login: username, from, to } }) }); if (!res.ok) throw new Error(`GitHub API error: ${res.status}`); const json = await res.json(); if (json.errors) throw new Error((_b = (_a = json.errors[0]) == null ? void 0 : _a.message) != null ? _b : "GitHub API error"); const weeks = (_f = (_e = (_d = (_c = json == null ? void 0 : json.data) == null ? void 0 : _c.user) == null ? void 0 : _d.contributionsCollection) == null ? void 0 : _e.contributionCalendar) == null ? void 0 : _f.weeks; if (!weeks) throw new Error("No data returned. Check your username."); const map = /* @__PURE__ */ new Map(); for (const week of weeks) { for (const day of week.contributionDays) { map.set(day.date, day.contributionCount); } } return map; } var isDesktop = !!window.require; function runGit(args, cwd) { if (!isDesktop) return ""; try { const { execSync } = window.require("child_process"); return execSync(`git ${args}`, { cwd, timeout: 8e3, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }); } catch (e) { return ""; } } async function discoverRepos(rootPath, maxDepth = 2) { if (!isDesktop) return []; const repos = []; try { const fs = window.require("fs"); const path = window.require("path"); const scanDir = (dir, depth) => { if (maxDepth !== 0 && depth > maxDepth) return; let entries; try { entries = fs.readdirSync(dir); } catch (e) { return; } if (entries.includes(".git")) { const name = path.basename(dir); const lastCommitRaw = runGit(`log -1 --format=%ai`, dir).trim(); const lastCommit = lastCommitRaw ? lastCommitRaw.split(" ")[0] : null; repos.push({ name, path: dir, lastCommit }); return; } for (const entry of entries) { if (entry.startsWith(".")) continue; const full = path.join(dir, entry); try { if (fs.statSync(full).isDirectory()) scanDir(full, depth + 1); } catch (e) { } } }; scanDir(rootPath, 0); } catch (e) { console.error("gh-contributions: repo discovery failed", e); } return repos; } function fetchLocalCommits(repo, year) { var _a; const map = /* @__PURE__ */ new Map(); const after = `${year - 1}-12-31`; const before = `${year + 1}-01-01`; const raw = runGit( `log --after="${after}" --before="${before}" --format=%ad --date=format:%Y-%m-%d`, repo.path ); if (!raw) return map; for (const line of raw.split("\n")) { const date = line.trim(); if (!date || !date.startsWith(String(year))) continue; map.set(date, ((_a = map.get(date)) != null ? _a : 0) + 1); } return map; } async function buildDayMap(settings, year, repos) { var _a; const days = /* @__PURE__ */ new Map(); const getOrCreate = (date) => { if (!days.has(date)) days.set(date, { date, count: 0, githubCount: 0, localCount: 0, repos: {} }); return days.get(date); }; let totalGH = 0, totalLocal = 0; if (settings.dataSource === "github" || settings.dataSource === "both") { if (settings.githubToken && settings.githubUsername) { const ghMap = await fetchGitHubContributions(settings.githubUsername, settings.githubToken, year); for (const [date, count] of ghMap) { const d = getOrCreate(date); d.githubCount = count; d.count += count; totalGH += count; } } } if (settings.dataSource === "local" || settings.dataSource === "both") { for (const repo of repos) { const commits = fetchLocalCommits(repo, year); for (const [date, count] of commits) { const d = getOrCreate(date); d.localCount += count; d.count += count; d.repos[repo.name] = ((_a = d.repos[repo.name]) != null ? _a : 0) + count; totalLocal += count; } } } return { days, totalGH, totalLocal, repoList: repos }; } function buildYearWeeks(year, days) { var _a; const jan1 = new Date(year, 0, 1); const startOffset = jan1.getDay(); const weeks = []; let week = []; for (let i = 0; i < startOffset; i++) week.push({ date: "", count: 0, githubCount: 0, localCount: 0, repos: {} }); const isLeap = year % 4 === 0 && year % 100 !== 0 || year % 400 === 0; const daysInYear = isLeap ? 366 : 365; 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")}`; week.push((_a = days.get(dateStr)) != null ? _a : { date: dateStr, count: 0, githubCount: 0, localCount: 0, repos: {} }); if (week.length === 7) { weeks.push(week); week = []; } } if (week.length > 0) { while (week.length < 7) week.push({ date: "", count: 0, githubCount: 0, localCount: 0, repos: {} }); weeks.push(week); } return weeks; } function buildMonthDays(year, month, days) { var _a; const firstDay = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month + 1, 0).getDate(); const weeks = []; let week = []; for (let i = 0; i < firstDay; i++) week.push({ date: "", count: 0, githubCount: 0, localCount: 0, repos: {} }); for (let i = 1; i <= daysInMonth; i++) { const dateStr = `${year}-${String(month + 1).padStart(2, "0")}-${String(i).padStart(2, "0")}`; week.push((_a = days.get(dateStr)) != null ? _a : { date: dateStr, count: 0, githubCount: 0, localCount: 0, repos: {} }); if (week.length === 7) { weeks.push(week); week = []; } } if (week.length > 0) { while (week.length < 7) week.push({ date: "", count: 0, githubCount: 0, localCount: 0, repos: {} }); weeks.push(week); } return weeks; } function calculateStreaks(days, year) { var _a, _b, _c, _d, _e, _f; const isLeap = year % 4 === 0 && year % 100 !== 0 || year % 400 === 0; const daysInYear = isLeap ? 366 : 365; const today = (0, import_obsidian.moment)().format("YYYY-MM-DD"); const allDates = []; for (let i = 0; i < daysInYear; i++) { const d = new Date(year, 0, i + 1); allDates.push(`${year}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`); } let longest = 0, run = 0; for (const date of allDates) { const count = (_b = (_a = days.get(date)) == null ? void 0 : _a.count) != null ? _b : 0; if (count > 0) { run++; if (run > longest) longest = run; } else run = 0; } let current = 0; const past = allDates.filter((d) => d <= today).reverse(); let skipFirst = past.length > 0 && ((_d = (_c = days.get(past[0])) == null ? void 0 : _c.count) != null ? _d : 0) === 0; for (const date of past) { if (skipFirst) { skipFirst = false; continue; } if (((_f = (_e = days.get(date)) == null ? void 0 : _e.count) != null ? _f : 0) > 0) current++; else break; } return { current, longest }; } function daysSinceLastCommit(days) { const today = (0, import_obsidian.moment)().format("YYYY-MM-DD"); const sorted = [...days.entries()].filter(([d]) => d <= today && d !== "").sort((a, b) => b[0].localeCompare(a[0])); for (const [date, data] of sorted) { if (data.count > 0) { return (0, import_obsidian.moment)(today).diff((0, import_obsidian.moment)(date), "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) return null; const sorted = [...repos].filter((r) => r.lastCommit).sort((a, b) => { var _a2, _b2; return ((_a2 = b.lastCommit) != null ? _a2 : "").localeCompare((_b2 = a.lastCommit) != null ? _b2 : ""); }); return (_b = (_a = sorted[0]) == null ? void 0 : _a.name) != null ? _b : null; } var ContributionsView = class extends import_obsidian.ItemView { constructor(leaf, plugin) { super(leaf); this.tooltipEl = null; this.resizeObserver = null; this.panelWidth = 0; this.plugin = plugin; this.displayYear = plugin.settings.selectedYear; this.displayMonth = new Date().getMonth(); this.viewMode = plugin.settings.defaultView; } getViewType() { return VIEW_TYPE; } getDisplayText() { return "GitHub Contributions"; } getIcon() { return "github"; } async onOpen() { this.resizeObserver = new ResizeObserver(() => { const w = this.containerEl.clientWidth; if (Math.abs(w - this.panelWidth) > 5) { this.panelWidth = w; if (this.plugin.settings.sizePreset === "fit") this.render(); } }); this.resizeObserver.observe(this.containerEl); this.panelWidth = this.containerEl.clientWidth; await this.render(); } async onClose() { var _a, _b; (_a = this.tooltipEl) == null ? void 0 : _a.remove(); this.tooltipEl = null; (_b = this.resizeObserver) == null ? void 0 : _b.disconnect(); } async refresh() { this.displayYear = this.plugin.settings.selectedYear; this.viewMode = this.plugin.settings.defaultView; await this.render(); } getCellSize() { const preset = this.plugin.settings.sizePreset; if (preset !== "fit") return PRESET_SIZES[preset]; const cols = this.viewMode === "year" ? 53 : 7; const gap = 2; const available = Math.max(100, (this.panelWidth || 260) - 28); const cell = Math.max(5, Math.floor((available - gap * (cols - 1)) / cols)); return { cell, gap }; } async render() { var _a; const container = this.containerEl.children[1]; container.empty(); container.addClass("gh-contributions-view"); if (!this.tooltipEl) { this.tooltipEl = document.body.createDiv({ cls: "gh-tooltip" }); } const s = this.plugin.settings; const needsGH = s.dataSource === "github" || s.dataSource === "both"; const needsLocal = s.dataSource === "local" || s.dataSource === "both"; if (needsGH && (!s.githubToken || !s.githubUsername)) { this.renderEmpty(container, "github"); return; } if (needsLocal && !s.localRepoRoot) { this.renderEmpty(container, "local"); return; } this.renderSkeleton(container); try { let repos = []; if (needsLocal && 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; } container.empty(); const streaks = calculateStreaks(days, this.displayYear); const sinceCommit = daysSinceLastCommit(days); 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"; if (recentRepo && style === "default") { const repoLine = container.createDiv({ cls: "gh-repo-line" }); repoLine.createEl("span", { cls: "gh-repo-line-icon", text: "\u{1F4C1} " }); repoLine.createEl("span", { cls: "gh-repo-line-name", text: recentRepo }); } if (this.viewMode === "year") { const weeks = buildYearWeeks(this.displayYear, days); this.renderYearGrid(container, weeks); } else { const weeks = buildMonthDays(this.displayYear, this.displayMonth, days); this.renderMonthGrid(container, weeks); } this.renderLegend(container); } catch (e) { container.empty(); this.renderError(container, e.message); } } renderHeader(container) { const s = this.plugin.settings; const header = container.createDiv({ cls: "gh-header" }); if (s.githubUsername) header.createEl("span", { cls: "gh-username", text: s.githubUsername }); const nav = header.createDiv({ cls: "gh-nav" }); const currentYear = new Date().getFullYear(); if (this.viewMode === "year") { const prev = nav.createEl("button", { cls: "gh-nav-btn", text: "\u2039" }); nav.createEl("span", { cls: "gh-nav-label", text: String(this.displayYear) }); const next = nav.createEl("button", { cls: "gh-nav-btn", text: "\u203A" }); next.disabled = this.displayYear >= currentYear; prev.onclick = async () => { this.displayYear--; await this.render(); }; next.onclick = async () => { if (this.displayYear < currentYear) { this.displayYear++; await this.render(); } }; } else { const prev = nav.createEl("button", { cls: "gh-nav-btn", text: "\u2039" }); nav.createEl("span", { cls: "gh-nav-label", text: `${MONTHS_SHORT[this.displayMonth]} ${this.displayYear}` }); const next = nav.createEl("button", { cls: "gh-nav-btn", text: "\u203A" }); const isLatest = this.displayYear >= currentYear && this.displayMonth >= new Date().getMonth(); next.disabled = isLatest; prev.onclick = async () => { if (this.displayMonth === 0) { this.displayMonth = 11; this.displayYear--; } else this.displayMonth--; await this.render(); }; next.onclick = async () => { if (isLatest) return; if (this.displayMonth === 11) { this.displayMonth = 0; this.displayYear++; } else this.displayMonth++; await this.render(); }; } const refreshBtn = nav.createEl("button", { cls: "gh-nav-btn gh-refresh-icon", text: "\u21BB" }); refreshBtn.title = "Refresh"; refreshBtn.onclick = () => this.render(); } renderStats(container, info) { var _a; const s = this.plugin.settings; const style = (_a = s.statsStyle) != null ? _a : "default"; const total = info.totalGH + info.totalLocal; const showLocal = s.dataSource === "local" || s.dataSource === "both"; if (style === "compact") { const row = container.createDiv({ cls: "gh-stats-compact" }); const items = [ ["\u2191", String(total), "Total contributions"], ["\u{1F525}", info.streaks.current + "d", "Current streak"], ["\u2B50", info.streaks.longest + "d", "Best streak"] ]; 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) { const chip = row.createEl("span", { cls: "gh-chip", title }); chip.createEl("span", { cls: "gh-chip-icon", text: icon }); chip.createEl("span", { cls: "gh-chip-val", text: val }); } } else if (style === "grid") { const stats = container.createDiv({ cls: "gh-stats gh-stats--grid" }); this.pill(stats, String(total), "contributions"); 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 === 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; const pill = stats.createDiv({ cls: "gh-stat gh-stat--wide" }); pill.title = info.recentRepo; pill.createEl("span", { cls: "gh-stat-val", text: name }); pill.createEl("span", { cls: "gh-stat-lbl", text: "recent repo" }); } } else { const list = container.createDiv({ cls: "gh-stats-list" }); const items = [ ["\u2191", String(total), "contributions"], ["\u{1F525}", info.streaks.current + "d", "streak"], ["\u2B50", info.streaks.longest + "d", "best"] ]; 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 }); row.createEl("span", { cls: "gh-stats-list-val", text: val }); row.createEl("span", { cls: "gh-stats-list-lbl", text: " " + label }); } } } pill(parent, value, label) { const p = parent.createDiv({ cls: "gh-stat" }); p.createEl("span", { cls: "gh-stat-val", text: value }); p.createEl("span", { cls: "gh-stat-lbl", text: label }); } renderYearGrid(container, weeks) { const { cell, gap } = this.getCellSize(); const graphWrap = container.createDiv({ cls: "gh-graph-wrap" }); graphWrap.style.overflowX = "auto"; const monthRow = graphWrap.createDiv({ cls: "gh-month-row" }); monthRow.style.cssText = `display:grid;grid-template-columns:repeat(${weeks.length},${cell}px);gap:${gap}px;margin-bottom:3px`; let lastMonth = -1; weeks.forEach((week, wi) => { const first = week.find((d) => d.date); if (!first) return; const m = new Date(first.date).getUTCMonth(); if (m !== lastMonth) { lastMonth = m; const lbl = monthRow.createEl("span", { cls: "gh-month-lbl", text: MONTHS_SHORT[m] }); lbl.style.gridColumn = String(wi + 1); lbl.style.fontSize = Math.max(8, cell - 2) + "px"; } }); const grid = graphWrap.createDiv({ cls: "gh-grid" }); grid.style.cssText = `display:flex;gap:${gap}px`; weeks.forEach((week) => { const col = grid.createDiv({ cls: "gh-col" }); col.style.cssText = `display:flex;flex-direction:column;gap:${gap}px`; week.forEach((day) => this.renderCell(col, day, cell)); }); } renderMonthGrid(container, weeks) { const { cell, gap } = this.getCellSize(); const graphWrap = container.createDiv({ cls: "gh-graph-wrap" }); const colTemplate = `repeat(7,${cell}px)`; const dowRow = graphWrap.createDiv({ cls: "gh-dow-row" }); dowRow.style.cssText = `display:grid;grid-template-columns:${colTemplate};gap:${gap}px;margin-bottom:4px`; ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"].forEach((d) => { const lbl = dowRow.createEl("span", { cls: "gh-dow-lbl", text: d }); lbl.style.cssText = `font-size:10px;color:var(--text-faint);text-align:center;overflow:hidden`; }); const grid = graphWrap.createDiv({ cls: "gh-month-grid" }); grid.style.cssText = `display:flex;flex-direction:column;gap:${gap}px`; weeks.forEach((week) => { const row = grid.createDiv({ cls: "gh-month-row-cells" }); row.style.cssText = `display:grid;grid-template-columns:${colTemplate};gap:${gap}px`; week.forEach((day) => this.renderCell(row, day, cell)); }); } renderCell(parent, day, size) { const cell = parent.createDiv({ cls: "gh-cell" }); cell.style.cssText = `width:${size}px;height:${size}px`; if (!day.date) { cell.addClass("gh-cell--empty"); return; } const today = (0, import_obsidian.moment)().format("YYYY-MM-DD"); cell.dataset.level = String(countToLevel(day.count)); if (day.date === today) cell.addClass("gh-today"); cell.addEventListener("mouseenter", (e) => this.showTooltip(e, day)); cell.addEventListener("mouseleave", () => { if (this.tooltipEl) this.tooltipEl.style.display = "none"; }); cell.addEventListener("click", () => this.openOrCreateDailyNote(day.date)); } showTooltip(e, day) { if (!this.tooltipEl) return; const s = this.plugin.settings; const dateStr = (0, import_obsidian.moment)(day.date).format("YYYY-MM-DD"); const lines = [dateStr]; if (day.count === 0) { lines.push("No contributions"); } else { lines.push(`${day.count} contribution${day.count !== 1 ? "s" : ""}`); if (s.dataSource === "both") { if (day.githubCount > 0) lines.push(` GitHub: ${day.githubCount}`); if (day.localCount > 0) lines.push(` Local: ${day.localCount}`); } const repoEntries = Object.entries(day.repos).sort((a, b) => b[1] - a[1]); for (const [repo, count] of repoEntries) { lines.push(` ${repo} (${count})`); } } this.tooltipEl.empty(); lines.forEach((line, i) => { if (i > 0) this.tooltipEl.createEl("br"); if (i === 0) { this.tooltipEl.createEl("strong", { text: line }); } else { this.tooltipEl.appendText(line); } }); this.tooltipEl.style.display = "block"; this.tooltipEl.style.left = e.pageX + 12 + "px"; this.tooltipEl.style.top = e.pageY - 34 + "px"; } renderLegend(container) { const { cell, gap } = this.getCellSize(); const legend = container.createDiv({ cls: "gh-legend" }); legend.createEl("span", { cls: "gh-legend-lbl", text: "Less" }); for (let i = 0; i <= 4; i++) { const sq = legend.createDiv({ cls: "gh-cell gh-legend-cell" }); sq.dataset.level = String(i); sq.style.cssText = `width:${cell}px;height:${cell}px`; } legend.createEl("span", { cls: "gh-legend-lbl", text: "More" }); } renderEmpty(container, missing) { const wrap = container.createDiv({ cls: "gh-empty" }); wrap.createEl("div", { cls: "gh-empty-icon", text: "\u{1F419}" }); const msg = missing === "github" ? "Connect your GitHub account in Settings to see your contribution graph." : "Set a local repo root folder in Settings to scan for git commits."; wrap.createEl("p", { text: msg }); const btn = wrap.createEl("button", { cls: "gh-btn", text: "Open Settings" }); btn.onclick = () => { this.app.setting.open(); this.app.setting.openTabById("github-contributions"); }; } renderSkeleton(container) { const wrap = container.createDiv({ cls: "gh-skeleton-wrap" }); wrap.createDiv({ cls: "gh-skeleton gh-skeleton-header" }); wrap.createDiv({ cls: "gh-skeleton gh-skeleton-stats" }); const grid = wrap.createDiv({ cls: "gh-skeleton-grid" }); const cols = this.viewMode === "year" ? 53 : 7; const rows = this.viewMode === "year" ? 7 : 6; const { cell, gap } = this.getCellSize(); grid.style.cssText = `display:grid;grid-template-columns:repeat(${cols},${cell}px);grid-template-rows:repeat(${rows},${cell}px);gap:${gap}px`; for (let i = 0; i < cols * rows; i++) grid.createDiv({ cls: "gh-skeleton gh-skeleton-cell" }); } renderError(container, msg) { container.createDiv({ cls: "gh-error", text: `\u26A0 ${msg}` }); } async openOrCreateDailyNote(date) { const s = this.plugin.settings; const fmt = s.dailyNoteDateFormat || "YYYY-MM-DD"; const folder = s.dailyNoteFolder ? s.dailyNoteFolder.replace(/\/$/, "") + "/" : ""; const name = (0, import_obsidian.moment)(date).format(fmt); const filePath = `${folder}${name}.md`; let file = this.app.vault.getAbstractFileByPath(filePath); if (!file) { try { file = await this.app.vault.create(filePath, `# ${name} `); new import_obsidian.Notice(`Created: ${name}`); } catch (err) { new import_obsidian.Notice(`Error creating note: ${err.message}`); return; } } await this.app.workspace.getLeaf(false).openFile(file); } }; function countToLevel(n) { if (n === 0) return 0; if (n <= 2) return 1; if (n <= 5) return 2; if (n <= 9) return 3; return 4; } var GitHubContributionsSettingTab = class extends import_obsidian.PluginSettingTab { constructor(app, plugin) { super(app, plugin); this.plugin = plugin; } display() { const { containerEl } = this; containerEl.empty(); containerEl.createEl("h2", { text: "GitHub Contributions" }); containerEl.createEl("h3", { text: "Data Sources" }); new import_obsidian.Setting(containerEl).setName("Source").setDesc("Which contributions to display").addDropdown( (d) => d.addOption("github", "GitHub only").addOption("local", "Local git only").addOption("both", "Both").setValue(this.plugin.settings.dataSource).onChange(async (v) => { this.plugin.settings.dataSource = v; await this.plugin.saveSettings(); this.display(); }) ); const src = this.plugin.settings.dataSource; if (src === "github" || src === "both") { new import_obsidian.Setting(containerEl).setName("Authentication").setDesc("Connect GitHub to fetch your contribution data").addDropdown( (d) => d.addOption("oauth", "Connect with GitHub (recommended)").addOption("pat", "Personal Access Token (advanced)").setValue(this.plugin.settings.authMethod).onChange(async (v) => { this.plugin.settings.authMethod = v; await this.plugin.saveSettings(); this.display(); }) ); if (this.plugin.settings.authMethod === "oauth") { const isConnected = !!this.plugin.settings.githubToken && !!this.plugin.settings.githubUsername; if (isConnected) { new import_obsidian.Setting(containerEl).setName("Connected as " + this.plugin.settings.githubUsername).setDesc("Your GitHub account is connected via OAuth").addButton( (btn) => btn.setButtonText("Disconnect").setWarning().onClick(async () => { this.plugin.settings.githubToken = ""; this.plugin.settings.githubUsername = ""; await this.plugin.saveSettings(); this.display(); }) ); } else { let cancelled = false; const oauthSetting = new import_obsidian.Setting(containerEl).setName("Connect GitHub account").setDesc("Click to start the authorization flow"); oauthSetting.addButton( (btn) => btn.setButtonText("Connect GitHub").setCta().onClick(async () => { btn.setButtonText("Connecting...").setDisabled(true); cancelled = false; try { const device = await requestDeviceCode(); oauthSetting.setDesc( createFragment((f) => { f.appendText("Enter this code at "); f.createEl("strong", { text: "github.com/login/device" }); f.createEl("br"); f.createEl("span", { cls: "gh-oauth-code", text: device.user_code }); f.createEl("br"); f.createEl("em", { text: "Waiting for approval\u2026" }); }) ); window.open(device.verification_uri); const token = await pollForToken(device.device_code, device.interval, () => cancelled); const username = await fetchGitHubUsername(token); this.plugin.settings.githubToken = token; this.plugin.settings.githubUsername = username; await this.plugin.saveSettings(); new import_obsidian.Notice("Connected to GitHub as " + username); this.display(); } catch (e) { const msg = e.message; if (msg !== "Cancelled") { oauthSetting.setDesc("Error: " + msg); new import_obsidian.Notice("GitHub connection failed: " + msg); } btn.setButtonText("Connect GitHub").setDisabled(false); } }) ); oauthSetting.addButton( (btn) => btn.setButtonText("Cancel").onClick(() => { cancelled = true; oauthSetting.setDesc("Cancelled."); btn.setDisabled(true); }) ); } } else { new import_obsidian.Setting(containerEl).setName("GitHub username").addText((t) => t.setPlaceholder("username").setValue(this.plugin.settings.githubUsername).onChange(async (v) => { this.plugin.settings.githubUsername = v.trim(); await this.plugin.saveSettings(); })); new import_obsidian.Setting(containerEl).setName("Personal Access Token").setDesc("PAT with read:user scope \u2014 github.com/settings/tokens").addText((t) => { t.inputEl.type = "password"; t.setPlaceholder("ghp_\u2026").setValue(this.plugin.settings.githubToken).onChange(async (v) => { this.plugin.settings.githubToken = v.trim(); await this.plugin.saveSettings(); }); }); } } if (src === "local" || src === "both") { 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").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" }); new import_obsidian.Setting(containerEl).setName("Sidebar side").addDropdown((d) => d.addOption("left", "Left").addOption("right", "Right").setValue(this.plugin.settings.sidebarSide).onChange(async (v) => { this.plugin.settings.sidebarSide = v; await this.plugin.saveSettings(); })); new import_obsidian.Setting(containerEl).setName("Stats display").setDesc("How the stats bar is shown").addDropdown( (d) => d.addOption("default", "Default (pills with labels)").addOption("compact", "Compact (icons + numbers)").addOption("grid", "Grid (2-column)").setValue(this.plugin.settings.statsStyle).onChange(async (v) => { this.plugin.settings.statsStyle = v; await this.plugin.saveSettings(); }) ); new import_obsidian.Setting(containerEl).setName("Colour palette").setDesc("Colour scheme for contribution cells").addDropdown( (d) => d.addOption("default", "Default (GitHub greens)").addOption("high-contrast", "High contrast (vivid greens)").addOption("colorblind", "Colorblind friendly (blue to cyan)").addOption("neon", "Neon (purple to yellow)").addOption("ember", "Ember (amber to gold)").setValue(this.plugin.settings.palette).onChange(async (v) => { this.plugin.settings.palette = v; await this.plugin.saveSettings(); }) ); new import_obsidian.Setting(containerEl).setName("Size").setDesc('Cell size. "Fit to sidebar" auto-sizes to fill the panel width.').addDropdown((d) => d.addOption("ultra-compact", "Ultra compact").addOption("compact", "Compact").addOption("medium", "Medium").addOption("large", "Large").addOption("fit", "Fit to sidebar").setValue(this.plugin.settings.sizePreset).onChange(async (v) => { this.plugin.settings.sizePreset = v; await this.plugin.saveSettings(); })); new import_obsidian.Setting(containerEl).setName("Default view").addDropdown((d) => d.addOption("year", "Year").addOption("month", "Month").setValue(this.plugin.settings.defaultView).onChange(async (v) => { this.plugin.settings.defaultView = v; await this.plugin.saveSettings(); })); new import_obsidian.Setting(containerEl).setName("Default year").addText((t) => t.setPlaceholder(String(new Date().getFullYear())).setValue(String(this.plugin.settings.selectedYear)).onChange(async (v) => { const y = parseInt(v); if (!isNaN(y) && y >= 2008 && y <= new Date().getFullYear()) { this.plugin.settings.selectedYear = y; 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; await this.plugin.saveSettings(); })); new import_obsidian.Setting(containerEl).setName("Date format").setDesc("Moment.js format for filenames. Default: YYYY-MM-DD").addText((t) => t.setPlaceholder("YYYY-MM-DD").setValue(this.plugin.settings.dailyNoteDateFormat).onChange(async (v) => { this.plugin.settings.dailyNoteDateFormat = v; await this.plugin.saveSettings(); })); } }; var GitHubContributionsPlugin = class extends import_obsidian.Plugin { constructor() { super(...arguments); this.settings = { ...DEFAULT_SETTINGS }; } async onload() { await this.loadSettings(); this.addSettingTab(new GitHubContributionsSettingTab(this.app, this)); this.registerView(VIEW_TYPE, (leaf) => new ContributionsView(leaf, this)); this.addRibbonIcon("github", "GitHub Contributions", () => this.activateView()); this.addCommand({ id: "open-github-contributions", name: "Open GitHub Contributions panel", callback: () => this.activateView() }); this.injectStyles(); this.injectPaletteStyles(); } onunload() { var _a, _b; this.app.workspace.detachLeavesOfType(VIEW_TYPE); (_a = document.getElementById("gh-contributions-styles")) == null ? void 0 : _a.remove(); (_b = document.getElementById("gh-palette-styles")) == null ? void 0 : _b.remove(); document.querySelectorAll(".gh-tooltip").forEach((el) => el.remove()); } async activateView() { const existing = this.app.workspace.getLeavesOfType(VIEW_TYPE); if (existing.length > 0) { this.app.workspace.revealLeaf(existing[0]); return; } const leaf = this.settings.sidebarSide === "left" ? this.app.workspace.getLeftLeaf(false) : this.app.workspace.getRightLeaf(false); if (leaf) { await leaf.setViewState({ type: VIEW_TYPE, active: true }); this.app.workspace.revealLeaf(leaf); } } async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); if (this.settings.selectedYear > new Date().getFullYear()) this.settings.selectedYear = new Date().getFullYear(); } async saveSettings() { await this.saveData(this.settings); this.injectPaletteStyles(); this.app.workspace.getLeavesOfType(VIEW_TYPE).forEach((leaf) => { if (leaf.view instanceof ContributionsView) leaf.view.refresh(); }); } injectPaletteStyles() { var _a; const existing = document.getElementById("gh-palette-styles"); if (existing) existing.remove(); const p = (_a = PALETTES[this.settings.palette]) != null ? _a : PALETTES["default"]; const style = document.createElement("style"); style.id = "gh-palette-styles"; style.textContent = ` body{--gh-c0:${p.dark[0]};--gh-c1:${p.dark[1]};--gh-c2:${p.dark[2]};--gh-c3:${p.dark[3]};--gh-c4:${p.dark[4]}} body.theme-light{--gh-c0:${p.light[0]};--gh-c1:${p.light[1]};--gh-c2:${p.light[2]};--gh-c3:${p.light[3]};--gh-c4:${p.light[4]}} `; document.head.appendChild(style); } injectStyles() { if (document.getElementById("gh-contributions-styles")) return; const style = document.createElement("style"); style.id = "gh-contributions-styles"; style.textContent = ` .gh-contributions-view{padding:12px 10px 16px;overflow-y:auto;overflow-x:hidden;user-select:none} .gh-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;gap:6px} .gh-username{font-size:13px;font-weight:600;color:var(--text-normal);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0} .gh-nav{display:flex;align-items:center;gap:3px;flex-shrink:0} .gh-nav-label{font-size:12px;color:var(--text-muted);min-width:52px;text-align:center;white-space:nowrap} .gh-nav-btn{background:none;border:1px solid var(--background-modifier-border);border-radius:4px;color:var(--text-muted);cursor:pointer;font-size:14px;line-height:1;padding:1px 6px;transition:background .15s} .gh-nav-btn:hover:not(:disabled){background:var(--background-modifier-hover);color:var(--text-normal)} .gh-nav-btn:disabled{opacity:.3;cursor:default} .gh-stats{display:flex;gap:5px;margin-bottom:10px;flex-wrap:wrap} .gh-stats--grid{display:grid!important;grid-template-columns:1fr 1fr} .gh-stat{display:flex;flex-direction:column;align-items:center;background:var(--background-secondary);border-radius:6px;padding:5px 7px;flex:1;min-width:0} .gh-stats--grid .gh-stat{flex:unset} .gh-stat--wide{grid-column:1 / -1} .gh-stat-val{font-size:13px;font-weight:700;color:var(--interactive-accent);line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%} .gh-stat-lbl{font-size:9px;color:var(--text-faint);text-transform:uppercase;letter-spacing:.04em;margin-top:2px;text-align:center} .gh-stats-compact{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px;align-items:center} .gh-chip{display:inline-flex;align-items:center;gap:3px;background:var(--background-secondary);border-radius:4px;padding:3px 6px;font-size:11px;white-space:nowrap} .gh-chip-icon{font-size:10px} .gh-chip-val{font-weight:700;color:var(--interactive-accent)} .gh-stats-list{display:flex;flex-direction:column;gap:1px;margin-bottom:8px} .gh-stats-list-row{display:flex;align-items:baseline;gap:4px;font-size:11px;line-height:1.6} .gh-stats-list-icon{font-size:10px;width:14px;text-align:center;flex-shrink:0} .gh-stats-list-val{font-weight:700;color:var(--interactive-accent);font-size:12px} .gh-stats-list-lbl{color:var(--text-muted);font-size:11px} .gh-oauth-code{display:inline-block;font-size:22px;font-weight:700;letter-spacing:4px;color:var(--interactive-accent);font-family:var(--font-monospace);margin:6px 0;padding:4px 8px;background:var(--background-secondary);border-radius:6px} .gh-repo-line{margin-bottom:6px;padding-bottom:5px;border-bottom:1px solid var(--background-modifier-border)} .gh-repo-line-icon{font-size:10px;color:var(--text-faint)} .gh-repo-line-name{font-size:11px;color:var(--text-muted)} .gh-graph-wrap{overflow-x:auto;padding-bottom:4px} .gh-month-lbl{font-size:9px;color:var(--text-faint)} .gh-cell{border-radius:2px;cursor:pointer;transition:transform .1s;flex-shrink:0;box-sizing:border-box} .gh-cell:hover{transform:scale(1.3);z-index:1;position:relative} .gh-cell--empty{background:transparent!important;cursor:default!important} .gh-cell--empty:hover{transform:none!important} .gh-today{outline:2px solid var(--interactive-accent)!important;outline-offset:1px} .gh-cell[data-level="0"]{background:var(--gh-c0)} .gh-cell[data-level="1"]{background:var(--gh-c1)} .gh-cell[data-level="2"]{background:var(--gh-c2)} .gh-cell[data-level="3"]{background:var(--gh-c3)} .gh-cell[data-level="4"]{background:var(--gh-c4)} .gh-legend{display:flex;align-items:center;gap:3px;margin-top:8px;justify-content:flex-end} .gh-legend-lbl{font-size:9px;color:var(--text-faint)} .gh-legend-cell{cursor:default!important} .gh-legend-cell:hover{transform:none!important} .gh-refresh-icon{margin-left:2px;font-size:13px!important} .gh-empty{display:flex;flex-direction:column;align-items:center;padding:24px 12px;text-align:center;gap:10px} .gh-empty-icon{font-size:32px} .gh-empty p{font-size:12px;color:var(--text-muted);line-height:1.5;margin:0} .gh-btn{background:var(--interactive-accent);border:none;border-radius:6px;color:var(--text-on-accent);cursor:pointer;font-size:12px;padding:6px 14px} .gh-btn:hover{filter:brightness(1.1)} .gh-error{padding:10px 12px;background:var(--background-modifier-error);border-radius:6px;color:var(--text-error);font-size:12px;margin:8px 0} .gh-skeleton-wrap{padding:4px 0} .gh-skeleton-header{height:14px;width:55%;margin-bottom:10px;border-radius:4px} .gh-skeleton-stats{height:44px;width:100%;margin-bottom:10px;border-radius:6px} .gh-skeleton-cell{border-radius:2px} .gh-skeleton{animation:gh-shimmer 1.4s infinite linear;background:linear-gradient(90deg,var(--background-modifier-border) 25%,var(--background-secondary) 50%,var(--background-modifier-border) 75%);background-size:200% 100%} @keyframes gh-shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}} .gh-tooltip{position:fixed;background:#1a1a1a;color:#eee;border-radius:6px;padding:7px 10px;font-size:11px;line-height:1.6;pointer-events:none;display:none;z-index:9999;white-space:pre;box-shadow:0 2px 10px rgba(0,0,0,.35);font-family:var(--font-monospace)} .gh-tooltip strong{color:#fff;display:block;margin-bottom:2px;font-family:var(--font-interface)} .theme-light .gh-tooltip{background:#222} `; document.head.appendChild(style); } };