a8163c1139
Signed-off-by: Peter Siegmund <developer@mars3142.org>
257 lines
9.6 KiB
JavaScript
257 lines
9.6 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.username = null;
|
|
|
|
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) return;
|
|
const filter = this.config.filter || 'review_requested';
|
|
const filterMap = {
|
|
your_repos: 'type=your_repositories',
|
|
assigned: 'type=your_repositories&assignee=0',
|
|
created: 'type=your_repositories&poster=0',
|
|
review_requested: 'type=your_repositories&review_requested=true',
|
|
reviewed: 'type=your_repositories&reviewed=true',
|
|
mentioned: 'type=your_repositories&mentioned=0',
|
|
};
|
|
const q = filterMap[filter] || filterMap.your_repos;
|
|
this.$UD.openUrl(`${this.config.url}/pulls?${q}&state=open`);
|
|
}
|
|
|
|
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 fetchPages = async (url, paramChar = '&') => {
|
|
const all = [];
|
|
let page = 1;
|
|
while (true) {
|
|
const r = await fetch(`${url}${paramChar}page=${page}`, { headers, signal: AbortSignal.timeout(8000) });
|
|
if (r.status === 404 || r.status === 403) return [];
|
|
if (!r.ok) {
|
|
console.log('GiteaPR error:', r.status, url);
|
|
return null;
|
|
}
|
|
const data = await r.json();
|
|
if (!Array.isArray(data) || data.length === 0) break;
|
|
all.push(...data);
|
|
if (data.length < 50) break;
|
|
page++;
|
|
}
|
|
return all;
|
|
};
|
|
|
|
const paramMap = {
|
|
your_repos: '', all: '', both: '',
|
|
assigned: '&assigned=true',
|
|
created: '&created=true',
|
|
review_requested: '&review_requested=true',
|
|
reviewed: '&reviewed=true',
|
|
mentioned: '&mentioned=true',
|
|
};
|
|
const raw = await fetchPages(`${base}${paramMap[filter] ?? ''}`);
|
|
if (raw === null) { this.renderButton(null, true); return; }
|
|
const prs = raw.filter(pr => pr.pull_request != null);
|
|
|
|
const prDetails = await Promise.all(
|
|
prs.map(pr => {
|
|
const [owner, repo] = (pr.repository?.full_name || '').split('/');
|
|
if (!owner || !repo) return Promise.resolve(null);
|
|
return fetch(`${this.config.url}/api/v1/repos/${owner}/${repo}/pulls/${pr.number}`,
|
|
{ headers, signal: AbortSignal.timeout(5000) })
|
|
.then(r => r.ok ? r.json() : null)
|
|
.catch(() => null);
|
|
})
|
|
);
|
|
|
|
const wip = prs.filter(pr => pr.title?.startsWith('WIP: ')).length;
|
|
const mergeConflict = prs.filter((pr, i) =>
|
|
!pr.title?.startsWith('WIP: ') && prDetails[i]?.mergeable === false
|
|
).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, mergeConflict, overdue);
|
|
} catch (e) {
|
|
// timeout or network error = VPN likely down
|
|
this.renderButton(null, false);
|
|
}
|
|
});
|
|
}
|
|
|
|
renderButton(count, isError, wip = 0, mergeConflict = 0, overdue = 0) {
|
|
const { canvas, ctx } = this.createCanvas();
|
|
|
|
// Label — top
|
|
ctx.fillStyle = '#7ec8e3';
|
|
ctx.font = 'bold 22px "Source Han Sans SC"';
|
|
ctx.fillText(this.$UD.t('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 ? this.$UD.t('API Error') : this.$UD.t('Offline'), 98, 166);
|
|
} else {
|
|
const filter = this.config.filter || 'review_requested';
|
|
const labelMap = {
|
|
your_repos: 'your repos',
|
|
assigned: 'assigned',
|
|
created: 'created',
|
|
review_requested: 'review req.',
|
|
reviewed: 'reviewed',
|
|
mentioned: 'mentioned',
|
|
};
|
|
const label = labelMap[filter] ?? 'your repos';
|
|
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';
|
|
};
|
|
|
|
const extras = (wip > 0 ? 1 : 0) + (mergeConflict > 0 ? 1 : 0) + (overdue > 0 ? 1 : 0);
|
|
|
|
if (extras === 3) {
|
|
drawCount(68, [38, 50, 60]);
|
|
ctx.fillStyle = '#555555';
|
|
ctx.font = '14px "Source Han Sans SC"';
|
|
ctx.fillText(label, 98, 108);
|
|
ctx.fillStyle = '#ff8c00';
|
|
ctx.font = 'bold 22px "Source Han Sans SC"';
|
|
ctx.fillText(`WIP: ${wip}`, 98, 133);
|
|
ctx.fillStyle = '#e040fb';
|
|
ctx.font = 'bold 22px "Source Han Sans SC"';
|
|
ctx.fillText(`MC: ${mergeConflict}`, 98, 157);
|
|
ctx.fillStyle = '#ff3333';
|
|
ctx.font = 'bold 22px "Source Han Sans SC"';
|
|
ctx.fillText(`>24h: ${overdue}`, 98, 181);
|
|
} else if (wip > 0 && mergeConflict > 0) {
|
|
drawCount(78, [44, 58, 70]);
|
|
ctx.fillStyle = '#555555';
|
|
ctx.font = '15px "Source Han Sans SC"';
|
|
ctx.fillText(label, 98, 122);
|
|
ctx.fillStyle = '#ff8c00';
|
|
ctx.font = 'bold 26px "Source Han Sans SC"';
|
|
ctx.fillText(`WIP: ${wip}`, 98, 149);
|
|
ctx.fillStyle = '#e040fb';
|
|
ctx.font = 'bold 26px "Source Han Sans SC"';
|
|
ctx.fillText(`MC: ${mergeConflict}`, 98, 176);
|
|
} else if (wip > 0 && overdue > 0) {
|
|
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 (mergeConflict > 0 && overdue > 0) {
|
|
drawCount(75, [44, 56, 68]);
|
|
ctx.fillStyle = '#555555';
|
|
ctx.font = '15px "Source Han Sans SC"';
|
|
ctx.fillText(label, 98, 120);
|
|
ctx.fillStyle = '#e040fb';
|
|
ctx.font = 'bold 26px "Source Han Sans SC"';
|
|
ctx.fillText(`MC: ${mergeConflict}`, 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 (mergeConflict > 0) {
|
|
drawCount(85, [48, 64, 76]);
|
|
ctx.fillStyle = '#666666';
|
|
ctx.font = '16px "Source Han Sans SC"';
|
|
ctx.fillText(label, 98, 140);
|
|
ctx.fillStyle = '#e040fb';
|
|
ctx.font = 'bold 30px "Source Han Sans SC"';
|
|
ctx.fillText(`MC: ${mergeConflict}`, 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);
|
|
}
|
|
}
|