mirror of
https://github.com/gearheaded/obsidian-github-contributions.git
synced 2026-07-22 07:43:36 +00:00
1213 lines
50 KiB
JavaScript
1213 lines
50 KiB
JavaScript
/*
|
|
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_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 GITHUB_CLIENT_ID = "Ov23litfj5GbQ8mw81VV";
|
|
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,
|
|
showLegend: true,
|
|
tooltipStyle: "standard",
|
|
selectedYear: new Date().getFullYear(),
|
|
sizePreset: "medium",
|
|
defaultView: "year",
|
|
palette: "classic",
|
|
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 }
|
|
// sentinel - calculated at render time
|
|
};
|
|
var PALETTES = {
|
|
"classic": {
|
|
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"]
|
|
},
|
|
"cobalt": {
|
|
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"]
|
|
}
|
|
};
|
|
function debounce(fn, ms) {
|
|
let timer = 0;
|
|
return (...args) => {
|
|
window.clearTimeout(timer);
|
|
timer = window.setTimeout(() => fn(...args), ms);
|
|
};
|
|
}
|
|
async function requestDeviceCode() {
|
|
let res;
|
|
try {
|
|
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" })
|
|
});
|
|
} catch (e) {
|
|
throw new Error("Could not reach GitHub. Check your internet connection.");
|
|
}
|
|
if (res.status !== 200)
|
|
throw new Error(`GitHub returned an error (${res.status}). Try again.`);
|
|
return res.json;
|
|
}
|
|
async function pollForToken(deviceCode, intervalSecs, expiresIn, onCancel) {
|
|
var _a, _b;
|
|
const delay = (ms) => new Promise((r) => window.setTimeout(r, ms));
|
|
let currentInterval = Math.max(intervalSecs, 5) * 1e3;
|
|
const deadline = Date.now() + expiresIn * 1e3;
|
|
while (true) {
|
|
if (onCancel())
|
|
throw new Error("Cancelled");
|
|
await delay(currentInterval);
|
|
if (onCancel())
|
|
throw new Error("Cancelled");
|
|
if (Date.now() > deadline)
|
|
throw new Error("Code expired. Please try again.");
|
|
let data;
|
|
try {
|
|
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"
|
|
})
|
|
});
|
|
data = res.json;
|
|
} catch (e) {
|
|
await delay(5e3);
|
|
if (onCancel())
|
|
throw new Error("Cancelled");
|
|
continue;
|
|
}
|
|
if (data.access_token)
|
|
return data.access_token;
|
|
switch (data.error) {
|
|
case "authorization_pending":
|
|
continue;
|
|
case "slow_down":
|
|
currentInterval += 5e3;
|
|
continue;
|
|
case "expired_token":
|
|
throw new Error("Code expired. Please try again.");
|
|
case "access_denied":
|
|
throw new Error("Access was denied. You can try again anytime.");
|
|
case "incorrect_device_code":
|
|
throw new Error("Invalid device code. Please try again.");
|
|
default:
|
|
throw new Error((_b = (_a = data.error_description) != null ? _a : data.error) != null ? _b : "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;
|
|
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 (0, import_obsidian.requestUrl)({
|
|
url: GITHUB_GRAPHQL,
|
|
method: "POST",
|
|
headers: { Authorization: `bearer ${token}`, "Content-Type": "application/json" },
|
|
body: JSON.stringify({ query, variables: { login: username, from, to } })
|
|
});
|
|
if (res.status !== 200)
|
|
throw new Error(`GitHub API error: ${res.status}`);
|
|
const json = res.json;
|
|
if (json.errors && json.errors.length > 0)
|
|
throw new Error(json.errors[0].message);
|
|
const weeks = (_d = (_c = (_b = (_a = json.data) == null ? void 0 : _a.user) == null ? void 0 : _b.contributionsCollection) == null ? void 0 : _c.contributionCalendar) == null ? void 0 : _d.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 = typeof window.require === "function";
|
|
function electronRequire(moduleName) {
|
|
return window.require(moduleName);
|
|
}
|
|
function runGit(args, cwd) {
|
|
if (!isDesktop)
|
|
return "";
|
|
try {
|
|
const { execSync } = electronRequire("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 = electronRequire("fs");
|
|
const path = electronRequire("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) {
|
|
var _a, _b;
|
|
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 ghRepos = ["my-project", "obsidian-plugin"];
|
|
const localRepos = ["dotfiles", "personal-vault"];
|
|
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 recencyBoost = i / daysInYear;
|
|
if (rand() >= 0.45 + recencyBoost * 0.3)
|
|
continue;
|
|
const totalCount = Math.floor(rand() * 10) + 1;
|
|
const ghCount = Math.floor(rand() * totalCount);
|
|
const localCount = totalCount - ghCount;
|
|
const repos = {};
|
|
if (ghCount > 0) {
|
|
let rem = ghCount;
|
|
for (let ri = 0; ri < ghRepos.length; ri++) {
|
|
if (rem <= 0)
|
|
break;
|
|
const n = ri === ghRepos.length - 1 ? rem : Math.min(rem, Math.floor(rand() * rem) + 1);
|
|
if (n > 0 && rand() > 0.3) {
|
|
repos[ghRepos[ri]] = n;
|
|
rem -= n;
|
|
}
|
|
}
|
|
if (rem > 0)
|
|
repos[ghRepos[0]] = ((_a = repos[ghRepos[0]]) != null ? _a : 0) + rem;
|
|
}
|
|
if (localCount > 0) {
|
|
let rem = localCount;
|
|
for (let ri = 0; ri < localRepos.length; ri++) {
|
|
if (rem <= 0)
|
|
break;
|
|
const n = ri === localRepos.length - 1 ? rem : Math.min(rem, Math.floor(rand() * rem) + 1);
|
|
if (n > 0 && rand() > 0.3) {
|
|
repos[localRepos[ri]] = n;
|
|
rem -= n;
|
|
}
|
|
}
|
|
if (rem > 0)
|
|
repos[localRepos[0]] = ((_b = repos[localRepos[0]]) != null ? _b : 0) + rem;
|
|
}
|
|
days.set(dateStr, {
|
|
date: dateStr,
|
|
count: ghCount + localCount,
|
|
githubCount: ghCount,
|
|
localCount,
|
|
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 = activeDocument.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);
|
|
}
|
|
if (this.plugin.settings.showLegend)
|
|
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 = () => {
|
|
void 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.setCssStyles({ overflowX: "auto" });
|
|
const monthRow = graphWrap.createDiv({ cls: "gh-month-row" });
|
|
monthRow.setCssStyles({ display: "grid", gridTemplateColumns: `repeat(${weeks.length},${cell}px)`, gap: gap + "px", marginBottom: "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.setCssStyles({ gridColumn: String(wi + 1), fontSize: Math.max(8, cell - 2) + "px" });
|
|
}
|
|
});
|
|
const grid = graphWrap.createDiv({ cls: "gh-grid" });
|
|
grid.setCssStyles({ display: "flex", gap: gap + "px" });
|
|
weeks.forEach((week) => {
|
|
const col = grid.createDiv({ cls: "gh-col" });
|
|
col.setCssStyles({ display: "flex", flexDirection: "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.setCssStyles({ display: "grid", gridTemplateColumns: colTemplate, gap: gap + "px", marginBottom: "4px" });
|
|
["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"].forEach((d) => {
|
|
const lbl = dowRow.createEl("span", { cls: "gh-dow-lbl", text: d });
|
|
lbl.setCssStyles({ fontSize: "10px", color: "var(--text-faint)", textAlign: "center", overflow: "hidden" });
|
|
});
|
|
const grid = graphWrap.createDiv({ cls: "gh-month-grid" });
|
|
grid.setCssStyles({ display: "flex", flexDirection: "column", gap: gap + "px" });
|
|
weeks.forEach((week) => {
|
|
const row = grid.createDiv({ cls: "gh-month-row-cells" });
|
|
row.setCssStyles({ display: "grid", gridTemplateColumns: colTemplate, gap: gap + "px" });
|
|
week.forEach((day) => this.renderCell(row, day, cell));
|
|
});
|
|
}
|
|
renderCell(parent, day, size) {
|
|
const cell = parent.createDiv({ cls: "gh-cell" });
|
|
cell.setCssStyles({ 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.setCssStyles({ display: "none" });
|
|
});
|
|
cell.addEventListener("click", () => this.openOrCreateDailyNote(day.date));
|
|
}
|
|
showTooltip(e, day) {
|
|
if (!this.tooltipEl)
|
|
return;
|
|
const s = this.plugin.settings;
|
|
const showSources = s.dataSource === "both" || s.demoMode;
|
|
const repoEntries = Object.entries(day.repos).sort((a, b) => b[1] - a[1]);
|
|
this.tooltipEl.empty();
|
|
this.tooltipEl.removeClass("gh-tooltip--console", "gh-tooltip--modern");
|
|
if (s.tooltipStyle === "simple") {
|
|
this.renderSimpleTooltip(day, showSources);
|
|
} else {
|
|
this.renderStandardTooltip(day, showSources, repoEntries);
|
|
}
|
|
this.tooltipEl.setCssStyles({ display: "block", visibility: "hidden" });
|
|
const tooltipWidth = this.tooltipEl.offsetWidth || 180;
|
|
const tooltipHeight = this.tooltipEl.offsetHeight || 80;
|
|
const spaceOnRight = window.innerWidth - e.pageX;
|
|
const left = spaceOnRight < tooltipWidth + 20 ? e.pageX - tooltipWidth - 12 : e.pageX + 12;
|
|
const spaceBelow = window.innerHeight - e.pageY;
|
|
const top = spaceBelow < tooltipHeight + 20 ? e.pageY - tooltipHeight - 8 : e.pageY - 34;
|
|
this.tooltipEl.setCssStyles({ left: left + "px", top: top + "px", visibility: "visible" });
|
|
}
|
|
renderSimpleTooltip(day, showSources) {
|
|
const t = this.tooltipEl;
|
|
t.addClass("gh-tooltip--modern");
|
|
t.createEl("div", { cls: "gh-tip-date", text: (0, import_obsidian.moment)(day.date).format("MMM D, YYYY") });
|
|
if (day.count === 0) {
|
|
t.createEl("div", { cls: "gh-tip-no-contrib", text: "No contributions" });
|
|
return;
|
|
}
|
|
const countRow = t.createDiv({ cls: "gh-tip-count-row" });
|
|
countRow.createEl("span", { cls: "gh-tip-count", text: String(day.count) });
|
|
countRow.createEl("span", { cls: "gh-tip-count-lbl", text: ` contribution${day.count !== 1 ? "s" : ""}` });
|
|
if (showSources && (day.githubCount > 0 || day.localCount > 0)) {
|
|
const sources = t.createDiv({ cls: "gh-tip-sources" });
|
|
if (day.githubCount > 0) {
|
|
const row = sources.createDiv({ cls: "gh-tip-source-row" });
|
|
const ghIco = row.createEl("span", { cls: "gh-tip-source-icon" });
|
|
ghIco.appendChild(this.makeGithubSvg());
|
|
row.createEl("span", { cls: "gh-tip-source-lbl", text: "GitHub" });
|
|
row.createEl("span", { cls: "gh-tip-source-val", text: String(day.githubCount) });
|
|
}
|
|
if (day.localCount > 0) {
|
|
const row = sources.createDiv({ cls: "gh-tip-source-row" });
|
|
const localIco = row.createEl("span", { cls: "gh-tip-source-icon" });
|
|
localIco.appendChild(this.makeLocalSvg());
|
|
row.createEl("span", { cls: "gh-tip-source-lbl", text: "Local" });
|
|
row.createEl("span", { cls: "gh-tip-source-val", text: String(day.localCount) });
|
|
}
|
|
}
|
|
}
|
|
makeGithubSvg() {
|
|
const svg = activeDocument.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
svg.setAttribute("width", "13");
|
|
svg.setAttribute("height", "13");
|
|
svg.setAttribute("viewBox", "0 0 24 24");
|
|
svg.setAttribute("fill", "none");
|
|
svg.setAttribute("stroke", "currentColor");
|
|
svg.setAttribute("stroke-width", "2");
|
|
svg.setAttribute("stroke-linecap", "round");
|
|
svg.setAttribute("stroke-linejoin", "round");
|
|
const p1 = activeDocument.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
p1.setAttribute("d", "M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4");
|
|
svg.appendChild(p1);
|
|
const p2 = activeDocument.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
p2.setAttribute("d", "M9 18c-4.51 2-5-2-7-2");
|
|
svg.appendChild(p2);
|
|
return svg;
|
|
}
|
|
makeLocalSvg() {
|
|
const svg = activeDocument.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
svg.setAttribute("width", "13");
|
|
svg.setAttribute("height", "13");
|
|
svg.setAttribute("viewBox", "0 0 24 24");
|
|
svg.setAttribute("fill", "none");
|
|
svg.setAttribute("stroke", "currentColor");
|
|
svg.setAttribute("stroke-width", "2");
|
|
svg.setAttribute("stroke-linecap", "round");
|
|
svg.setAttribute("stroke-linejoin", "round");
|
|
const rect = activeDocument.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
rect.setAttribute("width", "18");
|
|
rect.setAttribute("height", "12");
|
|
rect.setAttribute("x", "3");
|
|
rect.setAttribute("y", "4");
|
|
rect.setAttribute("rx", "2");
|
|
rect.setAttribute("ry", "2");
|
|
svg.appendChild(rect);
|
|
const line = activeDocument.createElementNS("http://www.w3.org/2000/svg", "line");
|
|
line.setAttribute("x1", "2");
|
|
line.setAttribute("x2", "22");
|
|
line.setAttribute("y1", "20");
|
|
line.setAttribute("y2", "20");
|
|
svg.appendChild(line);
|
|
return svg;
|
|
}
|
|
renderStandardTooltip(day, showSources, repoEntries) {
|
|
const t = this.tooltipEl;
|
|
t.addClass("gh-tooltip--modern");
|
|
t.createEl("div", { cls: "gh-tip-date", text: (0, import_obsidian.moment)(day.date).format("MMM D, YYYY") });
|
|
if (day.count === 0) {
|
|
t.createEl("div", { cls: "gh-tip-no-contrib", text: "No contributions" });
|
|
return;
|
|
}
|
|
const countRow = t.createDiv({ cls: "gh-tip-count-row" });
|
|
countRow.createEl("span", { cls: "gh-tip-count", text: String(day.count) });
|
|
countRow.createEl("span", { cls: "gh-tip-count-lbl", text: ` contribution${day.count !== 1 ? "s" : ""}` });
|
|
if (showSources && (day.githubCount > 0 || day.localCount > 0)) {
|
|
const sources = t.createDiv({ cls: "gh-tip-sources" });
|
|
if (day.githubCount > 0) {
|
|
const row = sources.createDiv({ cls: "gh-tip-source-row" });
|
|
const ico = row.createEl("span", { cls: "gh-tip-source-icon" });
|
|
ico.appendChild(this.makeGithubSvg());
|
|
row.createEl("span", { cls: "gh-tip-source-lbl", text: "GitHub" });
|
|
row.createEl("span", { cls: "gh-tip-source-val", text: String(day.githubCount) });
|
|
}
|
|
if (day.localCount > 0) {
|
|
const row = sources.createDiv({ cls: "gh-tip-source-row" });
|
|
const ico = row.createEl("span", { cls: "gh-tip-source-icon" });
|
|
ico.appendChild(this.makeLocalSvg());
|
|
row.createEl("span", { cls: "gh-tip-source-lbl", text: "Local" });
|
|
row.createEl("span", { cls: "gh-tip-source-val", text: String(day.localCount) });
|
|
}
|
|
}
|
|
if (repoEntries.length > 0) {
|
|
t.createDiv({ cls: "gh-tip-divider" });
|
|
const repoColors = ["#3fb950", "#3b82f6", "#a855f7", "#f59e0b", "#ef4444"];
|
|
repoEntries.forEach(([repo, count], i) => {
|
|
const row = t.createDiv({ cls: "gh-tip-repo-row" });
|
|
const dot = row.createEl("span", { cls: "gh-tip-dot" });
|
|
dot.setCssStyles({ background: repoColors[i % repoColors.length] });
|
|
row.createEl("span", { cls: "gh-tip-repo-name", text: repo });
|
|
row.createEl("span", { cls: "gh-tip-repo-count", text: `(${count})` });
|
|
});
|
|
}
|
|
}
|
|
renderLegend(container) {
|
|
const { cell } = 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.setCssStyles({ 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 = () => {
|
|
const appWithSettings = this.app;
|
|
appWithSettings.setting.open();
|
|
appWithSettings.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.setCssStyles({ display: "grid", gridTemplateColumns: `repeat(${cols},${cell}px)`, gridTemplateRows: `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;
|
|
}
|
|
}
|
|
if (file instanceof import_obsidian.TFile)
|
|
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();
|
|
new import_obsidian.Setting(containerEl).setName("Data Sources").setHeading();
|
|
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();
|
|
const expiryMins = Math.floor(device.expires_in / 60);
|
|
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 (code expires in ${expiryMins} minutes)` });
|
|
})
|
|
);
|
|
window.open(device.verification_uri);
|
|
const token = await pollForToken(device.device_code, device.interval, device.expires_in, () => 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("Connection cancelled. Click Connect GitHub to try again.");
|
|
} else {
|
|
oauthSetting.setDesc("\u26A0 " + 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 - 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\\You\\Projects").setValue(this.plugin.settings.localRepoRoot);
|
|
t.onChange((v) => {
|
|
this.plugin.settings.localRepoRoot = v.trim();
|
|
});
|
|
}).addButton(
|
|
(btn) => btn.setButtonText("Scan").setTooltip("Save path and scan for repositories").onClick(async () => {
|
|
btn.setButtonText("Scanning\u2026").setDisabled(true);
|
|
await this.plugin.saveSettings();
|
|
btn.setButtonText("Scan").setDisabled(false);
|
|
new import_obsidian.Notice("Repo scan complete - refresh the panel to see results");
|
|
})
|
|
);
|
|
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();
|
|
})
|
|
);
|
|
}
|
|
}
|
|
new import_obsidian.Setting(containerEl).setName("Display").setHeading();
|
|
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("classic", "Classic (GitHub greens)").addOption("high-contrast", "High contrast (vivid greens)").addOption("cobalt", "Cobalt (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("Tooltip style").addDropdown(
|
|
(d) => d.addOption("standard", "Standard (with repos)").addOption("simple", "Simple (no repos)").setValue(this.plugin.settings.tooltipStyle).onChange(async (v) => {
|
|
this.plugin.settings.tooltipStyle = v;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian.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();
|
|
}));
|
|
new import_obsidian.Setting(containerEl).setName("Show legend").setDesc("Show the Less / More colour legend below the graph").addToggle((t) => t.setValue(this.plugin.settings.showLegend).onChange(async (v) => {
|
|
this.plugin.settings.showLegend = v;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
new import_obsidian.Setting(containerEl).setName("Daily Notes").setHeading();
|
|
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);
|
|
t.onChange(debounce(async (v) => {
|
|
this.plugin.settings.dailyNoteFolder = v;
|
|
await this.plugin.saveSettings();
|
|
}, 800));
|
|
});
|
|
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);
|
|
t.onChange(debounce(async (v) => {
|
|
this.plugin.settings.dailyNoteDateFormat = v;
|
|
await this.plugin.saveSettings();
|
|
}, 800));
|
|
});
|
|
}
|
|
};
|
|
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", () => {
|
|
void this.activateView();
|
|
});
|
|
this.addCommand({ id: "open-panel", name: "Open panel", callback: () => {
|
|
void this.activateView();
|
|
} });
|
|
this.injectPaletteStyles();
|
|
}
|
|
onunload() {
|
|
var _a;
|
|
(_a = activeDocument.getElementById("gh-palette-styles")) == null ? void 0 : _a.remove();
|
|
activeDocument.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.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 doc = activeDocument;
|
|
const existing = doc.getElementById("gh-palette-styles");
|
|
if (existing)
|
|
existing.remove();
|
|
const p = (_a = PALETTES[this.settings.palette]) != null ? _a : PALETTES["classic"];
|
|
const style = doc.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]}}
|
|
`;
|
|
doc.head.appendChild(style);
|
|
}
|
|
};
|