Files
mars3142_collection/plugin/actions/GiteaPRAction.js
T
2026-04-10 17:11:20 +02:00

176 lines
6.3 KiB
JavaScript

class GiteaPRAction extends ActionBase {
constructor($UD, context) {
super($UD, context);
this.config = {
url: '',
token: '',
filter: 'review_requested',
refreshRate: '4',
};
this.firstSeen = {};
this.settingsLoaded = false;
this.$UD.onDidReceiveSettings(jsn => {
if (jsn.context !== this.context) return;
const s = jsn.settings || {};
if (s.firstSeen) {
// Persisted timestamps win over in-memory ones set during race window
this.firstSeen = Object.assign({}, this.firstSeen, s.firstSeen);
}
this.settingsLoaded = true;
});
}
setActive() {}
setParams(jsn) {
this.config = Object.assign(this.config, (jsn && jsn.param) || {});
this.$UD.getSettings(this.context);
this.startTimer(() => this.fetchPRs(), this.config.refreshRate);
}
onRun() {
if (this.config.url) {
this.$UD.openUrl(`${this.config.url}/pulls?type=your_repositories&sort=&state=open&q=`);
}
}
fetchPRs() {
this.debounce(async () => {
if (!this.config.url || !this.config.token) {
this.renderButton(null, false);
return;
}
try {
const headers = { Authorization: `token ${this.config.token}` };
const filter = this.config.filter || 'review_requested';
const base = `${this.config.url}/api/v1/repos/issues/search?type=pullrequest&state=open&limit=50`;
const fetchPRs = async (url) => {
const r = await fetch(url, { headers, signal: AbortSignal.timeout(8000) });
console.log('GiteaPR status:', r.status, url);
if (!r.ok) {
const body = await r.text();
console.log('GiteaPR error body:', body);
return null;
}
const data = await r.json();
return Array.isArray(data) ? data : [];
};
let prs;
if (filter === 'both') {
const [a, b] = await Promise.all([
fetchPRs(`${base}&assigned=true`),
fetchPRs(`${base}&review_requested=true`),
]);
if (a === null || b === null) { this.renderButton(null, true); return; }
const seen = new Set();
prs = [...a, ...b].filter(pr => seen.has(pr.id) ? false : seen.add(pr.id));
} else {
const param = filter === 'assigned' ? '&assigned=true' : '&review_requested=true';
prs = await fetchPRs(`${base}${param}`);
if (prs === null) { this.renderButton(null, true); return; }
}
const wip = prs.filter(pr => pr.title?.startsWith('WIP: ')).length;
const now = Date.now();
// Update first-seen map: add new, remove gone
const currentIds = new Set(prs.map(pr => String(pr.id)));
for (const pr of prs) {
if (!(String(pr.id) in this.firstSeen)) this.firstSeen[String(pr.id)] = now;
}
for (const id of Object.keys(this.firstSeen)) {
if (!currentIds.has(id)) delete this.firstSeen[id];
}
if (this.settingsLoaded) {
this.$UD.setSettings({ ...this.config, firstSeen: this.firstSeen }, this.context);
}
const overdue = prs.filter(pr =>
!pr.title?.startsWith('WIP: ') &&
(now - (this.firstSeen[String(pr.id)] ?? now)) > 24 * 60 * 60 * 1000
).length;
this.renderButton(prs.length, false, wip, overdue);
} catch (e) {
// timeout or network error = VPN likely down
this.renderButton(null, false);
}
});
}
renderButton(count, isError, wip = 0, overdue = 0) {
const { canvas, ctx } = this.createCanvas();
// Label — top
ctx.fillStyle = '#7ec8e3';
ctx.font = 'bold 22px "Source Han Sans SC"';
ctx.fillText('Pull Requests', 98, 30);
if (count === null) {
ctx.fillStyle = isError ? '#ff6b6b' : '#888888';
ctx.font = '70px "Source Han Sans SC"';
ctx.textBaseline = 'alphabetic';
const bm = ctx.measureText('⚡');
ctx.fillText('⚡', 98, 98 + (bm.actualBoundingBoxAscent - bm.actualBoundingBoxDescent) / 2);
ctx.textBaseline = 'middle';
ctx.fillStyle = isError ? '#ff6b6b' : '#888888';
ctx.font = 'bold 22px "Source Han Sans SC"';
ctx.fillText(isError ? 'API Error' : 'Offline', 98, 166);
} else {
const filter = this.config.filter || 'review_requested';
const label = filter === 'assigned' ? 'assigned' : filter === 'review_requested' ? 'review req.' : 'open';
const color = count === 0 ? '#6bff6b' : count < 5 ? '#f0c040' : '#ff6b6b';
const text = count > 999 ? '999+' : String(count);
const drawCount = (centerY, fSizes) => {
ctx.fillStyle = color;
const fSize = count > 99 ? fSizes[0] : count > 9 ? fSizes[1] : fSizes[2];
ctx.font = `bold ${fSize}px "Source Han Sans SC"`;
ctx.textBaseline = 'alphabetic';
const m = ctx.measureText(text);
ctx.fillText(text, 98, centerY + (m.actualBoundingBoxAscent - m.actualBoundingBoxDescent) / 2);
ctx.textBaseline = 'middle';
};
if (wip > 0 && overdue > 0) {
// Most compact: 4 items
drawCount(75, [44, 56, 68]);
ctx.fillStyle = '#555555';
ctx.font = '15px "Source Han Sans SC"';
ctx.fillText(label, 98, 120);
ctx.fillStyle = '#ff8c00';
ctx.font = 'bold 26px "Source Han Sans SC"';
ctx.fillText(`WIP: ${wip}`, 98, 147);
ctx.fillStyle = '#ff3333';
ctx.font = 'bold 24px "Source Han Sans SC"';
ctx.fillText(`>24h: ${overdue}`, 98, 175);
} else if (wip > 0) {
drawCount(85, [48, 64, 76]);
ctx.fillStyle = '#666666';
ctx.font = '16px "Source Han Sans SC"';
ctx.fillText(label, 98, 140);
ctx.fillStyle = '#ff8c00';
ctx.font = 'bold 30px "Source Han Sans SC"';
ctx.fillText(`WIP: ${wip}`, 98, 168);
} else if (overdue > 0) {
drawCount(88, [52, 68, 80]);
ctx.fillStyle = '#666666';
ctx.font = '16px "Source Han Sans SC"';
ctx.fillText(label, 98, 142);
ctx.fillStyle = '#ff3333';
ctx.font = 'bold 28px "Source Han Sans SC"';
ctx.fillText(`>24h: ${overdue}`, 98, 170);
} else {
drawCount(98, [60, 80, 96]);
ctx.fillStyle = '#888888';
ctx.font = '18px "Source Han Sans SC"';
ctx.fillText(label, 98, 166);
}
}
this.setIcon(canvas);
}
}