diff --git a/core/lib/core.dart b/core/lib/core.dart index 4fecad801..7cb178146 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -99,6 +99,7 @@ export 'presentation/views/avatar/gradient_circle_avatar_icon.dart'; export 'presentation/views/loading/cupertino_loading_widget.dart'; export 'presentation/views/image/image_loader_mixin.dart'; export 'presentation/views/pull_to_refresh/pull_to_refresh_widget.dart'; +export 'presentation/views/button/multi_click_widget.dart'; // Resources export 'presentation/resources/assets_paths.dart'; diff --git a/core/lib/domain/exceptions/file_exception.dart b/core/lib/domain/exceptions/file_exception.dart new file mode 100644 index 000000000..16e8980a8 --- /dev/null +++ b/core/lib/domain/exceptions/file_exception.dart @@ -0,0 +1,21 @@ +import 'package:equatable/equatable.dart'; + +abstract class FileException with EquatableMixin implements Exception { + final String message; + + FileException(this.message); + + @override + String toString() => message; + + @override + List get props => [message]; +} + +class NotFoundFileInFolderException extends FileException { + NotFoundFileInFolderException() : super('No files found in the folder'); +} + +class UserCancelShareFileException extends FileException { + UserCancelShareFileException() : super('User cancel share file'); +} \ No newline at end of file diff --git a/core/lib/presentation/views/button/multi_click_widget.dart b/core/lib/presentation/views/button/multi_click_widget.dart new file mode 100644 index 000000000..4312ef6a2 --- /dev/null +++ b/core/lib/presentation/views/button/multi_click_widget.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +import '../../../utils/app_logger.dart'; + +class MultiClickWidget extends StatefulWidget { + final Widget child; + final int requiredClicks; + final Duration interval; + final VoidCallback onMultiTap; + + const MultiClickWidget({ + Key? key, + required this.child, + required this.onMultiTap, + this.requiredClicks = 7, + this.interval = const Duration(seconds: 2), + }) : super(key: key); + + @override + State createState() => _MultiClickWidgetState(); +} + +class _MultiClickWidgetState extends State { + int _clickCount = 0; + DateTime? _lastClickTime; + + void _handleTap() { + final now = DateTime.now(); + if (_lastClickTime == null || + now.difference(_lastClickTime!) > widget.interval) { + _clickCount = 1; + } else { + _clickCount++; + } + + _lastClickTime = now; + log('_MultiClickWidgetState::_handleTap: ClickCount: $_clickCount'); + if (_clickCount >= widget.requiredClicks) { + _clickCount = 0; + widget.onMultiTap(); + } + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: _handleTap, + child: widget.child, + ); + } +} diff --git a/core/lib/utils/logger/log_tracking.dart b/core/lib/utils/logger/log_tracking.dart index 5b5f0ed7d..de05fb61e 100644 --- a/core/lib/utils/logger/log_tracking.dart +++ b/core/lib/utils/logger/log_tracking.dart @@ -3,7 +3,10 @@ import 'dart:collection'; import 'dart:io'; import 'package:core/domain/exceptions/download_file_exception.dart'; +import 'package:core/domain/exceptions/file_exception.dart'; +import 'package:core/utils/logger/trace_log.dart'; import 'package:core/utils/platform_info.dart'; +import 'package:flutter/foundation.dart'; import 'package:intl/intl.dart'; import 'package:path_provider/path_provider.dart'; @@ -147,4 +150,78 @@ class LogTracking { return await file.writeAsString(content, mode: fileMode); } -} + + Future getTraceLog() async { + final folderPath = await _getInternalStorageDirPath(folderPath: logFolder); + final directory = Directory(folderPath); + if (directory.existsSync()) { + final directoryInfo = await getDirInfo(directory); + return TraceLog( + path: folderPath, + size: directoryInfo.$1, + listFilePaths: directoryInfo.$2, + ); + } else { + throw Exception('Trace folder not exist'); + } + } + + Future<(int, List)> getDirInfo(Directory dir) async { + var files = await dir.list(recursive: true).toList(); + var dirSize = files.fold(0, (int sum, file) => sum + file.statSync().size); + var listPath = files.map((file) => file.path).toList(); + return (dirSize, listPath); + } + + static Future getExternalDocumentPath({String? folderPath}) async { + Directory directory = Directory(''); + if (Platform.isAndroid) { + if (folderPath?.isNotEmpty == true) { + directory = Directory('/storage/emulated/0/Download/$folderPath'); + } else { + directory = Directory('/storage/emulated/0/Download'); + } + } else { + directory = await getApplicationDocumentsDirectory(); + if (folderPath?.isNotEmpty == true) { + directory = Directory('${directory.absolute.path}/$folderPath'); + } + } + + final exPath = directory.path; + await Directory(exPath).create(recursive: true); + return exPath; + } + + static Future copyInternalFilesToDownloadExternal(List listFilePaths) async { + final externalPath = await getExternalDocumentPath(); + + List externalListPaths = []; + for (var filePath in listFilePaths) { + final file = File(filePath); + final fileName = filePath.substring(filePath.lastIndexOf('/') + 1); + final externalFile = File('$externalPath/$fileName'); + await externalFile.writeAsBytes(file.readAsBytesSync()); + externalListPaths.add(externalFile.path); + } + + if (externalListPaths.isNotEmpty) { + return externalPath; + } else { + throw NotFoundFileInFolderException(); + } + } + + Future exportTraceLog(TraceLog traceLog) async { + if (PlatformInfo.isIOS) { + final savePath = getExternalDocumentPath(folderPath: logFolder); + return savePath; + } else { + final savePath = await compute( + copyInternalFilesToDownloadExternal, + traceLog.listFilePaths, + ); + return savePath; + } + } +} \ No newline at end of file diff --git a/core/lib/utils/logger/trace_log.dart b/core/lib/utils/logger/trace_log.dart new file mode 100644 index 000000000..c1a73970a --- /dev/null +++ b/core/lib/utils/logger/trace_log.dart @@ -0,0 +1,16 @@ +import 'package:equatable/equatable.dart'; + +class TraceLog with EquatableMixin { + final String path; + final int size; + final List listFilePaths; + + TraceLog({ + required this.path, + required this.size, + required this.listFilePaths + }); + + @override + List get props => [path, size, listFilePaths]; +} \ No newline at end of file diff --git a/lib/features/manage_account/data/datasource/trace_log_datasource.dart b/lib/features/manage_account/data/datasource/trace_log_datasource.dart new file mode 100644 index 000000000..82ede5e1c --- /dev/null +++ b/lib/features/manage_account/data/datasource/trace_log_datasource.dart @@ -0,0 +1,4 @@ + +abstract class TraceLogDataSource { + Future exportTraceLog(); +} \ No newline at end of file diff --git a/lib/features/manage_account/data/datasource_impl/trace_log_datasource_impl.dart b/lib/features/manage_account/data/datasource_impl/trace_log_datasource_impl.dart new file mode 100644 index 000000000..3a42e67ad --- /dev/null +++ b/lib/features/manage_account/data/datasource_impl/trace_log_datasource_impl.dart @@ -0,0 +1,59 @@ + +import 'package:core/data/utils/device_manager.dart'; +import 'package:core/domain/exceptions/file_exception.dart'; +import 'package:core/utils/logger/log_tracking.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:tmail_ui_user/features/manage_account/data/datasource/trace_log_datasource.dart'; +import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; +import 'package:tmail_ui_user/main/exceptions/permission_exception.dart'; +import 'package:tmail_ui_user/main/permissions/permission_service.dart'; + +class TraceLogDataSourceImpl extends TraceLogDataSource { + + final LogTracking _logTracking; + final DeviceManager _deviceManager; + final PermissionService _permissionService; + final ExceptionThrower _exceptionThrower; + + TraceLogDataSourceImpl( + this._logTracking, + this._deviceManager, + this._permissionService, + this._exceptionThrower, + ); + + @override + Future exportTraceLog() { + return Future.sync(() async { + if (PlatformInfo.isAndroid) { + final permissionGranted = await _validateStoragePermissionOnAndroid(); + if (permissionGranted) { + final traceLog = await _logTracking.getTraceLog(); + return await _logTracking.exportTraceLog(traceLog); + } else { + throw const NotGrantedPermissionStorageException(); + } + } else { + final traceLog = await _logTracking.getTraceLog(); + final savePath = await _logTracking.exportTraceLog(traceLog); + final result = await Share.shareXFiles([XFile(savePath)]); + if (result.status == ShareResultStatus.success) { + return savePath; + } + throw UserCancelShareFileException(); + } + }).catchError(_exceptionThrower.throwException); + } + + Future _validateStoragePermissionOnAndroid() async { + final needRequestPermission = await _deviceManager.isNeedRequestStoragePermissionOnAndroid(); + if (needRequestPermission) { + final isGranted = await _permissionService.isGranted(Permission.storage); + return isGranted; + } else { + return true; + } + } +} \ No newline at end of file diff --git a/lib/features/manage_account/data/repository/trace_log_repository.dart b/lib/features/manage_account/data/repository/trace_log_repository.dart new file mode 100644 index 000000000..9a6694093 --- /dev/null +++ b/lib/features/manage_account/data/repository/trace_log_repository.dart @@ -0,0 +1,4 @@ + +abstract class TraceLogRepository { + Future exportTraceLog(); +} \ No newline at end of file diff --git a/lib/features/manage_account/domain/repository/trace_log_repository_impl.dart b/lib/features/manage_account/domain/repository/trace_log_repository_impl.dart new file mode 100644 index 000000000..8df3c6380 --- /dev/null +++ b/lib/features/manage_account/domain/repository/trace_log_repository_impl.dart @@ -0,0 +1,13 @@ +import 'package:tmail_ui_user/features/manage_account/data/datasource/trace_log_datasource.dart'; +import 'package:tmail_ui_user/features/manage_account/data/repository/trace_log_repository.dart'; + +class TraceLogRepositoryImpl extends TraceLogRepository { + final TraceLogDataSource _traceLogDataSource; + + TraceLogRepositoryImpl(this._traceLogDataSource); + + @override + Future exportTraceLog() { + return _traceLogDataSource.exportTraceLog(); + } +} \ No newline at end of file diff --git a/lib/features/manage_account/domain/state/export_trace_log_state.dart b/lib/features/manage_account/domain/state/export_trace_log_state.dart new file mode 100644 index 000000000..41f79aa4e --- /dev/null +++ b/lib/features/manage_account/domain/state/export_trace_log_state.dart @@ -0,0 +1,19 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; + +class ExportTraceLogLoading extends LoadingState {} + +class ExportTraceLogSuccess extends UIState { + + String savePath; + + ExportTraceLogSuccess(this.savePath); + + @override + List get props => [savePath]; +} + +class ExportTraceLogFailure extends FeatureFailure { + + ExportTraceLogFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/manage_account/domain/usecases/export_trace_log_interactor.dart b/lib/features/manage_account/domain/usecases/export_trace_log_interactor.dart new file mode 100644 index 000000000..e4af253df --- /dev/null +++ b/lib/features/manage_account/domain/usecases/export_trace_log_interactor.dart @@ -0,0 +1,21 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:tmail_ui_user/features/manage_account/data/repository/trace_log_repository.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/export_trace_log_state.dart'; + +class ExportTraceLogInteractor { + final TraceLogRepository _traceLogRepository; + + ExportTraceLogInteractor(this._traceLogRepository); + + Stream> execute() async* { + try { + yield Right(ExportTraceLogLoading()); + final savePath = await _traceLogRepository.exportTraceLog(); + yield Right(ExportTraceLogSuccess(savePath)); + } catch (exception) { + yield Left(ExportTraceLogFailure(exception)); + } + } +} \ No newline at end of file diff --git a/lib/features/manage_account/presentation/extensions/export_trace_log_extension.dart b/lib/features/manage_account/presentation/extensions/export_trace_log_extension.dart new file mode 100644 index 000000000..5925413d7 --- /dev/null +++ b/lib/features/manage_account/presentation/extensions/export_trace_log_extension.dart @@ -0,0 +1,48 @@ + +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/export_trace_log_state.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/export_trace_log_interactor.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/manage_account_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/menu/trace_log_interactor_bindings.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +extension ExportTraceLogExtension on ManageAccountDashBoardController { + + void injectTraceLogDependencies() { + TraceLogInteractorBindings().dependencies(); + } + + void disposeTraceLogDependencies() { + TraceLogInteractorBindings().dispose(); + } + + Future showExportTraceLogConfirmDialog(BuildContext context) async { + await showConfirmDialogAction( + context, + AppLocalizations.of(context).messageExportTraceLogDialog, + AppLocalizations.of(context).yes, + title: AppLocalizations.of(context).exportTraceLog, + alignCenter: true, + outsideDismissible: false, + useIconAsBasicLogo: false, + cancelTitle: AppLocalizations.of(context).no, + onConfirmAction: _exportTraceLog, + ); + } + + void _exportTraceLog() { + final exportTraceLogInteractor = getBinding(); + if (exportTraceLogInteractor != null) { + consumeState(exportTraceLogInteractor.execute()); + } + } + + void handleExportTraceLogSuccess(ExportTraceLogSuccess success) { + toastManager.showMessageSuccess(success); + } + + void handleExportTraceLogFailure(ExportTraceLogFailure failure) { + toastManager.showMessageFailure(failure); + } +} \ No newline at end of file diff --git a/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart b/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart index 883ca51b3..4a88373d4 100644 --- a/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart +++ b/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart @@ -16,12 +16,14 @@ import 'package:server_settings/server_settings/capability_server_settings.dart' import 'package:tmail_ui_user/features/base/action/ui_action.dart'; import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mixin/user_setting_popup_menu_mixin.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/export_trace_log_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_vacation_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/update_vacation_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_vacation_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/update_vacation_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/action/dashboard_setting_action.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/email_rules/bindings/email_rules_bindings.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/extensions/export_trace_log_extension.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/extensions/vacation_response_extension.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/forward/bindings/forward_bindings.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/language_and_region/language_and_region_bindings.dart'; @@ -60,6 +62,9 @@ class ManageAccountDashBoardController extends ReloadableController with UserSet void onInit() { BackButtonInterceptor.add(_onBackButtonInterceptor, name: AppRoutes.settings); super.onInit(); + if (AppConfig.isApiLoggingEnabled) { + injectTraceLogDependencies(); + } } @override @@ -77,11 +82,22 @@ class ManageAccountDashBoardController extends ReloadableController with UserSet } } else if (success is UpdateVacationSuccess) { _handleUpdateVacationSuccess(success); + } else if (success is ExportTraceLogSuccess) { + handleExportTraceLogSuccess(success); } else { super.handleSuccessViewState(success); } } + @override + void handleFailureViewState(Failure failure) { + if (failure is ExportTraceLogFailure) { + handleExportTraceLogFailure(failure); + } else { + super.handleFailureViewState(failure); + } + } + @override void handleReloaded(Session session) { log('ManageAccountDashBoardController::handleReloaded:'); @@ -400,6 +416,9 @@ class ManageAccountDashBoardController extends ReloadableController with UserSet @override void onClose() { BackButtonInterceptor.removeByName(AppRoutes.settings); + if (AppConfig.isApiLoggingEnabled) { + disposeTraceLogDependencies(); + } super.onClose(); } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/menu/settings/settings_view.dart b/lib/features/manage_account/presentation/menu/settings/settings_view.dart index 35b4da6e9..5f4ec03c7 100644 --- a/lib/features/manage_account/presentation/menu/settings/settings_view.dart +++ b/lib/features/manage_account/presentation/menu/settings/settings_view.dart @@ -1,11 +1,13 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/utils/style_utils.dart'; import 'package:core/presentation/views/button/icon_button_web.dart'; +import 'package:core/presentation/views/button/multi_click_widget.dart'; import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/email_rules/email_rules_view.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/extensions/export_trace_log_extension.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/extensions/vacation_response_extension.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/forward/forward_view.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/language_and_region/language_and_region_view.dart'; @@ -21,6 +23,7 @@ import 'package:tmail_ui_user/features/manage_account/presentation/profiles/prof import 'package:tmail_ui_user/features/manage_account/presentation/vacation/vacation_view.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/vacation/widgets/vacation_notification_message_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/utils/app_config.dart'; typedef CloseSettingsViewAction = void Function(); @@ -84,7 +87,7 @@ class SettingsView extends GetWidget { } Widget _buildUniversalSettingAppBar(BuildContext context) { - return Stack( + final appBarWidget = Stack( alignment: Alignment.center, children: [ Positioned(left: 0,child: _buildCloseSettingButton(context)), @@ -98,10 +101,23 @@ class SettingsView extends GetWidget { style: const TextStyle( fontSize: 20, color: AppColor.colorNameEmail, - fontWeight: FontWeight.w700)) - ) - ] + fontWeight: FontWeight.w700, + ), + ), + ), + ], ); + + if (AppConfig.isApiLoggingEnabled) { + return MultiClickWidget( + onMultiTap: () => controller + .manageAccountDashboardController + .showExportTraceLogConfirmDialog(context), + child: appBarWidget, + ); + } else { + return appBarWidget; + } } Widget _buildSettingLevel1AppBar(BuildContext context) { diff --git a/lib/features/manage_account/presentation/menu/trace_log_interactor_bindings.dart b/lib/features/manage_account/presentation/menu/trace_log_interactor_bindings.dart new file mode 100644 index 000000000..3af1cd893 --- /dev/null +++ b/lib/features/manage_account/presentation/menu/trace_log_interactor_bindings.dart @@ -0,0 +1,53 @@ +import 'package:core/data/utils/device_manager.dart'; +import 'package:core/utils/logger/log_tracking.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/base/interactors_bindings.dart'; +import 'package:tmail_ui_user/features/manage_account/data/datasource/trace_log_datasource.dart'; +import 'package:tmail_ui_user/features/manage_account/data/datasource_impl/trace_log_datasource_impl.dart'; +import 'package:tmail_ui_user/features/manage_account/data/repository/trace_log_repository.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/repository/trace_log_repository_impl.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/export_trace_log_interactor.dart'; +import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; +import 'package:tmail_ui_user/main/permissions/permission_service.dart'; + +class TraceLogInteractorBindings extends InteractorsBindings { + @override + void bindingsDataSource() { + Get.lazyPut(() => Get.find()); + } + + @override + void bindingsDataSourceImpl() { + Get.lazyPut(() => LogTracking()); + Get.lazyPut(() => TraceLogDataSourceImpl( + Get.find(), + Get.find(), + Get.find(), + Get.find(), + )); + } + + @override + void bindingsInteractor() { + Get.lazyPut(() => ExportTraceLogInteractor(Get.find())); + } + + @override + void bindingsRepository() { + Get.lazyPut(() => Get.find()); + } + + @override + void bindingsRepositoryImpl() { + Get.lazyPut(() => TraceLogRepositoryImpl(Get.find())); + } + + void dispose() { + Get.delete(); + Get.delete(); + Get.delete(); + Get.delete(); + Get.delete(); + Get.delete(); + } +} diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index b055b3e01..a28fc54df 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2025-04-14T04:16:15.237055", + "@@last_modified": "2025-05-16T20:14:59.453269", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -4469,5 +4469,45 @@ "placeholders": { "description": {} } + }, + "exportTraceLog": "Export trace log", + "@exportTraceLog": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageExportTraceLogDialog": "The mobile API error message log has been tracked. Do you want to export it?", + "@messageExportTraceLogDialog": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "youNeedToGrantFilesPermissionToExportFile": "You need to grant files permission to export files", + "@youNeedToGrantFilesPermissionToExportFile": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "exportTraceLogSuccess": "Export successful tracking logs at \"{path}\"", + "@exportTraceLogSuccess": { + "type": "text", + "placeholders_order": [ + "path" + ], + "placeholders": { + "path": {} + } + }, + "noLogsHaveBeenRecordedYet": "No logs have been recorded yet.", + "@noLogsHaveBeenRecordedYet": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "exportTraceLogFailed": "Export trace log failed", + "@exportTraceLogFailed": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/bindings/core/core_bindings.dart b/lib/main/bindings/core/core_bindings.dart index d929d2ff7..639af730e 100644 --- a/lib/main/bindings/core/core_bindings.dart +++ b/lib/main/bindings/core/core_bindings.dart @@ -17,6 +17,7 @@ import 'package:tmail_ui_user/features/base/before_reconnect_manager.dart'; import 'package:tmail_ui_user/features/caching/utils/local_storage_manager.dart'; import 'package:tmail_ui_user/features/caching/utils/session_storage_manager.dart'; import 'package:tmail_ui_user/features/sending_queue/presentation/utils/sending_queue_isolate_manager.dart'; +import 'package:tmail_ui_user/main/permissions/permission_service.dart'; import 'package:tmail_ui_user/main/utils/app_config.dart'; import 'package:tmail_ui_user/main/utils/email_receive_manager.dart'; import 'package:tmail_ui_user/main/utils/ios_notification_manager.dart'; @@ -59,6 +60,7 @@ class CoreBindings extends Bindings { void _bindingDeviceManager() { Get.put(DeviceInfoPlugin()); Get.put(DeviceManager(Get.find())); + Get.put(PermissionService()); } void _bindingReceivingSharingStream() { diff --git a/lib/main/exceptions/permission_exception.dart b/lib/main/exceptions/permission_exception.dart new file mode 100644 index 000000000..69f9b2cbd --- /dev/null +++ b/lib/main/exceptions/permission_exception.dart @@ -0,0 +1,15 @@ +import 'package:equatable/equatable.dart'; + +abstract class PermissionException with EquatableMixin implements Exception { + + final String? message; + + const PermissionException({this.message}); +} + +class NotGrantedPermissionStorageException extends PermissionException { + const NotGrantedPermissionStorageException() : super(message: 'Permission Storage has not been granted access'); + + @override + List get props => [super.message]; +} \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 55e437da5..7b1882de4 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4702,4 +4702,47 @@ class AppLocalizations { args: [description], ); } + + String get exportTraceLog { + return Intl.message( + 'Export trace log', + name: 'exportTraceLog', + ); + } + + String get messageExportTraceLogDialog { + return Intl.message( + 'The mobile API error message log has been tracked. Do you want to export it?', + name: 'messageExportTraceLogDialog', + ); + } + + String get youNeedToGrantFilesPermissionToExportFile { + return Intl.message( + 'You need to grant files permission to export files', + name: 'youNeedToGrantFilesPermissionToExportFile', + ); + } + + String exportTraceLogSuccess(String path) { + return Intl.message( + 'Export successful tracking logs at "$path"', + name: 'exportTraceLogSuccess', + args: [path], + ); + } + + String get noLogsHaveBeenRecordedYet { + return Intl.message( + 'No logs have been recorded yet.', + name: 'noLogsHaveBeenRecordedYet', + ); + } + + String get exportTraceLogFailed { + return Intl.message( + 'Export trace log failed', + name: 'exportTraceLogFailed', + ); + } } diff --git a/lib/main/utils/toast_manager.dart b/lib/main/utils/toast_manager.dart index e6845c8ce..2fac9f69e 100644 --- a/lib/main/utils/toast_manager.dart +++ b/lib/main/utils/toast_manager.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:core/domain/exceptions/file_exception.dart'; import 'package:core/domain/exceptions/web_session_exception.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; @@ -19,12 +20,14 @@ import 'package:tmail_ui_user/features/home/domain/state/get_session_state.dart' import 'package:tmail_ui_user/features/login/data/network/oidc_error.dart'; import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/clear_mailbox_state.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/export_trace_log_state.dart'; import 'package:tmail_ui_user/features/starting_page/domain/state/sign_in_twake_workplace_state.dart'; import 'package:tmail_ui_user/features/starting_page/domain/state/sign_up_twake_workplace_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; import 'package:tmail_ui_user/main/error/error_method_response_exception_extension.dart'; +import 'package:tmail_ui_user/main/exceptions/permission_exception.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; @@ -132,6 +135,14 @@ class ToastManager { } else { message = AppLocalizations.of(currentContext!).previewEmailFromEMLFileFailed; } + } else if (failure is ExportTraceLogFailure) { + if (failure.exception is NotGrantedPermissionStorageException) { + message = AppLocalizations.of(currentContext!).youNeedToGrantFilesPermissionToExportFile; + } else if (failure.exception is NotFoundFileInFolderException) { + message = AppLocalizations.of(currentContext!).noLogsHaveBeenRecordedYet; + } else { + message = AppLocalizations.of(currentContext!).exportTraceLogFailed; + } } if (message?.isNotEmpty == true) { @@ -151,6 +162,8 @@ class ToastManager { success is EmptySpamFolderSuccess || success is EmptyTrashFolderSuccess) { message = AppLocalizations.of(currentContext!).toast_message_empty_trash_folder_success; + } else if (success is ExportTraceLogSuccess) { + message = AppLocalizations.of(currentContext!).exportTraceLogSuccess(success.savePath); } if (message?.isNotEmpty == true) {