916 lines
41 KiB
Dart
916 lines
41 KiB
Dart
import 'dart:async';
|
|
import 'dart:collection';
|
|
import 'dart:io';
|
|
import 'dart:math';
|
|
import 'dart:typed_data';
|
|
import 'dart:isolate';
|
|
import 'package:file_picker/file_picker.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'widgets/scrollview.dart' as custom;
|
|
|
|
ThemeProvider _themeProvider = ThemeProvider();
|
|
|
|
void main() async {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
await _themeProvider.loadTheme();
|
|
runApp(
|
|
MultiProvider(
|
|
providers: [
|
|
ChangeNotifierProvider<ThemeProvider>.value(value: _themeProvider),
|
|
],
|
|
child: BinaryViewerApp(),
|
|
),
|
|
);
|
|
}
|
|
|
|
|
|
enum ThemesEnum {
|
|
Light_Default(colorScheme: ColorSchemes.lightDefaultColor),
|
|
Dark_Default(colorScheme: ColorSchemes.darkDefaultColor),
|
|
Light_Blue(colorScheme: ColorSchemes.lightBlue),
|
|
Dark_Blue(colorScheme: ColorSchemes.darkBlue),
|
|
Light_Green(colorScheme: ColorSchemes.lightGreen),
|
|
Dark_Green(colorScheme: ColorSchemes.darkGreen),
|
|
Light_Orange(colorScheme: ColorSchemes.lightOrange),
|
|
Dark_Orange(colorScheme: ColorSchemes.darkOrange),
|
|
Light_Violet(colorScheme: ColorSchemes.lightViolet),
|
|
Dark_Violet(colorScheme: ColorSchemes.darkViolet),
|
|
Light_Rose(colorScheme: ColorSchemes.lightRose),
|
|
Dark_Rose(colorScheme: ColorSchemes.darkRose);
|
|
|
|
final ColorScheme colorScheme;
|
|
const ThemesEnum({required this.colorScheme});
|
|
|
|
|
|
}
|
|
class ThemeProvider extends ChangeNotifier {
|
|
|
|
ThemesEnum _currentTheme = ThemesEnum.Light_Rose;
|
|
|
|
ThemesEnum get currentTheme => _currentTheme;
|
|
|
|
Future<void> loadTheme() async {
|
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
String? themeStr = prefs.getString('theme');
|
|
if (themeStr != null) {
|
|
_currentTheme = ThemesEnum.values.firstWhere((theme) => theme.name == themeStr, orElse: () => ThemesEnum.Light_Rose);
|
|
} else {
|
|
_currentTheme = ThemesEnum.Light_Rose;
|
|
}
|
|
notifyListeners();
|
|
}
|
|
|
|
void setTheme(ThemesEnum theme) async {
|
|
_currentTheme = theme;
|
|
|
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
await prefs.setString('theme', _currentTheme.name);
|
|
|
|
notifyListeners();
|
|
}
|
|
|
|
}
|
|
|
|
class BinaryViewerApp extends StatelessWidget {
|
|
const BinaryViewerApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Consumer<ThemeProvider>(
|
|
builder: (context, themeProvider, child) {
|
|
print("Current theme: ${themeProvider.currentTheme.name}");
|
|
return ShadcnApp(
|
|
title: 'Binary Viewer',
|
|
theme: ThemeData(
|
|
colorScheme: themeProvider.currentTheme.colorScheme,
|
|
radius: 0.5,
|
|
),
|
|
home: BinaryViewerScreen(),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class BinaryViewerScreen extends StatefulWidget {
|
|
@override
|
|
State<BinaryViewerScreen> createState() => _BinaryViewerScreenState();
|
|
}
|
|
|
|
enum ViewMode {
|
|
hex,
|
|
ascii,
|
|
binary,
|
|
integer,
|
|
float,
|
|
double,
|
|
}
|
|
enum ViewEndian {
|
|
little,
|
|
big,
|
|
}
|
|
|
|
class _BinaryViewerScreenState extends State<BinaryViewerScreen> {
|
|
RandomAccessFile? _file;
|
|
int _fileLength = 0;
|
|
|
|
Isolate? _readerIsolate;
|
|
SendPort? _readerSendPort;
|
|
ReceivePort? _mainReceivePort;
|
|
int _isolateId = 0;
|
|
final Map<int, Completer<Uint8List>> _pendingReads = {};
|
|
final Queue<int> _readQueue = Queue<int>();
|
|
Timer? _throttleTimer;
|
|
Timer? _debounceTimer;
|
|
static const int maxReadsPerSecond = 40;
|
|
|
|
ViewEndian _leftEndian = ViewEndian.little;
|
|
ViewMode _leftMode = ViewMode.hex;
|
|
ViewEndian _rightEndian = ViewEndian.little;
|
|
ViewMode _rightMode = ViewMode.ascii;
|
|
|
|
final ScrollController _scrollController = ScrollController();
|
|
final ScrollController _horizontalScrollController = ScrollController();
|
|
int _firstVisibleRow = 0;
|
|
int _lastVisibleRow = 0;
|
|
static const int bufferRows = 10;
|
|
|
|
final Map<int, Uint8List> _rowCache = {};
|
|
|
|
@override
|
|
void dispose() {
|
|
_debounceTimer?.cancel();
|
|
_readerIsolate?.kill(priority: Isolate.immediate);
|
|
_mainReceivePort?.close();
|
|
_throttleTimer?.cancel();
|
|
_scrollController.dispose();
|
|
_horizontalScrollController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _updateVisibleRows() {
|
|
if (_scrollController.hasClients) {
|
|
double offset = _scrollController.offset;
|
|
double rowHeight = 35; // Updated row height based on user info
|
|
int first = (offset / rowHeight).floor();
|
|
int last = ((offset + _scrollController.position.viewportDimension) / rowHeight).ceil();
|
|
int newFirst = max(0, first - bufferRows);
|
|
int newLast = min((_fileLength / 16).ceil() - 1, last + bufferRows);
|
|
if (newFirst != _firstVisibleRow || newLast != _lastVisibleRow) {
|
|
_debounceTimer?.cancel();
|
|
_debounceTimer = Timer(const Duration(milliseconds: 50), () {
|
|
print('setState: _firstVisibleRow=$newFirst, _lastVisibleRow=$newLast');
|
|
setState(() {
|
|
_firstVisibleRow = newFirst;
|
|
_lastVisibleRow = newLast;
|
|
});
|
|
// Evict cache rows far outside the buffer
|
|
final minKeep = max(0, newFirst - bufferRows * 2);
|
|
final maxKeep = min((_fileLength / 16).ceil() - 1, newLast + bufferRows * 2);
|
|
_rowCache.removeWhere((offset, _) {
|
|
int row = offset ~/ 16;
|
|
return row < minKeep || row > maxKeep;
|
|
});
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_scrollController.addListener(_updateVisibleRows);
|
|
}
|
|
|
|
Future<void> _openFile(String path) async {
|
|
RandomAccessFile file = await File(path).open();
|
|
int length = await File(path).length();
|
|
await _file?.close();
|
|
_readerIsolate?.kill(priority: Isolate.immediate);
|
|
_mainReceivePort?.close();
|
|
_mainReceivePort = ReceivePort();
|
|
_isolateId++;
|
|
final isolateId = _isolateId;
|
|
_readerIsolate = await Isolate.spawn(_fileReaderIsolate, {
|
|
'sendPort': _mainReceivePort!.sendPort,
|
|
'path': path,
|
|
'isolateId': isolateId,
|
|
});
|
|
_mainReceivePort!.listen((msg) {
|
|
if (msg is SendPort) {
|
|
_readerSendPort = msg;
|
|
} else if (msg is Map && msg['isolateId'] == _isolateId) {
|
|
int offset = msg['offset'];
|
|
Uint8List bytes = msg['bytes'];
|
|
final completer = _pendingReads.remove(offset);
|
|
if (completer != null && !completer.isCompleted) {
|
|
completer.complete(bytes);
|
|
}
|
|
}
|
|
});
|
|
_throttleTimer?.cancel();
|
|
_throttleTimer = Timer.periodic(Duration(milliseconds: 1000 ~/ maxReadsPerSecond), (_) {
|
|
if (_readQueue.isNotEmpty && _readerSendPort != null) {
|
|
int offset = _readQueue.removeFirst();
|
|
final completer = _pendingReads[offset];
|
|
if (completer != null && !completer.isCompleted) {
|
|
int bytesToRead = min(16, _fileLength - offset);
|
|
if (bytesToRead > 0) {
|
|
_readerSendPort!.send({
|
|
'offset': offset,
|
|
'length': bytesToRead,
|
|
'isolateId': _isolateId,
|
|
});
|
|
} else {
|
|
completer.complete(Uint8List(0));
|
|
_pendingReads.remove(offset);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
setState(() {
|
|
_file = file;
|
|
_fileLength = length;
|
|
});
|
|
// Ensure initial visible rows are calculated and loaded
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_updateVisibleRows();
|
|
});
|
|
}
|
|
|
|
static void _fileReaderIsolate(Map args) async {
|
|
final mainSendPort = args['sendPort'] as SendPort;
|
|
final path = args['path'] as String;
|
|
final isolateId = args['isolateId'] as int;
|
|
final file = await File(path).open();
|
|
final port = ReceivePort();
|
|
mainSendPort.send(port.sendPort);
|
|
await for (final msg in port) {
|
|
if (msg is Map && msg['isolateId'] == isolateId) {
|
|
int offset = msg['offset'];
|
|
int length = msg['length'];
|
|
file.setPositionSync(offset);
|
|
Uint8List bytes = file.readSync(length);
|
|
mainSendPort.send({
|
|
'offset': offset,
|
|
'bytes': bytes,
|
|
'isolateId': isolateId,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<Uint8List> _readBytes(int offset) async {
|
|
if (_file == null) return Uint8List(0);
|
|
if (_pendingReads.containsKey(offset)) {
|
|
return _pendingReads[offset]!.future;
|
|
}
|
|
final completer = Completer<Uint8List>();
|
|
_pendingReads[offset] = completer;
|
|
_readQueue.add(offset);
|
|
return completer.future;
|
|
}
|
|
|
|
int selectedAddress = -1;
|
|
void _onAddressClicked(int address, int byteIndex) {
|
|
int absoluteAddress = address + byteIndex;
|
|
print('Address clicked: 0x${absoluteAddress.toRadixString(16).padLeft(8, '0').toUpperCase()}');
|
|
// TODO: Add your address click handling logic here
|
|
setState(() {
|
|
selectedAddress = absoluteAddress;
|
|
});
|
|
}
|
|
|
|
double _calculateViewModeWidth(ViewMode mode) {
|
|
double width;
|
|
switch (mode) {
|
|
case ViewMode.hex:
|
|
width = 16 * 32;
|
|
break;
|
|
case ViewMode.ascii:
|
|
width = 16 * 24;
|
|
break;
|
|
case ViewMode.binary:
|
|
width = 16 * 75;
|
|
break;
|
|
case ViewMode.integer:
|
|
width = 4 * 96;
|
|
break;
|
|
case ViewMode.float:
|
|
width = 4 * 190;
|
|
break;
|
|
case ViewMode.double:
|
|
width = 2 * 190;
|
|
break;
|
|
}
|
|
|
|
// Account for padding
|
|
width += 20;
|
|
|
|
return max(width, 330);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
|
|
double leftWidth = _calculateViewModeWidth(_leftMode);
|
|
double rightWidth = _calculateViewModeWidth(_rightMode);
|
|
|
|
|
|
return Scaffold(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
|
|
Row(
|
|
children: [
|
|
Menubar(
|
|
children: [
|
|
MenuButton(
|
|
subMenu: [
|
|
MenuButton(
|
|
child: const Text("Open"),
|
|
onPressed: (context) async {
|
|
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
|
initialDirectory: _file != null ? File(_file!.path).parent.path : null,
|
|
);
|
|
if (result != null && result.files.isNotEmpty) {
|
|
String path = result.files.first.path!;
|
|
await _openFile(path);
|
|
}
|
|
},
|
|
)
|
|
],
|
|
child: const Text("File"),
|
|
),
|
|
MenuButton(
|
|
subMenu: [
|
|
MenuButton(
|
|
subMenu: [
|
|
for (var theme in ThemesEnum.values)
|
|
MenuButton(
|
|
leading: Container(
|
|
width: 16,
|
|
height: 16,
|
|
decoration: BoxDecoration(
|
|
color: theme.colorScheme.primary,
|
|
borderRadius: BorderRadius.circular(4),
|
|
border: Border.all(
|
|
color: theme.colorScheme.primaryForeground,
|
|
width: 1,
|
|
),
|
|
),
|
|
),
|
|
child: Text(theme.name.replaceAll('_', ' ')),
|
|
onPressed: (context) {
|
|
_themeProvider.setTheme(theme);
|
|
},
|
|
)
|
|
],
|
|
child: const Text("Theme"),
|
|
),
|
|
],
|
|
child: const Text("Options"),
|
|
)
|
|
],
|
|
).withMargin(
|
|
horizontal: 10
|
|
),
|
|
|
|
Builder(
|
|
builder: (context) {
|
|
|
|
if (_file == null) {
|
|
return Breadcrumb(
|
|
children: [
|
|
const Text("No file opened"),
|
|
],
|
|
);
|
|
}
|
|
|
|
List<String> parts = _file?.path.split(Platform.pathSeparator) ?? [];
|
|
|
|
String fileName = parts.last;
|
|
String parentDir = parts.length > 1 ? parts[parts.length - 2] : "";
|
|
|
|
return Breadcrumb(
|
|
children: [
|
|
const MoreDots(),
|
|
Text(parentDir),
|
|
Text(fileName),
|
|
],
|
|
);
|
|
}
|
|
)
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 6),
|
|
|
|
Expanded(
|
|
child: OutlinedContainer(
|
|
child: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
return custom.RawScrollbar(
|
|
controller: _scrollController,
|
|
thumbVisibility: true,
|
|
trackVisibility: true,
|
|
notificationPredicate: (notification) => notification.depth == 1,
|
|
scrollbarMargin: EdgeInsets.only(left: 2, top: 2, right: 1),
|
|
renderOffset: Offset(0, 56),
|
|
radius: Radius.circular(10),
|
|
thumbColor: _themeProvider._currentTheme.colorScheme.primaryForeground,
|
|
thickness: 8,
|
|
trackBorderColor: _themeProvider._currentTheme.colorScheme.border,
|
|
trackColor: _themeProvider._currentTheme.colorScheme.background,
|
|
child: custom.RawScrollbar(
|
|
controller: _horizontalScrollController,
|
|
thumbColor: _themeProvider._currentTheme.colorScheme.primaryForeground,
|
|
thickness: 8,
|
|
trackBorderColor: _themeProvider._currentTheme.colorScheme.border,
|
|
trackColor: _themeProvider._currentTheme.colorScheme.background,
|
|
trackVisibility: true,
|
|
thumbVisibility: true,
|
|
interactive: true,
|
|
forceInteractive: true,
|
|
radius: Radius.circular(10),
|
|
scrollbarMargin: EdgeInsets.only(left: 2, top: 3, bottom: 2, right: 11),
|
|
|
|
child: SingleChildScrollView(
|
|
controller: _horizontalScrollController,
|
|
scrollDirection: Axis.horizontal,
|
|
physics: const ClampingScrollPhysics(),
|
|
child: SizedBox(
|
|
width: max(100 + leftWidth + rightWidth + 60, constraints.maxWidth),
|
|
height: constraints.maxHeight,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
IntrinsicHeight(
|
|
child: Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 100,
|
|
child: Center(
|
|
child: Text(
|
|
"Address",
|
|
).withMargin(
|
|
all: 10
|
|
),
|
|
),
|
|
),
|
|
VerticalDivider(),
|
|
SizedBox(
|
|
width: leftWidth,
|
|
child: Row(
|
|
children: [
|
|
const SizedBox(width: 10),
|
|
SizedBox(
|
|
width: 200,
|
|
child: Select<String>(
|
|
itemBuilder: (context, item) {
|
|
return Text(
|
|
item,
|
|
);
|
|
},
|
|
value: _leftMode.name,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_leftMode = ViewMode.values.firstWhere((mode) => mode.name == value);
|
|
});
|
|
},
|
|
popup: SelectPopup(
|
|
items: SelectItemList(
|
|
children: [
|
|
for (var mode in ViewMode.values)
|
|
SelectItemButton(
|
|
value: mode.name,
|
|
child: Text(mode.name),
|
|
)
|
|
]
|
|
)
|
|
),
|
|
).withMargin(
|
|
vertical: 10
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
SizedBox(
|
|
width: 100,
|
|
child: Select<String>(
|
|
itemBuilder: (context, item) {
|
|
return Text(
|
|
item,
|
|
);
|
|
},
|
|
value: _leftEndian.name,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_leftEndian = ViewEndian.values.firstWhere((mode) => mode.name == value);
|
|
});
|
|
},
|
|
popup: SelectPopup(
|
|
items: SelectItemList(
|
|
children: [
|
|
for (var mode in ViewEndian.values)
|
|
SelectItemButton(
|
|
value: mode.name,
|
|
child: Text(mode.name),
|
|
)
|
|
]
|
|
)
|
|
),
|
|
).withMargin(
|
|
vertical: 10
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
],
|
|
),
|
|
),
|
|
VerticalDivider(),
|
|
SizedBox(
|
|
width: rightWidth,
|
|
child: Row(
|
|
children: [
|
|
const SizedBox(width: 10),
|
|
SizedBox(
|
|
width: 200,
|
|
child: Select<String>(
|
|
itemBuilder: (context, item) {
|
|
return Text(
|
|
item,
|
|
);
|
|
},
|
|
value: _rightMode.name,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_rightMode = ViewMode.values.firstWhere((mode) => mode.name == value);
|
|
});
|
|
},
|
|
popup: SelectPopup(
|
|
items: SelectItemList(
|
|
children: [
|
|
for (var mode in ViewMode.values)
|
|
SelectItemButton(
|
|
value: mode.name,
|
|
child: Text(mode.name),
|
|
)
|
|
]
|
|
)
|
|
),
|
|
).withMargin(
|
|
vertical: 10
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
SizedBox(
|
|
width: 100,
|
|
child: Select<String>(
|
|
itemBuilder: (context, item) {
|
|
return Text(
|
|
item,
|
|
);
|
|
},
|
|
value: _rightEndian.name,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_rightEndian = ViewEndian.values.firstWhere((mode) => mode.name == value);
|
|
});
|
|
},
|
|
popup: SelectPopup(
|
|
items: SelectItemList(
|
|
children: [
|
|
for (var mode in ViewEndian.values)
|
|
SelectItemButton(
|
|
value: mode.name,
|
|
child: Text(mode.name),
|
|
)
|
|
]
|
|
)
|
|
),
|
|
).withMargin(
|
|
vertical: 10
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Divider(),
|
|
if (_file != null)
|
|
Expanded(
|
|
child: ScrollConfiguration(
|
|
behavior: ScrollConfiguration.of(context).copyWith(
|
|
scrollbars: false,
|
|
overscroll: false,
|
|
),
|
|
child: SizedBox(
|
|
width: double.infinity,
|
|
child: NotificationListener<ScrollNotification>(
|
|
onNotification: (scrollInfo) {
|
|
_updateVisibleRows();
|
|
return false;
|
|
},
|
|
child: SizedBox(
|
|
width: max(100 + leftWidth + rightWidth + 60, MediaQuery.of(context).size.width - 20),
|
|
child: ListView.builder(
|
|
controller: _scrollController,
|
|
itemCount: (_fileLength / 16).ceil() + 1,
|
|
itemExtent: 35.0, // Match the actual row height
|
|
cacheExtent: 2000.0, // Increase cache for smoother scrolling
|
|
addAutomaticKeepAlives: false, // Reduce memory usage
|
|
addRepaintBoundaries: true, // Improve repaint performance
|
|
|
|
itemBuilder: (context, i) {
|
|
String address = (i * 16).toRadixString(16).padLeft(8, '0').toUpperCase();
|
|
address = "0x$address";
|
|
|
|
if (i == (_fileLength / 16).ceil()) {
|
|
return SizedBox(
|
|
height: 35,
|
|
child: Column(
|
|
children: [
|
|
Divider(),
|
|
Expanded(
|
|
child: Center(
|
|
child: Center(child: Text('End of File', style: TextStyle(fontSize: 14)).h4),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
if (i < _firstVisibleRow || i > _lastVisibleRow) {
|
|
return IntrinsicHeight(
|
|
child: Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 100,
|
|
child: Center(
|
|
child: Text(
|
|
address,
|
|
style: TextStyle(fontSize: 12),
|
|
).muted,
|
|
),
|
|
).withMargin(vertical: 10),
|
|
VerticalDivider(),
|
|
AspectRatio(
|
|
aspectRatio: 1,
|
|
child: CircularProgressIndicator().withMargin(all: 10)
|
|
)
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
if (_rowCache.containsKey(i * 16)) {
|
|
// Row data is cached, build directly
|
|
Uint8List bytes = _rowCache[i * 16]!;
|
|
String address = (i * 16).toRadixString(16).padLeft(8, '0').toUpperCase();
|
|
address = "0x$address";
|
|
return IntrinsicHeight(
|
|
child: Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 100,
|
|
child: Center(
|
|
child: Text(
|
|
address,
|
|
style: TextStyle(fontSize: 12),
|
|
).muted,
|
|
),
|
|
).withMargin(vertical: 10),
|
|
VerticalDivider(),
|
|
SizedBox(
|
|
width: leftWidth - 20,
|
|
child: Row(
|
|
children: [
|
|
...getView(context, _leftMode, _leftEndian, bytes, i * 16, _onAddressClicked, selectedAddress),
|
|
],
|
|
),
|
|
).withMargin(horizontal: 10),
|
|
VerticalDivider(),
|
|
SizedBox(
|
|
width: rightWidth - 20,
|
|
child: Row(
|
|
children: [
|
|
...getView(context, _rightMode, _rightEndian, bytes, i * 16, _onAddressClicked, selectedAddress),
|
|
],
|
|
),
|
|
).withMargin(horizontal: 10),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// Not cached, use FutureBuilder
|
|
return FutureBuilder<Uint8List>(
|
|
future: _readBytes(i * 16),
|
|
builder: (context, snapshot) {
|
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
return IntrinsicHeight(
|
|
child: Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 100,
|
|
child: Center(
|
|
child: Text(
|
|
address,
|
|
style: TextStyle(fontSize: 12),
|
|
).muted,
|
|
),
|
|
).withMargin(vertical: 10),
|
|
VerticalDivider(),
|
|
AspectRatio(
|
|
aspectRatio: 1,
|
|
child: CircularProgressIndicator().withMargin(all: 10)
|
|
)
|
|
],
|
|
),
|
|
);
|
|
}
|
|
if (snapshot.hasError) {
|
|
print('Error loading bytes for offset ${i * 16}: ${snapshot.error}');
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
|
child: Center(child: Text('Error loading data', style: TextStyle(color: Colors.red))),
|
|
);
|
|
}
|
|
Uint8List bytes = snapshot.data ?? Uint8List(0);
|
|
// Cache the loaded data
|
|
_rowCache[i * 16] = bytes;
|
|
|
|
return IntrinsicHeight(
|
|
child: Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 100,
|
|
child: Center(
|
|
child: Text(
|
|
address,
|
|
style: TextStyle(fontSize: 12),
|
|
).muted,
|
|
),
|
|
).withMargin(vertical: 10),
|
|
VerticalDivider(),
|
|
SizedBox(
|
|
width: leftWidth - 20,
|
|
child: Row(
|
|
children: [
|
|
...getView(context, _leftMode, _leftEndian, bytes, i * 16, _onAddressClicked, selectedAddress),
|
|
],
|
|
),
|
|
).withMargin(horizontal: 10),
|
|
VerticalDivider(),
|
|
SizedBox(
|
|
width: rightWidth - 20,
|
|
child: Row(
|
|
children: [
|
|
...getView(context, _rightMode, _rightEndian, bytes, i * 16, _onAddressClicked, selectedAddress),
|
|
],
|
|
),
|
|
).withMargin(horizontal: 10),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
).withMargin(
|
|
horizontal: 10
|
|
),
|
|
),
|
|
|
|
],
|
|
).withMargin(
|
|
vertical: 10
|
|
),
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
Widget _createViewButton(String text, int byteOffset, int baseAddress, Function(int, int) onAddressClick, int selectedAddress, {double? width, bool isBold = false, AlignmentGeometry? alignment}) {
|
|
int currentAddress = baseAddress + byteOffset;
|
|
bool isSelected = currentAddress == selectedAddress;
|
|
|
|
Widget button;
|
|
|
|
if (isSelected) {
|
|
button = SizedBox(
|
|
child: OutlinedContainer(
|
|
padding: EdgeInsets.symmetric(horizontal: 7),
|
|
borderRadius: BorderRadius.circular(8),
|
|
height: 31,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: _themeProvider.currentTheme.colorScheme.primary.withOpacity(0.5),
|
|
blurRadius: 4,
|
|
spreadRadius: 2,
|
|
),
|
|
],
|
|
child: Align(
|
|
alignment: alignment ?? Alignment.centerLeft,
|
|
child: Text(
|
|
text,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: isBold ? FontWeight.bold : null,
|
|
),
|
|
).mono,
|
|
),
|
|
),
|
|
);
|
|
} else {
|
|
button = GhostButton(
|
|
onPressed: () => onAddressClick(baseAddress, byteOffset),
|
|
density: ButtonDensity.icon,
|
|
alignment: alignment ?? Alignment.centerLeft,
|
|
child: Text(
|
|
text,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: isBold ? FontWeight.bold : null,
|
|
),
|
|
).mono,
|
|
);
|
|
}
|
|
|
|
|
|
|
|
return width != null ? SizedBox(width: width, child: button) : button;
|
|
}
|
|
|
|
List<Widget> getView(BuildContext context, ViewMode mode, ViewEndian endian, Uint8List bytes, int baseAddress, Function(int, int) onAddressClick, int selectedAddress) {
|
|
List<Widget> leftWidgets = [];
|
|
|
|
if (mode == ViewMode.hex) {
|
|
for (int i = 0; i < bytes.length; i++) {
|
|
String byteStr = bytes[i].toRadixString(16).padLeft(2, '0').toUpperCase();
|
|
leftWidgets.add(_createViewButton( byteStr, i, baseAddress, onAddressClick, selectedAddress, width: 32, isBold: true));
|
|
}
|
|
} else if (mode == ViewMode.ascii) {
|
|
for (int i = 0; i < bytes.length; i++) {
|
|
String char = bytes[i] >= 32 && bytes[i] <= 126 ? String.fromCharCode(bytes[i]) : '.';
|
|
leftWidgets.add(_createViewButton( char, i, baseAddress, onAddressClick, selectedAddress, width: 24));
|
|
}
|
|
} else if (mode == ViewMode.binary) {
|
|
for (int i = 0; i < bytes.length; i++) {
|
|
String byteStr = bytes[i].toRadixString(2).padLeft(8, '0');
|
|
leftWidgets.add(_createViewButton( byteStr, i, baseAddress, onAddressClick, selectedAddress));
|
|
}
|
|
} else if (mode == ViewMode.integer) {
|
|
ByteData byteData = ByteData.sublistView(bytes);
|
|
int maxIntegers = bytes.length ~/ 4;
|
|
for (int j = 0; j < 4 && j < maxIntegers; j++) {
|
|
if (j * 4 + 4 <= bytes.length) {
|
|
int value = byteData.getInt32(j * 4, endian == ViewEndian.little ? Endian.little : Endian.big);
|
|
String valueStr = value.toString().padLeft(11, ' ');
|
|
leftWidgets.add(_createViewButton( valueStr, j * 4, baseAddress, onAddressClick, selectedAddress, alignment: Alignment.centerLeft));
|
|
}
|
|
}
|
|
} else if (mode == ViewMode.float) {
|
|
ByteData byteData = ByteData.sublistView(bytes);
|
|
int maxFloats = bytes.length ~/ 4;
|
|
for (int j = 0; j < 4 && j < maxFloats; j++) {
|
|
if (j * 4 + 4 <= bytes.length) {
|
|
double value = byteData.getFloat32(j * 4, endian == ViewEndian.little ? Endian.little : Endian.big);
|
|
String valueStr = value.toStringAsFixed(2);
|
|
leftWidgets.add(_createViewButton( valueStr, j * 4, baseAddress, onAddressClick, selectedAddress, width: 190, alignment: Alignment.centerLeft));
|
|
}
|
|
}
|
|
} else if (mode == ViewMode.double) {
|
|
ByteData byteData = ByteData.sublistView(bytes);
|
|
int maxDoubles = bytes.length ~/ 8;
|
|
for (int j = 0; j < 2 && j < maxDoubles; j++) {
|
|
if (j * 8 + 8 <= bytes.length) {
|
|
double value = byteData.getFloat64(j * 8, endian == ViewEndian.little ? Endian.little : Endian.big);
|
|
String valueStr = value.toStringAsFixed(2);
|
|
leftWidgets.add(_createViewButton(valueStr, j * 8, baseAddress, onAddressClick, selectedAddress, width: 190, alignment: Alignment.centerLeft));
|
|
}
|
|
}
|
|
}
|
|
return leftWidgets;
|
|
} |