diff --git a/lib/main.dart b/lib/main.dart index 5b420c5..f0272c5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,6 +16,5 @@ void main() async { // 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()); + runApp(LogViewerApp(settingsController: settingsController)); } diff --git a/lib/src/app.dart b/lib/src/app.dart index ee16d1c..6812714 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -1,97 +1,209 @@ +import 'package:fast_log_viewer/src/settings/settings_controller.dart'; import 'package:flutter/material.dart'; import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:desktop_drop/desktop_drop.dart'; -class LogViewerApp extends StatelessWidget { +class LogViewerApp extends StatefulWidget { + final SettingsController settingsController; + const LogViewerApp({super.key, required this.settingsController}); + + @override + LogViewerAppState createState() => + LogViewerAppState(settingsController: settingsController); +} + +class LogViewerAppState extends State { + final SettingsController settingsController; + LogViewerAppState({required this.settingsController}); + @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Log Viewer', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: LogViewerScreen(), - ); + return ListenableBuilder( + listenable: settingsController, + builder: (BuildContext context, Widget? child) { + return MaterialApp( + title: 'Fast Log Viewer', + theme: ThemeData( + primarySwatch: Colors.blue, + brightness: Brightness.light, + ), + darkTheme: ThemeData( + primarySwatch: Colors.blue, + brightness: Brightness.dark, + ), + themeMode: settingsController.themeMode, + home: LogViewerScreen( + onThemeChanged: (ThemeMode mode) async { + await settingsController.updateThemeMode(mode); + }, + settingsController: settingsController, + ), + ); + }); } } class LogViewerScreen extends StatefulWidget { + final Function(ThemeMode) onThemeChanged; + final SettingsController settingsController; + + const LogViewerScreen( + {required this.onThemeChanged, required this.settingsController}); + @override - _LogViewerScreenState createState() => _LogViewerScreenState(); + LogViewerScreenState createState() => + LogViewerScreenState(settingsController: settingsController); } -class _LogViewerScreenState extends State { +class LogViewerScreenState extends State { List logs = []; List visibleLogs = []; String search = ''; + LogLevel selectedLogLevel = LogLevel.info; bool _isDragging = false; + int? selectedLogIndex; + + final SettingsController settingsController; + LogViewerScreenState({required this.settingsController}); @override Widget build(BuildContext context) { - return Scaffold( + return GestureDetector( + onTap: () { + setState(() { + selectedLogIndex = null; + FocusScope.of(context).unfocus(); + }); + }, + child: Scaffold( appBar: AppBar( title: const Text('Log Viewer'), actions: [ IconButton( - icon: Icon(Icons.folder_open), + icon: const Icon(Icons.folder_open), onPressed: _openFile, ), ], ), body: DropTarget( - onDragEntered: (details) { - setState(() { - _isDragging = true; - }); - }, - onDragExited: (details) { + 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); - visibleLogs = logs.toList(); - _isDragging = false; - }); - } - }, - child: Container( - color: _isDragging ? Colors.grey[200] : Colors.white, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: TextField( - decoration: const InputDecoration( - labelText: 'Search Logs', - border: OutlineInputBorder(), + } + }, + 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(); + }); + }, + ), ), - onChanged: (value) { - setState(() { - search = value; - visibleLogs = - logs.where((log) => log.contains(value)).toList(); - }); - }, - ), + 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 LogEntryWidget(log: visibleLogs[index]); - }, - ), + ), + Expanded( + child: ListView.builder( + itemCount: visibleLogs.length, + itemBuilder: (context, index) { + return GestureDetector( + onTap: () { + setState(() { + selectedLogIndex = index; + }); + }, + child: LogEntryWidget( + log: visibleLogs[index], + isSelected: selectedLogIndex == index, + ), + ); + }, ), - ], - ), - ))); + ), + ], + ), + ), + ), + ), + ); } Future _openFile() async { @@ -101,11 +213,20 @@ class _LogViewerScreenState extends State { String fileContent = await file.readAsString(); setState(() { logs = _parseLogFile(fileContent); - visibleLogs = logs.where((log) => log.contains(search)).toList(); + _filterLogs(); }); } } + void _filterLogs() { + setState(() { + visibleLogs = logs + .where((log) => + log.level.index >= selectedLogLevel.index && log.contains(search)) + .toList(); + }); + } + List _parseLogFile(String content) { List parsedLogs = []; List lines = content.split('\n'); @@ -213,32 +334,46 @@ class LogEntry { class LogEntryWidget extends StatelessWidget { final LogEntry log; + final bool isSelected; - const LogEntryWidget({Key? key, required this.log}) : super(key: key); + const LogEntryWidget( + {super.key, required this.log, required this.isSelected}); @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: [ - Text( + SelectableText( '${log.timestamp} - [${log.level.name}] - ${log.category} - ${log.threadId}', style: TextStyle( fontWeight: FontWeight.bold, color: _getLevelColor(log.level), ), + onTap: () { + FocusScope.of(context).unfocus(); + }, ), - Text( + SelectableText( 'Component: ${log.component.name}@${log.component.address}', style: const TextStyle( fontStyle: FontStyle.italic, ), + onTap: () { + FocusScope.of(context).unfocus(); + }, + ), + SelectableText( + log.message.join('\n'), + onTap: () { + FocusScope.of(context).unfocus(); + }, ), - Text(log.message.join('\n')), ], ), ), diff --git a/lib/src/settings/settings_service.dart b/lib/src/settings/settings_service.dart index 6f94dc3..e03e57c 100644 --- a/lib/src/settings/settings_service.dart +++ b/lib/src/settings/settings_service.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; /// A service that stores and retrieves user settings. /// @@ -7,11 +8,18 @@ import 'package:flutter/material.dart'; /// you'd like to store settings on a web server, use the http package. class SettingsService { /// Loads the User's preferred ThemeMode from local or remote storage. - Future themeMode() async => ThemeMode.system; + Future themeMode() async { + var prefs = await SharedPreferences.getInstance(); + var index = prefs.getInt("theme"); + if (index != null) { + return ThemeMode.values[index]; + } + return ThemeMode.system; + } /// Persists the user's preferred ThemeMode to local or remote storage. Future updateThemeMode(ThemeMode theme) async { - // Use the shared_preferences package to persist settings locally or the - // http package to persist settings over the network. + var prefs = await SharedPreferences.getInstance(); + prefs.setInt("theme", theme.index); } } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 9c49a5a..444bea8 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,7 +6,9 @@ import FlutterMacOS import Foundation import desktop_drop +import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index ce8f584..66ef9e4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -73,6 +73,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" file_picker: dependency: "direct main" description: @@ -189,6 +197,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + url: "https://pub.dev" + source: hosted + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -197,6 +237,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -282,6 +378,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.5.5" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" sdks: dart: ">=3.5.3 <4.0.0" flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index 6e6b732..f362b21 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: sdk: flutter file_picker: desktop_drop: + shared_preferences: dev_dependencies: flutter_test: