yunsi-toolbox-vue/components/app-image-viewer/HexDump.vue

136 lines
3.6 KiB
Vue

<script setup lang="ts">
import { ref, computed } from 'vue'
const props = withDefaults(defineProps<{
data: Uint8Array
height?: number
highlight?: { start: number; end: number } | null
}>(), { height: 400, highlight: null })
const emit = defineEmits<{
'byte-hover': [offset: number | null]
}>()
const BYTES = 16
const ROW_H = 20
const wrapEl = ref<HTMLElement | null>(null)
const scrollTop = ref(0)
const totalRows = computed(() => Math.ceil(props.data.length / BYTES))
const firstRow = computed(() => Math.floor(scrollTop.value / ROW_H))
const lastRow = computed(() => Math.min(firstRow.value + Math.ceil(props.height / ROW_H) + 4, totalRows.value))
const rows = computed(() => {
const out = []
for (let r = firstRow.value; r < lastRow.value; r++) {
const off = r * BYTES
const slice = props.data.subarray(off, off + BYTES)
const hex = Array.from(slice).map(b => b.toString(16).padStart(2, '0'))
const asc = Array.from(slice).map(b => (b >= 0x20 && b < 0x7f) ? String.fromCharCode(b) : '.')
out.push({ off, hex, asc })
}
return out
})
function isHi(offset: number): boolean {
return props.highlight != null
&& offset >= props.highlight.start
&& offset < props.highlight.end
}
function onScroll(e: Event) {
scrollTop.value = (e.target as HTMLElement).scrollTop
}
defineExpose({
scrollTo(offset: number) {
const row = Math.floor(offset / BYTES)
wrapEl.value?.scrollTo({ top: row * ROW_H, behavior: 'smooth' })
},
})
</script>
<template>
<div ref="wrapEl" class="hd-wrap" :style="{ height: height + 'px' }" @scroll="onScroll">
<div :style="{ height: totalRows * ROW_H + 'px', position: 'relative' }">
<div :style="{ transform: `translateY(${firstRow * ROW_H}px)` }">
<div v-for="row in rows" :key="row.off" class="hd-row">
<span class="hd-off">{{ row.off.toString(16).padStart(8, '0') }}</span>
<span class="hd-hex">
<span
v-for="(h, bi) in row.hex"
:key="bi"
:class="['hd-byte', { 'hd-byte-hi': isHi(row.off + bi) }, bi === 8 ? 'hd-byte-gap' : '']"
@mouseenter="emit('byte-hover', row.off + bi)"
@mouseleave="emit('byte-hover', null)"
>{{ h }}</span>
</span>
<span class="hd-asc">|<span
v-for="(a, bi) in row.asc"
:key="bi"
:class="['hd-asc-char', { 'hd-byte-hi': isHi(row.off + bi) }]"
@mouseenter="emit('byte-hover', row.off + bi)"
@mouseleave="emit('byte-hover', null)"
>{{ a }}</span>|</span>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.hd-wrap {
overflow-y: auto;
font-family: 'Courier New', Courier, monospace;
font-size: 12px;
border: 1px solid var(--el-border-color);
border-radius: 4px;
background: var(--el-fill-color-blank);
}
.hd-row {
display: flex;
height: 20px;
align-items: center;
gap: 12px;
padding: 0 8px;
}
.hd-row:hover {
background: var(--el-fill-color-light);
}
.hd-off {
color: var(--el-text-color-placeholder);
min-width: 6em;
user-select: none;
}
.hd-hex {
display: inline-flex;
color: var(--el-text-color-primary);
}
.hd-byte {
display: inline-block;
width: 3ch;
cursor: default;
border-radius: 2px;
}
/* Extra left margin to create the double-space gap between the two groups of 8 */
.hd-byte-gap {
margin-left: 1ch;
}
.hd-byte-hi {
background: var(--el-color-primary-light-7);
color: var(--el-color-primary);
}
.hd-asc {
color: var(--el-text-color-secondary);
}
.hd-asc-char {
display: inline-block;
width: 1ch;
cursor: default;
border-radius: 2px;
}
</style>