persist theme

This commit is contained in:
hardliner66 2024-10-06 02:51:15 +02:00
parent 41821a71d4
commit 8f95b644d7
6 changed files with 317 additions and 68 deletions

View File

@ -16,6 +16,5 @@ void main() async {
// Run the app and pass in the SettingsController. The app listens to the // Run the app and pass in the SettingsController. The app listens to the
// SettingsController for changes, then passes it further down to the // SettingsController for changes, then passes it further down to the
// SettingsView. // SettingsView.
// runApp(LogViewerApp(settingsController: settingsController)); runApp(LogViewerApp(settingsController: settingsController));
runApp(LogViewerApp());
} }

View File

@ -1,97 +1,209 @@
import 'package:fast_log_viewer/src/settings/settings_controller.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'dart:io'; import 'dart:io';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:desktop_drop/desktop_drop.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<LogViewerApp> {
final SettingsController settingsController;
LogViewerAppState({required this.settingsController});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return ListenableBuilder(
title: 'Log Viewer', listenable: settingsController,
theme: ThemeData( builder: (BuildContext context, Widget? child) {
primarySwatch: Colors.blue, return MaterialApp(
), title: 'Fast Log Viewer',
home: LogViewerScreen(), 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 { class LogViewerScreen extends StatefulWidget {
final Function(ThemeMode) onThemeChanged;
final SettingsController settingsController;
const LogViewerScreen(
{required this.onThemeChanged, required this.settingsController});
@override @override
_LogViewerScreenState createState() => _LogViewerScreenState(); LogViewerScreenState createState() =>
LogViewerScreenState(settingsController: settingsController);
} }
class _LogViewerScreenState extends State<LogViewerScreen> { class LogViewerScreenState extends State<LogViewerScreen> {
List<LogEntry> logs = []; List<LogEntry> logs = [];
List<LogEntry> visibleLogs = []; List<LogEntry> visibleLogs = [];
String search = ''; String search = '';
LogLevel selectedLogLevel = LogLevel.info;
bool _isDragging = false; bool _isDragging = false;
int? selectedLogIndex;
final SettingsController settingsController;
LogViewerScreenState({required this.settingsController});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return GestureDetector(
onTap: () {
setState(() {
selectedLogIndex = null;
FocusScope.of(context).unfocus();
});
},
child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Log Viewer'), title: const Text('Log Viewer'),
actions: [ actions: [
IconButton( IconButton(
icon: Icon(Icons.folder_open), icon: const Icon(Icons.folder_open),
onPressed: _openFile, onPressed: _openFile,
), ),
], ],
), ),
body: DropTarget( body: DropTarget(
onDragEntered: (details) { onDragEntered: (details) {
setState(() { setState(() {
_isDragging = true; _isDragging = true;
}); });
}, },
onDragExited: (details) { 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(() { setState(() {
logs = _parseLogFile(fileContent);
_filterLogs();
_isDragging = false; _isDragging = false;
}); });
}, }
onDragDone: (details) async { },
if (details.files.isNotEmpty) { child: Container(
File file = File(details.files.first.path); color: _isDragging
String fileContent = await file.readAsString(); ? Colors.grey[200]
setState(() { : Theme.of(context).scaffoldBackgroundColor,
logs = _parseLogFile(fileContent); child: Column(
visibleLogs = logs.toList(); children: [
_isDragging = false; Padding(
}); padding: const EdgeInsets.all(8.0),
} child: Row(
}, children: [
child: Container( Expanded(
color: _isDragging ? Colors.grey[200] : Colors.white, child: TextField(
child: Column( decoration: const InputDecoration(
children: [ labelText: 'Search Logs',
Padding( border: OutlineInputBorder(),
padding: const EdgeInsets.all(8.0), ),
child: TextField( onChanged: (value) {
decoration: const InputDecoration( setState(() {
labelText: 'Search Logs', search = value;
border: OutlineInputBorder(), _filterLogs();
});
},
),
), ),
onChanged: (value) { const SizedBox(width: 8.0),
setState(() { DropdownButton<LogLevel>(
search = value; value: selectedLogLevel,
visibleLogs = onChanged: (LogLevel? newValue) {
logs.where((log) => log.contains(value)).toList(); if (newValue != null) {
}); setState(() {
}, selectedLogLevel = newValue;
), _filterLogs();
});
}
},
items: LogLevel.values
.map<DropdownMenuItem<LogLevel>>((LogLevel level) {
return DropdownMenuItem<LogLevel>(
value: level,
child: Text(level.name),
);
}).toList(),
),
const SizedBox(width: 8.0),
DropdownButton<ThemeMode>(
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( Expanded(
itemCount: visibleLogs.length, child: ListView.builder(
itemBuilder: (context, index) { itemCount: visibleLogs.length,
return LogEntryWidget(log: visibleLogs[index]); itemBuilder: (context, index) {
}, return GestureDetector(
), onTap: () {
setState(() {
selectedLogIndex = index;
});
},
child: LogEntryWidget(
log: visibleLogs[index],
isSelected: selectedLogIndex == index,
),
);
},
), ),
], ),
), ],
))); ),
),
),
),
);
} }
Future<void> _openFile() async { Future<void> _openFile() async {
@ -101,11 +213,20 @@ class _LogViewerScreenState extends State<LogViewerScreen> {
String fileContent = await file.readAsString(); String fileContent = await file.readAsString();
setState(() { setState(() {
logs = _parseLogFile(fileContent); 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<LogEntry> _parseLogFile(String content) { List<LogEntry> _parseLogFile(String content) {
List<LogEntry> parsedLogs = []; List<LogEntry> parsedLogs = [];
List<String> lines = content.split('\n'); List<String> lines = content.split('\n');
@ -213,32 +334,46 @@ class LogEntry {
class LogEntryWidget extends StatelessWidget { class LogEntryWidget extends StatelessWidget {
final LogEntry log; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
color: isSelected ? Colors.blue[100] : null,
margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( SelectableText(
'${log.timestamp} - [${log.level.name}] - ${log.category} - ${log.threadId}', '${log.timestamp} - [${log.level.name}] - ${log.category} - ${log.threadId}',
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: _getLevelColor(log.level), color: _getLevelColor(log.level),
), ),
onTap: () {
FocusScope.of(context).unfocus();
},
), ),
Text( SelectableText(
'Component: ${log.component.name}@${log.component.address}', 'Component: ${log.component.name}@${log.component.address}',
style: const TextStyle( style: const TextStyle(
fontStyle: FontStyle.italic, fontStyle: FontStyle.italic,
), ),
onTap: () {
FocusScope.of(context).unfocus();
},
),
SelectableText(
log.message.join('\n'),
onTap: () {
FocusScope.of(context).unfocus();
},
), ),
Text(log.message.join('\n')),
], ],
), ),
), ),

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// A service that stores and retrieves user settings. /// 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. /// you'd like to store settings on a web server, use the http package.
class SettingsService { class SettingsService {
/// Loads the User's preferred ThemeMode from local or remote storage. /// Loads the User's preferred ThemeMode from local or remote storage.
Future<ThemeMode> themeMode() async => ThemeMode.system; Future<ThemeMode> 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. /// Persists the user's preferred ThemeMode to local or remote storage.
Future<void> updateThemeMode(ThemeMode theme) async { Future<void> updateThemeMode(ThemeMode theme) async {
// Use the shared_preferences package to persist settings locally or the var prefs = await SharedPreferences.getInstance();
// http package to persist settings over the network. prefs.setInt("theme", theme.index);
} }
} }

View File

@ -6,7 +6,9 @@ import FlutterMacOS
import Foundation import Foundation
import desktop_drop import desktop_drop
import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin")) DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
} }

View File

@ -73,6 +73,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.3" version: "2.1.3"
file:
dependency: transitive
description:
name: file
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
file_picker: file_picker:
dependency: "direct main" dependency: "direct main"
description: description:
@ -189,6 +197,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.0" 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: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -197,6 +237,62 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" 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: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -282,6 +378,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.5.5" 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: sdks:
dart: ">=3.5.3 <4.0.0" dart: ">=3.5.3 <4.0.0"
flutter: ">=3.24.0" flutter: ">=3.24.0"

View File

@ -16,6 +16,7 @@ dependencies:
sdk: flutter sdk: flutter
file_picker: file_picker:
desktop_drop: desktop_drop:
shared_preferences:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: