TF-3719 Add handle export log to device if need

Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
dab246
2025-05-16 20:16:11 +07:00
committed by Dat H. Pham
parent a891e07887
commit 0ca3ca170d
20 changed files with 541 additions and 6 deletions
+1
View File
@@ -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';
@@ -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<Object> 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');
}
@@ -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<MultiClickWidget> createState() => _MultiClickWidgetState();
}
class _MultiClickWidgetState extends State<MultiClickWidget> {
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,
);
}
}
+78 -1
View File
@@ -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<TraceLog> 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<String>)> 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<String> 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<String> copyInternalFilesToDownloadExternal(List<String> listFilePaths) async {
final externalPath = await getExternalDocumentPath();
List<String> 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<String> exportTraceLog(TraceLog traceLog) async {
if (PlatformInfo.isIOS) {
final savePath = getExternalDocumentPath(folderPath: logFolder);
return savePath;
} else {
final savePath = await compute(
copyInternalFilesToDownloadExternal,
traceLog.listFilePaths,
);
return savePath;
}
}
}
+16
View File
@@ -0,0 +1,16 @@
import 'package:equatable/equatable.dart';
class TraceLog with EquatableMixin {
final String path;
final int size;
final List<String> listFilePaths;
TraceLog({
required this.path,
required this.size,
required this.listFilePaths
});
@override
List<Object?> get props => [path, size, listFilePaths];
}