mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2025-12-06 11:09:28 +00:00
267 lines
8.3 KiB
Dart
267 lines
8.3 KiB
Dart
|
|
import 'dart:io';
|
||
|
|
|
||
|
|
import 'package:file_picker/file_picker.dart';
|
||
|
|
import 'package:flutter/material.dart';
|
||
|
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||
|
|
import 'package:logging/logging.dart';
|
||
|
|
import 'package:share_plus/share_plus.dart';
|
||
|
|
import 'package:vaani/features/logging/providers/logs_provider.dart';
|
||
|
|
import 'package:vaani/main.dart';
|
||
|
|
|
||
|
|
class LogsPage extends HookConsumerWidget {
|
||
|
|
const LogsPage({super.key});
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
||
|
|
final logs = ref.watch(logsProvider);
|
||
|
|
final theme = Theme.of(context);
|
||
|
|
final scrollController = useScrollController();
|
||
|
|
return Scaffold(
|
||
|
|
appBar: AppBar(
|
||
|
|
title: const Text('Logs'),
|
||
|
|
actions: [
|
||
|
|
IconButton(
|
||
|
|
tooltip: 'Clear logs',
|
||
|
|
icon: const Icon(Icons.delete_forever),
|
||
|
|
onPressed: () async {
|
||
|
|
// ask for confirmation
|
||
|
|
final shouldClear = await showDialog<bool>(
|
||
|
|
context: context,
|
||
|
|
builder: (context) {
|
||
|
|
return AlertDialog(
|
||
|
|
title: const Text('Clear logs?'),
|
||
|
|
actions: [
|
||
|
|
TextButton(
|
||
|
|
onPressed: () {
|
||
|
|
Navigator.of(context).pop(false);
|
||
|
|
},
|
||
|
|
child: const Text('Cancel'),
|
||
|
|
),
|
||
|
|
TextButton(
|
||
|
|
onPressed: () {
|
||
|
|
Navigator.of(context).pop(true);
|
||
|
|
},
|
||
|
|
child: const Text('Clear'),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
if (shouldClear == true) {
|
||
|
|
ref.read(logsProvider.notifier).clear();
|
||
|
|
}
|
||
|
|
},
|
||
|
|
),
|
||
|
|
IconButton(
|
||
|
|
tooltip: 'Share logs',
|
||
|
|
icon: const Icon(Icons.share),
|
||
|
|
onPressed: () async {
|
||
|
|
appLogger.info('Preparing logs for sharing');
|
||
|
|
final zipLogFilePath =
|
||
|
|
await ref.read(logsProvider.notifier).getZipFilePath();
|
||
|
|
|
||
|
|
// submit logs
|
||
|
|
final result = await Share.shareXFiles([XFile(zipLogFilePath)]);
|
||
|
|
|
||
|
|
switch (result.status) {
|
||
|
|
case ShareResultStatus.success:
|
||
|
|
appLogger.info('Share success');
|
||
|
|
break;
|
||
|
|
case ShareResultStatus.dismissed:
|
||
|
|
appLogger.info('Share dismissed');
|
||
|
|
break;
|
||
|
|
case ShareResultStatus.unavailable:
|
||
|
|
appLogger.severe('Share unavailable');
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
},
|
||
|
|
),
|
||
|
|
IconButton(
|
||
|
|
tooltip: 'Download logs',
|
||
|
|
icon: const Icon(Icons.download),
|
||
|
|
onPressed: () async {
|
||
|
|
appLogger.info('Preparing logs for download');
|
||
|
|
final zipLogFilePath =
|
||
|
|
await ref.read(logsProvider.notifier).getZipFilePath();
|
||
|
|
|
||
|
|
// save to folder
|
||
|
|
String? outputFile = await FilePicker.platform.saveFile(
|
||
|
|
dialogTitle: 'Please select an output file:',
|
||
|
|
fileName: zipLogFilePath.split('/').last,
|
||
|
|
);
|
||
|
|
if (outputFile != null) {
|
||
|
|
try {
|
||
|
|
final file = File(outputFile);
|
||
|
|
final zipFile = File(zipLogFilePath);
|
||
|
|
await zipFile.copy(file.path);
|
||
|
|
} catch (e) {
|
||
|
|
appLogger.severe('Error saving file: $e');
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
appLogger.info('Download cancelled');
|
||
|
|
}
|
||
|
|
},
|
||
|
|
),
|
||
|
|
IconButton(
|
||
|
|
tooltip: 'Refresh logs',
|
||
|
|
icon: const Icon(Icons.refresh),
|
||
|
|
onPressed: () {
|
||
|
|
ref.invalidate(logsProvider);
|
||
|
|
},
|
||
|
|
),
|
||
|
|
IconButton(
|
||
|
|
tooltip: 'Scroll to top',
|
||
|
|
icon: const Icon(Icons.arrow_upward),
|
||
|
|
onPressed: () {
|
||
|
|
scrollController.animateTo(
|
||
|
|
0,
|
||
|
|
duration: const Duration(milliseconds: 500),
|
||
|
|
curve: Curves.easeInOut,
|
||
|
|
);
|
||
|
|
},
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
// a column with listview.builder and a scrollable list of logs
|
||
|
|
body: Column(
|
||
|
|
children: [
|
||
|
|
// a filter for log levels, loggers, and search
|
||
|
|
// TODO: implement filters and search
|
||
|
|
|
||
|
|
Expanded(
|
||
|
|
child: logs.when(
|
||
|
|
data: (logRecords) {
|
||
|
|
return Scrollbar(
|
||
|
|
controller: scrollController,
|
||
|
|
child: ListView.builder(
|
||
|
|
controller: scrollController,
|
||
|
|
itemCount: logRecords.length,
|
||
|
|
itemBuilder: (context, index) {
|
||
|
|
final logRecord = logRecords[index];
|
||
|
|
return LogRecordTile(logRecord: logRecord, theme: theme);
|
||
|
|
},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||
|
|
error: (error, stackTrace) => Center(
|
||
|
|
child: Column(
|
||
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
||
|
|
children: [
|
||
|
|
const Text('Error loading logs'),
|
||
|
|
ElevatedButton(
|
||
|
|
onPressed: () {
|
||
|
|
ref.invalidate(logsProvider);
|
||
|
|
},
|
||
|
|
child: const Text('Retry'),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class LogRecordTile extends StatelessWidget {
|
||
|
|
final LogRecord logRecord;
|
||
|
|
final ThemeData theme;
|
||
|
|
const LogRecordTile({
|
||
|
|
required this.logRecord,
|
||
|
|
required this.theme,
|
||
|
|
super.key,
|
||
|
|
});
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
return ListTile(
|
||
|
|
leading: CircleAvatar(
|
||
|
|
backgroundColor: getLogLevelColor(logRecord.level, theme),
|
||
|
|
child: Icon(
|
||
|
|
getLogLevelIcon(logRecord.level),
|
||
|
|
color: getLogLevelTextColor(logRecord.level, theme),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
title: Text(logRecord.loggerName),
|
||
|
|
subtitle: Text(logRecord.message),
|
||
|
|
onTap: () {
|
||
|
|
showDialog(
|
||
|
|
context: context,
|
||
|
|
builder: (context) {
|
||
|
|
return AlertDialog(
|
||
|
|
icon: Icon(getLogLevelIcon(logRecord.level)),
|
||
|
|
title: Text(logRecord.loggerName),
|
||
|
|
content: Text.rich(
|
||
|
|
TextSpan(
|
||
|
|
children: [
|
||
|
|
TextSpan(
|
||
|
|
text: logRecord.time.toIso8601String(),
|
||
|
|
style: const TextStyle(fontStyle: FontStyle.italic),
|
||
|
|
),
|
||
|
|
const TextSpan(text: '\n\n'),
|
||
|
|
TextSpan(
|
||
|
|
text: logRecord.message,
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
actions: [
|
||
|
|
TextButton(
|
||
|
|
onPressed: () {
|
||
|
|
Navigator.of(context).pop();
|
||
|
|
},
|
||
|
|
child: const Text('Close'),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
IconData getLogLevelIcon(Level level) {
|
||
|
|
switch (level) {
|
||
|
|
case Level.INFO:
|
||
|
|
return (Icons.info);
|
||
|
|
case Level.WARNING:
|
||
|
|
return (Icons.warning);
|
||
|
|
case Level.SEVERE:
|
||
|
|
case Level.SHOUT:
|
||
|
|
return (Icons.error);
|
||
|
|
default:
|
||
|
|
return (Icons.bug_report);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Color? getLogLevelColor(Level level, ThemeData theme) {
|
||
|
|
switch (level) {
|
||
|
|
case Level.INFO:
|
||
|
|
return theme.colorScheme.surfaceContainerLow;
|
||
|
|
case Level.WARNING:
|
||
|
|
return theme.colorScheme.surfaceBright;
|
||
|
|
case Level.SEVERE:
|
||
|
|
case Level.SHOUT:
|
||
|
|
return theme.colorScheme.errorContainer;
|
||
|
|
default:
|
||
|
|
return theme.colorScheme.primaryContainer;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Color? getLogLevelTextColor(Level level, ThemeData theme) {
|
||
|
|
switch (level) {
|
||
|
|
case Level.INFO:
|
||
|
|
return theme.colorScheme.onSurface;
|
||
|
|
case Level.WARNING:
|
||
|
|
return theme.colorScheme.onSurface;
|
||
|
|
case Level.SEVERE:
|
||
|
|
case Level.SHOUT:
|
||
|
|
return theme.colorScheme.onErrorContainer;
|
||
|
|
default:
|
||
|
|
return theme.colorScheme.onPrimaryContainer;
|
||
|
|
}
|
||
|
|
}
|