-
-
-
issue reader coming next
-
Connection works — UI is the next step.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ —
+
+
+
+
+
+
+
+
+
+
+
+ select an issue
+
+
+
+
+
+
+
+
+
+
+ ·
+ opened by
+ ·
+
+
+
+
+
+
+
+
+
+
+
+
+
+
add comment
+
+
+
+
+
+
+
diff --git a/public/plugins/gitea/gitea.js b/public/plugins/gitea/gitea.js
index 8fbc627..d81b93b 100644
--- a/public/plugins/gitea/gitea.js
+++ b/public/plugins/gitea/gitea.js
@@ -18,6 +18,14 @@ const $ = (id) => document.getElementById(id);
let accounts = [];
let currentAccountId = null;
let editingAccountId = null;
+let currentIssues = [];
+let currentIssue = null; // detail data
+let currentPage = 1;
+let currentHasMore = false;
+let currentState = 'open';
+let currentQuery = '';
+let currentScope = 'all'; // all | assigned | created | mentioned
+const PAGE_SIZE = 30;
// ── Overlay open / close ──────────────────────────────────────────
@@ -32,6 +40,8 @@ async function openGitea() {
}
$('gitea-account-select').value = currentAccountId;
showIssuesView();
+ currentPage = 1;
+ loadIssues();
}
}
@@ -286,6 +296,799 @@ async function testCurrentForm() {
await testExistingAccount(editingAccountId);
}
+// ── Issues: load + render ─────────────────────────────────────────
+
+async function loadIssues() {
+ if (!currentAccountId) return;
+ const list = $('gitea-issue-list');
+ list.innerHTML = '
';
+ $('gitea-status').textContent = 'fetching...';
+
+ const params = new URLSearchParams({
+ account: currentAccountId,
+ state: currentState,
+ scope: currentScope,
+ page: currentPage,
+ limit: PAGE_SIZE,
+ });
+ if (currentQuery) params.set('q', currentQuery);
+
+ try {
+ const res = await fetch('api/gitea/issues?' + params.toString());
+ const data = await res.json();
+ if (!data.ok) {
+ list.innerHTML = `
${escapeHtml(data.error || 'failed to load')}
`;
+ $('gitea-status').textContent = 'error';
+ renderPagination();
+ return;
+ }
+ currentIssues = data.issues || [];
+ currentHasMore = !!data.hasMore;
+ renderIssueList();
+ renderPagination();
+ $('gitea-status').textContent = `${currentIssues.length} issue${currentIssues.length === 1 ? '' : 's'}`;
+ } catch (err) {
+ list.innerHTML = `
${escapeHtml(err.message)}
`;
+ $('gitea-status').textContent = 'error';
+ }
+}
+
+function renderIssueList() {
+ const list = $('gitea-issue-list');
+ if (!currentIssues.length) {
+ list.innerHTML = '
no issues
';
+ return;
+ }
+ list.innerHTML = currentIssues.map(issue => {
+ const [owner, repoName] = (issue.repo || '/').split('/');
+ return `
+
+
${escapeHtml(issue.repo || '')}
+
${escapeHtml(issue.title || '(no title)')}
+ ${issue.labels.length ? `
${issue.labels.map(l => `${escapeHtml(l.name)}`).join('')}
` : ''}
+
+ ${issue.state}
+ #${issue.number}
+ ${escapeHtml(issue.author || '')}
+ ${issue.comments_count > 0 ? ` ${issue.comments_count}` : ''}
+ ${escapeHtml(formatRelative(issue.updated_at || issue.created_at))}
+
+
+ `;}).join('');
+
+ list.querySelectorAll('.gitea-issue-row').forEach(row => {
+ row.addEventListener('click', () => {
+ openIssueDetail(row.dataset.owner, row.dataset.repo, parseInt(row.dataset.number), row);
+ });
+ });
+}
+
+function renderPagination() {
+ $('gitea-page-info').textContent = `page ${currentPage}`;
+ const prev = $('gitea-page-prev');
+ const next = $('gitea-page-next');
+ prev.disabled = currentPage <= 1;
+ next.disabled = !currentHasMore;
+ prev.classList.toggle('opacity-30', prev.disabled);
+ prev.classList.toggle('cursor-not-allowed', prev.disabled);
+ next.classList.toggle('opacity-30', next.disabled);
+ next.classList.toggle('cursor-not-allowed', next.disabled);
+}
+
+// Decide black/white text on a label background based on luminance
+function labelTextColor(hex) {
+ if (!hex || hex.length < 6) return '#000';
+ const r = parseInt(hex.substring(0, 2), 16);
+ const g = parseInt(hex.substring(2, 4), 16);
+ const b = parseInt(hex.substring(4, 6), 16);
+ const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
+ return lum > 0.55 ? '#000' : '#fff';
+}
+
+// ── Attachment URL rewriter ───────────────────────────────────────
+//
+// Gitea embeds screenshots as

