function createReviewController({ fetchJson }) { function endpoint(item) { const [owner, repo] = String(item.repository || '').split('/'); if (!owner || !repo || !Number.isInteger(Number(item.number))) { throw new Error('This review link is invalid.'); } return 'api/v1/repos/' + encodeURIComponent(owner) + '/' + encodeURIComponent(repo) + '/pulls/' + Number(item.number) + '/review'; } async function load(item) { return fetchJson(endpoint(item), { headers: { Accept: 'application/json' } }); } return { load }; } function diffLineClass(line) { if (line.startsWith('@@')) return 'hunk'; if (line.startsWith('+')) return 'added'; if (line.startsWith('-')) return 'removed'; return 'context'; } function renderDiffFile(file, index, escapeHtml) { const panelId = 'review-diff-' + index; let preview; if (file.diff_available) { const lines = (file.diff_lines || []).map(line => '' + escapeHtml(String(line)) + '' ).join(''); preview = ''; } else { const message = file.diff_binary ? 'Binary file · preview unavailable.' : 'Diff preview unavailable.'; preview = ''; } const filename = escapeHtml(file.filename || 'Unknown file'); return '
' + preview + '
'; } function toggleDiff(button, panel) { const expanded = button.getAttribute('aria-expanded') === 'true'; button.setAttribute('aria-expanded', String(!expanded)); panel.hidden = expanded; } function createProgress({ storage, repository, number, headSha, files }) { const filenames = (files || []).map(file => file && file.filename).filter(Boolean); const key = 'stackchain.review-progress.v1:' + repository + '#' + number + '@' + headSha; const headKey = 'stackchain.review-progress.v1:' + repository + '#' + number + ':head'; let reviewed = []; let newHead = false; try { const previousHead = storage.getItem(headKey); newHead = Boolean(previousHead && previousHead !== headSha); storage.setItem(headKey, headSha); const saved = JSON.parse(storage.getItem(key) || '[]'); if (Array.isArray(saved)) reviewed = filenames.filter(filename => saved.includes(filename)); } catch (error) { reviewed = []; } function snapshot() { const pending = filenames.find(filename => !reviewed.includes(filename)) || null; return { reviewed: [...reviewed], reviewedCount: reviewed.length, total: filenames.length, nextFilename: pending, ...(newHead ? { newHead: true } : {}), }; } function markReviewed(filename) { if (filenames.includes(filename) && !reviewed.includes(filename)) { reviewed.push(filename); reviewed = filenames.filter(item => reviewed.includes(item)); try { storage.setItem(key, JSON.stringify(reviewed)); } catch (error) { /* local progress remains usable */ } } return snapshot(); } return { snapshot, markReviewed }; } createReviewController.renderDiffFile = renderDiffFile; createReviewController.toggleDiff = toggleDiff; createReviewController.createProgress = createProgress; if (typeof module !== 'undefined' && module.exports) { module.exports = createReviewController; }