diff --git a/lib/main.dart b/lib/main.dart index f0272c5..eb69b48 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,12 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'src/app.dart'; import 'src/settings/settings_controller.dart'; import 'src/settings/settings_service.dart'; -void main() async { +void main(List args) async { // Set up the SettingsController, which will glue user settings to multiple // Flutter Widgets. final settingsController = SettingsController(SettingsService()); @@ -13,8 +15,13 @@ void main() async { // This prevents a sudden theme change when the app is first displayed. await settingsController.loadSettings(); + File? file; + if (args.isNotEmpty) { + file = File(args[0]); + } + // Run the app and pass in the SettingsController. The app listens to the // SettingsController for changes, then passes it further down to the // SettingsView. - runApp(LogViewerApp(settingsController: settingsController)); + runApp(LogViewerApp(settingsController: settingsController, file: file)); } diff --git a/lib/src/app.dart b/lib/src/app.dart index 6812714..0b9225c 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -6,16 +6,24 @@ import 'package:desktop_drop/desktop_drop.dart'; class LogViewerApp extends StatefulWidget { final SettingsController settingsController; - const LogViewerApp({super.key, required this.settingsController}); + final File? file; + const LogViewerApp( + {super.key, required this.settingsController, required this.file}); @override - LogViewerAppState createState() => - LogViewerAppState(settingsController: settingsController); + LogViewerAppState createState() => LogViewerAppState(); } class LogViewerAppState extends State { - final SettingsController settingsController; - LogViewerAppState({required this.settingsController}); + late SettingsController settingsController; + late File? file; + + @override + void initState() { + super.initState(); + settingsController = widget.settingsController; + file = widget.file; + } @override Widget build(BuildContext context) { @@ -23,6 +31,7 @@ class LogViewerAppState extends State { listenable: settingsController, builder: (BuildContext context, Widget? child) { return MaterialApp( + debugShowCheckedModeBanner: false, title: 'Fast Log Viewer', theme: ThemeData( primarySwatch: Colors.blue, @@ -34,6 +43,7 @@ class LogViewerAppState extends State { ), themeMode: settingsController.themeMode, home: LogViewerScreen( + file: file, onThemeChanged: (ThemeMode mode) async { await settingsController.updateThemeMode(mode); }, @@ -47,13 +57,16 @@ class LogViewerAppState extends State { class LogViewerScreen extends StatefulWidget { final Function(ThemeMode) onThemeChanged; final SettingsController settingsController; + final File? file; const LogViewerScreen( - {required this.onThemeChanged, required this.settingsController}); + {super.key, + required this.onThemeChanged, + required this.settingsController, + required this.file}); @override - LogViewerScreenState createState() => - LogViewerScreenState(settingsController: settingsController); + LogViewerScreenState createState() => LogViewerScreenState(); } class LogViewerScreenState extends State { @@ -62,144 +75,139 @@ class LogViewerScreenState extends State { String search = ''; LogLevel selectedLogLevel = LogLevel.info; bool _isDragging = false; - int? selectedLogIndex; - final SettingsController settingsController; - LogViewerScreenState({required this.settingsController}); + late SettingsController settingsController; + @override + void initState() { + super.initState(); + if (widget.file != null) { + widget.file?.readAsString().then((String fileContent) { + logs = _parseLogFile(fileContent); + _filterLogs(); + }); + } + settingsController = widget.settingsController; + } @override Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - setState(() { - selectedLogIndex = null; - FocusScope.of(context).unfocus(); - }); - }, - child: Scaffold( - appBar: AppBar( - title: const Text('Log Viewer'), - actions: [ - IconButton( - icon: const Icon(Icons.folder_open), - onPressed: _openFile, - ), - ], - ), - body: DropTarget( - onDragEntered: (details) { - setState(() { - _isDragging = true; - }); - }, - onDragExited: (details) { + return Scaffold( + appBar: AppBar( + title: const Text('Fast Log Viewer'), + actions: [ + DropdownButton( + value: settingsController.themeMode, + onChanged: (ThemeMode? newValue) async { + if (newValue != null) { + await settingsController.updateThemeMode(newValue); + setState(() { + widget.onThemeChanged(newValue); + }); + } + }, + items: const [ + DropdownMenuItem( + value: ThemeMode.light, + child: Text('Light'), + ), + DropdownMenuItem( + value: ThemeMode.dark, + child: Text('Dark'), + ), + DropdownMenuItem( + value: ThemeMode.system, + child: Text('System'), + ), + ], + ), + IconButton( + icon: const Icon(Icons.folder_open), + onPressed: _openFile, + ), + ], + ), + body: DropTarget( + onDragEntered: (details) { + setState(() { + _isDragging = true; + }); + }, + onDragExited: (details) { + setState(() { + _isDragging = false; + }); + }, + onDragDone: (details) async { + if (details.files.isNotEmpty) { + File file = File(details.files.first.path); + String fileContent = await file.readAsString(); setState(() { + logs = _parseLogFile(fileContent); + _filterLogs(); _isDragging = false; }); - }, - onDragDone: (details) async { - if (details.files.isNotEmpty) { - File file = File(details.files.first.path); - String fileContent = await file.readAsString(); - setState(() { - logs = _parseLogFile(fileContent); - _filterLogs(); - _isDragging = false; - }); - } - }, - child: Container( - color: _isDragging - ? Colors.grey[200] - : Theme.of(context).scaffoldBackgroundColor, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Expanded( - child: TextField( - decoration: const InputDecoration( - labelText: 'Search Logs', - border: OutlineInputBorder(), - ), - onChanged: (value) { - setState(() { - search = value; - _filterLogs(); - }); - }, + } + }, + child: Container( + color: _isDragging + ? Colors.grey[200] + : Theme.of(context).scaffoldBackgroundColor, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: TextField( + decoration: const InputDecoration( + labelText: 'Search Logs', + border: OutlineInputBorder(), ), - ), - const SizedBox(width: 8.0), - DropdownButton( - value: selectedLogLevel, - onChanged: (LogLevel? newValue) { - if (newValue != null) { - setState(() { - selectedLogLevel = newValue; - _filterLogs(); - }); - } - }, - items: LogLevel.values - .map>((LogLevel level) { - return DropdownMenuItem( - value: level, - child: Text(level.name), - ); - }).toList(), - ), - const SizedBox(width: 8.0), - DropdownButton( - value: settingsController.themeMode, - onChanged: (ThemeMode? newValue) async { - if (newValue != null) { - await settingsController.updateThemeMode(newValue); - setState(() { - widget.onThemeChanged(newValue); - }); - } - }, - items: const [ - DropdownMenuItem( - value: ThemeMode.light, - child: Text('Light'), - ), - DropdownMenuItem( - value: ThemeMode.dark, - child: Text('Dark'), - ), - DropdownMenuItem( - value: ThemeMode.system, - child: Text('System'), - ), - ], - ), - ], - ), - ), - Expanded( - child: ListView.builder( - itemCount: visibleLogs.length, - itemBuilder: (context, index) { - return GestureDetector( - onTap: () { + onChanged: (value) { setState(() { - selectedLogIndex = index; + search = value; + _filterLogs(); }); }, - child: LogEntryWidget( - log: visibleLogs[index], - isSelected: selectedLogIndex == index, - ), - ); - }, - ), + ), + ), + const SizedBox(width: 8.0), + DropdownButton( + value: selectedLogLevel, + onChanged: (LogLevel? newValue) { + if (newValue != null) { + setState(() { + selectedLogLevel = newValue; + _filterLogs(); + }); + } + }, + items: LogLevel.values + .map>((LogLevel level) { + return DropdownMenuItem( + value: level, + child: Text(level.name), + ); + }).toList(), + ), + ], ), - ], - ), + ), + Expanded( + child: SelectionArea( + child: ListView.builder( + itemCount: visibleLogs.length, + itemBuilder: (context, index) { + return GestureDetector( + child: LogEntryWidget( + log: visibleLogs[index], + ), + ); + }, + )), + ), + ], ), ), ), @@ -334,45 +342,33 @@ class LogEntry { class LogEntryWidget extends StatelessWidget { final LogEntry log; - final bool isSelected; - const LogEntryWidget( - {super.key, required this.log, required this.isSelected}); + const LogEntryWidget({super.key, required this.log}); @override Widget build(BuildContext context) { return Card( - color: isSelected ? Colors.blue[100] : null, margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), child: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SelectableText( + Text( '${log.timestamp} - [${log.level.name}] - ${log.category} - ${log.threadId}', style: TextStyle( fontWeight: FontWeight.bold, color: _getLevelColor(log.level), ), - onTap: () { - FocusScope.of(context).unfocus(); - }, ), - SelectableText( + Text( 'Component: ${log.component.name}@${log.component.address}', style: const TextStyle( fontStyle: FontStyle.italic, ), - onTap: () { - FocusScope.of(context).unfocus(); - }, ), - SelectableText( + Text( log.message.join('\n'), - onTap: () { - FocusScope.of(context).unfocus(); - }, ), ], ), diff --git a/lib/src/sample_feature/sample_item.dart b/lib/src/sample_feature/sample_item.dart deleted file mode 100644 index b376e0d..0000000 --- a/lib/src/sample_feature/sample_item.dart +++ /dev/null @@ -1,6 +0,0 @@ -/// A placeholder class that represents an entity or model. -class SampleItem { - const SampleItem(this.id); - - final int id; -} diff --git a/lib/src/sample_feature/sample_item_details_view.dart b/lib/src/sample_feature/sample_item_details_view.dart deleted file mode 100644 index 37df4a8..0000000 --- a/lib/src/sample_feature/sample_item_details_view.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; - -/// Displays detailed information about a SampleItem. -class SampleItemDetailsView extends StatelessWidget { - const SampleItemDetailsView({super.key}); - - static const routeName = '/sample_item'; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Item Details'), - ), - body: const Center( - child: Text('More Information Here'), - ), - ); - } -} diff --git a/lib/src/sample_feature/sample_item_list_view.dart b/lib/src/sample_feature/sample_item_list_view.dart deleted file mode 100644 index 78066e9..0000000 --- a/lib/src/sample_feature/sample_item_list_view.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../settings/settings_view.dart'; -import 'sample_item.dart'; -import 'sample_item_details_view.dart'; - -/// Displays a list of SampleItems. -class SampleItemListView extends StatelessWidget { - const SampleItemListView({ - super.key, - this.items = const [SampleItem(1), SampleItem(2), SampleItem(3)], - }); - - static const routeName = '/'; - - final List items; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Sample Items'), - actions: [ - IconButton( - icon: const Icon(Icons.settings), - onPressed: () { - // Navigate to the settings page. If the user leaves and returns - // to the app after it has been killed while running in the - // background, the navigation stack is restored. - Navigator.restorablePushNamed(context, SettingsView.routeName); - }, - ), - ], - ), - - // To work with lists that may contain a large number of items, it’s best - // to use the ListView.builder constructor. - // - // In contrast to the default ListView constructor, which requires - // building all Widgets up front, the ListView.builder constructor lazily - // builds Widgets as they’re scrolled into view. - body: ListView.builder( - // Providing a restorationId allows the ListView to restore the - // scroll position when a user leaves and returns to the app after it - // has been killed while running in the background. - restorationId: 'sampleItemListView', - itemCount: items.length, - itemBuilder: (BuildContext context, int index) { - final item = items[index]; - - return ListTile( - title: Text('SampleItem ${item.id}'), - leading: const CircleAvatar( - // Display the Flutter Logo image asset. - foregroundImage: AssetImage('assets/images/flutter_logo.png'), - ), - onTap: () { - // Navigate to the details page. If the user leaves and returns to - // the app after it has been killed while running in the - // background, the navigation stack is restored. - Navigator.restorablePushNamed( - context, - SampleItemDetailsView.routeName, - ); - } - ); - }, - ), - ); - } -}