This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / appview / pages / templates / repo / pulls / fragments / diff.html
13 kB 334 lines
1{{ define "repo/pulls/fragments/diff" }} 2 <div id="diff-files-content" hx-swap-oob="outerHTML"> 3 {{ template "repo/fragments/fileTree" .FileTree }} 4 </div> 5 <div id="diff-stats" hx-swap-oob="outerHTML"> 6 {{ template "repo/fragments/diffStatPill" .Stats }} 7 </div> 8 <div id="diff-settings" hx-swap-oob="outerHTML"> 9 {{ template "repo/pulls/fragments/diffSettings" 10 (dict "DiffUrl" .DiffUrl 11 "Unified" .Unified) }} 12 </div> 13 <script> 14 (() => { 15 if (window.__diffRestrictSelect) return; 16 window.__diffRestrictSelect = true; 17 document.addEventListener('mousedown', (e) => { 18 document.querySelectorAll('.diff[data-restrict-select]') 19 .forEach(d => d.removeAttribute('data-restrict-select')); 20 const side = e.target.closest('.diff-side'); 21 if (!side) return; 22 const diff = side.closest('.diff'); 23 if (!diff) return; 24 const isLeft = side === side.parentElement.firstElementChild; 25 diff.setAttribute('data-restrict-select', isLeft ? 'left' : 'right'); 26 }); 27 })(); 28 </script> 29 <div id="diff-list" class="space-y-4"> 30 {{ if .ErrorMsg }} 31 <div class="flex items-center justify-center w-full min-h-32 p-4 rounded text-red-600 dark:text-red-300 bg-red-50 dark:bg-red-900/30 border border-red-300 dark:border-red-700"> 32 <span>{{ .ErrorMsg }}</span> 33 </div> 34 {{ else if .Files }} 35 {{ range .Files }} 36 <div class="drop-shadow-sm rounded overflow-clip"> 37 <details id="file-{{ .Id }}" class="group w-full" open> 38 <summary class="list-none cursor-pointer sticky top-12 z-10 bg-slate-100 dark:bg-gray-900"> 39 <div class="flex justify-between rounded border bg-white dark:bg-gray-800 group-open:rounded-b-none border-gray-200 dark:border-gray-700"> 40 <div class="p-2 flex gap-2 items-center"> 41 <span class="group-open:hidden inline">{{ i "chevron-right" "size-4" }}</span> 42 <span class="hidden group-open:inline">{{ i "chevron-down" "size-4" }}</span> 43 {{ template "repo/fragments/diffStatPill" .Stats }} 44 <span>{{ .Path }}</span> 45 </div> 46 <div class="px-2 flex gap-2 items-center"> 47 <label 48 data-review-btn="file-{{ .Id }}" 49 onclick="event.stopPropagation()" 50 class="review-btn hidden p-2 items-center gap-1 text-xs text-gray-400 dark:text-gray-500 hover:text-green-600 dark:hover:text-green-400 transition-colors cursor-pointer" 51 title="Mark as reviewed" 52 > 53 <input 54 type="checkbox" 55 class="sr-only peer review-checkbox" 56 data-file-id="file-{{ .Id }}" 57 /> 58 <span class="peer-checked:hidden">{{ i "circle" "size-4" }}</span> 59 <span class="hidden peer-checked:inline text-green-600 dark:text-green-400">{{ i "circle-check" "size-4" }}</span> 60 <span class="hidden md:inline">Reviewed</span> 61 </label> 62 </div> 63 </div> 64 </summary> 65 66 <div class="rounded-b overflow-clip border-x border-b border-gray-200 dark:border-gray-700"> 67 {{ if .Note }} 68 <div class="p-4 text-center text-gray-400 dark:text-gray-500 text-sm">{{ .Note }}</div> 69 {{ else if $.Unified }} 70 {{ template "diffUnified" . }} 71 {{ else }} 72 {{ template "diffSplit" . }} 73 {{ end }} 74 </div> 75 </details> 76 </div> 77 {{ end }} 78 {{ else }} 79 <div class="flex items-center justify-center w-full min-h-32 p-4 rounded text-blue-600 dark:text-blue-300 bg-blue-50 dark:bg-blue-900/30 border border-blue-300 dark:border-blue-700"> 80 <span>No change between two revisions.</span> 81 </div> 82 {{ end }} 83 </div> 84 {{ template "activeFileHighlightScript" }} 85 {{ template "fragments/line-quote-button" }} 86 {{ template "reviewStateScript" }} 87{{ end }} 88 89{{ define "diffUnified" }} 90<div class="diff w-full"> 91 {{ $name := .Id }} 92 {{- range .Hunks -}} 93 {{- if not .AtFileStart -}}<div class="diff-splitter">&middot;&middot;&middot;</div>{{- end -}} 94 {{- range $i, $line := .Lines -}} 95 {{- $lineId := "" -}} 96 {{- if ge .New 0 -}} 97 {{- $lineId = printf "%s-N%d" $name .New -}} 98 {{- else -}} 99 {{- $lineId = printf "%s-O%d" $name .Old -}} 100 {{- end -}} 101 {{- $cls := "" -}} 102 {{- if eq .Op "+" -}} 103 {{- $cls = "add" -}} 104 {{- else if eq .Op "-" -}} 105 {{- $cls = "del" -}} 106 {{- end -}} 107 <div id="{{ $lineId }}" class="diff-line {{ $cls }}"> 108 <a href="#{{ $lineId }}" class="diff-num"> 109 <span>{{ if gt .Old 0 }}{{ .Old }}{{ end }}</span> 110 <span>{{ if gt .New 0 }}{{ .New }}{{ end }}</span> 111 </a> 112 <span class="diff-indicator">{{ .Op }}</span> 113 <div class="diff-content">{{ .Content }}</div> 114 </div> 115 {{- end -}} 116 {{- end -}} 117</div> 118{{ end }} 119 120{{ define "diffSplit" }} 121<div class="diff w-full"> 122 {{ $name := .Id }} 123 {{- range .Hunks -}} 124 {{- if not .AtFileStart -}}<div class="diff-splitter">&middot;&middot;&middot;</div>{{- end -}} 125 {{- range $i, $row := .Rows -}} 126 <div class="diff-line"> 127 {{- template "diffSplitSide" (list $name "O" $row.Left) -}} 128 {{- template "diffSplitSide" (list $name "N" $row.Right) -}} 129 </div> 130 {{- end -}} 131 {{- end -}} 132</div> 133{{ end }} 134 135{{ define "diffSplitSide" }} 136 {{- $name := index . 0 -}} 137 {{- $side := index . 1 -}} 138 {{- $line := index . 2 -}} 139 {{- $lineId := printf "%s-%s%d" $name $side $line.Num -}} 140 {{- $cls := "" -}} 141 {{- $mark := "" -}} 142 {{- if eq $line.Kind "del" }}{{ $cls = "del" }}{{ $mark = "-" -}} 143 {{- else if eq $line.Kind "add" }}{{ $cls = "add" }}{{ $mark = "+" -}} 144 {{- else if eq $line.Kind "empty" }}{{ $cls = "empty" -}} 145 {{- end -}} 146 <div {{ if ge $line.Num 0 }}id="{{ $lineId }}"{{ end }} class="diff-side {{ $cls }}"> 147 {{ if gt $line.Num 0 -}} 148 <a href="#{{ $lineId }}" class="diff-num"><span>{{ $line.Num }}</span></a> 149 {{- else -}} 150 <span aria-hidden="true" class="diff-num"><span></span></span> 151 {{- end }} 152 <span class="diff-indicator">{{ $mark }}</span> 153 <div class="diff-content">{{ $line.Content }}</div> 154 </div> 155{{ end }} 156 157{{ define "activeFileHighlightScript" }} 158 <script> 159 (() => { 160 if (window.__activeFileScrollHandler) { 161 document.removeEventListener('scroll', window.__activeFileScrollHandler); 162 } 163 164 const filetreeLinks = document.querySelectorAll('.filetree-link'); 165 if (filetreeLinks.length === 0) return; 166 167 const linkMap = new Map(); 168 filetreeLinks.forEach(link => { 169 const path = link.getAttribute('data-path'); 170 if (path) linkMap.set('file-' + path, link); 171 }); 172 173 let currentActive = null; 174 const setActive = (link) => { 175 if (link && link !== currentActive) { 176 if (currentActive) currentActive.classList.remove('font-bold'); 177 link.classList.add('font-bold'); 178 currentActive = link; 179 } 180 }; 181 182 filetreeLinks.forEach(link => { 183 link.addEventListener('click', () => setActive(link)); 184 }); 185 186 const topbar = document.querySelector('.sticky.top-0.z-20'); 187 const headerHeight = topbar ? topbar.offsetHeight : 0; 188 189 const updateActiveFile = () => { 190 const diffFiles = document.querySelectorAll('details[id^="file-"]'); 191 Array.from(diffFiles).some(file => { 192 const rect = file.getBoundingClientRect(); 193 if (rect.top <= headerHeight && rect.bottom > headerHeight) { 194 setActive(linkMap.get(file.id)); 195 return true; 196 } 197 return false; 198 }); 199 }; 200 201 window.__activeFileScrollHandler = updateActiveFile; 202 document.addEventListener('scroll', updateActiveFile); 203 updateActiveFile(); 204 })(); 205 </script> 206{{ end }} 207 208{{ define "reviewStateScript" }} 209 <script> 210 (() => { 211 const targetRepoDid = document.getElementById('pull-target-repo-did')?.value; 212 const pullId = document.getElementById('pull-id')?.value; 213 const activeVersionId = document.getElementById('pull-active-version-id')?.value; 214 if (!targetRepoDid || !pullId || !activeVersionId) return; 215 216 const REVIEWED_PREFIX = 'reviewed:'; 217 const MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; 218 219 const storageKey = REVIEWED_PREFIX + ':' + targetRepoDid + ':' + pullId + ':' + activeVersionId; 220 221 const load = () => { 222 try { 223 const entry = JSON.parse(localStorage.getItem(storageKey) || '{}'); 224 return new Set(Array.isArray(entry) ? entry : (entry.files || [])); 225 } 226 catch { return new Set(); } 227 }; 228 229 const save = (reviewed) => { 230 const liveIds = new Set(Array.from(allFiles()).map(d => d.id)); 231 localStorage.setItem(storageKey, JSON.stringify({ 232 files: Array.from(reviewed).filter(id => liveIds.has(id)), 233 ts: Date.now(), 234 })); 235 }; 236 237 const pruneStale = () => { 238 const now = Date.now(); 239 Object.keys(localStorage) 240 .filter(k => k.startsWith(REVIEWED_PREFIX) && k !== storageKey) 241 .forEach(k => { 242 try { 243 const entry = JSON.parse(localStorage.getItem(k)); 244 if (!entry.ts || now - entry.ts > MAX_AGE_MS) localStorage.removeItem(k); 245 } catch { localStorage.removeItem(k); } 246 }); 247 }; 248 if (Math.random() < 0.1) pruneStale(); 249 250 const allFiles = () => 251 document.querySelectorAll('details[id^="file-"]'); 252 253 const applyOne = (fileId, isReviewed) => { 254 const detail = document.getElementById(fileId); 255 if (!detail) return; 256 257 const btn = detail.querySelector('[data-review-btn]'); 258 const checkbox = btn?.querySelector('input[type="checkbox"]'); 259 const path = CSS.escape(fileId.replace('file-', '')); 260 const treeLink = document.querySelector(`.filetree-link[data-path="${path}"]`); 261 262 detail.classList.toggle('opacity-60', isReviewed); 263 264 if (checkbox) checkbox.checked = isReviewed; 265 266 if (treeLink) { 267 const existing = treeLink.parentElement.querySelector('.review-indicator'); 268 if (isReviewed && !existing) { 269 const indicator = document.createElement('span'); 270 indicator.className = 'review-indicator text-green-600 dark:text-green-400 flex-shrink-0'; 271 indicator.innerHTML = '&#10003;'; 272 treeLink.parentElement.appendChild(indicator); 273 } else if (!isReviewed && existing) { 274 existing.remove(); 275 } 276 } 277 }; 278 279 const updateProgress = (reviewed) => { 280 const el = document.getElementById('changed-files-label'); 281 if (!el) return; 282 const total = parseInt(el.dataset.total, 10); 283 const files = allFiles(); 284 const count = Array.from(files).filter(d => reviewed.has(d.id)).length; 285 const suffix = total === 1 ? 'file' : 'files'; 286 const allDone = count === total; 287 el.classList.toggle('text-green-600', allDone); 288 el.classList.toggle('dark:text-green-400', allDone); 289 el.classList.toggle('text-gray-600', !allDone); 290 el.classList.toggle('dark:text-gray-400', !allDone); 291 el.textContent = count > 0 292 ? `${count}/${total} ${suffix} reviewed` 293 : `${total} changed ${suffix}`; 294 }; 295 296 const reviewed = load(); 297 298 const toggleReview = (fileId) => { 299 const detail = document.getElementById(fileId); 300 if (!detail) return; 301 const isNowReviewed = !reviewed.has(fileId); 302 if (isNowReviewed) { 303 reviewed.add(fileId); 304 detail.open = false; 305 } else { 306 reviewed.delete(fileId); 307 } 308 save(reviewed); 309 applyOne(fileId, isNowReviewed); 310 updateProgress(reviewed); 311 }; 312 313 document.getElementById('diff-list').addEventListener('change', (e) => { 314 const checkbox = e.target.closest('.review-checkbox'); 315 if (!checkbox) return; 316 const fileId = checkbox.dataset.fileId; 317 if (fileId) toggleReview(fileId); 318 }); 319 320 document.querySelectorAll('.review-btn').forEach(btn => { 321 btn.classList.remove('hidden'); 322 btn.classList.add('flex'); 323 }); 324 325 allFiles().forEach(detail => { 326 if (reviewed.has(detail.id)) { 327 applyOne(detail.id, true); 328 detail.open = false; 329 } 330 }); 331 updateProgress(reviewed); 332 })(); 333 </script> 334{{ end }}