Files
ue4ss-explorer/app/components/ReportModal.vue
2026-08-22 12:16:54 -04:00

248 lines
5.4 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, computed, watch, onBeforeUnmount } from 'vue'
const props = defineProps<{
open: boolean
root: string
text: string
objects: number
truncated: boolean
loading: boolean
error: string
}>()
const emit = defineEmits<{ (e: 'close'): void }>()
const PREVIEW_LINES = 400
const lines = computed(() => (props.text ? props.text.split('\n') : []))
const preview = computed(() => lines.value.slice(0, PREVIEW_LINES).join('\n'))
const hidden = computed(() => Math.max(0, lines.value.length - PREVIEW_LINES))
/** Last path segment, which is what names the file usefully. */
const slug = computed(() => {
const seg = props.root.split(/[/.:]/).filter(Boolean).pop() || 'objects'
return seg.replace(/[^\w.-]+/g, '_').slice(0, 60)
})
const copied = ref(false)
async function copy() {
try {
await navigator.clipboard.writeText(props.text)
copied.value = true
setTimeout(() => (copied.value = false), 1400)
} catch {
/* clipboard blocked; the download still works */
}
}
function save() {
const blob = new Blob([props.text], { type: 'text/plain;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${slug.value}.dump.txt`
a.click()
setTimeout(() => URL.revokeObjectURL(url), 0)
}
/** Escape has to come off the window: the scrim is a plain div, so it never
* takes focus and never sees a key event of its own. */
function onKey(e: KeyboardEvent) {
if (e.key === 'Escape') emit('close')
}
watch(
() => props.open,
(v) => {
if (v) {
copied.value = false
window.addEventListener('keydown', onKey)
} else {
window.removeEventListener('keydown', onKey)
}
},
)
onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
const fmt = new Intl.NumberFormat()
</script>
<template>
<div v-if="open" class="scrim" @click.self="emit('close')">
<section class="sheet">
<header>
<div class="what">
<p class="eyebrow">Subtree dump</p>
<p class="root mono" :title="root">{{ root || 'everything' }}</p>
</div>
<button class="x" aria-label="Close" @click="emit('close')">×</button>
</header>
<div class="meta mono">
<template v-if="loading">Walking the subtree…</template>
<span v-else-if="error" class="bad">{{ error }}</span>
<template v-else>
<span>{{ fmt.format(objects) }} objects</span>
<span class="sep">·</span>
<span>{{ fmt.format(lines.length) }} lines</span>
<span v-if="truncated" class="sep">·</span>
<span v-if="truncated" class="warn">capped</span>
</template>
</div>
<pre v-if="!loading && !error" class="body mono">{{ preview }}</pre>
<p v-if="hidden" class="more mono">
… {{ fmt.format(hidden) }} more lines. Download or copy for the whole report.
</p>
<footer>
<button class="act" :disabled="loading || !!error" @click="copy">
{{ copied ? 'Copied' : 'Copy' }}
</button>
<button class="act primary" :disabled="loading || !!error" @click="save">
Download .txt
</button>
</footer>
</section>
</div>
</template>
<style scoped>
.scrim {
position: fixed;
inset: 0;
z-index: 40;
display: grid;
place-items: center;
padding: 24px;
background: color-mix(in srgb, var(--void) 78%, transparent);
}
.sheet {
display: flex;
flex-direction: column;
width: min(920px, 100%);
max-height: 100%;
background: var(--panel);
border: 1px solid var(--rule);
border-radius: 4px;
overflow: hidden;
}
header {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 14px;
border-bottom: 1px solid var(--rule);
flex: none;
}
.what {
min-width: 0;
}
.root {
margin: 4px 0 0;
font-size: 12px;
color: var(--ink-strong);
word-break: break-all;
line-height: 1.5;
}
.x {
margin-left: auto;
flex: none;
font-size: 16px;
color: var(--dim);
background: none;
border: none;
cursor: pointer;
}
.x:hover {
color: var(--t-bool);
}
.meta {
display: flex;
gap: 8px;
align-items: center;
padding: 8px 14px;
font-size: 11px;
color: var(--dim);
border-bottom: 1px solid var(--rule-soft);
flex: none;
}
.sep {
color: var(--dimmer);
}
.warn {
color: var(--amber);
}
.bad {
color: var(--t-bool);
}
.body {
flex: 1;
min-height: 0;
overflow: auto;
margin: 0;
padding: 12px 14px;
font-size: 11px;
line-height: 1.55;
color: var(--ink);
background: var(--void);
white-space: pre;
tab-size: 2;
}
.more {
flex: none;
margin: 0;
padding: 7px 14px;
font-size: 10px;
color: var(--dimmer);
background: var(--void);
border-top: 1px solid var(--rule-soft);
}
footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 10px 14px;
border-top: 1px solid var(--rule);
flex: none;
}
.act {
font-family: inherit;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--dim);
background: none;
border: 1px solid var(--rule);
border-radius: 3px;
padding: 7px 12px;
cursor: pointer;
}
.act:hover:not(:disabled) {
color: var(--ink);
border-color: var(--dim);
}
.act.primary {
color: var(--amber);
}
.act.primary:hover:not(:disabled) {
color: var(--amber);
border-color: var(--amber);
}
.act:disabled {
color: var(--dimmer);
cursor: default;
}
</style>