// ── 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 => ``).join('') : ''; } // ── Settings tab: rendering ─────────────────────────────────────── function buildSettingsPanel(panelEl) { panelEl.innerHTML = `
gitea accounts

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 Settings → Applications.

add account
`; 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 = '

No accounts configured yet. Use the form below to add one.

'; return; } list.innerHTML = accounts.map(a => `
`).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 = 'gitea'; 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)', });