persist theme
This commit is contained in:
parent
41821a71d4
commit
8f95b644d7
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
261
lib/src/app.dart
261
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<LogViewerApp> {
|
||||
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<LogViewerScreen> {
|
||||
class LogViewerScreenState extends State<LogViewerScreen> {
|
||||
List<LogEntry> logs = [];
|
||||
List<LogEntry> 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<LogLevel>(
|
||||
value: selectedLogLevel,
|
||||
onChanged: (LogLevel? newValue) {
|
||||
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(
|
||||
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<void> _openFile() async {
|
||||
|
|
@ -101,11 +213,20 @@ class _LogViewerScreenState extends State<LogViewerScreen> {
|
|||
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<LogEntry> _parseLogFile(String content) {
|
||||
List<LogEntry> parsedLogs = [];
|
||||
List<String> 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')),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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> 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.
|
||||
Future<void> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
|||
104
pubspec.lock
104
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"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ dependencies:
|
|||
sdk: flutter
|
||||
file_picker:
|
||||
desktop_drop:
|
||||
shared_preferences:
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
Loading…
Reference in New Issue