main
1import 'dart:io';
2
3import 'package:file_picker/file_picker.dart';
4import 'package:flutter/material.dart';
5import 'package:flutter/services.dart';
6import 'package:package_info_plus/package_info_plus.dart';
7import 'package:path/path.dart';
8import 'package:sqflite/sqflite.dart';
9import 'package:url_launcher/url_launcher.dart';
10
11/// A static location to report errors to and disrupt the program flow in case
12/// there is the risk of data loss when continuing.
13class ErrorReporting {
14 ErrorReporting._create();
15
16 /// Whether there is already an critical error displayed.
17 static bool isErrorState = false;
18
19 /// Replaces the application with an ErrorScreen.
20 ///
21 /// This method can be used to avoid running any further code in your current function, by awaiting
22 static Future<void> reportCriticalError(String title, String text) async {
23 if (isErrorState) throw Exception('Tried to report another error:\n title = $title,\n text = $text');
24 isErrorState = true;
25 runApp(ErrorScreen(
26 title: title,
27 text: text,
28 debugInfo: await _carefullyCollectDebugInfo(),
29 ),);
30 return Future.delayed(const Duration(days: 30,));
31 }
32
33 static Future<PackageInfo> _carefullyCollectDebugInfo() async {
34 PackageInfo packageInfo;
35 try {
36 packageInfo = await PackageInfo.fromPlatform();
37 } catch (e) {
38 packageInfo = PackageInfo(appName: 'err', packageName: 'err', version: 'err', buildNumber: 'err');
39 }
40
41 return packageInfo;
42 }
43}
44
45/// A full [MaterialApp] that is especially safe against throwing errors and
46/// allows for debugging and data extraction.
47class ErrorScreen extends StatelessWidget {
48
49 const ErrorScreen({super.key, required this.title, required this.text, required this.debugInfo});
50
51 final String title;
52 final String text;
53 final PackageInfo debugInfo;
54
55 @override
56 Widget build(BuildContext context) => MaterialApp(
57 title: 'Critical error',
58 home: Scaffold(
59 appBar: AppBar(
60 title: const Text('Critical error'),
61 backgroundColor: Colors.red,
62 ),
63 body: Builder(
64 builder: (context) {
65 final scaffoldMessenger = ScaffoldMessenger.of(context);
66 return SingleChildScrollView(
67 padding: const EdgeInsets.symmetric(horizontal: 10),
68 child: Column(
69 mainAxisSize: MainAxisSize.min,
70 children: [
71 const SizedBox(height: 20,),
72 Text('App version: ${debugInfo.version}'),
73 Text('Build number: ${debugInfo.buildNumber}'),
74 const Divider(),
75 Text(title, style: const TextStyle(fontSize: 20, ), ),
76 Text(text),
77 const Divider(),
78 TextButton(
79 onPressed: () {
80 Clipboard.setData(ClipboardData(
81 text: 'Error:\nBuild number:${debugInfo.buildNumber}\n-----\n$title:\n---\n$text\n',
82 ),);
83 scaffoldMessenger.showSnackBar(const SnackBar(
84 content: Text('Copied to clipboard'),),);
85 },
86 child: const Text('copy error message'),
87 ),
88 TextButton(
89 onPressed: () async {
90 try {
91 final url = Uri.parse('https://github.com/derdilla/blood-pressure-monitor-fl/issues');
92 if (await canLaunchUrl(url)) {
93 await launchUrl(url, mode: LaunchMode.externalApplication);
94 } else {
95 scaffoldMessenger.showSnackBar(const SnackBar(
96 content: Text('ERR: Please open this website: https://github.com/derdilla/blood-pressure-monitor-fl/issues'),),);
97 }
98 } catch (e) {
99 scaffoldMessenger.showSnackBar(SnackBar(
100 content: Text('ERR: $e'),),);
101 }
102 },
103 child: const Text('open issue reporting website'),
104 ),
105 TextButton(
106 onPressed: () async {
107 try {
108 String dbPath = await getDatabasesPath();
109
110 assert(dbPath != inMemoryDatabasePath);
111 dbPath = join(dbPath, 'config.db');
112
113 await FilePicker.platform.saveFile(
114 fileName: 'config.db',
115 bytes: File(dbPath).readAsBytesSync(),
116 type: FileType.any, // application/vnd.sqlite3
117 );
118 } catch(e) {
119 scaffoldMessenger.showSnackBar(SnackBar(
120 content: Text('ERR: $e'),),);
121 }
122 },
123 child: const Text('rescue config.db'),
124 ),
125 TextButton(
126 onPressed: () async {
127 try {
128 String dbPath = await getDatabasesPath();
129
130 assert(dbPath != inMemoryDatabasePath);
131 dbPath = join(dbPath, 'bp.db');
132 await FilePicker.platform.saveFile(
133 fileName: 'bp.db',
134 bytes: File(dbPath).readAsBytesSync(),
135 type: FileType.any, // application/vnd.sqlite3
136 );
137 } catch(e) {
138 scaffoldMessenger.showSnackBar(SnackBar(
139 content: Text('ERR: $e'),),);
140 }
141 },
142 child: const Text('rescue db'),
143 ),
144 ],
145 ),
146 );
147 },
148 ),
149 ),
150 );
151
152}