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:
PartyDonut 2024-12-20 15:48:05 +01:00 committed by GitHub
parent 0cf1b5aef8
commit 0ae613a666
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 306 additions and 58 deletions

View file

@ -1147,5 +1147,6 @@
"mediaSegmentPreview": "Preview", "mediaSegmentPreview": "Preview",
"mediaSegmentRecap": "Recap", "mediaSegmentRecap": "Recap",
"mediaSegmentOutro": "Outro", "mediaSegmentOutro": "Outro",
"mediaSegmentIntro": "Intro" "mediaSegmentIntro": "Intro",
"errorLogs": "Error logs"
} }

View file

@ -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});

View 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}');
}
}
}

View 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)),
),
),
);
}
}

View file

@ -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)),
), ),
); );

View file

@ -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),
], ],
), ),