mirror of
https://gitlab.com/mysocialportal/relatica
synced 2024-10-18 18:13:31 +00:00
Merge branch 'error-logging-update' into 'main'
Add Error Log/Event Screen and Exporting See merge request mysocialportal/relatica!53
This commit is contained in:
commit
55bc1ffd23
22 changed files with 379 additions and 36 deletions
|
@ -57,7 +57,7 @@ class _MediaKitAvControlState extends State<MediaKitAvControl> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_logger.finer('Building MediaKit Control for ${widget.videoUrl}');
|
_logger.finest('Building MediaKit Control for ${widget.videoUrl}');
|
||||||
if (controller == null) {
|
if (controller == null) {
|
||||||
return Container(
|
return Container(
|
||||||
width: widget.width,
|
width: widget.width,
|
||||||
|
|
|
@ -51,14 +51,14 @@ class _InteractionsBarControlState extends State<InteractionsBarControl> {
|
||||||
isProcessing = true;
|
isProcessing = true;
|
||||||
});
|
});
|
||||||
final newState = !isFavorited;
|
final newState = !isFavorited;
|
||||||
_logger.finest('Trying to toggle favorite from $isFavorited to $newState');
|
_logger.fine('Trying to toggle favorite from $isFavorited to $newState');
|
||||||
final result = await getIt<ActiveProfileSelector<TimelineManager>>()
|
final result = await getIt<ActiveProfileSelector<TimelineManager>>()
|
||||||
.activeEntry
|
.activeEntry
|
||||||
.andThenAsync(
|
.andThenAsync(
|
||||||
(tm) async => await tm.toggleFavorited(widget.entry.id, newState));
|
(tm) async => await tm.toggleFavorited(widget.entry.id, newState));
|
||||||
result.match(onSuccess: (update) {
|
result.match(onSuccess: (update) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_logger.finest(
|
_logger.fine(
|
||||||
'Success toggling! $isFavorited -> ${update.entry.isFavorited}');
|
'Success toggling! $isFavorited -> ${update.entry.isFavorited}');
|
||||||
});
|
});
|
||||||
}, onError: (error) {
|
}, onError: (error) {
|
||||||
|
@ -93,14 +93,14 @@ class _InteractionsBarControlState extends State<InteractionsBarControl> {
|
||||||
}
|
}
|
||||||
|
|
||||||
final id = widget.entry.id;
|
final id = widget.entry.id;
|
||||||
_logger.finest('Trying to reshare $id');
|
_logger.fine('Trying to reshare $id');
|
||||||
final result = await getIt<ActiveProfileSelector<TimelineManager>>()
|
final result = await getIt<ActiveProfileSelector<TimelineManager>>()
|
||||||
.activeEntry
|
.activeEntry
|
||||||
.andThenAsync((tm) async => await tm.resharePost(id));
|
.andThenAsync((tm) async => await tm.resharePost(id));
|
||||||
|
|
||||||
result.match(onSuccess: (update) {
|
result.match(onSuccess: (update) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_logger.finest('Success resharing post by ${widget.entry.author}');
|
_logger.fine('Success resharing post by ${widget.entry.author}');
|
||||||
});
|
});
|
||||||
}, onError: (error) {
|
}, onError: (error) {
|
||||||
buildSnackbar(context, 'Error resharing post by ${widget.entry.author}');
|
buildSnackbar(context, 'Error resharing post by ${widget.entry.author}');
|
||||||
|
@ -122,13 +122,13 @@ class _InteractionsBarControlState extends State<InteractionsBarControl> {
|
||||||
isProcessing = true;
|
isProcessing = true;
|
||||||
});
|
});
|
||||||
final id = widget.entry.id;
|
final id = widget.entry.id;
|
||||||
_logger.finest('Trying to un-reshare $id');
|
_logger.fine('Trying to un-reshare $id');
|
||||||
final result = await getIt<ActiveProfileSelector<TimelineManager>>()
|
final result = await getIt<ActiveProfileSelector<TimelineManager>>()
|
||||||
.activeEntry
|
.activeEntry
|
||||||
.andThenAsync((tm) async => await tm.unResharePost(id));
|
.andThenAsync((tm) async => await tm.unResharePost(id));
|
||||||
result.match(onSuccess: (update) {
|
result.match(onSuccess: (update) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_logger.finest('Success un-resharing post by ${widget.entry.author}');
|
_logger.fine('Success un-resharing post by ${widget.entry.author}');
|
||||||
});
|
});
|
||||||
}, onError: (error) {
|
}, onError: (error) {
|
||||||
buildSnackbar(
|
buildSnackbar(
|
||||||
|
|
|
@ -74,7 +74,7 @@ class _PostControlState extends State<PostControl> {
|
||||||
final int scrollToIndex = _scrollToIndexCalc(items);
|
final int scrollToIndex = _scrollToIndexCalc(items);
|
||||||
|
|
||||||
// TODO Figure out why doesn't scroll to correct position on loading
|
// TODO Figure out why doesn't scroll to correct position on loading
|
||||||
_logger.finer('Building view with initial position at $scrollToIndex');
|
_logger.finest('Building view with initial position at $scrollToIndex');
|
||||||
|
|
||||||
return ScrollablePositionedList.builder(
|
return ScrollablePositionedList.builder(
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
|
|
@ -817,6 +817,8 @@ class StatusesClient extends FriendicaClient {
|
||||||
...descendants
|
...descendants
|
||||||
.map((d) => TimelineEntryMastodonExtensions.fromJson(d))
|
.map((d) => TimelineEntryMastodonExtensions.fromJson(d))
|
||||||
];
|
];
|
||||||
|
_logger.finer(() =>
|
||||||
|
'Got status $id with full context which returned ${ancestors.length} ancestors and ${descendants.length} descendants');
|
||||||
return items;
|
return items;
|
||||||
} else {
|
} else {
|
||||||
return [TimelineEntryMastodonExtensions.fromJson(json)];
|
return [TimelineEntryMastodonExtensions.fromJson(json)];
|
||||||
|
|
|
@ -19,6 +19,7 @@ import 'services/follow_requests_manager.dart';
|
||||||
import 'services/gallery_service.dart';
|
import 'services/gallery_service.dart';
|
||||||
import 'services/hashtag_service.dart';
|
import 'services/hashtag_service.dart';
|
||||||
import 'services/interactions_manager.dart';
|
import 'services/interactions_manager.dart';
|
||||||
|
import 'services/log_service.dart';
|
||||||
import 'services/notifications_manager.dart';
|
import 'services/notifications_manager.dart';
|
||||||
import 'services/setting_service.dart';
|
import 'services/setting_service.dart';
|
||||||
import 'services/status_service.dart';
|
import 'services/status_service.dart';
|
||||||
|
@ -32,14 +33,14 @@ void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
MediaKit.ensureInitialized();
|
MediaKit.ensureInitialized();
|
||||||
|
|
||||||
|
final logService = LogService();
|
||||||
|
getIt.registerSingleton<LogService>(logService);
|
||||||
|
|
||||||
// await dotenv.load(fileName: '.env');
|
// await dotenv.load(fileName: '.env');
|
||||||
const enablePreview = false;
|
const enablePreview = false;
|
||||||
Logger.root.level = Level.FINER;
|
Logger.root.level = Level.OFF;
|
||||||
Logger.root.onRecord.listen((event) {
|
Logger.root.onRecord.listen((event) {
|
||||||
final logName = event.loggerName.isEmpty ? 'ROOT' : event.loggerName;
|
logService.add(event);
|
||||||
final msg =
|
|
||||||
'${event.level.name} - $logName @ ${event.time}: ${event.message}';
|
|
||||||
print(msg);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await fixLetsEncryptCertOnOldAndroid();
|
await fixLetsEncryptCertOnOldAndroid();
|
||||||
|
@ -60,9 +61,15 @@ class App extends StatelessWidget {
|
||||||
final settingsService = getIt<SettingsService>();
|
final settingsService = getIt<SettingsService>();
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
|
Logger.root.level = settingsService.logLevel;
|
||||||
|
print('Log level: ${settingsService.logLevel}');
|
||||||
return Portal(
|
return Portal(
|
||||||
child: MultiProvider(
|
child: MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
|
ChangeNotifierProvider<LogService>(
|
||||||
|
create: (_) => getIt<LogService>(),
|
||||||
|
lazy: true,
|
||||||
|
),
|
||||||
ChangeNotifierProvider<StatusService>(
|
ChangeNotifierProvider<StatusService>(
|
||||||
create: (_) => getIt<StatusService>(),
|
create: (_) => getIt<StatusService>(),
|
||||||
lazy: true,
|
lazy: true,
|
||||||
|
|
|
@ -17,6 +17,7 @@ import 'screens/gallery_screen.dart';
|
||||||
import 'screens/home.dart';
|
import 'screens/home.dart';
|
||||||
import 'screens/image_editor_screen.dart';
|
import 'screens/image_editor_screen.dart';
|
||||||
import 'screens/interactions_viewer_screen.dart';
|
import 'screens/interactions_viewer_screen.dart';
|
||||||
|
import 'screens/logviewer_screen.dart';
|
||||||
import 'screens/message_thread_screen.dart';
|
import 'screens/message_thread_screen.dart';
|
||||||
import 'screens/message_threads_browser_screen.dart';
|
import 'screens/message_threads_browser_screen.dart';
|
||||||
import 'screens/messages_new_thread.dart';
|
import 'screens/messages_new_thread.dart';
|
||||||
|
@ -51,6 +52,7 @@ class ScreenPaths {
|
||||||
static String likes = '/likes';
|
static String likes = '/likes';
|
||||||
static String reshares = '/reshares';
|
static String reshares = '/reshares';
|
||||||
static String search = '/search';
|
static String search = '/search';
|
||||||
|
static String logViewer = '/logViewer';
|
||||||
}
|
}
|
||||||
|
|
||||||
bool needAuthChangeInitialized = true;
|
bool needAuthChangeInitialized = true;
|
||||||
|
@ -314,4 +316,11 @@ final appRouter = GoRouter(
|
||||||
child: const SearchScreen(),
|
child: const SearchScreen(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: ScreenPaths.logViewer,
|
||||||
|
name: ScreenPaths.logViewer,
|
||||||
|
pageBuilder: (context, state) => const NoTransitionPage(
|
||||||
|
child: LogViewerScreen(),
|
||||||
|
),
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -84,7 +84,7 @@ class _EditorScreenState extends State<EditorScreen> {
|
||||||
parentEntry = entry;
|
parentEntry = entry;
|
||||||
visibility = entry.visibility;
|
visibility = entry.visibility;
|
||||||
}, onError: (error) {
|
}, onError: (error) {
|
||||||
_logger.finest('Error trying to get parent entry: $error');
|
_logger.severe('Error trying to get parent entry: $error');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,13 +96,13 @@ class _EditorScreenState extends State<EditorScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void restoreStatusData() async {
|
void restoreStatusData() async {
|
||||||
_logger.fine('Attempting to load status for editing');
|
_logger.finer('Attempting to load status for editing');
|
||||||
loaded = false;
|
loaded = false;
|
||||||
final result = await getIt<ActiveProfileSelector<TimelineManager>>()
|
final result = await getIt<ActiveProfileSelector<TimelineManager>>()
|
||||||
.activeEntry
|
.activeEntry
|
||||||
.andThenAsync((manager) async => manager.getEntryById(widget.id));
|
.andThenAsync((manager) async => manager.getEntryById(widget.id));
|
||||||
result.match(onSuccess: (entry) {
|
result.match(onSuccess: (entry) {
|
||||||
_logger.fine('Loading status ${widget.id} information into fields');
|
_logger.finer('Loading status ${widget.id} information into fields');
|
||||||
contentController.text = htmlToSimpleText(entry.body);
|
contentController.text = htmlToSimpleText(entry.body);
|
||||||
spoilerController.text = entry.spoilerText;
|
spoilerController.text = entry.spoilerText;
|
||||||
existingMediaItems
|
existingMediaItems
|
||||||
|
@ -227,7 +227,7 @@ class _EditorScreenState extends State<EditorScreen> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_logger.finest('Build editor $isComment $parentEntry');
|
_logger.finer('Build editor $isComment $parentEntry');
|
||||||
final manager = context
|
final manager = context
|
||||||
.read<ActiveProfileSelector<TimelineManager>>()
|
.read<ActiveProfileSelector<TimelineManager>>()
|
||||||
.activeEntry
|
.activeEntry
|
||||||
|
|
|
@ -67,7 +67,7 @@ class _FilterEditorScreenState extends State<FilterEditorScreen> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_logger.finer('Build for filter ${widget.id}');
|
_logger.finest('Build for filter ${widget.id}');
|
||||||
final fieldWidth = MediaQuery.of(context).size.width * 0.8;
|
final fieldWidth = MediaQuery.of(context).size.width * 0.8;
|
||||||
final service = context
|
final service = context
|
||||||
.watch<ActiveProfileSelector<TimelineEntryFilterService>>()
|
.watch<ActiveProfileSelector<TimelineEntryFilterService>>()
|
||||||
|
|
|
@ -84,7 +84,7 @@ class GalleryBrowsersScreen extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_logger.finest('Building');
|
_logger.finer('Building');
|
||||||
final service = context
|
final service = context
|
||||||
.watch<ActiveProfileSelector<GalleryService>>()
|
.watch<ActiveProfileSelector<GalleryService>>()
|
||||||
.activeEntry
|
.activeEntry
|
||||||
|
|
|
@ -24,7 +24,7 @@ class GalleryScreen extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_logger.finest('Building $galleryName');
|
_logger.finer('Building $galleryName');
|
||||||
final nss = getIt<NetworkStatusService>();
|
final nss = getIt<NetworkStatusService>();
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: StandardAppBar.build(context, galleryName, actions: [
|
appBar: StandardAppBar.build(context, galleryName, actions: [
|
||||||
|
@ -59,7 +59,7 @@ class _GalleryScreenBody extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_logger.finest('Building');
|
_logger.finer('Building');
|
||||||
final service = context
|
final service = context
|
||||||
.watch<ActiveProfileSelector<GalleryService>>()
|
.watch<ActiveProfileSelector<GalleryService>>()
|
||||||
.activeEntry
|
.activeEntry
|
||||||
|
|
|
@ -29,7 +29,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||||
TimelineIdentifiers currentTimeline = TimelineIdentifiers.home();
|
TimelineIdentifiers currentTimeline = TimelineIdentifiers.home();
|
||||||
|
|
||||||
void updateTimeline(TimelineManager manager) {
|
void updateTimeline(TimelineManager manager) {
|
||||||
_logger.finest('Updating timeline: $currentTimeline');
|
_logger.finer('Updating timeline: $currentTimeline');
|
||||||
Future.delayed(const Duration(milliseconds: 100), () async {
|
Future.delayed(const Duration(milliseconds: 100), () async {
|
||||||
await manager.updateTimeline(
|
await manager.updateTimeline(
|
||||||
currentTimeline, TimelineRefreshType.refresh);
|
currentTimeline, TimelineRefreshType.refresh);
|
||||||
|
|
224
lib/screens/logviewer_screen.dart
Normal file
224
lib/screens/logviewer_screen.dart
Normal file
|
@ -0,0 +1,224 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../controls/padding.dart';
|
||||||
|
import '../controls/responsive_max_width.dart';
|
||||||
|
import '../controls/standard_appbar.dart';
|
||||||
|
import '../services/log_service.dart';
|
||||||
|
import '../services/setting_service.dart';
|
||||||
|
import '../utils/clipboard_utils.dart';
|
||||||
|
import '../utils/dateutils.dart';
|
||||||
|
import '../utils/json_printer.dart';
|
||||||
|
import '../utils/logrecord_extensions.dart';
|
||||||
|
import '../utils/snackbar_builder.dart';
|
||||||
|
|
||||||
|
final _logger = Logger('LogViewerScreen');
|
||||||
|
|
||||||
|
class LogViewerScreen extends StatefulWidget {
|
||||||
|
const LogViewerScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LogViewerScreen> createState() => _LogViewerScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LogViewerScreenState extends State<LogViewerScreen> {
|
||||||
|
var filterText = '';
|
||||||
|
var filterByText = false;
|
||||||
|
var filterByModule = false;
|
||||||
|
var filterModuleName = '';
|
||||||
|
var attemptingWrite = false;
|
||||||
|
|
||||||
|
Future<void> _writeEventsLog(List<LogRecord> events) async {
|
||||||
|
if (attemptingWrite) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
attemptingWrite = true;
|
||||||
|
try {
|
||||||
|
final json =
|
||||||
|
PrettyJsonEncoder().convert(events.map((e) => e.toJson()).toList());
|
||||||
|
final filename = 'EventsLog_${DateTime.now().toFileNameString()}.json';
|
||||||
|
if (Platform.isAndroid || Platform.isIOS) {
|
||||||
|
final params = SaveFileDialogParams(
|
||||||
|
data: Uint8List.fromList(json.codeUnits),
|
||||||
|
fileName: filename,
|
||||||
|
);
|
||||||
|
await FlutterFileDialog.saveFile(params: params);
|
||||||
|
if (mounted) {
|
||||||
|
buildSnackbar(context, 'Wrote Events Log to: $filename');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final appsDir = await getApplicationDocumentsDirectory();
|
||||||
|
final location = await FilePicker.platform.saveFile(
|
||||||
|
dialogTitle: 'Save Events Log',
|
||||||
|
fileName: filename,
|
||||||
|
initialDirectory: appsDir.path,
|
||||||
|
);
|
||||||
|
if (location != null) {
|
||||||
|
await File(location).writeAsString(json);
|
||||||
|
if (mounted) {
|
||||||
|
buildSnackbar(context, 'Wrote Events Log to: $location');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
_logger.severe('Error attempting to write out log: $e');
|
||||||
|
if (mounted) {
|
||||||
|
buildSnackbar(context, 'Error attempting to write out log: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attemptingWrite = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final settings = context.watch<SettingsService>();
|
||||||
|
final logService = context.watch<LogService>();
|
||||||
|
final events = logService.events;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: StandardAppBar.build(context, 'Log Viewer'),
|
||||||
|
body: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: ResponsiveMaxWidth(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
buildLogPanel(settings),
|
||||||
|
buildModuleFilter(settings, events),
|
||||||
|
buildTextSearchPanel(),
|
||||||
|
const VerticalPadding(),
|
||||||
|
buildLogList(settings.logLevel, events),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton.small(
|
||||||
|
onPressed: () => _writeEventsLog(events),
|
||||||
|
child: const Icon(Icons.save),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildLogPanel(SettingsService settings) {
|
||||||
|
return ListTile(
|
||||||
|
title: const Text('Log Level'),
|
||||||
|
trailing: DropdownButton<Level>(
|
||||||
|
value: settings.logLevel,
|
||||||
|
items: Level.LEVELS
|
||||||
|
.map((c) => DropdownMenuItem(value: c, child: Text(c.name)))
|
||||||
|
.toList(),
|
||||||
|
onChanged: (value) {
|
||||||
|
settings.logLevel = value ?? Level.OFF;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildModuleFilter(SettingsService settings, List<LogRecord> events) {
|
||||||
|
final modules = events.map((e) => e.loggerName).toSet().toList();
|
||||||
|
modules.sort();
|
||||||
|
modules.add('');
|
||||||
|
return ListTile(
|
||||||
|
leading: Checkbox(
|
||||||
|
value: filterByModule,
|
||||||
|
onChanged: (bool? value) {
|
||||||
|
setState(() {
|
||||||
|
filterByModule = value ?? false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: filterByModule ? null : const Text('Filter by module'),
|
||||||
|
trailing: !filterByModule
|
||||||
|
? null
|
||||||
|
: DropdownButton<String>(
|
||||||
|
value: filterModuleName,
|
||||||
|
items: modules
|
||||||
|
.map((c) => DropdownMenuItem(value: c, child: Text(c)))
|
||||||
|
.toList(),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
filterModuleName = value ?? '';
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildTextSearchPanel() {
|
||||||
|
return ListTile(
|
||||||
|
leading: Checkbox(
|
||||||
|
value: filterByText,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
filterByText = value ?? false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: TextField(
|
||||||
|
enabled: filterByText,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
filterText = value.toLowerCase();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Filter by text',
|
||||||
|
alignLabelWithHint: true,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Theme.of(context).highlightColor,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(5.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildLogList(Level logLevel, List<LogRecord> allEvents) {
|
||||||
|
final events = allEvents.where((e) {
|
||||||
|
final levelFilterPasses = e.level >= logLevel;
|
||||||
|
final passesTextFilter = filterByText
|
||||||
|
? e.message.toLowerCase().contains(filterText.toLowerCase())
|
||||||
|
: true;
|
||||||
|
final passesModuleFilter = filterByModule
|
||||||
|
? filterModuleName.isEmpty || e.loggerName == filterModuleName
|
||||||
|
: true;
|
||||||
|
|
||||||
|
return levelFilterPasses && passesTextFilter && passesModuleFilter;
|
||||||
|
}).toList();
|
||||||
|
return Expanded(
|
||||||
|
child: ListView.separated(
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final event = events[index];
|
||||||
|
return ListTile(
|
||||||
|
onLongPress: () {
|
||||||
|
copyToClipboard(
|
||||||
|
context: context,
|
||||||
|
text: jsonEncode(event.toJson()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
titleAlignment: ListTileTitleAlignment.titleHeight,
|
||||||
|
leading: Text(event.level.toString()),
|
||||||
|
title:
|
||||||
|
Text('${event.loggerName} at ${event.time.toIso8601String()}'),
|
||||||
|
subtitle: Text(
|
||||||
|
event.message,
|
||||||
|
softWrap: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (_, __) => const Divider(),
|
||||||
|
itemCount: events.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,7 +27,7 @@ class NotificationsScreen extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_logger.finest('Building');
|
_logger.finer('Building');
|
||||||
final nss = getIt<NetworkStatusService>();
|
final nss = getIt<NetworkStatusService>();
|
||||||
final managerResult = context
|
final managerResult = context
|
||||||
.watch<ActiveProfileSelector<NotificationsManager>>()
|
.watch<ActiveProfileSelector<NotificationsManager>>()
|
||||||
|
|
|
@ -101,7 +101,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_logger.info('Build');
|
_logger.finer('Build');
|
||||||
final nss = getIt<NetworkStatusService>();
|
final nss = getIt<NetworkStatusService>();
|
||||||
final profileService = context.watch<AccountsService>();
|
final profileService = context.watch<AccountsService>();
|
||||||
final profile = profileService.currentProfile;
|
final profile = profileService.currentProfile;
|
||||||
|
@ -205,7 +205,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildResultBody(Profile profile) {
|
Widget buildResultBody(Profile profile) {
|
||||||
_logger.fine('Building search result body with: $searchResult');
|
_logger.finer('Building search result body with: $searchResult');
|
||||||
switch (searchType) {
|
switch (searchType) {
|
||||||
case SearchTypes.hashTag:
|
case SearchTypes.hashTag:
|
||||||
return buildHashtagResultWidget(profile);
|
return buildHashtagResultWidget(profile);
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
import 'package:color_blindness/color_blindness.dart';
|
import 'package:color_blindness/color_blindness.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../controls/padding.dart';
|
||||||
import '../controls/responsive_max_width.dart';
|
import '../controls/responsive_max_width.dart';
|
||||||
import '../controls/standard_appbar.dart';
|
import '../controls/standard_appbar.dart';
|
||||||
import '../di_initialization.dart';
|
import '../di_initialization.dart';
|
||||||
import '../globals.dart';
|
import '../globals.dart';
|
||||||
|
import '../routes.dart';
|
||||||
import '../services/setting_service.dart';
|
import '../services/setting_service.dart';
|
||||||
import '../utils/theme_mode_extensions.dart';
|
import '../utils/theme_mode_extensions.dart';
|
||||||
|
|
||||||
|
@ -28,6 +32,7 @@ class SettingsScreen extends StatelessWidget {
|
||||||
buildThemeWidget(settings),
|
buildThemeWidget(settings),
|
||||||
if (!kReleaseMode) buildColorBlindnessTestSettings(settings),
|
if (!kReleaseMode) buildColorBlindnessTestSettings(settings),
|
||||||
buildClearCaches(context),
|
buildClearCaches(context),
|
||||||
|
buildLogPanel(context, settings),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -96,4 +101,27 @@ class SettingsScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildLogPanel(BuildContext context, SettingsService settings) {
|
||||||
|
return ListTile(
|
||||||
|
title: Wrap(
|
||||||
|
children: [
|
||||||
|
const Text('Log Level'),
|
||||||
|
const HorizontalPadding(),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => context.pushNamed(ScreenPaths.logViewer),
|
||||||
|
child: const Text('Open Log Viewer'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: DropdownButton<Level>(
|
||||||
|
value: settings.logLevel,
|
||||||
|
items: Level.LEVELS
|
||||||
|
.map((c) => DropdownMenuItem(value: c, child: Text(c.name)))
|
||||||
|
.toList(),
|
||||||
|
onChanged: (value) {
|
||||||
|
settings.logLevel = value ?? Level.OFF;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,7 @@ class BlocksManager extends ChangeNotifier {
|
||||||
);
|
);
|
||||||
}).match(
|
}).match(
|
||||||
onSuccess: (blockedUser) {
|
onSuccess: (blockedUser) {
|
||||||
_logger.finest(
|
_logger.fine(
|
||||||
'Successfully blocked ${blockedUser.name}: ${blockedUser.status}');
|
'Successfully blocked ${blockedUser.name}: ${blockedUser.status}');
|
||||||
final existingIndex = _blocks.indexOf(connection);
|
final existingIndex = _blocks.indexOf(connection);
|
||||||
if (existingIndex < 0) {
|
if (existingIndex < 0) {
|
||||||
|
@ -71,8 +71,8 @@ class BlocksManager extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> unblockConnection(Connection connection) async {
|
Future<void> unblockConnection(Connection connection) async {
|
||||||
_logger.finest(
|
_logger
|
||||||
'Attempting to unblock ${connection.name}: ${connection.status}');
|
.fine('Attempting to unblock ${connection.name}: ${connection.status}');
|
||||||
await RelationshipsClient(profile)
|
await RelationshipsClient(profile)
|
||||||
.unblockConnection(connection)
|
.unblockConnection(connection)
|
||||||
.withResult((blockedUser) {
|
.withResult((blockedUser) {
|
||||||
|
@ -83,7 +83,7 @@ class BlocksManager extends ChangeNotifier {
|
||||||
);
|
);
|
||||||
}).match(
|
}).match(
|
||||||
onSuccess: (unblockedUser) {
|
onSuccess: (unblockedUser) {
|
||||||
_logger.finest(
|
_logger.fine(
|
||||||
'Successfully unblocked ${unblockedUser.name}: ${unblockedUser.status}');
|
'Successfully unblocked ${unblockedUser.name}: ${unblockedUser.status}');
|
||||||
final existingIndex = _blocks.indexOf(connection);
|
final existingIndex = _blocks.indexOf(connection);
|
||||||
if (existingIndex >= 0) {
|
if (existingIndex >= 0) {
|
||||||
|
|
26
lib/services/log_service.dart
Normal file
26
lib/services/log_service.dart
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
const _defaultMaxItems = 1000;
|
||||||
|
|
||||||
|
class LogService extends ChangeNotifier {
|
||||||
|
var maxItems = _defaultMaxItems;
|
||||||
|
|
||||||
|
final _events = Queue<LogRecord>();
|
||||||
|
|
||||||
|
List<LogRecord> get events => UnmodifiableListView(_events);
|
||||||
|
|
||||||
|
void add(LogRecord event) {
|
||||||
|
// final logName = event.loggerName.isEmpty ? 'ROOT' : event.loggerName;
|
||||||
|
// final msg =
|
||||||
|
// '${event.level.name} - $logName @ ${event.time}: ${event.message}';
|
||||||
|
// print(msg);
|
||||||
|
_events.add(event);
|
||||||
|
if (_events.length > maxItems) {
|
||||||
|
_events.removeFirst();
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:color_blindness/color_blindness.dart';
|
import 'package:color_blindness/color_blindness.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../utils/theme_mode_extensions.dart';
|
import '../utils/theme_mode_extensions.dart';
|
||||||
|
@ -40,6 +41,16 @@ class SettingsService extends ChangeNotifier {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Level _logLevel = Level.SEVERE;
|
||||||
|
|
||||||
|
Level get logLevel => _logLevel;
|
||||||
|
|
||||||
|
set logLevel(Level level) {
|
||||||
|
_logLevel = level;
|
||||||
|
_prefs.setString(_logLevelKey, level.name);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
if (_initialized) {
|
if (_initialized) {
|
||||||
return;
|
return;
|
||||||
|
@ -48,6 +59,7 @@ class SettingsService extends ChangeNotifier {
|
||||||
_lowBandwidthMode = _prefs.getBool(_lowBandwidthModeKey) ?? false;
|
_lowBandwidthMode = _prefs.getBool(_lowBandwidthModeKey) ?? false;
|
||||||
_themeMode = ThemeModeExtensions.parse(_prefs.getString(_themeModeKey));
|
_themeMode = ThemeModeExtensions.parse(_prefs.getString(_themeModeKey));
|
||||||
_colorBlindnessType = _colorBlindnessTypeFromPrefs(_prefs);
|
_colorBlindnessType = _colorBlindnessTypeFromPrefs(_prefs);
|
||||||
|
_logLevel = _levelFromPrefs(_prefs);
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,6 +67,7 @@ class SettingsService extends ChangeNotifier {
|
||||||
const _lowBandwidthModeKey = 'LowBandwidthMode';
|
const _lowBandwidthModeKey = 'LowBandwidthMode';
|
||||||
const _themeModeKey = 'ThemeMode';
|
const _themeModeKey = 'ThemeMode';
|
||||||
const _colorBlindnessTestingModeKey = 'ColorBlindnessTestingMode';
|
const _colorBlindnessTestingModeKey = 'ColorBlindnessTestingMode';
|
||||||
|
const _logLevelKey = 'LogLevel';
|
||||||
|
|
||||||
ColorBlindnessType _colorBlindnessTypeFromPrefs(SharedPreferences prefs) {
|
ColorBlindnessType _colorBlindnessTypeFromPrefs(SharedPreferences prefs) {
|
||||||
final cbString = prefs.getString(_colorBlindnessTestingModeKey);
|
final cbString = prefs.getString(_colorBlindnessTestingModeKey);
|
||||||
|
@ -66,3 +79,20 @@ ColorBlindnessType _colorBlindnessTypeFromPrefs(SharedPreferences prefs) {
|
||||||
orElse: () => ColorBlindnessType.none,
|
orElse: () => ColorBlindnessType.none,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Level _levelFromPrefs(SharedPreferences prefs) {
|
||||||
|
final levelString = prefs.getString(_logLevelKey);
|
||||||
|
return switch (levelString) {
|
||||||
|
'ALL' => Level.ALL,
|
||||||
|
'FINEST' => Level.FINEST,
|
||||||
|
'FINER' => Level.FINER,
|
||||||
|
'FINE' => Level.FINE,
|
||||||
|
'CONFIG' => Level.CONFIG,
|
||||||
|
'INFO' => Level.INFO,
|
||||||
|
'WARNING' => Level.WARNING,
|
||||||
|
'SEVERE' => Level.SEVERE,
|
||||||
|
'SHOUT' => Level.SHOUT,
|
||||||
|
'OFF' => Level.OFF,
|
||||||
|
_ => Level.OFF,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ class TimelineManager extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _refreshCircleData() async {
|
Future<void> _refreshCircleData() async {
|
||||||
_logger.finest('Refreshing member circle data ');
|
_logger.finer('Refreshing member circle data ');
|
||||||
await CirclesClient(profile).getCircles().match(
|
await CirclesClient(profile).getCircles().match(
|
||||||
onSuccess: (circles) {
|
onSuccess: (circles) {
|
||||||
circlesRepo.addAllCircles(circles);
|
circlesRepo.addAllCircles(circles);
|
||||||
|
@ -217,10 +217,10 @@ class TimelineManager extends ChangeNotifier {
|
||||||
|
|
||||||
FutureResult<EntryTreeItem, ExecError> toggleFavorited(
|
FutureResult<EntryTreeItem, ExecError> toggleFavorited(
|
||||||
String id, bool newStatus) async {
|
String id, bool newStatus) async {
|
||||||
_logger.finest('Attempting toggling favorite $id to $newStatus');
|
_logger.finer('Attempting toggling favorite $id to $newStatus');
|
||||||
final result = await entryManagerService.toggleFavorited(id, newStatus);
|
final result = await entryManagerService.toggleFavorited(id, newStatus);
|
||||||
if (result.isFailure) {
|
if (result.isFailure) {
|
||||||
_logger.finest('Error toggling favorite $id: ${result.error}');
|
_logger.info('Error toggling favorite $id: ${result.error}');
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,3 +58,9 @@ class ElapsedDateUtils {
|
||||||
return 'seconds ago';
|
return 'seconds ago';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _separator = '_';
|
||||||
|
|
||||||
|
extension DateTimeExtensions on DateTime {
|
||||||
|
String toFileNameString() => '$year$month$day$_separator$hour$minute$second';
|
||||||
|
}
|
||||||
|
|
11
lib/utils/logrecord_extensions.dart
Normal file
11
lib/utils/logrecord_extensions.dart
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
extension LogRecordExtensions on LogRecord {
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'logger': loggerName,
|
||||||
|
'level': level.toString(),
|
||||||
|
'time': time.toIso8601String(),
|
||||||
|
'message': message,
|
||||||
|
'stackTrace': stackTrace?.toString() ?? 'NONE',
|
||||||
|
};
|
||||||
|
}
|
|
@ -17,7 +17,7 @@ FutureResult<PagedResponse<String>, ExecError> getUrl(
|
||||||
Map<String, String>? headers,
|
Map<String, String>? headers,
|
||||||
Duration? timeout,
|
Duration? timeout,
|
||||||
}) async {
|
}) async {
|
||||||
_logger.finer('GET: $url');
|
_logger.fine('GET: $url');
|
||||||
final requestHeaders = headers ?? {};
|
final requestHeaders = headers ?? {};
|
||||||
if (usePhpDebugging) {
|
if (usePhpDebugging) {
|
||||||
requestHeaders['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/';
|
requestHeaders['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/';
|
||||||
|
@ -54,7 +54,7 @@ FutureResult<String, ExecError> postUrl(
|
||||||
Map<String, String>? headers,
|
Map<String, String>? headers,
|
||||||
Duration? timeout,
|
Duration? timeout,
|
||||||
}) async {
|
}) async {
|
||||||
_logger.finer('POST: $url \n Body: $body');
|
_logger.fine('POST: $url \n Body: $body');
|
||||||
final requestHeaders = headers ?? {};
|
final requestHeaders = headers ?? {};
|
||||||
if (usePhpDebugging) {
|
if (usePhpDebugging) {
|
||||||
requestHeaders['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/';
|
requestHeaders['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/';
|
||||||
|
@ -88,7 +88,7 @@ FutureResult<String, ExecError> putUrl(
|
||||||
Map<String, dynamic> body, {
|
Map<String, dynamic> body, {
|
||||||
Map<String, String>? headers,
|
Map<String, String>? headers,
|
||||||
}) async {
|
}) async {
|
||||||
_logger.finer('PUT: $url \n Body: $body');
|
_logger.fine('PUT: $url \n Body: $body');
|
||||||
try {
|
try {
|
||||||
final request = http.put(
|
final request = http.put(
|
||||||
url,
|
url,
|
||||||
|
@ -118,7 +118,7 @@ FutureResult<String, ExecError> deleteUrl(
|
||||||
Map<String, dynamic> body, {
|
Map<String, dynamic> body, {
|
||||||
Map<String, String>? headers,
|
Map<String, String>? headers,
|
||||||
}) async {
|
}) async {
|
||||||
_logger.finer('DELETE: $url');
|
_logger.fine('DELETE: $url');
|
||||||
try {
|
try {
|
||||||
final request = http.delete(
|
final request = http.delete(
|
||||||
url,
|
url,
|
||||||
|
|
Loading…
Reference in a new issue