gitea
This commit is contained in:
1 parent
cc97754ae6
commit
86508ffcca
13 files changed
+945
-35
No files matched your search
@@ -0,0 +1,352 @@
|
||||
// ── Gitea Plugin Frontend (scaffold) ──
|
||||
//
|
||||
// Phase 1 (this file): account management + connection test.
|
||||
// Phase 2: issue list, detail view, extract-as-card, comments.
|
||||
//
|
||||
// Self-registers via window.hyprcrd:
|
||||
// - Header button "gitea"
|
||||
// - Settings tab "gitea" with account CRUD
|
||||
// - Welcome view in the overlay until at least one account exists
|
||||
|
||||
const hyprcrd = window.hyprcrd;
|
||||
if (!hyprcrd) throw new Error('[gitea plugin] window.hyprcrd missing');
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
// ── State ─────────────────────────────────────────────────────────
|
||||
|
||||
let accounts = [];
|
||||
let currentAccountId = null;
|
||||
let editingAccountId = null;
|
||||
|
||||
// ── Overlay open / close ──────────────────────────────────────────
|
||||
|
||||
async function openGitea() {
|
||||
$('gitea-overlay').classList.add('open');
|
||||
await loadAccounts();
|
||||
if (accounts.length === 0) {
|
||||
showWelcomeView();
|
||||
} else {
|
||||
if (!accounts.find(a => a.id === currentAccountId)) {
|
||||
currentAccountId = accounts[0].id;
|
||||
}
|
||||
$('gitea-account-select').value = currentAccountId;
|
||||
showIssuesView();
|
||||
}
|
||||
}
|
||||
|
||||
function closeGitea() {
|
||||
$('gitea-overlay').classList.remove('open');
|
||||
}
|
||||
|
||||
function showWelcomeView() {
|
||||
$('gitea-view-welcome').classList.remove('hidden');
|
||||
$('gitea-view-issues').classList.add('hidden');
|
||||
$('gitea-view-issues').classList.remove('flex');
|
||||
$('gitea-status').textContent = '';
|
||||
}
|
||||
|
||||
function showIssuesView() {
|
||||
$('gitea-view-welcome').classList.add('hidden');
|
||||
$('gitea-view-issues').classList.remove('hidden');
|
||||
$('gitea-view-issues').classList.add('flex');
|
||||
}
|
||||
|
||||
function gotoAccountsSettings() {
|
||||
closeGitea();
|
||||
hyprcrd.settings.openTab('gitea');
|
||||
}
|
||||
|
||||
// ── Accounts: data ────────────────────────────────────────────────
|
||||
|
||||
async function loadAccounts() {
|
||||
const res = await fetch('api/gitea/accounts');
|
||||
accounts = await res.json();
|
||||
if (!Array.isArray(accounts)) accounts = [];
|
||||
renderAccountSelect();
|
||||
}
|
||||
|
||||
function renderAccountSelect() {
|
||||
const sel = $('gitea-account-select');
|
||||
if (!sel) return;
|
||||
sel.innerHTML = accounts.length
|
||||
? accounts.map(a => `<option value="${a.id}">${escapeHtml(a.name)}</option>`).join('')
|
||||
: '<option value="">— no accounts —</option>';
|
||||
}
|
||||
|
||||
// ── Settings tab: rendering ───────────────────────────────────────
|
||||
|
||||
function buildSettingsPanel(panelEl) {
|
||||
panelEl.innerHTML = `
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<div class="font-pixel text-[9px] uppercase tracking-wider text-t-dim mb-2"><i class="ph ph-git-pull-request text-xs"></i> gitea accounts</div>
|
||||
<p class="text-[10px] text-t-dim leading-relaxed mb-3">
|
||||
Configure Gitea instances to read issues inside hyprcrd. Tokens are encrypted with the vault key.
|
||||
Create a personal access token in your Gitea profile under <span class="font-mono">Settings → Applications</span>.
|
||||
</p>
|
||||
<div id="gitea-accounts-list" class="space-y-2"></div>
|
||||
</div>
|
||||
<div class="border-t border-t-border pt-4">
|
||||
<div class="font-pixel text-[9px] uppercase tracking-wider text-t-dim mb-3"><span id="gitea-form-title">add account</span></div>
|
||||
<form id="gitea-account-form" class="space-y-2">
|
||||
<input type="hidden" id="gitea-acc-id" value="">
|
||||
<label class="block">
|
||||
<span class="font-pixel text-[9px] text-t-dim uppercase">name</span>
|
||||
<input id="gitea-acc-name" type="text" placeholder="My Gitea" class="w-full bg-t-bg border border-t-border text-t-bright px-2 py-1.5 text-xs focus:border-t-accent focus:outline-none font-mono">
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="font-pixel text-[9px] text-t-dim uppercase">base url</span>
|
||||
<input id="gitea-acc-baseurl" type="text" placeholder="https://git.example.com" class="w-full bg-t-bg border border-t-border text-t-bright px-2 py-1.5 text-xs focus:border-t-accent focus:outline-none font-mono">
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="font-pixel text-[9px] text-t-dim uppercase">access token <span id="gitea-tok-hint" class="text-t-muted normal-case text-[8px]"></span></span>
|
||||
<input id="gitea-acc-token" type="password" autocomplete="off" class="w-full bg-t-bg border border-t-border text-t-bright px-2 py-1.5 text-xs focus:border-t-accent focus:outline-none font-mono">
|
||||
</label>
|
||||
<div class="flex items-center gap-2 pt-1">
|
||||
<button type="submit" id="gitea-acc-save" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-accent text-t-accent hover:bg-t-accent/10 transition-colors"><i class="ph ph-floppy-disk text-sm"></i><span class="font-pixel text-[10px]">save</span></button>
|
||||
<button type="button" id="gitea-acc-test" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors"><i class="ph ph-plug text-sm"></i><span class="font-pixel text-[10px]">test</span></button>
|
||||
<button type="button" id="gitea-acc-cancel" class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-danger hover:text-t-danger transition-colors"><i class="ph ph-x text-sm"></i><span class="font-pixel text-[10px]">cancel</span></button>
|
||||
</div>
|
||||
<div id="gitea-acc-status" class="hidden mt-1 px-3 py-2 text-[10px] font-mono break-words border border-t-border bg-t-bg"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
panelEl.querySelector('#gitea-account-form').addEventListener('submit', saveAccount);
|
||||
panelEl.querySelector('#gitea-acc-cancel').addEventListener('click', resetAccountForm);
|
||||
panelEl.querySelector('#gitea-acc-test').addEventListener('click', testCurrentForm);
|
||||
}
|
||||
|
||||
async function refreshSettingsPanel(panelEl) {
|
||||
if (!panelEl.querySelector('#gitea-account-form')) {
|
||||
buildSettingsPanel(panelEl);
|
||||
}
|
||||
await loadAccounts();
|
||||
renderAccountsList();
|
||||
resetAccountForm();
|
||||
}
|
||||
|
||||
function renderAccountsList() {
|
||||
const list = $('gitea-accounts-list');
|
||||
if (!list) return;
|
||||
if (!accounts.length) {
|
||||
list.innerHTML = '<p class="text-xs text-t-dim italic">No accounts configured yet. Use the form below to add one.</p>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = accounts.map(a => `
|
||||
<div class="gitea-account-row" data-id="${a.id}">
|
||||
<i class="ph ph-git-pull-request text-t-accent text-base"></i>
|
||||
<div class="gitea-account-info">
|
||||
<div class="gitea-account-name">${escapeHtml(a.name)}</div>
|
||||
<div class="gitea-account-meta">${escapeHtml(a.base_url)}</div>
|
||||
</div>
|
||||
<div class="gitea-account-actions">
|
||||
<button class="toolbar-btn gitea-edit-btn" title="Edit"><i class="ph ph-pencil-simple text-sm"></i></button>
|
||||
<button class="toolbar-btn gitea-test-btn" title="Test"><i class="ph ph-plug text-sm"></i></button>
|
||||
<button class="toolbar-btn gitea-del-btn" title="Delete"><i class="ph ph-trash text-sm"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
list.querySelectorAll('.gitea-account-row').forEach(row => {
|
||||
const id = parseInt(row.dataset.id);
|
||||
row.querySelector('.gitea-edit-btn').addEventListener('click', () => editAccount(id));
|
||||
row.querySelector('.gitea-del-btn').addEventListener('click', () => deleteAccount(id));
|
||||
row.querySelector('.gitea-test-btn').addEventListener('click', () => testExistingAccount(id));
|
||||
});
|
||||
}
|
||||
|
||||
function setAccountStatus(text, kind = 'info') {
|
||||
const el = $('gitea-acc-status');
|
||||
if (!el) return;
|
||||
if (!text) {
|
||||
el.classList.add('hidden');
|
||||
el.textContent = '';
|
||||
el.style.borderColor = '';
|
||||
el.style.color = '';
|
||||
return;
|
||||
}
|
||||
el.classList.remove('hidden');
|
||||
el.textContent = text;
|
||||
if (kind === 'error') {
|
||||
el.style.borderColor = 'var(--t-danger)';
|
||||
el.style.color = 'var(--t-danger)';
|
||||
} else if (kind === 'success') {
|
||||
el.style.borderColor = 'var(--t-accent)';
|
||||
el.style.color = 'var(--t-accent)';
|
||||
} else {
|
||||
el.style.borderColor = 'var(--t-border)';
|
||||
el.style.color = 'var(--t-muted)';
|
||||
}
|
||||
}
|
||||
|
||||
function resetAccountForm() {
|
||||
editingAccountId = null;
|
||||
const form = $('gitea-account-form');
|
||||
if (!form) return;
|
||||
$('gitea-form-title').textContent = 'add account';
|
||||
$('gitea-acc-id').value = '';
|
||||
$('gitea-acc-name').value = '';
|
||||
$('gitea-acc-baseurl').value = '';
|
||||
$('gitea-acc-token').value = '';
|
||||
setAccountStatus('');
|
||||
$('gitea-tok-hint').textContent = '';
|
||||
}
|
||||
|
||||
function editAccount(id) {
|
||||
const acc = accounts.find(a => a.id === id);
|
||||
if (!acc) return;
|
||||
editingAccountId = id;
|
||||
$('gitea-form-title').textContent = 'edit account: ' + acc.name;
|
||||
$('gitea-acc-id').value = id;
|
||||
$('gitea-acc-name').value = acc.name;
|
||||
$('gitea-acc-baseurl').value = acc.base_url;
|
||||
$('gitea-acc-token').value = '';
|
||||
$('gitea-tok-hint').textContent = '(leave blank to keep existing)';
|
||||
setAccountStatus('');
|
||||
$('gitea-acc-name').focus();
|
||||
}
|
||||
|
||||
async function deleteAccount(id) {
|
||||
const acc = accounts.find(a => a.id === id);
|
||||
if (!acc) return;
|
||||
const ok = await hyprcrd.dialog.confirm(
|
||||
`Delete gitea account "${acc.name}"? Stored token will be removed but no issues are deleted on the server.`,
|
||||
{ title: 'delete account', okLabel: 'delete', cancelLabel: 'cancel', danger: true }
|
||||
);
|
||||
if (!ok) return;
|
||||
await hyprcrd.api(`api/gitea/accounts/${id}`, { method: 'DELETE' });
|
||||
await loadAccounts();
|
||||
renderAccountsList();
|
||||
}
|
||||
|
||||
async function saveAccount(e) {
|
||||
e.preventDefault();
|
||||
setAccountStatus('saving...');
|
||||
|
||||
const payload = {
|
||||
name: $('gitea-acc-name').value.trim(),
|
||||
base_url: $('gitea-acc-baseurl').value.trim(),
|
||||
token: $('gitea-acc-token').value,
|
||||
};
|
||||
if (!payload.name || !payload.base_url || (!editingAccountId && !payload.token)) {
|
||||
setAccountStatus('name, base url, and token are required', 'error');
|
||||
return;
|
||||
}
|
||||
if (editingAccountId && !payload.token) delete payload.token;
|
||||
|
||||
try {
|
||||
let res;
|
||||
if (editingAccountId) {
|
||||
res = await hyprcrd.api(`api/gitea/accounts/${editingAccountId}`, { method: 'PATCH', body: payload });
|
||||
} else {
|
||||
res = await hyprcrd.api('api/gitea/accounts', { method: 'POST', body: payload });
|
||||
}
|
||||
if (res.error) {
|
||||
setAccountStatus('error: ' + res.error, 'error');
|
||||
return;
|
||||
}
|
||||
setAccountStatus('saved', 'success');
|
||||
await loadAccounts();
|
||||
renderAccountsList();
|
||||
// Don't fully reset — keep saved state visible briefly
|
||||
setTimeout(() => { resetAccountForm(); }, 1500);
|
||||
} catch (err) {
|
||||
setAccountStatus('error: ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function testExistingAccount(id) {
|
||||
setAccountStatus('testing...');
|
||||
try {
|
||||
const res = await hyprcrd.api(`api/gitea/accounts/${id}/test`, { method: 'POST' });
|
||||
if (res.ok) {
|
||||
const u = res.user || {};
|
||||
const who = u.login || u.full_name || 'unknown';
|
||||
const extras = [];
|
||||
if (u.full_name && u.full_name !== u.login) extras.push(u.full_name);
|
||||
if (u.email) extras.push(u.email);
|
||||
const detail = extras.length ? ` (${extras.join(', ')})` : '';
|
||||
setAccountStatus(`ok — connected as ${who}${detail}`, 'success');
|
||||
} else {
|
||||
setAccountStatus('failed: ' + (res.error || 'unknown'), 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
setAccountStatus('error: ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function testCurrentForm() {
|
||||
if (!editingAccountId) {
|
||||
setAccountStatus('save first, then test');
|
||||
return;
|
||||
}
|
||||
await testExistingAccount(editingAccountId);
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s ?? '').replace(/[&<>"']/g, c =>
|
||||
({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
}
|
||||
|
||||
// ── Header button injection ───────────────────────────────────────
|
||||
|
||||
function injectHeaderButton() {
|
||||
const header = document.querySelector('#main-header .flex.items-center.gap-3:last-of-type');
|
||||
if (!header) return;
|
||||
if (document.getElementById('gitea-btn')) return;
|
||||
const btn = document.createElement('button');
|
||||
btn.id = 'gitea-btn';
|
||||
btn.className = 'flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors';
|
||||
btn.innerHTML = '<i class="ph ph-git-pull-request text-sm"></i><span class="font-pixel text-[10px]">gitea</span>';
|
||||
btn.addEventListener('click', openGitea);
|
||||
const addBtn = document.getElementById('add-btn');
|
||||
if (addBtn) header.insertBefore(btn, addBtn);
|
||||
else header.appendChild(btn);
|
||||
}
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────
|
||||
|
||||
function init() {
|
||||
if (!$('gitea-overlay')) return;
|
||||
|
||||
$('gitea-close').addEventListener('click', closeGitea);
|
||||
$('gitea-overlay').addEventListener('click', e => {
|
||||
if (e.target === $('gitea-overlay')) closeGitea();
|
||||
});
|
||||
$('gitea-overlay').addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape') { closeGitea(); e.stopPropagation(); }
|
||||
});
|
||||
|
||||
$('gitea-account-select').addEventListener('change', e => {
|
||||
currentAccountId = parseInt(e.target.value);
|
||||
});
|
||||
$('gitea-show-settings').addEventListener('click', gotoAccountsSettings);
|
||||
$('gitea-welcome-configure').addEventListener('click', gotoAccountsSettings);
|
||||
|
||||
injectHeaderButton();
|
||||
|
||||
// Register the "gitea" tab inside the core settings overlay
|
||||
hyprcrd.settings.registerTab({
|
||||
id: 'gitea',
|
||||
label: 'gitea',
|
||||
icon: 'ph-git-pull-request',
|
||||
onShow: (panelEl) => refreshSettingsPanel(panelEl),
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
hyprcrd.registerPlugin({
|
||||
name: 'gitea',
|
||||
version: '0.1.0',
|
||||
description: 'Gitea issue browser (scaffold)',
|
||||
});
|
||||
Reference in new issue
Block a user