11<script setup lang="ts">
22import type { Labrinth } from ' @modrinth/api-client'
33import {
4+ CheckIcon ,
45 ChevronDownIcon ,
6+ CopyIcon ,
57 DownloadIcon ,
68 MoreVerticalIcon ,
79 ShieldCheckIcon ,
810 TriangleAlertIcon ,
911} from ' @modrinth/assets'
1012import { Avatar , ButtonStyled , getProjectTypeIcon , injectModrinthClient } from ' @modrinth/ui'
11- import { formatProjectType } from ' @modrinth/utils'
13+ import { capitalizeString , formatProjectType , highlightCodeLines } from ' @modrinth/utils'
1214import { computed , ref } from ' vue'
1315
1416const props = defineProps <{
@@ -81,6 +83,18 @@ function backToFileList() {
8183 selectedFile .value = null
8284}
8385
86+ async function copyToClipboard(code : string ) {
87+ try {
88+ await navigator .clipboard .writeText (code )
89+ showCopyFeedback .value = true
90+ setTimeout (() => {
91+ showCopyFeedback .value = false
92+ }, 2000 )
93+ } catch (error ) {
94+ console .error (' Failed to copy code:' , error )
95+ }
96+ }
97+
8498async function updateIssueStatus(
8599 issueId : string ,
86100 status : Labrinth .TechReview .Internal .DelphiReportIssueStatus ,
@@ -94,6 +108,7 @@ async function updateIssueStatus(
94108}
95109
96110const expandedIssues = ref <Set <string >>(new Set ())
111+ const showCopyFeedback = ref (false )
97112
98113function toggleIssue(issueId : string ) {
99114 if (expandedIssues .value .has (issueId )) {
@@ -102,6 +117,31 @@ function toggleIssue(issueId: string) {
102117 expandedIssues .value .add (issueId )
103118 }
104119}
120+
121+ function getSeverityBreakdown(file : Labrinth .TechReview .Internal .FileReview ) {
122+ const counts = {
123+ SEVERE: 0 ,
124+ HIGH: 0 ,
125+ MEDIUM: 0 ,
126+ LOW: 0 ,
127+ }
128+
129+ file .issues .forEach ((issue ) => {
130+ issue .details .forEach ((detail ) => {
131+ if (detail .severity in counts ) {
132+ counts [detail .severity as keyof typeof counts ]++
133+ }
134+ })
135+ })
136+
137+ const breakdown = []
138+ if (counts .SEVERE > 0 ) breakdown .push ({ count: counts .SEVERE , severity: ' SEVERE' })
139+ if (counts .HIGH > 0 ) breakdown .push ({ count: counts .HIGH , severity: ' HIGH' })
140+ if (counts .MEDIUM > 0 ) breakdown .push ({ count: counts .MEDIUM , severity: ' MEDIUM' })
141+ if (counts .LOW > 0 ) breakdown .push ({ count: counts .LOW , severity: ' LOW' })
142+
143+ return breakdown
144+ }
105145 </script >
106146
107147<template >
@@ -120,7 +160,7 @@ function toggleIssue(issueId: string) {
120160 <span class =" text-lg font-semibold text-contrast" >{{ item.project.name }}</span >
121161
122162 <div
123- class =" flex items-center gap-1 rounded-full border border-surface-5 bg-surface-4 px-2.5 py-1"
163+ class =" flex items-center gap-1 rounded-full border border-solid border- surface-5 bg-surface-4 px-2.5 py-1"
124164 >
125165 <component
126166 :is =" getProjectTypeIcon(item.project.project_types[0] as any)"
@@ -134,13 +174,15 @@ function toggleIssue(issueId: string) {
134174 >
135175 </div >
136176
137- <div class =" rounded-full border border-surface-5 bg-surface-4 px-2.5 py-1" >
177+ <div
178+ class =" rounded-full border border-solid border-surface-5 bg-surface-4 px-2.5 py-1"
179+ >
138180 <span class =" text-sm font-medium text-secondary" >Auto-Flagged</span >
139181 </div >
140182
141183 <div class =" rounded-full px-2.5 py-1" :class =" severityColor" >
142184 <span class =" text-sm font-medium" >{{
143- highestSeverity.charAt(0) + highestSeverity.slice(1). toLowerCase()
185+ capitalizeString( highestSeverity.toLowerCase() )
144186 }}</span >
145187 </div >
146188 </div >
@@ -183,8 +225,9 @@ function toggleIssue(issueId: string) {
183225 :key =" tab"
184226 class =" rounded-full px-3 py-1.5 text-base font-semibold transition-colors hover:cursor-pointer"
185227 :class =" {
186- 'bg-highlight-green text-green': currentTab === tab,
187- 'text-contrast': currentTab !== tab,
228+ 'bg-highlight-green text-green':
229+ currentTab === tab && !(tab === 'Files' && selectedFile),
230+ 'text-contrast': currentTab !== tab || (tab === 'Files' && selectedFile),
188231 }"
189232 @click ="
190233 () => {
@@ -196,12 +239,12 @@ function toggleIssue(issueId: string) {
196239 {{ tab }}
197240 </div >
198241
199- <span
242+ <div
200243 v-if =" currentTab === 'Files' && selectedFile"
201- class =" ml-2 text-sm font-medium text-secondary "
244+ class =" rounded-full bg-highlight-green px-3 py-1.5 text-base font-semibold text-green "
202245 >
203246 {{ selectedFile.file_name }}
204- </span >
247+ </div >
205248 </div >
206249 </div >
207250
@@ -227,13 +270,27 @@ function toggleIssue(issueId: string) {
227270 >
228271 <div class =" flex items-center gap-3" >
229272 <span class =" font-medium text-contrast" >{{ file.file_name }}</span >
230- <div class =" rounded-full border border-surface-5 bg-surface-3 px-2.5 py-1" >
273+ <div class =" rounded-full border border-solid border- surface-5 bg-surface-3 px-2.5 py-1" >
231274 <span class =" text-sm font-medium text-secondary" >{{
232275 formatFileSize(file.file_size)
233276 }}</span >
234277 </div >
235- <div class =" border-red/60 rounded-full border bg-highlight-red px-2.5 py-1" >
236- <span class =" text-sm font-medium text-red" >{{ file.issues.length }} flags</span >
278+ <div
279+ v-for =" severityItem in getSeverityBreakdown(file)"
280+ :key =" severityItem.severity"
281+ class =" rounded-full border border-solid px-2.5 py-1"
282+ :class =" {
283+ 'border-red/60 bg-highlight-red text-red': severityItem.severity === 'SEVERE',
284+ 'border-orange/60 bg-highlight-orange text-orange':
285+ severityItem.severity === 'HIGH',
286+ 'border-blue/60 bg-highlight-blue text-blue': severityItem.severity === 'MEDIUM',
287+ 'border-green/60 bg-highlight-green text-green': severityItem.severity === 'LOW',
288+ }"
289+ >
290+ <span class =" text-sm font-medium"
291+ >{{ severityItem.count }}
292+ {{ capitalizeString(severityItem.severity.toLowerCase()) }}</span
293+ >
237294 </div >
238295 </div >
239296
@@ -253,18 +310,20 @@ function toggleIssue(issueId: string) {
253310 <div
254311 v-for =" (issue, idx) in selectedFile.issues"
255312 :key =" issue.issue_id"
256- class =" border-x border-b border-surface-3 bg-surface-2"
313+ class =" border-x border-b border-t-0 border-solid border- surface-3 bg-surface-2"
257314 :class =" { 'rounded-bl-2xl rounded-br-2xl': idx === selectedFile.issues.length - 1 }"
258315 >
259316 <div class =" flex items-center justify-between p-4" >
260- <div class =" flex items-center gap-2" >
261- <button
262- class =" transition-transform"
263- :class =" { 'rotate-180': !expandedIssues.has(issue.issue_id) }"
264- @click =" toggleIssue(issue.issue_id)"
265- >
266- <ChevronDownIcon class =" h-5 w-5 text-contrast" />
267- </button >
317+ <div class =" my-auto flex items-center gap-2" >
318+ <ButtonStyled type =" transparent" circular >
319+ <button
320+ class =" transition-transform"
321+ :class =" { 'rotate-180': !expandedIssues.has(issue.issue_id) }"
322+ @click =" toggleIssue(issue.issue_id)"
323+ >
324+ <ChevronDownIcon class =" h-5 w-5 text-contrast" />
325+ </button >
326+ </ButtonStyled >
268327
269328 <span class =" text-base font-semibold text-contrast" >{{
270329 issue.kind.replace(/_/g, ' ')
@@ -290,18 +349,16 @@ function toggleIssue(issueId: string) {
290349 </div >
291350
292351 <div class =" flex items-center gap-2" >
293- <ButtonStyled
294- :outline =" issue.status !== 'safe'"
295- :color =" issue.status === 'safe' ? 'green' : undefined"
296- >
297- <button @click =" updateIssueStatus(issue.issue_id, 'safe')" >Safe</button >
352+ <ButtonStyled color =" brand" type =" outlined" >
353+ <button class =" !border-[1px]" @click =" updateIssueStatus(issue.issue_id, 'safe')" >
354+ Safe
355+ </button >
298356 </ButtonStyled >
299357
300- <ButtonStyled
301- :outline =" issue.status !== 'unsafe'"
302- :color =" issue.status === 'unsafe' ? 'red' : undefined"
303- >
304- <button @click =" updateIssueStatus(issue.issue_id, 'unsafe')" >Malware</button >
358+ <ButtonStyled color =" red" type =" outlined" >
359+ <button class =" !border-[1px]" @click =" updateIssueStatus(issue.issue_id, 'unsafe')" >
360+ Malware
361+ </button >
305362 </ButtonStyled >
306363 </div >
307364 </div >
@@ -310,23 +367,42 @@ function toggleIssue(issueId: string) {
310367 <div
311368 v-for =" (detail, detailIdx) in issue.details"
312369 :key =" detailIdx"
313- class =" flex flex-col gap-3 "
370+ class =" flex flex-col"
314371 >
315- <div class =" flex items-center gap-4" >
316- <p class =" font-mono text-sm text-secondary" >{{ detail.class_name }}</p >
317- </div >
372+ <p class =" mt-0 pt-0 font-mono text-sm text-secondary" >{{ detail.class_name }}</p >
318373
319374 <div
320375 v-if =" detail.decompiled_source"
321- class =" flex gap-3 overflow-x-auto rounded-2xl bg-surface-3 p-3 font-mono text-sm "
376+ class =" relative overflow-hidden rounded-lg border border-solid border-surface-5 bg-surface-4 "
322377 >
323- <div class =" flex flex-col border-r border-surface-5 pr-3 text-right text-secondary" >
324- <span v-for =" n in detail.decompiled_source.split('\n').length" :key =" n" >{{
325- n
326- }}</span >
378+ <ButtonStyled circular type =" transparent" >
379+ <button
380+ class =" absolute right-2 top-2 border-[1px]"
381+ @click =" copyToClipboard(detail.decompiled_source)"
382+ v-tooltip =" `Copy code`"
383+ >
384+ <CopyIcon v-if =" !showCopyFeedback" />
385+ <CheckIcon v-else />
386+ </button >
387+ </ButtonStyled >
388+
389+ <div class =" overflow-x-auto bg-surface-3 py-3" >
390+ <div
391+ v-for =" (line, n) in highlightCodeLines(detail.decompiled_source, 'java')"
392+ :key =" n"
393+ class =" flex font-mono text-[13px] leading-[1.6]"
394+ >
395+ <div
396+ class =" select-none border-0 border-r border-solid border-surface-5 px-4 py-0 text-right text-primary"
397+ style =" min-width : 3.5rem "
398+ >
399+ {{ n + 1 }}
400+ </div >
401+ <div class =" flex-1 px-4 py-0 text-primary" >
402+ <pre v-html =" line || ' '" ></pre >
403+ </div >
404+ </div >
327405 </div >
328-
329- <pre class =" flex-1 text-secondary" ><code >{{ detail.decompiled_source }}</code ></pre >
330406 </div >
331407 </div >
332408 </div >
@@ -335,3 +411,11 @@ function toggleIssue(issueId: string) {
335411 </div >
336412 </div >
337413</template >
414+
415+ <style scoped>
416+ pre {
417+ all : unset ;
418+ display : inline ;
419+ white-space : pre ;
420+ }
421+ </style >
0 commit comments