first commit
This commit is contained in:
714
src/components/BinaryEditor.tsx
Normal file
714
src/components/BinaryEditor.tsx
Normal file
@ -0,0 +1,714 @@
|
||||
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import {
|
||||
fileMetadata,
|
||||
isCompiling,
|
||||
hasModifications,
|
||||
totalHexChunks,
|
||||
compileImage,
|
||||
resetToOriginal,
|
||||
byteToChar,
|
||||
originalFileData,
|
||||
modifiedFileData,
|
||||
hexChunkSize,
|
||||
pushToUndoStack,
|
||||
undo,
|
||||
canUndo,
|
||||
} from '../stores/imageStore';
|
||||
|
||||
export default function BinaryEditor() {
|
||||
const metadata = useStore(fileMetadata);
|
||||
const compiling = useStore(isCompiling);
|
||||
const modified = useStore(hasModifications);
|
||||
const totalChunks = useStore(totalHexChunks);
|
||||
const originalData = useStore(originalFileData);
|
||||
const modifiedData = useStore(modifiedFileData);
|
||||
const chunkSize = useStore(hexChunkSize);
|
||||
const canUndoState = useStore(canUndo);
|
||||
|
||||
const [jumpToChunk, setJumpToChunk] = useState('');
|
||||
const [jumpToAddress, setJumpToAddress] = useState('');
|
||||
const [hexInput, setHexInput] = useState('');
|
||||
const [editingByte, setEditingByte] = useState<{ row: number; col: number; value: string; globalOffset: number } | null>(null);
|
||||
const [editingAscii, setEditingAscii] = useState<{ row: number; col: number; value: string; globalOffset: number } | null>(null);
|
||||
const [scrollTop, setScrollTop] = useState(0);
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Virtual scrolling configuration
|
||||
const ROW_HEIGHT = 20; // Height of each hex row in pixels
|
||||
const BYTES_PER_ROW = 16;
|
||||
const BUFFER_SIZE = 10; // Extra rows to render above/below viewport for smooth scrolling
|
||||
|
||||
// Calculate virtual scrolling parameters
|
||||
const virtualScrollData = useMemo(() => {
|
||||
if (!metadata || !originalData) return null;
|
||||
|
||||
const totalRows = Math.ceil(metadata.fileSize / BYTES_PER_ROW);
|
||||
const containerHeight = 400; // Fixed height for hex container
|
||||
const visibleRows = Math.ceil(containerHeight / ROW_HEIGHT);
|
||||
|
||||
const startRow = Math.floor(scrollTop / ROW_HEIGHT);
|
||||
const endRow = Math.min(startRow + visibleRows + BUFFER_SIZE * 2, totalRows);
|
||||
const actualStartRow = Math.max(0, startRow - BUFFER_SIZE);
|
||||
|
||||
const startByte = actualStartRow * BYTES_PER_ROW;
|
||||
const endByte = Math.min(endRow * BYTES_PER_ROW, metadata.fileSize);
|
||||
|
||||
return {
|
||||
totalRows,
|
||||
visibleRows,
|
||||
startRow: actualStartRow,
|
||||
endRow,
|
||||
startByte,
|
||||
endByte,
|
||||
totalHeight: Math.max(totalRows * ROW_HEIGHT, containerHeight),
|
||||
offsetY: actualStartRow * ROW_HEIGHT,
|
||||
};
|
||||
}, [metadata, originalData, scrollTop]);
|
||||
|
||||
// Get data for virtual scrolling viewport
|
||||
const viewportData = useMemo(() => {
|
||||
if (!virtualScrollData || !originalData) return null;
|
||||
|
||||
const dataToUse = modifiedData || originalData;
|
||||
const viewportBytes = dataToUse.slice(virtualScrollData.startByte, virtualScrollData.endByte);
|
||||
|
||||
return viewportBytes;
|
||||
}, [virtualScrollData, originalData, modifiedData]);
|
||||
|
||||
// Update hex input when viewport data changes
|
||||
useEffect(() => {
|
||||
if (viewportData) {
|
||||
const hexString = Array.from(viewportData)
|
||||
.map(byte => byte.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
setHexInput(hexString);
|
||||
}
|
||||
}, [viewportData]);
|
||||
|
||||
// Handle scroll events
|
||||
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
|
||||
setScrollTop(e.currentTarget.scrollTop);
|
||||
}, []);
|
||||
|
||||
// Random button functionality
|
||||
const handleRandomize = useCallback((count: number = 1) => {
|
||||
if (!originalData || !virtualScrollData) return;
|
||||
|
||||
// Push current state to undo stack
|
||||
const currentData = modifiedData || new Uint8Array(originalData);
|
||||
pushToUndoStack(currentData);
|
||||
|
||||
const newData = new Uint8Array(currentData);
|
||||
|
||||
// Get visible range
|
||||
const visibleStart = virtualScrollData.startByte;
|
||||
const visibleEnd = virtualScrollData.endByte;
|
||||
|
||||
// Make multiple random changes
|
||||
for (let i = 0; i < count; i++) {
|
||||
const randomIndex = Math.floor(Math.random() * (visibleEnd - visibleStart)) + visibleStart;
|
||||
const randomValue = Math.floor(Math.random() * 256);
|
||||
newData[randomIndex] = randomValue;
|
||||
}
|
||||
|
||||
modifiedFileData.set(newData);
|
||||
|
||||
// Auto-update image after random change
|
||||
setTimeout(() => compileImage(), 100);
|
||||
}, [originalData, modifiedData, virtualScrollData]);
|
||||
|
||||
// Undo functionality
|
||||
const handleUndo = useCallback(() => {
|
||||
undo();
|
||||
setTimeout(() => compileImage(), 100);
|
||||
}, []);
|
||||
|
||||
// Global random button functionality (affects entire file)
|
||||
const handleGlobalRandomize = useCallback((count: number = 1) => {
|
||||
if (!originalData || !metadata) return;
|
||||
|
||||
// Push current state to undo stack
|
||||
const currentData = modifiedData || new Uint8Array(originalData);
|
||||
pushToUndoStack(currentData);
|
||||
|
||||
const newData = new Uint8Array(currentData);
|
||||
|
||||
// Make random changes across the entire file
|
||||
for (let i = 0; i < count; i++) {
|
||||
const randomIndex = Math.floor(Math.random() * metadata.fileSize);
|
||||
const randomValue = Math.floor(Math.random() * 256);
|
||||
newData[randomIndex] = randomValue;
|
||||
}
|
||||
|
||||
modifiedFileData.set(newData);
|
||||
|
||||
// Auto-update image after random change
|
||||
setTimeout(() => compileImage(), 100);
|
||||
}, [originalData, modifiedData, metadata]);
|
||||
|
||||
const handleHexInputChange = (
|
||||
event: React.ChangeEvent<HTMLTextAreaElement>
|
||||
) => {
|
||||
const value = event.target.value.replace(/[^0-9a-fA-F]/gi, '');
|
||||
setHexInput(value);
|
||||
};
|
||||
|
||||
// Auto-updates happen immediately now
|
||||
|
||||
const handleByteClick = (row: number, col: number, currentValue: number, globalOffset: number) => {
|
||||
setEditingByte({
|
||||
row,
|
||||
col,
|
||||
value: currentValue.toString(16).padStart(2, '0').toUpperCase(),
|
||||
globalOffset
|
||||
});
|
||||
};
|
||||
|
||||
const handleByteEdit = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!editingByte) return;
|
||||
const value = e.target.value.replace(/[^0-9a-fA-F]/gi, '').slice(0, 2);
|
||||
setEditingByte({ ...editingByte, value });
|
||||
};
|
||||
|
||||
const handleByteSubmit = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && editingByte) {
|
||||
applyByteEdit();
|
||||
} else if (e.key === 'Escape') {
|
||||
setEditingByte(null);
|
||||
}
|
||||
};
|
||||
|
||||
const applyByteEdit = () => {
|
||||
if (!editingByte || !originalData) return;
|
||||
|
||||
const newValue = parseInt(editingByte.value, 16);
|
||||
if (isNaN(newValue)) {
|
||||
setEditingByte(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Push current state to undo stack
|
||||
const currentData = modifiedData || new Uint8Array(originalData);
|
||||
pushToUndoStack(currentData);
|
||||
|
||||
// Update the full modified data directly
|
||||
const newData = new Uint8Array(currentData);
|
||||
newData[editingByte.globalOffset] = newValue;
|
||||
|
||||
// Update store with new modified data
|
||||
modifiedFileData.set(newData);
|
||||
|
||||
setEditingByte(null);
|
||||
// Auto-update image after byte edit
|
||||
setTimeout(() => compileImage(), 100);
|
||||
};
|
||||
|
||||
const handleAsciiClick = (row: number, col: number, currentValue: number, globalOffset: number) => {
|
||||
const char = currentValue >= 32 && currentValue <= 126 ? String.fromCharCode(currentValue) : '.';
|
||||
setEditingAscii({
|
||||
row,
|
||||
col,
|
||||
value: char,
|
||||
globalOffset
|
||||
});
|
||||
};
|
||||
|
||||
const handleAsciiEdit = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!editingAscii) return;
|
||||
const value = e.target.value.slice(0, 1); // Only allow single character
|
||||
setEditingAscii({ ...editingAscii, value });
|
||||
};
|
||||
|
||||
const handleAsciiSubmit = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && editingAscii) {
|
||||
applyAsciiEdit();
|
||||
} else if (e.key === 'Escape') {
|
||||
setEditingAscii(null);
|
||||
}
|
||||
};
|
||||
|
||||
const applyAsciiEdit = () => {
|
||||
if (!editingAscii || !originalData) return;
|
||||
|
||||
const char = editingAscii.value;
|
||||
if (char.length !== 1) {
|
||||
setEditingAscii(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const newValue = char.charCodeAt(0);
|
||||
|
||||
// Push current state to undo stack
|
||||
const currentData = modifiedData || new Uint8Array(originalData);
|
||||
pushToUndoStack(currentData);
|
||||
|
||||
// Update the full modified data directly
|
||||
const newData = new Uint8Array(currentData);
|
||||
newData[editingAscii.globalOffset] = newValue;
|
||||
|
||||
// Update store with new modified data
|
||||
modifiedFileData.set(newData);
|
||||
|
||||
setEditingAscii(null);
|
||||
// Auto-update image after ASCII edit
|
||||
setTimeout(() => compileImage(), 100);
|
||||
};
|
||||
|
||||
const renderFallbackHexView = (data: Uint8Array) => {
|
||||
const rows = [];
|
||||
const bytesPerRow = BYTES_PER_ROW;
|
||||
|
||||
for (let i = 0; i < data.length; i += bytesPerRow) {
|
||||
const rowData = data.slice(i, i + bytesPerRow);
|
||||
const address = i;
|
||||
|
||||
// Address column
|
||||
const addressStr = address.toString(16).toUpperCase().padStart(8, '0');
|
||||
|
||||
// Hex column
|
||||
const hexBytes = Array.from(rowData).map((byte, colIndex) => {
|
||||
const globalOffset = address + colIndex;
|
||||
|
||||
return (
|
||||
<span
|
||||
key={colIndex}
|
||||
className="hex-byte"
|
||||
onClick={() => handleByteClick(Math.floor(i / bytesPerRow), colIndex, byte, globalOffset)}
|
||||
title={`Click to edit byte at offset ${globalOffset.toString(16).toUpperCase()}`}
|
||||
>
|
||||
{byte.toString(16).padStart(2, '0').toUpperCase()}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
// Pad remaining columns if row is incomplete
|
||||
while (hexBytes.length < bytesPerRow) {
|
||||
hexBytes.push(
|
||||
<span key={hexBytes.length} className="hex-byte hex-byte-empty">
|
||||
{' '}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ASCII column
|
||||
const asciiChars = Array.from(rowData).map((byte, colIndex) => {
|
||||
const globalOffset = address + colIndex;
|
||||
const char = byteToChar(byte);
|
||||
|
||||
return (
|
||||
<span
|
||||
key={colIndex}
|
||||
className="ascii-char"
|
||||
onClick={() => handleAsciiClick(Math.floor(i / bytesPerRow), colIndex, byte, globalOffset)}
|
||||
title={`Click to edit ASCII char at offset ${globalOffset.toString(16).toUpperCase()} (byte value: ${byte})`}
|
||||
>
|
||||
{char}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
// Pad remaining columns if row is incomplete
|
||||
while (asciiChars.length < bytesPerRow) {
|
||||
asciiChars.push(
|
||||
<span key={asciiChars.length} className="ascii-char ascii-char-empty">
|
||||
{' '}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
rows.push(
|
||||
<div key={address} className="hex-row" style={{ height: ROW_HEIGHT }}>
|
||||
<span className="hex-address">{addressStr}</span>
|
||||
<div className="hex-data">
|
||||
{hexBytes.map((byte, idx) => (
|
||||
<React.Fragment key={idx}>
|
||||
{byte}
|
||||
{idx < bytesPerRow - 1 && <span className="hex-space"> </span>}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
<div className="hex-ascii">
|
||||
{asciiChars}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return rows;
|
||||
};
|
||||
|
||||
const renderVirtualizedHexView = () => {
|
||||
if (!virtualScrollData || !viewportData) {
|
||||
// Fallback: render first chunk if virtual scrolling data not available
|
||||
if (!originalData) return null;
|
||||
|
||||
const fallbackData = originalData.slice(0, Math.min(512, originalData.length));
|
||||
return renderFallbackHexView(fallbackData);
|
||||
}
|
||||
|
||||
const rows = [];
|
||||
const bytesPerRow = BYTES_PER_ROW;
|
||||
|
||||
for (let i = 0; i < viewportData.length; i += bytesPerRow) {
|
||||
const rowData = viewportData.slice(i, i + bytesPerRow);
|
||||
const globalRowIndex = virtualScrollData.startRow + Math.floor(i / bytesPerRow);
|
||||
const address = globalRowIndex * bytesPerRow;
|
||||
|
||||
// Address column
|
||||
const addressStr = address.toString(16).toUpperCase().padStart(8, '0');
|
||||
|
||||
// Hex column with editable bytes
|
||||
const hexBytes = Array.from(rowData).map((byte, colIndex) => {
|
||||
const globalOffset = address + colIndex;
|
||||
const isEditing = editingByte?.globalOffset === globalOffset;
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<input
|
||||
key={colIndex}
|
||||
type="text"
|
||||
value={editingByte.value}
|
||||
onChange={handleByteEdit}
|
||||
onKeyDown={handleByteSubmit}
|
||||
onBlur={applyByteEdit}
|
||||
className="hex-byte-input"
|
||||
autoFocus
|
||||
maxLength={2}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
key={colIndex}
|
||||
className="hex-byte"
|
||||
onClick={() => handleByteClick(globalRowIndex, colIndex, byte, globalOffset)}
|
||||
title={`Click to edit byte at offset ${globalOffset.toString(16).toUpperCase()}`}
|
||||
>
|
||||
{byte.toString(16).padStart(2, '0').toUpperCase()}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
// Pad remaining columns if row is incomplete
|
||||
while (hexBytes.length < bytesPerRow) {
|
||||
hexBytes.push(
|
||||
<span key={hexBytes.length} className="hex-byte hex-byte-empty">
|
||||
{' '}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ASCII column with editable characters
|
||||
const asciiChars = Array.from(rowData).map((byte, colIndex) => {
|
||||
const globalOffset = address + colIndex;
|
||||
const isEditingAsciiChar = editingAscii?.globalOffset === globalOffset;
|
||||
|
||||
if (isEditingAsciiChar) {
|
||||
return (
|
||||
<input
|
||||
key={colIndex}
|
||||
type="text"
|
||||
value={editingAscii.value}
|
||||
onChange={handleAsciiEdit}
|
||||
onKeyDown={handleAsciiSubmit}
|
||||
onBlur={applyAsciiEdit}
|
||||
className="ascii-char-input"
|
||||
autoFocus
|
||||
maxLength={1}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const char = byteToChar(byte);
|
||||
return (
|
||||
<span
|
||||
key={colIndex}
|
||||
className="ascii-char"
|
||||
onClick={() => handleAsciiClick(globalRowIndex, colIndex, byte, globalOffset)}
|
||||
title={`Click to edit ASCII char at offset ${globalOffset.toString(16).toUpperCase()} (byte value: ${byte})`}
|
||||
>
|
||||
{char}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
// Pad remaining columns if row is incomplete
|
||||
while (asciiChars.length < bytesPerRow) {
|
||||
asciiChars.push(
|
||||
<span key={asciiChars.length} className="ascii-char ascii-char-empty">
|
||||
{' '}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
rows.push(
|
||||
<div key={address} className="hex-row" style={{ height: ROW_HEIGHT }}>
|
||||
<span className="hex-address">{addressStr}</span>
|
||||
<div className="hex-data">
|
||||
{hexBytes.map((byte, idx) => (
|
||||
<React.Fragment key={idx}>
|
||||
{byte}
|
||||
{idx < bytesPerRow - 1 && <span className="hex-space"> </span>}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
<div className="hex-ascii">
|
||||
{asciiChars}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return rows;
|
||||
};
|
||||
|
||||
const handleJumpToChunk = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const chunkIndex = parseInt(jumpToChunk) - 1;
|
||||
if (!isNaN(chunkIndex) && chunkIndex >= 0 && chunkIndex < totalChunks) {
|
||||
// Calculate the address of the chunk and scroll to it
|
||||
const chunkAddress = chunkIndex * chunkSize;
|
||||
const targetRow = Math.floor(chunkAddress / BYTES_PER_ROW);
|
||||
const targetScrollTop = targetRow * ROW_HEIGHT;
|
||||
|
||||
if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollTop = targetScrollTop;
|
||||
}
|
||||
setScrollTop(targetScrollTop);
|
||||
setJumpToChunk('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleJumpToAddress = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const address = parseInt(jumpToAddress, 16);
|
||||
if (!isNaN(address) && metadata) {
|
||||
if (address >= 0 && address < metadata.fileSize) {
|
||||
// For virtual scrolling, calculate the row and scroll to it
|
||||
const targetRow = Math.floor(address / BYTES_PER_ROW);
|
||||
const targetScrollTop = targetRow * ROW_HEIGHT;
|
||||
|
||||
if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollTop = targetScrollTop;
|
||||
}
|
||||
setScrollTop(targetScrollTop);
|
||||
}
|
||||
setJumpToAddress('');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (!metadata) {
|
||||
return (
|
||||
<div className="hex-editor">
|
||||
<div className="hex-editor-header">
|
||||
<span className="hex-editor-title">
|
||||
Hex Editor - No file loaded
|
||||
</span>
|
||||
</div>
|
||||
<div className="hex-editor-content">
|
||||
<div style={{ padding: '20px', textAlign: 'center', color: '#888' }}>
|
||||
Upload an image to start editing
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!originalData) {
|
||||
return (
|
||||
<div className="hex-editor">
|
||||
<div className="hex-editor-header">
|
||||
<span className="hex-editor-title">
|
||||
Hex Editor - {metadata.fileName} ({metadata.fileSize} bytes)
|
||||
</span>
|
||||
</div>
|
||||
<div className="hex-editor-content">
|
||||
<div style={{ padding: '20px', textAlign: 'center' }}>
|
||||
Loading file data...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={editorRef}
|
||||
className="hex-editor"
|
||||
>
|
||||
<div className="hex-editor-header">
|
||||
<span className="hex-editor-title">
|
||||
Hex Editor - {metadata.fileName} ({metadata.fileSize} bytes)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="hex-editor-content">
|
||||
<div className="hex-controls">
|
||||
<div className="control-row">
|
||||
<form onSubmit={handleJumpToChunk} className="jump-form">
|
||||
<input
|
||||
type="number"
|
||||
value={jumpToChunk}
|
||||
onChange={e => setJumpToChunk(e.target.value)}
|
||||
placeholder="Jump to chunk..."
|
||||
className="jump-input"
|
||||
min="1"
|
||||
max={totalChunks}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<form onSubmit={handleJumpToAddress} className="jump-form">
|
||||
<input
|
||||
type="text"
|
||||
value={jumpToAddress}
|
||||
onChange={e => setJumpToAddress(e.target.value)}
|
||||
placeholder="Go to address (hex)"
|
||||
className="jump-input address-input"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="chunk-info">
|
||||
<small>
|
||||
File: {metadata.fileSize} bytes • Scroll position: {Math.floor(scrollTop / ROW_HEIGHT * BYTES_PER_ROW).toString(16).toUpperCase()}
|
||||
{modified && ' • Modified'}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div className="action-buttons">
|
||||
<button
|
||||
onClick={compileImage}
|
||||
className="action-button compile"
|
||||
disabled={compiling}
|
||||
title="Update Image"
|
||||
>
|
||||
↻
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleRandomize(1)}
|
||||
className="action-button random"
|
||||
disabled={compiling || !originalData}
|
||||
title="Random (Visible)"
|
||||
>
|
||||
1
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleRandomize(10)}
|
||||
className="action-button random"
|
||||
disabled={compiling || !originalData}
|
||||
title="Random × 10 (Visible)"
|
||||
>
|
||||
10
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleRandomize(100)}
|
||||
className="action-button random"
|
||||
disabled={compiling || !originalData}
|
||||
title="Random × 100 (Visible)"
|
||||
>
|
||||
100
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleGlobalRandomize(1)}
|
||||
className="action-button global-random"
|
||||
disabled={compiling || !originalData}
|
||||
title="Global Random"
|
||||
>
|
||||
1
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleGlobalRandomize(10)}
|
||||
className="action-button global-random"
|
||||
disabled={compiling || !originalData}
|
||||
title="Global Random × 10"
|
||||
>
|
||||
10
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleGlobalRandomize(100)}
|
||||
className="action-button global-random"
|
||||
disabled={compiling || !originalData}
|
||||
title="Global Random × 100"
|
||||
>
|
||||
100
|
||||
</button>
|
||||
<button
|
||||
onClick={handleUndo}
|
||||
className="action-button undo"
|
||||
disabled={compiling || !canUndoState}
|
||||
title="Undo"
|
||||
>
|
||||
↶
|
||||
</button>
|
||||
<button
|
||||
onClick={resetToOriginal}
|
||||
className="action-button reset"
|
||||
disabled={compiling}
|
||||
title="Reset"
|
||||
>
|
||||
⟲
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hex-view-container">
|
||||
<div className="hex-view">
|
||||
<div className="hex-header">
|
||||
<span className="hex-address-header">Address</span>
|
||||
<span className="hex-data-header">Hex Data</span>
|
||||
<span className="hex-ascii-header">ASCII</span>
|
||||
</div>
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="hex-content-scroll"
|
||||
style={{
|
||||
height: '400px',
|
||||
overflow: 'auto',
|
||||
position: 'relative'
|
||||
}}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
<div
|
||||
className="hex-content-virtual"
|
||||
style={{
|
||||
height: virtualScrollData?.totalHeight || 0,
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="hex-content-viewport"
|
||||
style={{
|
||||
transform: `translateY(${virtualScrollData?.offsetY || 0}px)`,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0
|
||||
}}
|
||||
>
|
||||
{renderVirtualizedHexView()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hex-input-section">
|
||||
<label>Raw Hex Input (Current View):</label>
|
||||
<textarea
|
||||
className="hex-input"
|
||||
value={hexInput}
|
||||
onChange={handleHexInputChange}
|
||||
placeholder="Enter hex values..."
|
||||
rows={8}
|
||||
style={{ minHeight: '120px', maxHeight: '200px' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user