initialize Flutter project structure and add essential configuration files

This commit is contained in:
ImBenji
2025-09-04 04:22:23 +01:00
parent 19c9526bc7
commit fede06835a
134 changed files with 8883 additions and 0 deletions

916
lib/main.dart Normal file
View 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

File diff suppressed because it is too large Load Diff