From 139e455d0f58bb9562fbed093b0254ecbd77c716 Mon Sep 17 00:00:00 2001 From: Nico Mexis Date: Sun, 19 May 2024 18:38:40 +0200 Subject: [PATCH] Migrate screenshots to XFiles --- lib/core/catcher_2.dart | 18 +++++------ lib/core/catcher_2_screenshot_manager.dart | 17 +++++----- lib/handlers/discord_handler.dart | 36 +++++++++++++--------- lib/handlers/email_auto_handler.dart | 17 +++++++++- lib/handlers/slack_handler.dart | 27 ++++++++++------ lib/model/report.dart | 5 ++- lib/utils/catcher_2_utils.dart | 23 +++++++++----- 7 files changed, 90 insertions(+), 53 deletions(-) diff --git a/lib/core/catcher_2.dart b/lib/core/catcher_2.dart index d201ace..468b4d3 100644 --- a/lib/core/catcher_2.dart +++ b/lib/core/catcher_2.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:io'; import 'dart:isolate'; import 'package:catcher_2/core/application_profile_manager.dart'; @@ -14,6 +13,7 @@ import 'package:catcher_2/model/report_handler.dart'; import 'package:catcher_2/model/report_mode.dart'; import 'package:catcher_2/utils/catcher_2_error_widget.dart'; import 'package:catcher_2/utils/catcher_2_logger.dart'; +import 'package:cross_file/cross_file.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -460,7 +460,9 @@ class Catcher2 implements ReportModeAction { screenshotManager = Catcher2ScreenshotManager(_logger); final screenshotsPath = _currentConfig.screenshotsPath; if (!ApplicationProfileManager.isWeb() && screenshotsPath.isEmpty) { - _logger.warning("Screenshots path is empty. Screenshots won't work."); + _logger.warning( + "Screenshots path is empty. Screenshots won't be saved locally.", + ); } screenshotManager.path = screenshotsPath; } @@ -494,13 +496,11 @@ class Catcher2 implements ReportModeAction { _cleanPastReportsOccurrences(); - File? screenshot; - if (!ApplicationProfileManager.isWeb()) { - try { - screenshot = await screenshotManager.captureAndSave(); - } catch (e) { - _logger.warning('Failed to create screenshot file: $e'); - } + XFile? screenshot; + try { + screenshot = await screenshotManager.captureAndSave(); + } catch (e) { + _logger.warning('Failed to create screenshot file: $e'); } final report = Report( diff --git a/lib/core/catcher_2_screenshot_manager.dart b/lib/core/catcher_2_screenshot_manager.dart index 730b82e..dad56a5 100644 --- a/lib/core/catcher_2_screenshot_manager.dart +++ b/lib/core/catcher_2_screenshot_manager.dart @@ -2,11 +2,11 @@ library screenshot; import 'dart:async'; -import 'dart:io'; import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:catcher_2/utils/catcher_2_logger.dart'; +import 'package:cross_file/cross_file.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; @@ -24,14 +24,11 @@ class Catcher2ScreenshotManager { /// Create screenshot and save it in file. File will be created in directory /// specified in `Catcher2Options`. - Future captureAndSave({ + Future captureAndSave({ double? pixelRatio, Duration delay = const Duration(milliseconds: 20), }) async { try { - if (_path?.isEmpty ?? true) { - return null; - } final content = await _capture( pixelRatio: pixelRatio, delay: delay, @@ -46,11 +43,13 @@ class Catcher2ScreenshotManager { return null; } - Future saveFile(Uint8List fileContent) async { - assert(_path != null && _path!.isNotEmpty, 'path is empty'); + Future saveFile(Uint8List fileContent) async { final name = 'catcher_2_${DateTime.now().microsecondsSinceEpoch}.png'; - final file = await File('$_path/$name').create(recursive: true); - file.writeAsBytesSync(fileContent); + final path = (_path?.isEmpty ?? true) ? name : '$_path/$name'; + final file = XFile.fromData(fileContent, path: path, name: name); + if (_path != null && _path!.isNotEmpty) { + await file.saveTo(path); + } return file; } diff --git a/lib/handlers/discord_handler.dart b/lib/handlers/discord_handler.dart index 48d03bd..f021eb0 100644 --- a/lib/handlers/discord_handler.dart +++ b/lib/handlers/discord_handler.dart @@ -1,10 +1,10 @@ import 'dart:async'; -import 'dart:io'; import 'package:catcher_2/model/platform_type.dart'; import 'package:catcher_2/model/report.dart'; import 'package:catcher_2/model/report_handler.dart'; import 'package:catcher_2/utils/catcher_2_utils.dart'; +import 'package:cross_file/cross_file.dart'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; @@ -106,30 +106,36 @@ class DiscordHandler extends ReportHandler { return stringBuffer.toString(); } - Future _sendContent(String content, File? screenshot) async { + Future _sendContent(String content, XFile? screenshot) async { try { _printLog('Sending request to Discord server...'); Response? response; + + final data = { + 'content': content, + }; + if (screenshot != null) { - final screenshotPath = screenshot.path; - final formData = FormData.fromMap({ - 'content': content, - 'file': await MultipartFile.fromFile(screenshotPath), - }); - response = await _dio.post(webhookUrl, data: formData); - } else { - final data = { - 'content': content, - }; - response = await _dio.post(webhookUrl, data: data); + data.addAll( + { + 'file': MultipartFile.fromBytes( + await screenshot.readAsBytes(), + filename: screenshot.name, + ), + }, + ); } + response = await _dio.post( + webhookUrl, + data: FormData.fromMap(data), + ); + _printLog( 'Server responded with code: ${response.statusCode} and message: ' '${response.statusMessage}', ); - final statusCode = response.statusCode ?? 0; - return statusCode >= 200 && statusCode < 300; + return response.ok; } catch (exception) { _printLog('Failed to send data to Discord server: $exception'); return false; diff --git a/lib/handlers/email_auto_handler.dart b/lib/handlers/email_auto_handler.dart index 5d8f1c5..0912fee 100644 --- a/lib/handlers/email_auto_handler.dart +++ b/lib/handlers/email_auto_handler.dart @@ -1,6 +1,10 @@ +import 'dart:async'; + +import 'package:catcher_2/catcher_2.dart'; import 'package:catcher_2/handlers/base_email_handler.dart'; import 'package:catcher_2/model/platform_type.dart'; import 'package:catcher_2/model/report.dart'; +import 'package:cross_file/cross_file.dart'; import 'package:flutter/material.dart'; import 'package:mailer/mailer.dart'; import 'package:mailer/smtp_server.dart'; @@ -48,7 +52,7 @@ class EmailAutoHandler extends BaseEmailHandler { ..text = setupRawMessageText(report); if (report.screenshot != null) { - message.attachments = [FileAttachment(report.screenshot!)]; + message.attachments = [XFilePngAttachment(report.screenshot!)]; } if (sendHtml) { @@ -96,3 +100,14 @@ class EmailAutoHandler extends BaseEmailHandler { PlatformType.windows, ]; } + +class XFilePngAttachment extends Attachment { + XFilePngAttachment(this._xFile) { + contentType = 'image/png'; + } + + final XFile _xFile; + + @override + Stream> asStream() => _xFile.openRead(); +} diff --git a/lib/handlers/slack_handler.dart b/lib/handlers/slack_handler.dart index e85639c..0b1d371 100644 --- a/lib/handlers/slack_handler.dart +++ b/lib/handlers/slack_handler.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'package:catcher_2/model/platform_type.dart'; import 'package:catcher_2/model/report.dart'; @@ -66,18 +67,21 @@ class SlackHandler extends ReportHandler { if (screenshot != null) { data.addAll( - await _tryUploadScreenshot(screenshot: XFile(screenshot.path)), + await _tryUploadScreenshot(screenshot: screenshot), ); } - final response = await _dio.post(webhookUrl, data: data); + final response = await _dio.post( + webhookUrl, + data: json.encode(data), + options: Options(contentType: Headers.formUrlEncodedContentType), + ); _printLog( 'Server responded with code: ${response.statusCode} and ' 'message: ${response.statusMessage}', ); - final statusCode = response.statusCode ?? 0; - return statusCode >= 200 && statusCode < 300; + return response.ok; } catch (exception) { _printLog('Failed to send slack message: $exception'); return false; @@ -96,8 +100,7 @@ class SlackHandler extends ReportHandler { } try { - final screenshotPath = screenshot.path; - final name = 'catcher_2_${DateTime.now().microsecondsSinceEpoch}.png'; + final name = screenshot.name; final formData = FormData.fromMap({ 'token': apiToken, @@ -125,16 +128,21 @@ class SlackHandler extends ReportHandler { } final formDataPost = FormData.fromMap({ - 'file': await MultipartFile.fromFile(screenshotPath), + 'token': apiToken, + 'file': MultipartFile.fromBytes( + await screenshot.readAsBytes(), + filename: screenshot.name, + ), }); final responseFilePost = await _dio.post( responseFile.data['upload_url'], data: formDataPost, options: Options( contentType: Headers.multipartFormDataContentType, + validateStatus: (e) => true, ), ); - if (responseFilePost.statusCode != 200) { + if (!responseFilePost.ok) { _printLog( 'Server responded to upload file post with code: ' '${responseFilePost.statusCode} ' @@ -171,7 +179,8 @@ class SlackHandler extends ReportHandler { 'attachments': [ { 'image_url': responseFileComplete.data['files'][0]['url_private'], - 'text': responseFileComplete.data['files'][0]['permalink'], + 'text': 'Screenshot will soon be available here: ' + '${responseFileComplete.data['files'][0]['permalink']}', }, ], }; diff --git a/lib/model/report.dart b/lib/model/report.dart index d3394b6..6e00823 100644 --- a/lib/model/report.dart +++ b/lib/model/report.dart @@ -1,6 +1,5 @@ -import 'dart:io'; - import 'package:catcher_2/model/platform_type.dart'; +import 'package:cross_file/cross_file.dart'; import 'package:flutter/foundation.dart'; class Report { @@ -43,7 +42,7 @@ class Report { /// Screenshot of screen where error happens. Screenshot won't work everywhere /// (i.e. web platform), so this may be null. - final File? screenshot; + final XFile? screenshot; /// Creates json from current instance Map toJson({ diff --git a/lib/utils/catcher_2_utils.dart b/lib/utils/catcher_2_utils.dart index 4e04164..89ba12b 100644 --- a/lib/utils/catcher_2_utils.dart +++ b/lib/utils/catcher_2_utils.dart @@ -1,18 +1,27 @@ -import 'dart:io'; - +import 'package:catcher_2/core/application_profile_manager.dart'; +import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; +import 'package:universal_io/io.dart'; class Catcher2Utils { /// From https://stackoverflow.com/a/56959146/5894824 static Future isInternetConnectionAvailable() async { - try { - final result = await InternetAddress.lookup('google.com'); - return result.isNotEmpty && result[0].rawAddress.isNotEmpty; - } catch (exception) { - return false; + if (ApplicationProfileManager.isWeb()) { + return true; // TODO(HyperSpeeed): We could in theory handle this maybe? + } else { + try { + final result = await InternetAddress.lookup('google.com'); + return result.isNotEmpty && result[0].rawAddress.isNotEmpty; + } catch (_) {} } + return false; } static bool isCupertinoAppAncestor(BuildContext context) => context.findAncestorWidgetOfExactType() != null; } + +/// From https://stackoverflow.com/a/70282800/5894824 +extension IsOk on Response { + bool get ok => statusCode != null && (statusCode! ~/ 100) == 2; +}