mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-13 17:30:31 -07:00
chore: Added simple option to view/copy crash-logs locally (#185)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
parent
0cf1b5aef8
commit
0ae613a666
6 changed files with 306 additions and 58 deletions
|
|
@ -1147,5 +1147,6 @@
|
||||||
"mediaSegmentPreview": "Preview",
|
"mediaSegmentPreview": "Preview",
|
||||||
"mediaSegmentRecap": "Recap",
|
"mediaSegmentRecap": "Recap",
|
||||||
"mediaSegmentOutro": "Outro",
|
"mediaSegmentOutro": "Outro",
|
||||||
"mediaSegmentIntro": "Intro"
|
"mediaSegmentIntro": "Intro",
|
||||||
|
"errorLogs": "Error logs"
|
||||||
}
|
}
|
||||||
|
|
@ -11,7 +11,6 @@ import 'package:dynamic_color/dynamic_color.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
@ -21,6 +20,7 @@ import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
import 'package:fladder/models/account_model.dart';
|
import 'package:fladder/models/account_model.dart';
|
||||||
import 'package:fladder/models/syncing/i_synced_item.dart';
|
import 'package:fladder/models/syncing/i_synced_item.dart';
|
||||||
|
import 'package:fladder/providers/crash_log_provider.dart';
|
||||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||||
import 'package:fladder/providers/shared_provider.dart';
|
import 'package:fladder/providers/shared_provider.dart';
|
||||||
import 'package:fladder/providers/sync_provider.dart';
|
import 'package:fladder/providers/sync_provider.dart';
|
||||||
|
|
@ -50,7 +50,7 @@ Future<Map<String, dynamic>> loadConfig() async {
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
_setupLogging();
|
final crashProvider = CrashLogNotifier();
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
if (kIsWeb) {
|
if (kIsWeb) {
|
||||||
|
|
@ -88,6 +88,7 @@ void main() async {
|
||||||
overrides: [
|
overrides: [
|
||||||
sharedPreferencesProvider.overrideWith((ref) => sharedPreferences),
|
sharedPreferencesProvider.overrideWith((ref) => sharedPreferences),
|
||||||
applicationInfoProvider.overrideWith((ref) => applicationInfo),
|
applicationInfoProvider.overrideWith((ref) => applicationInfo),
|
||||||
|
crashLogProvider.overrideWith((ref) => crashProvider),
|
||||||
syncProvider.overrideWith((ref) => SyncNotifier(
|
syncProvider.overrideWith((ref) => SyncNotifier(
|
||||||
ref,
|
ref,
|
||||||
!kIsWeb
|
!kIsWeb
|
||||||
|
|
@ -112,15 +113,6 @@ void main() async {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setupLogging() {
|
|
||||||
Logger.root.level = Level.ALL;
|
|
||||||
Logger.root.onRecord.listen((rec) {
|
|
||||||
if (kDebugMode) {
|
|
||||||
print('${rec.level.name}: ${rec.time}: ${rec.message}');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class Main extends ConsumerStatefulWidget with WindowListener {
|
class Main extends ConsumerStatefulWidget with WindowListener {
|
||||||
const Main({super.key});
|
const Main({super.key});
|
||||||
|
|
||||||
|
|
|
||||||
106
lib/providers/crash_log_provider.dart
Normal file
106
lib/providers/crash_log_provider.dart
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
enum ErrorType {
|
||||||
|
severe,
|
||||||
|
warning,
|
||||||
|
shout,
|
||||||
|
}
|
||||||
|
|
||||||
|
class ErrorViewModel {
|
||||||
|
final LogRecord rec;
|
||||||
|
|
||||||
|
const ErrorViewModel({required this.rec});
|
||||||
|
|
||||||
|
ErrorType get type {
|
||||||
|
if (rec.level == Level.WARNING) {
|
||||||
|
return ErrorType.warning;
|
||||||
|
}
|
||||||
|
if (rec.level == Level.SHOUT) {
|
||||||
|
return ErrorType.shout;
|
||||||
|
}
|
||||||
|
return ErrorType.severe;
|
||||||
|
}
|
||||||
|
|
||||||
|
String get label {
|
||||||
|
var join = [
|
||||||
|
type.name.toUpperCase(),
|
||||||
|
" | ",
|
||||||
|
rec.message,
|
||||||
|
].join();
|
||||||
|
if (join.length > 250) {
|
||||||
|
return "${join.substring(0, 80)}... \n \nTruncated copy log to see more";
|
||||||
|
} else {
|
||||||
|
return join;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String get content => [
|
||||||
|
rec.time.toIso8601String(),
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
rec.stackTrace,
|
||||||
|
].whereNotNull().join();
|
||||||
|
|
||||||
|
String get clipBoard => [label, content].toString();
|
||||||
|
|
||||||
|
Color get color => switch (type) {
|
||||||
|
ErrorType.severe => Colors.redAccent,
|
||||||
|
ErrorType.warning => Colors.orange,
|
||||||
|
ErrorType.shout => Colors.yellowAccent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
final crashLogProvider = StateNotifierProvider<CrashLogNotifier, List<ErrorViewModel>>((ref) => CrashLogNotifier());
|
||||||
|
|
||||||
|
class CrashLogNotifier extends StateNotifier<List<ErrorViewModel>> {
|
||||||
|
CrashLogNotifier() : super([]) {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
late final Logger logger;
|
||||||
|
final maxLength = 100;
|
||||||
|
|
||||||
|
void init() {
|
||||||
|
logger = Logger.root;
|
||||||
|
logger.level = Level.ALL;
|
||||||
|
logger.onRecord.listen(logPrint);
|
||||||
|
FlutterError.onError = (FlutterErrorDetails details) => logFile(details);
|
||||||
|
PlatformDispatcher.instance.onError = (error, stack) {
|
||||||
|
logFile(FlutterErrorDetails(
|
||||||
|
exception: error,
|
||||||
|
stack: stack,
|
||||||
|
library: 'Unhandled',
|
||||||
|
));
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearLogs() {
|
||||||
|
state = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
void logPrint(LogRecord rec) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print('${rec.level.name}: ${rec.time}: ${rec.message}');
|
||||||
|
}
|
||||||
|
if (rec.level != Level.INFO) {
|
||||||
|
state = [ErrorViewModel(rec: rec), ...state];
|
||||||
|
if (state.length >= maxLength) {
|
||||||
|
state = state.sublist(0, maxLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void logFile(FlutterErrorDetails details) {
|
||||||
|
logger.severe('Flutter error: ${details.exception}', details.exception, details.stack!);
|
||||||
|
|
||||||
|
if (details.stack != null && kDebugMode) {
|
||||||
|
print('${details.stack}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
134
lib/screens/crash_screen/crash_screen.dart
Normal file
134
lib/screens/crash_screen/crash_screen.dart
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:fladder/providers/crash_log_provider.dart';
|
||||||
|
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
||||||
|
import 'package:fladder/util/list_padding.dart';
|
||||||
|
import 'package:fladder/util/localization_helper.dart';
|
||||||
|
import 'package:fladder/util/string_extensions.dart';
|
||||||
|
import 'package:fladder/widgets/shared/enum_selection.dart';
|
||||||
|
|
||||||
|
final _selectedWarningProvider = StateProvider<ErrorType?>((ref) => null);
|
||||||
|
|
||||||
|
class CrashScreen extends ConsumerWidget {
|
||||||
|
const CrashScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final provider = ref.read(crashLogProvider.notifier);
|
||||||
|
final selectedType = ref.watch(_selectedWarningProvider);
|
||||||
|
final crashLogs =
|
||||||
|
ref.watch(crashLogProvider).where((value) => selectedType == null ? true : value.type == selectedType);
|
||||||
|
return Dialog.fullscreen(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const CloseButton(),
|
||||||
|
Text(
|
||||||
|
"Error logs",
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
EnumBox(
|
||||||
|
current: selectedType == null ? context.localized.all : selectedType.name.capitalize(),
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
value: null,
|
||||||
|
child: Text(context.localized.all),
|
||||||
|
onTap: () => ref.read(_selectedWarningProvider.notifier).update((state) => null),
|
||||||
|
),
|
||||||
|
...ErrorType.values.map(
|
||||||
|
(entry) => PopupMenuItem(
|
||||||
|
value: entry,
|
||||||
|
child: Text(entry.name.capitalize()),
|
||||||
|
onTap: () => ref.read(_selectedWarningProvider.notifier).update((state) => entry),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
if (kDebugMode)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: provider.clearLogs,
|
||||||
|
child: const Text("Clear"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (crashLogs.isNotEmpty)
|
||||||
|
Flexible(
|
||||||
|
child: Card(
|
||||||
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
shrinkWrap: true,
|
||||||
|
children: crashLogs
|
||||||
|
.map(
|
||||||
|
(e) => Column(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Card(
|
||||||
|
color: e.color.withOpacity(0.1),
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Card(
|
||||||
|
color: e.color.withOpacity(0.2),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(4.0),
|
||||||
|
child: Text(
|
||||||
|
e.label,
|
||||||
|
style:
|
||||||
|
Theme.of(context).textTheme.titleLarge?.copyWith(color: e.color),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await Clipboard.setData(ClipboardData(text: e.clipBoard));
|
||||||
|
if (context.mounted) {
|
||||||
|
fladderSnackbar(context, title: "Copied to clipboard");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.copy_all_rounded),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(e.content),
|
||||||
|
].addInBetween(const SizedBox(height: 16)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const Text("No crash-logs")
|
||||||
|
].addInBetween(const SizedBox(height: 12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:ficonsax/ficonsax.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
|
||||||
|
import 'package:fladder/screens/crash_screen/crash_screen.dart';
|
||||||
import 'package:fladder/screens/settings/settings_scaffold.dart';
|
import 'package:fladder/screens/settings/settings_scaffold.dart';
|
||||||
import 'package:fladder/screens/shared/fladder_icon.dart';
|
import 'package:fladder/screens/shared/fladder_icon.dart';
|
||||||
import 'package:fladder/screens/shared/fladder_logo.dart';
|
import 'package:fladder/screens/shared/fladder_logo.dart';
|
||||||
|
|
@ -96,6 +97,18 @@ class AboutSettingsPage extends ConsumerWidget {
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
FilledButton.tonal(
|
||||||
|
onPressed: () => showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => const CrashScreen(),
|
||||||
|
),
|
||||||
|
child: Text(context.localized.errorLogs),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
].addInBetween(const SizedBox(height: 16)),
|
].addInBetween(const SizedBox(height: 16)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -502,54 +502,56 @@ class _ClientSettingsPageState extends ConsumerState<ClientSettingsPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 64),
|
if (kDebugMode) ...[
|
||||||
SettingsListTile(
|
const SizedBox(height: 64),
|
||||||
label: Text(
|
SettingsListTile(
|
||||||
context.localized.clearAllSettings,
|
label: Text(
|
||||||
),
|
context.localized.clearAllSettings,
|
||||||
contentColor: Theme.of(context).colorScheme.error,
|
),
|
||||||
onTap: () {
|
contentColor: Theme.of(context).colorScheme.error,
|
||||||
showDialog(
|
onTap: () {
|
||||||
context: context,
|
showDialog(
|
||||||
builder: (context) => Dialog(
|
context: context,
|
||||||
child: Padding(
|
builder: (context) => Dialog(
|
||||||
padding: const EdgeInsets.all(16),
|
child: Padding(
|
||||||
child: Column(
|
padding: const EdgeInsets.all(16),
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
Text(
|
children: [
|
||||||
context.localized.clearAllSettingsQuestion,
|
Text(
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
context.localized.clearAllSettingsQuestion,
|
||||||
),
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
const SizedBox(height: 16),
|
),
|
||||||
Text(
|
const SizedBox(height: 16),
|
||||||
context.localized.unableToReverseAction,
|
Text(
|
||||||
),
|
context.localized.unableToReverseAction,
|
||||||
const SizedBox(height: 16),
|
),
|
||||||
Row(
|
const SizedBox(height: 16),
|
||||||
mainAxisSize: MainAxisSize.min,
|
Row(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
FilledButton(
|
children: [
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
FilledButton(
|
||||||
child: Text(context.localized.cancel),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
),
|
child: Text(context.localized.cancel),
|
||||||
const SizedBox(width: 8),
|
),
|
||||||
ElevatedButton(
|
const SizedBox(width: 8),
|
||||||
onPressed: () async {
|
ElevatedButton(
|
||||||
await ref.read(sharedPreferencesProvider).clear();
|
onPressed: () async {
|
||||||
context.router.push(const LoginRoute());
|
await ref.read(sharedPreferencesProvider).clear();
|
||||||
},
|
context.router.push(const LoginRoute());
|
||||||
child: Text(context.localized.clear),
|
},
|
||||||
)
|
child: Text(context.localized.clear),
|
||||||
],
|
)
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
],
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue