initialize Flutter project structure and add essential configuration files
This commit is contained in:
916
lib/main.dart
Normal file
916
lib/main.dart
Normal file
@@ -0,0 +1,916 @@
|
||||
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;
|
||||
}
|
||||
2374
lib/widgets/scrollview.dart
Normal file
2374
lib/widgets/scrollview.dart
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user