Finishing touches on first version

This commit is contained in:
gearheaded 2026-06-10 20:42:36 -04:00
parent 20229250c1
commit 115910cf43
4 changed files with 131 additions and 72 deletions

99
main.js
View file

@ -70,7 +70,7 @@ var PALETTES = {
dark: ["var(--background-modifier-border)", "#1a5e2a", "#21a045", "#32d463", "#7fffb0"],
light: ["#ebedf0", "#b6f0c2", "#3dd668", "#1a9e40", "#0a5c25"]
},
"colorblind": {
"cobalt": {
dark: ["var(--background-modifier-border)", "#0a3a6b", "#0e6eb5", "#1ab3d8", "#57e8f5"],
light: ["#edf4fb", "#b3d4f0", "#4aa8e0", "#1478c8", "#064a8a"]
},
@ -84,50 +84,70 @@ var PALETTES = {
}
};
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" })
});
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 device flow error: ${res.status}`);
throw new Error(`GitHub returned an error (${res.status}). Try again.`);
return res.json;
}
async function pollForToken(deviceCode, intervalSecs, onCancel) {
var _a;
async function pollForToken(deviceCode, intervalSecs, expiresIn, onCancel) {
var _a, _b;
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
const pollInterval = Math.max(intervalSecs, 5) * 1e3;
let currentInterval = Math.max(intervalSecs, 5) * 1e3;
const deadline = Date.now() + expiresIn * 1e3;
while (true) {
if (onCancel())
throw new Error("Cancelled");
await delay(pollInterval);
await delay(currentInterval);
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") {
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.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");
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) {
@ -850,6 +870,7 @@ var GitHubContributionsSettingTab = class extends import_obsidian.PluginSettingT
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 ");
@ -857,11 +878,11 @@ var GitHubContributionsSettingTab = class extends import_obsidian.PluginSettingT
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" });
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, () => cancelled);
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;
@ -870,8 +891,10 @@ var GitHubContributionsSettingTab = class extends import_obsidian.PluginSettingT
this.display();
} catch (e) {
const msg = e.message;
if (msg !== "Cancelled") {
oauthSetting.setDesc("Error: " + msg);
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);
@ -928,7 +951,7 @@ var GitHubContributionsSettingTab = class extends import_obsidian.PluginSettingT
})
);
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) => {
(d) => d.addOption("default", "Default (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();
})

View file

@ -1,10 +1,10 @@
{
"id": "github-contributions",
"name": "GitHub Contributions",
"version": "2.5.0",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "Display GitHub and local Git contribution graphs in the Obsidian sidebar with streak tracking, repository insights, and daily note integration.",
"author": "gearheaded",
"authorUrl": "",
"authorUrl": "https://github.com/gearheaded",
"isDesktopOnly": false
}

View file

@ -95,7 +95,7 @@ const PRESET_SIZES: Record<SizePreset, { cell: number; gap: number }> = {
};
// ── Palettes ─────────────────────────────────────────────────────────────────
type Palette = "default" | "high-contrast" | "colorblind" | "neon" | "ember";
type Palette = "default" | "high-contrast" | "cobalt" | "neon" | "ember";
type StatsStyle = "compact" | "default" | "grid";
interface PaletteColors {
@ -112,7 +112,7 @@ const PALETTES: Record<Palette, PaletteColors> = {
dark: ["var(--background-modifier-border)", "#1a5e2a", "#21a045", "#32d463", "#7fffb0"],
light: ["#ebedf0", "#b6f0c2", "#3dd668", "#1a9e40", "#0a5c25"],
},
"colorblind": {
"cobalt": {
dark: ["var(--background-modifier-border)", "#0a3a6b", "#0e6eb5", "#1ab3d8", "#57e8f5"],
light: ["#edf4fb", "#b3d4f0", "#4aa8e0", "#1478c8", "#064a8a"],
},
@ -137,47 +137,77 @@ interface DeviceCodeResponse {
}
async function requestDeviceCode(): Promise<DeviceCodeResponse> {
const res = await 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}`);
let res;
try {
res = await 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 {
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: string,
intervalSecs: number,
expiresIn: number,
onCancel: () => boolean
): Promise<string> {
const delay = (ms: number) => new Promise(r => setTimeout(r, ms));
const pollInterval = Math.max(intervalSecs, 5) * 1000;
let currentInterval = Math.max(intervalSecs, 5) * 1000;
const deadline = Date.now() + expiresIn * 1000;
while (true) {
if (onCancel()) throw new Error("Cancelled");
await delay(pollInterval);
await delay(currentInterval);
if (onCancel()) throw new Error("Cancelled");
const res = await 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;
// Check if code has expired client-side before even polling
if (Date.now() > deadline) throw new Error("Code expired. Please try again.");
let data: Record<string, string>;
try {
const res = await 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 {
// Network error - wait and retry rather than failing immediately
await delay(5000);
if (onCancel()) throw new Error("Cancelled");
continue;
}
if (data.access_token) return data.access_token;
if (data.error === "authorization_pending") continue;
if (data.error === "slow_down") { await delay(5000); 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(data.error_description ?? "Unknown OAuth error");
switch (data.error) {
case "authorization_pending":
continue;
case "slow_down":
// GitHub asked us to back off - increase interval permanently
currentInterval += 5000;
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(data.error_description ?? data.error ?? "Unknown OAuth error");
}
}
}
@ -979,6 +1009,7 @@ class GitHubContributionsSettingTab extends PluginSettingTab {
const device = await requestDeviceCode();
// Show the user code prominently
const expiryMins = Math.floor(device.expires_in / 60);
oauthSetting.setDesc(
createFragment(f => {
f.appendText("Enter this code at ");
@ -986,7 +1017,7 @@ class GitHubContributionsSettingTab extends PluginSettingTab {
f.createEl("br");
f.createEl("span", { cls: "gh-oauth-code", text: device.user_code });
f.createEl("br");
f.createEl("em", { text: "Waiting for approval…" });
f.createEl("em", { text: `Waiting for approval… (code expires in ${expiryMins} minutes)` });
})
);
@ -995,7 +1026,7 @@ class GitHubContributionsSettingTab extends PluginSettingTab {
(window as any).open(device.verification_uri);
// Poll for token
const token = await pollForToken(device.device_code, device.interval, () => cancelled);
const token = await pollForToken(device.device_code, device.interval, device.expires_in, () => cancelled);
const username = await fetchGitHubUsername(token);
this.plugin.settings.githubToken = token;
@ -1005,8 +1036,10 @@ class GitHubContributionsSettingTab extends PluginSettingTab {
this.display();
} catch (e) {
const msg = (e as Error).message;
if (msg !== "Cancelled") {
oauthSetting.setDesc("Error: " + msg);
if (msg === "Cancelled") {
oauthSetting.setDesc("Connection cancelled. Click Connect GitHub to try again.");
} else {
oauthSetting.setDesc("⚠ " + msg);
new Notice("GitHub connection failed: " + msg);
}
btn.setButtonText("Connect GitHub").setDisabled(false);
@ -1088,7 +1121,7 @@ class GitHubContributionsSettingTab extends PluginSettingTab {
.addDropdown(d => d
.addOption("default", "Default (GitHub greens)")
.addOption("high-contrast", "High contrast (vivid greens)")
.addOption("colorblind", "Colorblind friendly (blue to cyan)")
.addOption("cobalt", "Cobalt (blue to cyan)")
.addOption("neon", "Neon (purple to yellow)")
.addOption("ember", "Ember (amber to gold)")
.setValue(this.plugin.settings.palette)

3
versions.json Normal file
View file

@ -0,0 +1,3 @@
{
"1.0.0": "0.15.0"
}