or
+//  in markdown. The browser can't fetch
+// those directly (CORS + no auth), so rewrite them to the hyprcrd
+// proxy which forwards with the account's token.
+
+function rewriteAttachmentPaths(text, accountId) {
+ if (!text) return text;
+ const acc = accounts.find(a => a.id === accountId);
+ const baseUrl = acc?.base_url || '';
+
+ let result = text;
+
+ // 1. Rewrite absolute URLs against the configured base_url
+ if (baseUrl) {
+ const baseEsc = baseUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ const absRe = new RegExp(`${baseEsc}(/attachments/[^"'\\s)<>]+)`, 'gi');
+ result = result.replace(absRe, (_m, path) => `api/gitea/attachment/${accountId}${path}`);
+ }
+
+ // 2. Rewrite relative /attachments/... paths.
+ // The capture group on the prefix preserves the surrounding char
+ // (quote, paren, whitespace) so we don't accidentally match
+ // /something/attachments/...
+ result = result.replace(
+ /(["'(\s])(\/attachments\/[^"'\s)<>]+)/gi,
+ (_m, prefix, path) => `${prefix}api/gitea/attachment/${accountId}${path}`
+ );
+
+ return result;
+}
+
+// ── Issue detail ──────────────────────────────────────────────────
+
+async function openIssueDetail(owner, repo, number, rowEl) {
+ document.querySelectorAll('.gitea-issue-row').forEach(r => r.classList.remove('active'));
+ rowEl?.classList.add('active');
+
+ // Hide the create form if it was open
+ $('gitea-create-form').classList.add('hidden');
+ $('gitea-create-form').classList.remove('flex');
+
+ $('gitea-detail').classList.add('hidden');
+ $('gitea-detail').classList.remove('flex');
+ $('gitea-detail-empty').classList.remove('hidden');
+ $('gitea-status').textContent = 'opening...';
+
+ try {
+ const res = await fetch(`api/gitea/issue/${currentAccountId}/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${number}`);
+ const data = await res.json();
+ if (!data.ok) {
+ $('gitea-status').textContent = 'error: ' + (data.error || '');
+ return;
+ }
+ currentIssue = { ...data.issue, comments: data.comments || [] };
+ renderDetail();
+ $('gitea-detail-empty').classList.add('hidden');
+ $('gitea-detail').classList.remove('hidden');
+ $('gitea-detail').classList.add('flex');
+ $('gitea-status').textContent = '';
+ } catch (err) {
+ $('gitea-status').textContent = 'error';
+ }
+}
+
+function renderDetail() {
+ if (!currentIssue) return;
+ const i = currentIssue;
+ $('gitea-detail-title').textContent = i.title || '(no title)';
+ $('gitea-detail-number').textContent = '#' + i.number;
+ $('gitea-detail-repo').textContent = i.repo || '';
+ $('gitea-detail-author').textContent = i.author || '';
+ $('gitea-detail-date').textContent = formatDateTime(i.created_at);
+
+ const stateBadge = $('gitea-detail-state');
+ stateBadge.className = 'gitea-state-badge ' + i.state;
+ stateBadge.textContent = i.state;
+
+ // Labels
+ const labelsEl = $('gitea-detail-labels');
+ labelsEl.innerHTML = (i.labels || []).map(l =>
+ `${escapeHtml(l.name)}`
+ ).join('');
+
+ // Body — render markdown via core, after rewriting attachment paths
+ const bodyEl = $('gitea-detail-body');
+ const bodyForRender = rewriteAttachmentPaths(i.body, currentAccountId);
+ if (typeof hyprcrd.markdown.render === 'function' && bodyForRender) {
+ bodyEl.innerHTML = hyprcrd.markdown.render(bodyForRender);
+ } else {
+ bodyEl.innerHTML = bodyForRender ? `${escapeHtml(bodyForRender)}` : '(no description)';
+ }
+
+ // Open on Gitea link
+ const link = $('gitea-open-link');
+ if (i.html_url) {
+ link.href = i.html_url;
+ link.style.display = '';
+ } else {
+ link.style.display = 'none';
+ }
+
+ // Comments — delegated to its own function so we can re-render after posting
+ renderCommentsSection();
+
+ // State toggle button label/icon
+ renderStateToggleButton();
+
+ // Reset edit modes if any leftover
+ cancelTitleEdit();
+ cancelBodyEdit();
+ closeLabelsEditor();
+ setExtractStatus('');
+ $('gitea-comment-input').value = '';
+ $('gitea-comment-status').textContent = '';
+}
+
+// ── Create issue ──────────────────────────────────────────────────
+
+let cachedRepos = null; // per-account cache
+let cachedReposForAccount = null;
+let cachedReposError = null; // last error message for repo fetch (so it shows on reopen)
+let createSelectedLabels = new Set();
+
+function setCreateStatus(text, kind = 'info') {
+ const el = $('gitea-create-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)';
+ }
+}
+
+async function openCreateForm() {
+ if (!currentAccountId) return;
+ // Hide other detail views
+ $('gitea-detail').classList.add('hidden');
+ $('gitea-detail').classList.remove('flex');
+ $('gitea-detail-empty').classList.add('hidden');
+ $('gitea-create-form').classList.remove('hidden');
+ $('gitea-create-form').classList.add('flex');
+
+ // Reset form
+ $('gitea-create-repo').value = '';
+ $('gitea-create-title').value = '';
+ $('gitea-create-body').value = '';
+ setCreateStatus('');
+ createSelectedLabels.clear();
+ $('gitea-create-labels').innerHTML = 'pick a repo first
';
+
+ // Try to populate the datalist autocomplete with the user's repos.
+ // If the token lacks read:repository scope this will 403 — the input is
+ // still usable manually, but we surface the error so the user knows why
+ // autocomplete isn't working and can update the token if they want.
+ const datalist = $('gitea-create-repo-list');
+ const hint = $('gitea-create-repo-hint');
+ const errBox = $('gitea-create-repo-error');
+ errBox.classList.add('hidden');
+ errBox.textContent = '';
+
+ if (cachedReposForAccount === currentAccountId && cachedRepos) {
+ renderRepoDatalist(cachedRepos);
+ // If the cached fetch had failed, also restore the error message
+ if (cachedReposError) {
+ errBox.textContent = cachedReposError;
+ errBox.classList.remove('hidden');
+ }
+ } else {
+ datalist.innerHTML = '';
+ hint.textContent = '(loading suggestions...)';
+ try {
+ const res = await fetch(`api/gitea/repos?account=${currentAccountId}`);
+ const data = await res.json();
+ if (!data.ok) {
+ hint.textContent = '(type owner/repo manually — autocomplete failed)';
+ cachedRepos = [];
+ cachedReposForAccount = currentAccountId;
+ cachedReposError = 'Could not load repo suggestions: ' + (data.error || 'unknown');
+ errBox.textContent = cachedReposError;
+ errBox.classList.remove('hidden');
+ return;
+ }
+ cachedRepos = data.repos || [];
+ cachedReposForAccount = currentAccountId;
+ cachedReposError = null;
+ renderRepoDatalist(cachedRepos);
+ } catch (err) {
+ hint.textContent = '(type owner/repo manually)';
+ cachedReposError = 'Network error loading repos: ' + err.message;
+ errBox.textContent = cachedReposError;
+ errBox.classList.remove('hidden');
+ }
+ }
+}
+
+function renderRepoDatalist(repos) {
+ const datalist = $('gitea-create-repo-list');
+ const hint = $('gitea-create-repo-hint');
+ if (!repos.length) {
+ datalist.innerHTML = '';
+ hint.textContent = '(type owner/repo manually)';
+ return;
+ }
+ datalist.innerHTML = repos.map(r =>
+ ``
+ ).join('');
+ hint.textContent = `(${repos.length} suggestion${repos.length === 1 ? '' : 's'})`;
+}
+
+async function loadCreateRepoLabels() {
+ const input = $('gitea-create-repo');
+ const fullName = input.value.trim();
+ const labelsEl = $('gitea-create-labels');
+ createSelectedLabels.clear();
+ if (!fullName || !fullName.includes('/')) {
+ labelsEl.innerHTML = 'pick a repo first (owner/repo)
';
+ return;
+ }
+ const [owner, repo] = fullName.split('/');
+ if (!owner || !repo) {
+ labelsEl.innerHTML = 'format: owner/repo
';
+ return;
+ }
+ labelsEl.innerHTML = 'loading labels...
';
+ try {
+ const res = await fetch(`api/gitea/repo/${currentAccountId}/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/labels`);
+ const data = await res.json();
+ if (!data.ok) {
+ labelsEl.innerHTML = `${escapeHtml(data.error || 'failed')}
`;
+ return;
+ }
+ const labels = data.labels || [];
+ if (!labels.length) {
+ labelsEl.innerHTML = 'repo has no labels
';
+ return;
+ }
+ labelsEl.innerHTML = labels.map(l => `
+
+ `).join('');
+ labelsEl.querySelectorAll('.gitea-label-checkbox input').forEach(cb => {
+ cb.addEventListener('change', () => {
+ const id = parseInt(cb.closest('.gitea-label-checkbox').dataset.id);
+ if (cb.checked) createSelectedLabels.add(id);
+ else createSelectedLabels.delete(id);
+ });
+ });
+ } catch (err) {
+ labelsEl.innerHTML = `${escapeHtml(err.message)}
`;
+ }
+}
+
+function closeCreateForm() {
+ $('gitea-create-form').classList.add('hidden');
+ $('gitea-create-form').classList.remove('flex');
+ if (currentIssue) {
+ $('gitea-detail').classList.remove('hidden');
+ $('gitea-detail').classList.add('flex');
+ } else {
+ $('gitea-detail-empty').classList.remove('hidden');
+ }
+}
+
+async function submitCreateIssue() {
+ const fullName = $('gitea-create-repo').value.trim();
+ const title = $('gitea-create-title').value.trim();
+ const body = $('gitea-create-body').value;
+
+ if (!fullName || !fullName.includes('/')) {
+ setCreateStatus('repo is required (format: owner/repo)', 'error');
+ return;
+ }
+ if (!title) { setCreateStatus('title is required', 'error'); return; }
+
+ const [owner, repo] = fullName.split('/');
+ setCreateStatus('creating...');
+
+ try {
+ const payload = { title, body };
+ if (createSelectedLabels.size > 0) payload.labels = [...createSelectedLabels];
+ const res = await hyprcrd.api(`api/gitea/repo/${currentAccountId}/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues`, {
+ method: 'POST',
+ body: payload,
+ });
+ if (!res.ok) {
+ setCreateStatus('error: ' + (res.error || 'failed'), 'error');
+ return;
+ }
+ setCreateStatus(`created #${res.issue.number}`, 'success');
+ // Refresh the list and open the new issue
+ currentPage = 1;
+ await loadIssues();
+ // Open it in detail view
+ setTimeout(() => {
+ const row = document.querySelector(`.gitea-issue-row[data-owner="${owner}"][data-repo="${repo}"][data-number="${res.issue.number}"]`);
+ openIssueDetail(owner, repo, res.issue.number, row);
+ }, 200);
+ } catch (err) {
+ setCreateStatus('error: ' + err.message, 'error');
+ }
+}
+
+// ── Status helper for the detail action area ──────────────────────
+
+function setExtractStatus(text, kind = 'info') {
+ const el = $('gitea-extract-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)';
+ }
+}
+
+// ── Title edit ────────────────────────────────────────────────────
+
+function startTitleEdit() {
+ if (!currentIssue) return;
+ $('gitea-title-display').classList.add('hidden');
+ $('gitea-title-edit').classList.remove('hidden');
+ $('gitea-title-edit').classList.add('flex');
+ $('gitea-title-input').value = currentIssue.title || '';
+ $('gitea-title-input').focus();
+ $('gitea-title-input').select();
+}
+
+function cancelTitleEdit() {
+ $('gitea-title-display').classList.remove('hidden');
+ $('gitea-title-edit').classList.add('hidden');
+ $('gitea-title-edit').classList.remove('flex');
+}
+
+async function saveTitleEdit() {
+ if (!currentIssue) return;
+ const newTitle = $('gitea-title-input').value.trim();
+ if (!newTitle || newTitle === currentIssue.title) {
+ cancelTitleEdit();
+ return;
+ }
+ setExtractStatus('saving title...');
+ try {
+ const res = await patchIssue({ title: newTitle });
+ if (!res.ok) {
+ setExtractStatus('error: ' + (res.error || 'failed'), 'error');
+ return;
+ }
+ currentIssue.title = res.issue.title;
+ $('gitea-detail-title').textContent = currentIssue.title;
+ cancelTitleEdit();
+ setExtractStatus('title updated', 'success');
+ // Also update the row in the list
+ updateListRow();
+ } catch (err) {
+ setExtractStatus('error: ' + err.message, 'error');
+ }
+}
+
+// ── Body edit ─────────────────────────────────────────────────────
+
+function startBodyEdit() {
+ if (!currentIssue) return;
+ $('gitea-body-display').classList.add('hidden');
+ $('gitea-body-display').classList.remove('flex');
+ $('gitea-body-edit').classList.remove('hidden');
+ $('gitea-body-input').value = currentIssue.body || '';
+ $('gitea-body-input').focus();
+}
+
+function cancelBodyEdit() {
+ $('gitea-body-display').classList.remove('hidden');
+ $('gitea-body-display').classList.add('flex');
+ $('gitea-body-edit').classList.add('hidden');
+}
+
+async function saveBodyEdit() {
+ if (!currentIssue) return;
+ const newBody = $('gitea-body-input').value;
+ if (newBody === (currentIssue.body || '')) {
+ cancelBodyEdit();
+ return;
+ }
+ setExtractStatus('saving body...');
+ try {
+ const res = await patchIssue({ body: newBody });
+ if (!res.ok) {
+ setExtractStatus('error: ' + (res.error || 'failed'), 'error');
+ return;
+ }
+ currentIssue.body = res.issue.body;
+ // Re-render body section
+ const bodyEl = $('gitea-detail-body');
+ const bodyForRender = rewriteAttachmentPaths(currentIssue.body, currentAccountId);
+ if (typeof hyprcrd.markdown.render === 'function' && bodyForRender) {
+ bodyEl.innerHTML = hyprcrd.markdown.render(bodyForRender);
+ } else {
+ bodyEl.innerHTML = bodyForRender
+ ? `${escapeHtml(bodyForRender)}`
+ : '(no description)';
+ }
+ cancelBodyEdit();
+ setExtractStatus('body updated', 'success');
+ } catch (err) {
+ setExtractStatus('error: ' + err.message, 'error');
+ }
+}
+
+// ── State toggle (close / reopen) ────────────────────────────────
+
+async function toggleState() {
+ if (!currentIssue) return;
+ const newState = currentIssue.state === 'open' ? 'closed' : 'open';
+ setExtractStatus(newState === 'closed' ? 'closing...' : 'reopening...');
+ try {
+ const res = await patchIssue({ state: newState });
+ if (!res.ok) {
+ setExtractStatus('error: ' + (res.error || 'failed'), 'error');
+ return;
+ }
+ currentIssue.state = res.issue.state;
+ // Update the badge + button
+ const badge = $('gitea-detail-state');
+ badge.className = 'gitea-state-badge ' + currentIssue.state;
+ badge.textContent = currentIssue.state;
+ renderStateToggleButton();
+ setExtractStatus(newState === 'closed' ? 'issue closed' : 'issue reopened', 'success');
+ updateListRow();
+ } catch (err) {
+ setExtractStatus('error: ' + err.message, 'error');
+ }
+}
+
+function renderStateToggleButton() {
+ const btn = $('gitea-state-toggle');
+ if (!btn || !currentIssue) return;
+ if (currentIssue.state === 'open') {
+ btn.innerHTML = 'close';
+ } else {
+ btn.innerHTML = 'reopen';
+ }
+}
+
+// ── Labels editor ─────────────────────────────────────────────────
+
+let availableRepoLabels = [];
+
+async function openLabelsEditor() {
+ if (!currentIssue) return;
+ const editor = $('gitea-labels-editor');
+ editor.classList.remove('hidden');
+ const list = $('gitea-labels-checkboxes');
+ list.innerHTML = 'loading labels...
';
+
+ try {
+ const res = await fetch(`api/gitea/repo/${currentAccountId}/${encodeURIComponent(currentIssue.owner)}/${encodeURIComponent(currentIssue.repo_name)}/labels`);
+ const data = await res.json();
+ if (!data.ok) {
+ list.innerHTML = `${escapeHtml(data.error || 'failed to load')}
`;
+ return;
+ }
+ availableRepoLabels = data.labels || [];
+ // Match current selections by name (since the issue's labels list contains
+ // {name, color} not ids)
+ const selectedNames = new Set((currentIssue.labels || []).map(l => l.name));
+ if (!availableRepoLabels.length) {
+ list.innerHTML = 'repo has no labels
';
+ return;
+ }
+ list.innerHTML = availableRepoLabels.map(l => `
+
+ `).join('');
+ } catch (err) {
+ list.innerHTML = `${escapeHtml(err.message)}
`;
+ }
+}
+
+function closeLabelsEditor() {
+ $('gitea-labels-editor').classList.add('hidden');
+}
+
+async function saveLabels() {
+ if (!currentIssue) return;
+ const checked = [];
+ document.querySelectorAll('#gitea-labels-checkboxes .gitea-label-checkbox').forEach(el => {
+ const cb = el.querySelector('input');
+ if (cb && cb.checked) checked.push(parseInt(el.dataset.id));
+ });
+ setExtractStatus('saving labels...');
+ try {
+ const res = await hyprcrd.api(`api/gitea/issue/${currentAccountId}/${encodeURIComponent(currentIssue.owner)}/${encodeURIComponent(currentIssue.repo_name)}/${currentIssue.number}/labels`, {
+ method: 'PUT',
+ body: { labels: checked },
+ });
+ if (!res.ok) {
+ setExtractStatus('error: ' + (res.error || 'failed'), 'error');
+ return;
+ }
+ currentIssue.labels = res.labels || [];
+ // Re-render labels area
+ $('gitea-detail-labels').innerHTML = (currentIssue.labels || []).map(l =>
+ `${escapeHtml(l.name)}`
+ ).join('');
+ closeLabelsEditor();
+ setExtractStatus('labels updated', 'success');
+ updateListRow();
+ } catch (err) {
+ setExtractStatus('error: ' + err.message, 'error');
+ }
+}
+
+// ── Comment posting ───────────────────────────────────────────────
+
+async function postComment() {
+ if (!currentIssue) return;
+ const input = $('gitea-comment-input');
+ const body = input.value.trim();
+ if (!body) return;
+ const status = $('gitea-comment-status');
+ status.textContent = 'posting...';
+ status.style.color = 'var(--t-dim)';
+
+ try {
+ const res = await hyprcrd.api(`api/gitea/issue/${currentAccountId}/${encodeURIComponent(currentIssue.owner)}/${encodeURIComponent(currentIssue.repo_name)}/${currentIssue.number}/comment`, {
+ method: 'POST',
+ body: { body },
+ });
+ if (!res.ok) {
+ status.textContent = 'error: ' + (res.error || 'failed');
+ status.style.color = 'var(--t-danger)';
+ return;
+ }
+ // Append the new comment to local state and re-render
+ currentIssue.comments = [...(currentIssue.comments || []), res.comment];
+ renderCommentsSection();
+ input.value = '';
+ status.textContent = 'posted';
+ status.style.color = 'var(--t-accent)';
+ setTimeout(() => { status.textContent = ''; }, 1500);
+ } catch (err) {
+ status.textContent = 'error: ' + err.message;
+ status.style.color = 'var(--t-danger)';
+ }
+}
+
+function renderCommentsSection() {
+ if (!currentIssue) return;
+ const section = $('gitea-detail-comments-section');
+ const commentsEl = $('gitea-detail-comments');
+ const comments = currentIssue.comments || [];
+ if (comments.length > 0) {
+ section.classList.remove('hidden');
+ $('gitea-detail-comments-count').textContent = comments.length;
+ commentsEl.innerHTML = comments.map(c => {
+ const bodyForRender = rewriteAttachmentPaths(c.body, currentAccountId);
+ const renderedBody = (typeof hyprcrd.markdown.render === 'function' && bodyForRender)
+ ? hyprcrd.markdown.render(bodyForRender)
+ : `${escapeHtml(bodyForRender || '')}`;
+ return `
+
+ `;
+ }).join('');
+ } else {
+ section.classList.add('hidden');
+ commentsEl.innerHTML = '';
+ }
+}
+
+// ── PATCH issue helper ────────────────────────────────────────────
+
+async function patchIssue(changes) {
+ return hyprcrd.api(`api/gitea/issue/${currentAccountId}/${encodeURIComponent(currentIssue.owner)}/${encodeURIComponent(currentIssue.repo_name)}/${currentIssue.number}`, {
+ method: 'PATCH',
+ body: changes,
+ });
+}
+
+// ── Sync the affected issue row in the list after an edit ────────
+
+function updateListRow() {
+ if (!currentIssue) return;
+ const row = document.querySelector(`.gitea-issue-row[data-owner="${currentIssue.owner}"][data-repo="${currentIssue.repo_name}"][data-number="${currentIssue.number}"]`);
+ if (!row) return;
+ // Update the cached object in currentIssues
+ const cached = currentIssues.find(i =>
+ i.repo === currentIssue.repo && i.number === currentIssue.number);
+ if (cached) {
+ cached.title = currentIssue.title;
+ cached.state = currentIssue.state;
+ cached.labels = currentIssue.labels;
+ }
+ // Re-render just that row by re-rendering the whole list (cheap enough)
+ renderIssueList();
+}
+
+// ── Extract → card ────────────────────────────────────────────────
+
+async function extractIssue() {
+ if (!currentIssue) return;
+ const status = $('gitea-extract-status');
+ status.textContent = 'creating...';
+ status.style.color = 'var(--t-dim)';
+
+ const i = currentIssue;
+ const title = `[${i.repo}#${i.number}] ${i.title || '(no title)'}`;
+ const lines = [
+ `**Repo:** ${i.repo}`,
+ `**Number:** #${i.number}`,
+ `**State:** ${i.state}`,
+ `**Author:** ${i.author || ''}`,
+ `**Created:** ${i.created_at || ''}`,
+ ];
+ if (i.assignees && i.assignees.length) lines.push(`**Assignees:** ${i.assignees.join(', ')}`);
+ if (i.labels && i.labels.length) lines.push(`**Labels:** ${i.labels.map(l => l.name).join(', ')}`);
+ if (i.milestone) lines.push(`**Milestone:** ${i.milestone}`);
+ if (i.html_url) lines.push(`**URL:** ${i.html_url}`);
+ lines.push('', '---', '', i.body || '_(no description)_');
+
+ if (i.comments && i.comments.length > 0) {
+ lines.push('', '---', '', `## Comments (${i.comments.length})`, '');
+ for (const c of i.comments) {
+ lines.push(`**${c.author || '?'}** — ${c.created_at || ''}`, '', c.body || '', '');
+ }
+ }
+
+ lines.push('', '#gitea #issue');
+ const body = lines.join('\n');
+
+ try {
+ const res = await hyprcrd.api('api/cards', {
+ method: 'POST',
+ body: { title, body, color: '#22c55e' },
+ });
+ if (res.error) {
+ status.textContent = 'error: ' + res.error;
+ status.style.color = 'var(--t-danger)';
+ return;
+ }
+ status.textContent = 'card created';
+ status.style.color = 'var(--t-accent)';
+ window._reloadAll?.();
+ } catch (err) {
+ status.textContent = 'error: ' + err.message;
+ status.style.color = 'var(--t-danger)';
+ }
+}
+
+// ── Resizable divider ─────────────────────────────────────────────
+
+function initDivider() {
+ const divider = $('gitea-divider');
+ const left = $('gitea-list-pane');
+ if (!divider || !left) return;
+ let dragging = false;
+ divider.addEventListener('mousedown', e => { dragging = true; e.preventDefault(); });
+ window.addEventListener('mousemove', e => {
+ if (!dragging) return;
+ const rect = $('gitea-view-issues').getBoundingClientRect();
+ const pct = ((e.clientX - rect.left) / rect.width) * 100;
+ if (pct > 15 && pct < 75) left.style.width = pct + '%';
+ });
+ window.addEventListener('mouseup', () => { dragging = false; });
+}
+
// ── Helpers ────────────────────────────────────────────────────────
function escapeHtml(s) {
@@ -293,6 +1096,24 @@ function escapeHtml(s) {
({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
}
+function formatDateTime(s) {
+ if (!s) return '';
+ const d = new Date(s);
+ return isNaN(d.getTime()) ? s : d.toLocaleString();
+}
+
+function formatRelative(s) {
+ if (!s) return '';
+ const d = new Date(s);
+ if (isNaN(d.getTime())) return s.substring(0, 10);
+ const diff = (Date.now() - d.getTime()) / 1000;
+ if (diff < 60) return `${Math.floor(diff)}s ago`;
+ if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
+ if (diff < 86400 * 30) return `${Math.floor(diff / 86400)}d ago`;
+ return d.toLocaleDateString();
+}
+
// ── Header button injection ───────────────────────────────────────
function injectHeaderButton() {
@@ -324,10 +1145,91 @@ function init() {
$('gitea-account-select').addEventListener('change', e => {
currentAccountId = parseInt(e.target.value);
+ currentPage = 1;
+ currentIssue = null;
+ cachedRepos = null;
+ cachedReposForAccount = null;
+ cachedReposError = null;
+ $('gitea-detail').classList.add('hidden');
+ $('gitea-detail').classList.remove('flex');
+ $('gitea-create-form').classList.add('hidden');
+ $('gitea-create-form').classList.remove('flex');
+ $('gitea-detail-empty').classList.remove('hidden');
+ loadIssues();
});
$('gitea-show-settings').addEventListener('click', gotoAccountsSettings);
$('gitea-welcome-configure').addEventListener('click', gotoAccountsSettings);
+ // Issue list controls
+ $('gitea-refresh').addEventListener('click', loadIssues);
+ $('gitea-filter-state').addEventListener('change', e => {
+ currentState = e.target.value;
+ currentPage = 1;
+ loadIssues();
+ });
+ $('gitea-filter-scope').addEventListener('change', e => {
+ currentScope = e.target.value;
+ currentPage = 1;
+ loadIssues();
+ });
+ let queryDebounce = null;
+ $('gitea-filter-q').addEventListener('input', e => {
+ clearTimeout(queryDebounce);
+ queryDebounce = setTimeout(() => {
+ currentQuery = e.target.value.trim();
+ currentPage = 1;
+ loadIssues();
+ }, 350);
+ });
+ $('gitea-page-prev').addEventListener('click', () => {
+ if (currentPage > 1) { currentPage--; loadIssues(); }
+ });
+ $('gitea-page-next').addEventListener('click', () => {
+ if (currentHasMore) { currentPage++; loadIssues(); }
+ });
+
+ // Create issue
+ $('gitea-new-issue').addEventListener('click', openCreateForm);
+ $('gitea-create-cancel').addEventListener('click', closeCreateForm);
+ $('gitea-create-cancel-2').addEventListener('click', closeCreateForm);
+ $('gitea-create-submit').addEventListener('click', submitCreateIssue);
+ // Auto-load labels after the user picks/types a repo (change fires on blur)
+ $('gitea-create-repo').addEventListener('change', loadCreateRepoLabels);
+
+ // Detail buttons
+ $('gitea-extract-btn').addEventListener('click', extractIssue);
+ $('gitea-state-toggle').addEventListener('click', toggleState);
+
+ // Title inline edit
+ $('gitea-title-edit-btn').addEventListener('click', startTitleEdit);
+ $('gitea-title-save').addEventListener('click', saveTitleEdit);
+ $('gitea-title-cancel').addEventListener('click', cancelTitleEdit);
+ $('gitea-title-input').addEventListener('keydown', e => {
+ if (e.key === 'Enter') { e.preventDefault(); saveTitleEdit(); }
+ if (e.key === 'Escape') { e.preventDefault(); cancelTitleEdit(); }
+ });
+
+ // Body inline edit
+ $('gitea-body-edit-btn').addEventListener('click', startBodyEdit);
+ $('gitea-body-save').addEventListener('click', saveBodyEdit);
+ $('gitea-body-cancel').addEventListener('click', cancelBodyEdit);
+ $('gitea-body-input').addEventListener('keydown', e => {
+ if (e.key === 'Escape') { e.preventDefault(); cancelBodyEdit(); }
+ if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); saveBodyEdit(); }
+ });
+
+ // Labels editor
+ $('gitea-labels-edit-btn').addEventListener('click', openLabelsEditor);
+ $('gitea-labels-save').addEventListener('click', saveLabels);
+ $('gitea-labels-cancel').addEventListener('click', closeLabelsEditor);
+
+ // Comment composer
+ $('gitea-comment-post').addEventListener('click', postComment);
+ $('gitea-comment-input').addEventListener('keydown', e => {
+ if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); postComment(); }
+ });
+
+ initDivider();
injectHeaderButton();
// Register the "gitea" tab inside the core settings overlay