mirror of
https://github.com/flyingnobita/obsidian-github-stars.git
synced 2026-07-22 05:42:52 +00:00
Initial commit
This commit is contained in:
commit
3e33be66e7
18 changed files with 3155 additions and 0 deletions
10
.editorconfig
Normal file
10
.editorconfig
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
tab_width = 4
|
||||||
3
.eslintignore
Normal file
3
.eslintignore
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
main.js
|
||||||
23
.eslintrc
Normal file
23
.eslintrc
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"env": { "node": true },
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
|
||||||
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
|
"no-prototype-builtins": "off",
|
||||||
|
"@typescript-eslint/no-empty-function": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# vscode
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# Intellij
|
||||||
|
*.iml
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# npm
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Don't include the compiled main.js file in the repo.
|
||||||
|
# They should be uploaded to GitHub releases instead.
|
||||||
|
main.js
|
||||||
|
|
||||||
|
# Exclude sourcemaps
|
||||||
|
*.map
|
||||||
|
|
||||||
|
# obsidian
|
||||||
|
data.json
|
||||||
|
|
||||||
|
# Exclude macOS Finder (System Explorer) View States
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Devenv
|
||||||
|
.devenv*
|
||||||
|
devenv.local.nix
|
||||||
|
|
||||||
|
# direnv
|
||||||
|
.direnv
|
||||||
|
|
||||||
|
# pre-commit
|
||||||
|
.pre-commit-config.yaml
|
||||||
0
.hotreload
Normal file
0
.hotreload
Normal file
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
tag-version-prefix=""
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Flying Nobita
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
61
README.md
Normal file
61
README.md
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
# Obsidian GitHub Stars Plugin
|
||||||
|
|
||||||
|
Display the number of stars next to GitHub repository links.
|
||||||
|
|
||||||
|
_Note: This plugin currently only display the stars in **Reading View**. In the future I hope to support it in Live Preview as well._
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Automatically detects GitHub repository URLs in your notes
|
||||||
|
- Displays the star count next to each GitHub repository link
|
||||||
|
- Caches star counts to minimize API requests
|
||||||
|
- Configurable display format for star counts
|
||||||
|
- Optional GitHub API token support for higher rate limits
|
||||||
|
- Supports abbreviated number formatting (e.g., 1.2k instead of 1,234)
|
||||||
|
- Command to refresh star counts for the current note
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
When you include a GitHub repository URL in your notes, the plugin will automatically enhance it to show the star count:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The plugin can be configured in the Settings tab:
|
||||||
|
|
||||||
|
- **Cache Expiry**: Time in minutes before the GitHub star count cache expires (default: 60 minutes)
|
||||||
|
- **Display Format**: Format for displaying star counts. Use `{stars}` as a placeholder for the number (default: `⭐ {stars}`)
|
||||||
|
- **Number Format**: Choose between full numbers (e.g., 1,234) or abbreviated format (e.g., 1.2k)
|
||||||
|
- **GitHub API Token**: Optional personal access token for GitHub API to increase rate limits
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
The plugin adds the following commands:
|
||||||
|
|
||||||
|
- **Refresh GitHub Stars for Current Note**: Refreshes all GitHub star counts in the current note
|
||||||
|
- **Clear GitHub Stars Cache**: Clears the cached star counts
|
||||||
|
|
||||||
|
## GitHub API Rate Limits
|
||||||
|
|
||||||
|
The GitHub API has rate limits for unauthenticated requests (60 requests per hour). If you use GitHub extensively in your notes, you might want to add a GitHub personal access token in the plugin settings to increase this limit (5,000 requests per hour for authenticated requests).
|
||||||
|
|
||||||
|
To create a GitHub personal access token:
|
||||||
|
|
||||||
|
1. Go to [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens)
|
||||||
|
2. Click "Generate new token"
|
||||||
|
3. Give it a name and select the "public_repo" scope
|
||||||
|
4. Click "Generate token"
|
||||||
|
5. Copy the token and paste it in the plugin settings
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
- [ ] Support for Live Preview mode
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||||
49
esbuild.config.mjs
Normal file
49
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import esbuild from "esbuild";
|
||||||
|
import process from "process";
|
||||||
|
import builtins from "builtin-modules";
|
||||||
|
|
||||||
|
const banner =
|
||||||
|
`/*
|
||||||
|
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||||
|
if you want to view the source, please visit the github repository of this plugin
|
||||||
|
*/
|
||||||
|
`;
|
||||||
|
|
||||||
|
const prod = (process.argv[2] === "production");
|
||||||
|
|
||||||
|
const context = await esbuild.context({
|
||||||
|
banner: {
|
||||||
|
js: banner,
|
||||||
|
},
|
||||||
|
entryPoints: ["main.ts"],
|
||||||
|
bundle: true,
|
||||||
|
external: [
|
||||||
|
"obsidian",
|
||||||
|
"electron",
|
||||||
|
"@codemirror/autocomplete",
|
||||||
|
"@codemirror/collab",
|
||||||
|
"@codemirror/commands",
|
||||||
|
"@codemirror/language",
|
||||||
|
"@codemirror/lint",
|
||||||
|
"@codemirror/search",
|
||||||
|
"@codemirror/state",
|
||||||
|
"@codemirror/view",
|
||||||
|
"@lezer/common",
|
||||||
|
"@lezer/highlight",
|
||||||
|
"@lezer/lr",
|
||||||
|
...builtins],
|
||||||
|
format: "cjs",
|
||||||
|
target: "es2018",
|
||||||
|
logLevel: "info",
|
||||||
|
sourcemap: prod ? false : "inline",
|
||||||
|
treeShaking: true,
|
||||||
|
outfile: "main.js",
|
||||||
|
minify: prod,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (prod) {
|
||||||
|
await context.rebuild();
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
await context.watch();
|
||||||
|
}
|
||||||
430
main.ts
Normal file
430
main.ts
Normal file
|
|
@ -0,0 +1,430 @@
|
||||||
|
import { App, MarkdownView, Notice, Plugin, PluginSettingTab, Setting, MarkdownPostProcessorContext } from 'obsidian';
|
||||||
|
|
||||||
|
|
||||||
|
interface GitHubStarsSettings {
|
||||||
|
cacheExpiry: number; // Time in minutes before cache expires
|
||||||
|
displayFormat: string; // Format for displaying stars (e.g., "⭐ {stars}")
|
||||||
|
apiToken: string; // Optional GitHub API token for higher rate limits
|
||||||
|
numberFormat: 'full' | 'abbreviated'; // Number formatting style
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_SETTINGS: GitHubStarsSettings = {
|
||||||
|
cacheExpiry: 60, // Default cache expiry: 60 minutes
|
||||||
|
displayFormat: "⭐ {stars}",
|
||||||
|
apiToken: "",
|
||||||
|
numberFormat: 'abbreviated'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface for cache entries
|
||||||
|
interface CacheEntry {
|
||||||
|
stars: number;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class GitHubStarsPlugin extends Plugin {
|
||||||
|
settings: GitHubStarsSettings;
|
||||||
|
cache: Record<string, CacheEntry> = {};
|
||||||
|
|
||||||
|
async onload() {
|
||||||
|
// Load settings
|
||||||
|
await this.loadSettings();
|
||||||
|
|
||||||
|
// This creates an icon in the left ribbon.
|
||||||
|
const ribbonIconEl = this.addRibbonIcon('star', 'Github Stars', (evt: MouseEvent) => {
|
||||||
|
// Called when the user clicks the icon.
|
||||||
|
new Notice('Processing GitHub Stars...');
|
||||||
|
|
||||||
|
// Get the active markdown view
|
||||||
|
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||||
|
if (activeView) {
|
||||||
|
// Force refresh by triggering the markdown processor
|
||||||
|
activeView.previewMode.rerender(true);
|
||||||
|
new Notice('GitHub star counts refreshed!');
|
||||||
|
} else {
|
||||||
|
new Notice('No active markdown view found');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Perform additional things with the ribbon
|
||||||
|
ribbonIconEl.addClass('github-stars-ribbon-class');
|
||||||
|
|
||||||
|
// Load cache from data
|
||||||
|
try {
|
||||||
|
|
||||||
|
const loadedCache = await this.loadData();
|
||||||
|
|
||||||
|
// Check if we have a cache property in the loaded data
|
||||||
|
if (loadedCache && loadedCache.cache) {
|
||||||
|
this.cache = loadedCache.cache;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading cache:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the markdown post processor to find and enhance GitHub links
|
||||||
|
this.registerMarkdownPostProcessor(this.processMarkdown.bind(this));
|
||||||
|
|
||||||
|
// Add settings tab
|
||||||
|
this.addSettingTab(new GitHubStarsSettingTab(this.app, this));
|
||||||
|
|
||||||
|
// Add a command to clear the cache
|
||||||
|
this.addCommand({
|
||||||
|
id: 'clear-github-stars-cache',
|
||||||
|
name: 'Clear GitHub Stars Cache',
|
||||||
|
callback: () => {
|
||||||
|
this.cache = {};
|
||||||
|
this.saveSettings();
|
||||||
|
new Notice('GitHub Stars cache cleared');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a command to refresh star counts for the current note
|
||||||
|
this.addCommand({
|
||||||
|
id: 'refresh-github-stars',
|
||||||
|
name: 'Refresh GitHub Stars for Current Note',
|
||||||
|
checkCallback: (checking: boolean) => {
|
||||||
|
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||||
|
if (activeView) {
|
||||||
|
if (!checking) {
|
||||||
|
// Force refresh by triggering the markdown processor
|
||||||
|
activeView.previewMode.rerender(true);
|
||||||
|
new Notice('Refreshing GitHub star counts...');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onunload() {
|
||||||
|
// Save settings when plugin is unloaded
|
||||||
|
this.saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadSettings() {
|
||||||
|
const data = await this.loadData();
|
||||||
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, data);
|
||||||
|
// Store cache separately if it exists in the loaded data
|
||||||
|
if (data && data.cache) {
|
||||||
|
this.cache = data.cache;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSettings() {
|
||||||
|
// Save both settings and cache in the same data object
|
||||||
|
const dataToSave = {
|
||||||
|
...this.settings,
|
||||||
|
cache: this.cache
|
||||||
|
};
|
||||||
|
await this.saveData(dataToSave);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process markdown content to find and enhance GitHub links
|
||||||
|
*/
|
||||||
|
async processMarkdown(el: HTMLElement, ctx: MarkdownPostProcessorContext) {
|
||||||
|
// Find all links in the document
|
||||||
|
const links = el.querySelectorAll('a');
|
||||||
|
|
||||||
|
for (let i = 0; i < links.length; i++) {
|
||||||
|
const link = links[i];
|
||||||
|
const url = link.getAttribute('href');
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the link is a GitHub repository URL
|
||||||
|
const repoInfo = this.extractRepoInfo(url);
|
||||||
|
if (repoInfo) {
|
||||||
|
// Create a span to hold the star count with loading indicator
|
||||||
|
const starSpan = document.createElement('span');
|
||||||
|
starSpan.addClass('github-stars-count');
|
||||||
|
starSpan.addClass('github-stars-loading');
|
||||||
|
starSpan.setText('⭐ ...');
|
||||||
|
|
||||||
|
// Insert the star count after the link
|
||||||
|
link.after(starSpan);
|
||||||
|
|
||||||
|
// Get star count and update the span using async/await
|
||||||
|
try {
|
||||||
|
const stars = await this.getStarCount(repoInfo.owner, repoInfo.repo);
|
||||||
|
starSpan.removeClass('github-stars-loading');
|
||||||
|
|
||||||
|
if (stars !== null) {
|
||||||
|
const formattedStars = this.formatStarCount(stars);
|
||||||
|
starSpan.setText(formattedStars);
|
||||||
|
} else {
|
||||||
|
starSpan.setText('⭐ ?');
|
||||||
|
starSpan.addClass('github-stars-error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error getting star count for ${repoInfo.owner}/${repoInfo.repo}:`, error);
|
||||||
|
starSpan.removeClass('github-stars-loading');
|
||||||
|
starSpan.setText('⭐ ?');
|
||||||
|
starSpan.addClass('github-stars-error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract repository owner and name from a GitHub URL
|
||||||
|
* Returns null if the URL is not a valid GitHub repository URL
|
||||||
|
*/
|
||||||
|
extractRepoInfo(url: string): { owner: string, repo: string } | null {
|
||||||
|
// Match GitHub repository URLs
|
||||||
|
// Examples:
|
||||||
|
// - https://github.com/owner/repo
|
||||||
|
// - https://github.com/owner/repo/
|
||||||
|
// - https://github.com/owner/repo/tree/master
|
||||||
|
// - https://github.com/owner/repo/blob/master/README.md
|
||||||
|
// - https://www.github.com/owner/repo
|
||||||
|
// - http://github.com/owner/repo
|
||||||
|
|
||||||
|
// First, normalize the URL
|
||||||
|
const normalizedUrl = url.trim().toLowerCase();
|
||||||
|
|
||||||
|
// Check if it's a GitHub URL
|
||||||
|
if (!normalizedUrl.includes('github.com')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract owner and repo using regex
|
||||||
|
const githubRegex = /https?:\/\/(www\.)?github\.com\/([^/\s]+)\/([^/\s#?]+)(\/.*)?$/;
|
||||||
|
const match = url.match(githubRegex);
|
||||||
|
|
||||||
|
if (match && match[2] && match[3]) {
|
||||||
|
const owner = match[2];
|
||||||
|
let repo = match[3];
|
||||||
|
|
||||||
|
// Remove .git suffix if present
|
||||||
|
if (repo.endsWith('.git')) {
|
||||||
|
repo = repo.slice(0, -4);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { owner, repo };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get star count for a GitHub repository
|
||||||
|
* Uses cache if available and not expired
|
||||||
|
*/
|
||||||
|
async getStarCount(owner: string, repo: string): Promise<number | null> {
|
||||||
|
const cacheKey = `${owner}/${repo}`;
|
||||||
|
|
||||||
|
// Check if we have a valid cache entry
|
||||||
|
if (this.cache[cacheKey]) {
|
||||||
|
const entry = this.cache[cacheKey];
|
||||||
|
const now = Date.now();
|
||||||
|
const expiryTime = this.settings.cacheExpiry * 60 * 1000; // Convert minutes to milliseconds
|
||||||
|
|
||||||
|
// If cache entry is still valid, use it
|
||||||
|
if (now - entry.timestamp < expiryTime) {
|
||||||
|
return entry.stars;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch star count from GitHub API
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Accept': 'application/vnd.github.v3+json',
|
||||||
|
'User-Agent': 'Obsidian-GitHub-Stars-Plugin'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add API token if available
|
||||||
|
if (this.settings.apiToken && this.settings.apiToken.trim() !== '') {
|
||||||
|
headers['Authorization'] = `token ${this.settings.apiToken.trim()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiUrl = `https://api.github.com/repos/${owner}/${repo}`;
|
||||||
|
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
|
headers,
|
||||||
|
method: 'GET'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle rate limiting
|
||||||
|
const rateLimitRemaining = response.headers.get('X-RateLimit-Remaining');
|
||||||
|
const rateLimitReset = response.headers.get('X-RateLimit-Reset');
|
||||||
|
|
||||||
|
if (rateLimitRemaining === '0' && rateLimitReset) {
|
||||||
|
const resetTime = new Date(parseInt(rateLimitReset) * 1000);
|
||||||
|
const now = new Date();
|
||||||
|
const minutesUntilReset = Math.ceil((resetTime.getTime() - now.getTime()) / (60 * 1000));
|
||||||
|
|
||||||
|
console.warn(`GitHub API rate limit exceeded. Resets in ${minutesUntilReset} minutes.`);
|
||||||
|
|
||||||
|
// If we have a cached value, use it even if expired
|
||||||
|
if (this.cache[cacheKey]) {
|
||||||
|
return this.cache[cacheKey].stars;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 404) {
|
||||||
|
console.error(`Repository ${owner}/${repo} not found`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Failed to fetch star count for ${owner}/${repo}: ${response.status} ${response.statusText}`);
|
||||||
|
|
||||||
|
// If we have a cached value, use it even if expired
|
||||||
|
if (this.cache[cacheKey]) {
|
||||||
|
return this.cache[cacheKey].stars;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data || typeof data.stargazers_count !== 'number') {
|
||||||
|
console.error(`Invalid response data for ${owner}/${repo}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stars = data.stargazers_count;
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
this.cache[cacheKey] = {
|
||||||
|
stars,
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save cache to disk
|
||||||
|
this.saveSettings();
|
||||||
|
|
||||||
|
return stars;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching star count for ${owner}/${repo}:`, error);
|
||||||
|
|
||||||
|
// If we have a cached value, use it even if expired
|
||||||
|
if (this.cache[cacheKey]) {
|
||||||
|
return this.cache[cacheKey].stars;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format star count according to settings
|
||||||
|
*/
|
||||||
|
formatStarCount(stars: number): string {
|
||||||
|
const formatted = this.settings.displayFormat.replace('{stars}', this.formatNumber(stars));
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format number with thousands separators or abbreviate for large numbers
|
||||||
|
*/
|
||||||
|
formatNumber(num: number): string {
|
||||||
|
// Use full format if configured
|
||||||
|
if (this.settings.numberFormat === 'full') {
|
||||||
|
return num.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise use abbreviated format
|
||||||
|
// For numbers less than 1000, just use locale string
|
||||||
|
if (num < 1000) {
|
||||||
|
return num.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// For numbers 1000 or greater, use abbreviations
|
||||||
|
if (num < 10000) {
|
||||||
|
// 1.2k format for 1000-9999
|
||||||
|
return (num / 1000).toFixed(1) + 'k';
|
||||||
|
} else if (num < 1000000) {
|
||||||
|
// 12k format for 10000-999999
|
||||||
|
return Math.round(num / 1000) + 'k';
|
||||||
|
} else {
|
||||||
|
// 1.2M format for 1000000+
|
||||||
|
return (num / 1000000).toFixed(1) + 'M';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GitHubStarsSettingTab extends PluginSettingTab {
|
||||||
|
plugin: GitHubStarsPlugin;
|
||||||
|
|
||||||
|
constructor(app: App, plugin: GitHubStarsPlugin) {
|
||||||
|
super(app, plugin);
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
display(): void {
|
||||||
|
const { containerEl } = this;
|
||||||
|
|
||||||
|
containerEl.empty();
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Cache expiry')
|
||||||
|
.setDesc('Time in minutes before the GitHub star count cache expires')
|
||||||
|
.addText(text => text
|
||||||
|
.setPlaceholder('60')
|
||||||
|
.setValue(this.plugin.settings.cacheExpiry.toString())
|
||||||
|
.onChange(async (value) => {
|
||||||
|
const numValue = parseInt(value);
|
||||||
|
if (!isNaN(numValue) && numValue > 0) {
|
||||||
|
this.plugin.settings.cacheExpiry = numValue;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Display format')
|
||||||
|
.setDesc('Format for displaying star counts. Use {stars} as a placeholder for the number.')
|
||||||
|
.addText(text => text
|
||||||
|
.setPlaceholder('⭐ {stars}')
|
||||||
|
.setValue(this.plugin.settings.displayFormat)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
if (value.includes('{stars}')) {
|
||||||
|
this.plugin.settings.displayFormat = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
} else {
|
||||||
|
new Notice('Display format must include {stars} placeholder');
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Number format')
|
||||||
|
.setDesc('How to format star counts')
|
||||||
|
.addDropdown(dropdown => dropdown
|
||||||
|
.addOption('full', 'Full numbers (e.g., 1,234)')
|
||||||
|
.addOption('abbreviated', 'Abbreviated (e.g., 1.2k)')
|
||||||
|
.setValue(this.plugin.settings.numberFormat)
|
||||||
|
.onChange(async (value: 'full' | 'abbreviated') => {
|
||||||
|
this.plugin.settings.numberFormat = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
}));
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('GitHub API token (optional)')
|
||||||
|
.setDesc('Personal access token for GitHub API to increase rate limits')
|
||||||
|
.addText(text => text
|
||||||
|
.setPlaceholder('ghp_xxxxxxxxxxxxxxxxxxxx')
|
||||||
|
.setValue(this.plugin.settings.apiToken)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.apiToken = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Add a button to clear the cache
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Clear cache')
|
||||||
|
.setDesc('Clear the GitHub stars cache')
|
||||||
|
.addButton(button => button
|
||||||
|
.setButtonText('Clear')
|
||||||
|
.onClick(async () => {
|
||||||
|
this.plugin.cache = {};
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
new Notice('GitHub Stars cache cleared');
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
10
manifest.json
Normal file
10
manifest.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"id": "obsidian-github-stars",
|
||||||
|
"name": "GitHub Stars",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"minAppVersion": "1.8.9",
|
||||||
|
"description": "Displays the number of stars for GitHub repositories mentioned in notes.",
|
||||||
|
"author": "Flying Nobita",
|
||||||
|
"authorUrl": "https://github.com/flyingnobita",
|
||||||
|
"isDesktopOnly": false
|
||||||
|
}
|
||||||
BIN
obsidian-github-stars-screenshot.png
Normal file
BIN
obsidian-github-stars-screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
2397
package-lock.json
generated
Normal file
2397
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
29
package.json
Normal file
29
package.json
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"name": "obsidian-github-stars",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Obsidian plugin that displays the number of stars for GitHub repositories mentioned in notes",
|
||||||
|
"main": "main.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "node esbuild.config.mjs",
|
||||||
|
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||||
|
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"obsidian",
|
||||||
|
"plugin",
|
||||||
|
"github",
|
||||||
|
"stars"
|
||||||
|
],
|
||||||
|
"author": "Your Name",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^16.11.6",
|
||||||
|
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||||
|
"@typescript-eslint/parser": "5.29.0",
|
||||||
|
"builtin-modules": "3.3.0",
|
||||||
|
"esbuild": "0.17.3",
|
||||||
|
"obsidian": "latest",
|
||||||
|
"tslib": "2.4.0",
|
||||||
|
"typescript": "4.7.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
48
styles.css
Normal file
48
styles.css
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
|
||||||
|
This CSS file will be included with your plugin, and
|
||||||
|
available in the app when your plugin is enabled.
|
||||||
|
|
||||||
|
If your plugin does not need CSS, delete this file.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* GitHub Stars Plugin Styles */
|
||||||
|
|
||||||
|
/* Style for the stars count span */
|
||||||
|
.github-stars-count {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 5px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
background-color: var(--background-secondary);
|
||||||
|
color: var(--text-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading state */
|
||||||
|
.github-stars-loading {
|
||||||
|
animation: github-stars-pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes github-stars-pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error state */
|
||||||
|
.github-stars-error {
|
||||||
|
color: var(--text-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover effect */
|
||||||
|
a:hover + .github-stars-count {
|
||||||
|
background-color: var(--background-modifier-hover);
|
||||||
|
}
|
||||||
24
tsconfig.json
Normal file
24
tsconfig.json
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"inlineSourceMap": true,
|
||||||
|
"inlineSources": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"target": "ES6",
|
||||||
|
"allowJs": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"importHelpers": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"lib": [
|
||||||
|
"DOM",
|
||||||
|
"ES5",
|
||||||
|
"ES6",
|
||||||
|
"ES7"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
14
version-bump.mjs
Normal file
14
version-bump.mjs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { readFileSync, writeFileSync } from "fs";
|
||||||
|
|
||||||
|
const targetVersion = process.env.npm_package_version;
|
||||||
|
|
||||||
|
// read minAppVersion from manifest.json and bump version to target version
|
||||||
|
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
|
||||||
|
const { minAppVersion } = manifest;
|
||||||
|
manifest.version = targetVersion;
|
||||||
|
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
|
||||||
|
|
||||||
|
// update versions.json with target version and minAppVersion from manifest.json
|
||||||
|
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
|
||||||
|
versions[targetVersion] = minAppVersion;
|
||||||
|
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
|
||||||
3
versions.json
Normal file
3
versions.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"1.0.0": "0.15.0"
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue