diff --git a/main.js b/main.js index e269fc9..2ae4068 100644 --- a/main.js +++ b/main.js @@ -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(); }) diff --git a/manifest.json b/manifest.json index 1b14884..fcadf36 100644 --- a/manifest.json +++ b/manifest.json @@ -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 } diff --git a/src/main.ts b/src/main.ts index 0d87043..c6953a1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -95,7 +95,7 @@ const PRESET_SIZES: Record = { }; // ── 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 = { 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 { - 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 { 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; + 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) diff --git a/versions.json b/versions.json new file mode 100644 index 0000000..f671e66 --- /dev/null +++ b/versions.json @@ -0,0 +1,3 @@ +{ + "1.0.0": "0.15.0" +} \ No newline at end of file