Skip to content

Commit c137d55

Browse files
github-contribution-grid@KopfdesDaemons: Initial release (#1586)
1 parent 405d434 commit c137d55

File tree

10 files changed

+565
-0
lines changed

10 files changed

+565
-0
lines changed
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
const Desklet = imports.ui.desklet;
2+
const GLib = imports.gi.GLib;
3+
const Mainloop = imports.mainloop;
4+
const St = imports.gi.St;
5+
const Gettext = imports.gettext;
6+
const Util = imports.misc.util;
7+
const Settings = imports.ui.settings;
8+
const Tooltips = imports.ui.tooltips;
9+
10+
const UUID = "github-contribution-grid@KopfdesDaemons";
11+
12+
const { GitHubHelper } = require("./helpers/github.helper");
13+
14+
Gettext.bindtextdomain(UUID, GLib.get_home_dir() + "/.local/share/locale");
15+
16+
function _(str) {
17+
return Gettext.dgettext(UUID, str);
18+
}
19+
20+
class MyDesklet extends Desklet.Desklet {
21+
constructor(metadata, deskletId) {
22+
super(metadata, deskletId);
23+
this.githubUsername = "";
24+
this.githubToken = "";
25+
this.blockSize = 11;
26+
this.refreshInterval = 15;
27+
this.backgroundColor = "rgba(255, 255, 255, 0)";
28+
this.showContributionCount = false;
29+
this.showUsername = true;
30+
this.timeoutId = null;
31+
this.contributionData = null;
32+
this.mainContainer = null;
33+
34+
this.bindSettings(metadata, deskletId);
35+
36+
this.setHeader(_("GitHub Contribution Grid"));
37+
38+
this.mainContainer = new St.BoxLayout({ vertical: true, style_class: "github-contribution-grid-main-container" });
39+
this.mainContainer.add_child(this._createHeader());
40+
this.setContent(this.mainContainer);
41+
42+
this._updateLoop();
43+
44+
// The first request after system start will fail
45+
// Delay to ensure network services are ready and try again
46+
Mainloop.timeout_add_seconds(10, () => {
47+
this._setupContributionData();
48+
});
49+
}
50+
51+
bindSettings(metadata, deskletId) {
52+
this.settings = new Settings.DeskletSettings(this, metadata["uuid"], deskletId);
53+
this.settings.bindProperty(Settings.BindingDirection.IN, "github-username", "githubUsername", this.onDataSettingChanged);
54+
this.settings.bindProperty(Settings.BindingDirection.IN, "github-token", "githubToken", this.onDataSettingChanged);
55+
this.settings.bindProperty(Settings.BindingDirection.IN, "refresh-interval", "refreshInterval", this.onDataSettingChanged);
56+
this.settings.bindProperty(Settings.BindingDirection.IN, "block-size", "blockSize", this.onStyleSettingChanged);
57+
this.settings.bindProperty(Settings.BindingDirection.IN, "background-color", "backgroundColor", this.onStyleSettingChanged);
58+
this.settings.bindProperty(Settings.BindingDirection.IN, "show-username", "showUsername", this.onStyleSettingChanged);
59+
this.settings.bindProperty(Settings.BindingDirection.IN, "show-contribution-count", "showContributionCount", this.onStyleSettingChanged);
60+
}
61+
62+
on_desklet_removed() {
63+
if (this.timeoutId) {
64+
Mainloop.source_remove(this.timeoutId);
65+
this.timeoutId = null;
66+
}
67+
}
68+
69+
onDataSettingChanged = () => {
70+
this.mainContainer.remove_all_children();
71+
this.mainContainer.add_child(this._createHeader());
72+
this._setupContributionData();
73+
};
74+
75+
onStyleSettingChanged = () => {
76+
this.mainContainer.remove_all_children();
77+
this.mainContainer.add_child(this._createHeader());
78+
this._renderContent(this.contributionData);
79+
};
80+
81+
async _setupContributionData() {
82+
this.contributionData = null;
83+
84+
if (!this.githubUsername || !this.githubToken) {
85+
this._renderContent(null, _("Please configure username and token in settings."));
86+
return;
87+
}
88+
89+
try {
90+
const response = await GitHubHelper.getContributionData(this.githubUsername, this.githubToken);
91+
this.contributionData = response;
92+
this._renderContent(this.contributionData);
93+
} catch (e) {
94+
global.logError(`[${UUID}] Error fetching contribution data: ${e}`);
95+
this._renderContent(null, e.message);
96+
}
97+
}
98+
99+
_createHeader() {
100+
const headerContainer = new St.BoxLayout({ style_class: "github-contribution-grid-header-container" });
101+
102+
// Reload button
103+
const reloadButton = new St.Button({ style_class: "github-contribution-grid-reload-bin" });
104+
reloadButton.connect("button-press-event", () => this._setupContributionData());
105+
const reloadIcon = new St.Icon({
106+
icon_name: "view-refresh-symbolic",
107+
icon_type: St.IconType.SYMBOLIC,
108+
icon_size: 16,
109+
});
110+
new Tooltips.Tooltip(reloadButton, _("Reload"));
111+
reloadButton.set_child(reloadIcon);
112+
headerContainer.add_child(reloadButton);
113+
114+
// Username
115+
if (this.showUsername) {
116+
const usernameButton = new St.Button({ label: this.githubUsername, style_class: "github-contribution-grid-label-bin" });
117+
usernameButton.connect("button-press-event", () => Util.spawnCommandLine(`xdg-open "https://github.com/${this.githubUsername}"`));
118+
new Tooltips.Tooltip(usernameButton, _("Open GitHub profile"));
119+
headerContainer.add_child(usernameButton);
120+
}
121+
122+
return headerContainer;
123+
}
124+
125+
_renderContent(weeks, error = null) {
126+
if (this.contentContainer) {
127+
this.mainContainer.remove_child(this.contentContainer);
128+
this.contentContainer.destroy();
129+
}
130+
131+
this.contentContainer = new St.BoxLayout({
132+
style_class: "github-contribution-grid-container",
133+
x_expand: true,
134+
style: `background-color: ${this.backgroundColor};`,
135+
});
136+
137+
if (!this.githubUsername || !this.githubToken) {
138+
// UI for Desklet Setup
139+
const setupBox = new St.BoxLayout({ vertical: true, style_class: "github-contribution-grid-setup-container" });
140+
setupBox.add_child(new St.Label({ text: "GitHub Contribution Grid", style_class: "github-contribution-grid-setup-headline" }));
141+
setupBox.add_child(new St.Label({ text: _("Please configure username and token in settings.") }));
142+
143+
const createTokenButton = new St.Button({ style_class: "github-contribution-grid-link", label: _("Create a GitHub token") });
144+
createTokenButton.connect("clicked", () => Util.spawnCommandLine(`xdg-open "${GitHubHelper.gitHubTokenCrationURL}"`));
145+
setupBox.add_child(createTokenButton);
146+
147+
this.contentContainer.add_child(setupBox);
148+
} else if (error) {
149+
// Error UI
150+
const errorBox = new St.BoxLayout({ vertical: true, style_class: "github-contribution-grid-error-container" });
151+
errorBox.add_child(new St.Label({ text: "GitHub Contribution Grid", style_class: "github-contribution-grid-error-headline" }));
152+
errorBox.add_child(new St.Label({ text: _("Error:") }));
153+
errorBox.add_child(new St.Label({ text: error, style_class: "github-contribution-grid-error-message" }));
154+
const reloadButton = new St.Button({ style_class: "github-contribution-grid-error-reload-button", label: _("Reload") });
155+
reloadButton.connect("clicked", () => this._setupContributionData());
156+
errorBox.add_child(reloadButton);
157+
this.contentContainer.add_child(errorBox);
158+
} else if (weeks) {
159+
// Render GitHub Grid
160+
const gridBox = new St.BoxLayout({ style_class: "github-contribution-grid-grid-box" });
161+
162+
for (const week of weeks) {
163+
const weekBox = new St.BoxLayout({ vertical: true, style_class: "week-container" });
164+
165+
for (const day of week.contributionDays) {
166+
const dayBin = new St.Bin({
167+
style_class: "day-bin",
168+
reactive: true,
169+
track_hover: true,
170+
style: `font-size: ${this.blockSize}px; background-color: ${GitHubHelper.getContributionColor(day.contributionCount)};`,
171+
});
172+
173+
new Tooltips.Tooltip(dayBin, `${day.date} ${day.contributionCount} ` + _("contributions"));
174+
175+
if (this.showContributionCount) {
176+
const countLabel = new St.Label({ text: day.contributionCount.toString() });
177+
dayBin.set_child(countLabel);
178+
}
179+
weekBox.add_child(dayBin);
180+
}
181+
gridBox.add_child(weekBox);
182+
}
183+
this.contentContainer.add_child(gridBox);
184+
}
185+
186+
this.mainContainer.add_child(this.contentContainer);
187+
}
188+
189+
_updateLoop() {
190+
this._setupContributionData().finally(() => {
191+
this.timeoutId = Mainloop.timeout_add_seconds(this.refreshInterval * 60, () => {
192+
this._updateLoop();
193+
});
194+
});
195+
}
196+
}
197+
198+
function main(metadata, deskletId) {
199+
return new MyDesklet(metadata, deskletId);
200+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
const Soup = imports.gi.Soup;
2+
const ByteArray = imports.byteArray;
3+
const GLib = imports.gi.GLib;
4+
5+
const _httpSession = new Soup.Session();
6+
7+
class GitHubHelper {
8+
static gitHubTokenCrationURL = "https://github.com/settings/tokens/new?description=Cinnamon%20Desklet";
9+
10+
static async getContributionData(username, token) {
11+
const query = `
12+
query {
13+
user(login: "${username}") {
14+
contributionsCollection {
15+
contributionCalendar {
16+
weeks {
17+
contributionDays {
18+
date
19+
contributionCount
20+
}
21+
}
22+
}
23+
}
24+
}
25+
}
26+
`;
27+
28+
const response = await GitHubHelper._makeGitHubGraphQLRequest(query, token);
29+
30+
if (response && response.data && response.data.user && response.data.user.contributionsCollection) {
31+
return response.data.user.contributionsCollection.contributionCalendar.weeks;
32+
}
33+
throw new Error("Could not retrieve contribution data.");
34+
}
35+
36+
static _makeGitHubGraphQLRequest(query, token) {
37+
return new Promise((resolve, reject) => {
38+
const url = "https://api.github.com/graphql";
39+
const body = JSON.stringify({ query });
40+
41+
const message = Soup.Message.new("POST", url);
42+
if (!message) {
43+
return reject(new Error("Failed to create new Soup.Message"));
44+
}
45+
46+
message.request_headers.append("Authorization", `bearer ${token}`);
47+
message.request_headers.append("User-Agent", "cinnamon-github-contribution-grid-desklet");
48+
message.request_headers.set_content_type("application/json", null);
49+
message.set_request_body_from_bytes("application/json", new GLib.Bytes(body));
50+
51+
_httpSession.send_and_read_async(message, Soup.MessagePriority.NORMAL, null, (session, result) => {
52+
if (message.get_status() === 200) {
53+
const bytes = _httpSession.send_and_read_finish(result);
54+
const responseJson = JSON.parse(ByteArray.toString(bytes.get_data()));
55+
responseJson.errors ? reject(new Error(responseJson.errors.map(e => e.message).join(", "))) : resolve(responseJson);
56+
} else {
57+
reject(new Error(`Failed to fetch data: ${message.get_status()} ${message.get_reason_phrase()}`));
58+
}
59+
});
60+
});
61+
}
62+
63+
static getContributionColor(count) {
64+
if (count >= 10) return "#56d364";
65+
if (count >= 9) return "#2ea043";
66+
if (count >= 6) return "#196c2e";
67+
if (count >= 4) return "#196c2e";
68+
if (count > 0) return "#033a16";
69+
if (count === 0) return "#151b23";
70+
}
71+
}
5.89 KB
Loading
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"uuid": "github-contribution-grid@KopfdesDaemons",
3+
"name": "GitHub Contribution Grid",
4+
"description": "Displays the daily contributions heatmap for a specified GitHub user profile.",
5+
"version": "1.0",
6+
"max-instances": "50"
7+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# GITHUB CONTRIBUTION GRID
2+
# This file is put in the public domain.
3+
# KopfdesDaemons, 2025
4+
#
5+
#, fuzzy
6+
msgid ""
7+
msgstr ""
8+
"Project-Id-Version: github-contribution-grid@KopfdesDaemons 1.0\n"
9+
"Report-Msgid-Bugs-To: https://github.com/linuxmint/cinnamon-spices-desklets/"
10+
"issues\n"
11+
"POT-Creation-Date: 2025-10-13 22:42+0200\n"
12+
"PO-Revision-Date: \n"
13+
"Last-Translator: \n"
14+
"Language-Team: \n"
15+
"Language: de\n"
16+
"MIME-Version: 1.0\n"
17+
"Content-Type: text/plain; charset=UTF-8\n"
18+
"Content-Transfer-Encoding: 8bit\n"
19+
"X-Generator: Poedit 3.4.2\n"
20+
21+
#. metadata.json->name
22+
#. desklet.js:36
23+
msgid "GitHub Contribution Grid"
24+
msgstr "GitHub Contribution Grid"
25+
26+
#. desklet.js:85 desklet.js:141
27+
msgid "Please configure username and token in settings."
28+
msgstr "Bitte konfiguriere Benutzername und Token in den Einstellungen."
29+
30+
#. desklet.js:110 desklet.js:154
31+
msgid "Reload"
32+
msgstr "Neuladen"
33+
34+
#. desklet.js:118
35+
msgid "Open GitHub profile"
36+
msgstr "GitHub Profil öffnen"
37+
38+
#. desklet.js:143
39+
msgid "Create a GitHub token"
40+
msgstr "Einen GitHub Token erstellen"
41+
42+
#. desklet.js:152
43+
msgid "Error:"
44+
msgstr "Error:"
45+
46+
#. desklet.js:173
47+
msgid "contributions"
48+
msgstr "Beiträge"
49+
50+
#. metadata.json->description
51+
msgid ""
52+
"Displays the daily contributions heatmap for a specified GitHub user profile."
53+
msgstr ""
54+
"Zeigt die Heatmap der täglichen Beiträge für ein angegebenes GitHub-"
55+
"Benutzerprofil an."
56+
57+
#. settings-schema.json->head0->description
58+
msgid "API Settings"
59+
msgstr "API-Einstellungen"
60+
61+
#. settings-schema.json->github-username->description
62+
msgid "GitHub Username"
63+
msgstr "GitHub Benutzername"
64+
65+
#. settings-schema.json->github-token->description
66+
msgid "GitHub Token"
67+
msgstr "GitHub Token"
68+
69+
#. settings-schema.json->refresh-interval->description
70+
msgid "Refresh interval in minutes"
71+
msgstr "Aktualisierungsintervall in Minuten"
72+
73+
#. settings-schema.json->head1->description
74+
msgid "Style"
75+
msgstr "Stil"
76+
77+
#. settings-schema.json->block-size->description
78+
msgid "Block size in pixels"
79+
msgstr "Blockgröße in Pixel"
80+
81+
#. settings-schema.json->background-color->description
82+
msgid "Background color"
83+
msgstr "Hintergrundfarbe"
84+
85+
#. settings-schema.json->show-contribution-count->description
86+
msgid "Show contribution count"
87+
msgstr "Beitragsanzahl anzeigen"
88+
89+
#. settings-schema.json->show-username->description
90+
msgid "Show username"
91+
msgstr "Benutzername anzeigen"

0 commit comments

Comments
 (0)