582 lines
18 KiB
Dart
582 lines
18 KiB
Dart
import 'dart:io';
|
|
import 'package:dio/dio.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
import 'package:device_info_plus/device_info_plus.dart';
|
|
import 'package:open_filex/open_filex.dart';
|
|
import 'package:gal/gal.dart';
|
|
import 'package:taxglide/services/notification_service.dart';
|
|
|
|
class DownloadHelper {
|
|
static const String API_BASE_URL = "https://www.taxglide.amrithaa.net/api/";
|
|
|
|
/// Request storage permission based on Android version
|
|
static Future<bool> requestStoragePermission() async {
|
|
if (!Platform.isAndroid) return true;
|
|
|
|
try {
|
|
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
|
final sdkInt = androidInfo.version.sdkInt;
|
|
|
|
print("📱 Android SDK: $sdkInt");
|
|
|
|
// Android 13+ (API 33+)
|
|
if (sdkInt >= 33) {
|
|
final photoStatus = await Permission.photos.request();
|
|
final videoStatus = await Permission.videos.request();
|
|
|
|
print("✅ Android 13+ - Media permissions requested");
|
|
return photoStatus.isGranted || videoStatus.isGranted;
|
|
}
|
|
|
|
// Android 11-12 (API 30-32)
|
|
if (sdkInt >= 30) {
|
|
if (await Permission.manageExternalStorage.isGranted) {
|
|
return true;
|
|
}
|
|
|
|
var status = await Permission.manageExternalStorage.request();
|
|
if (status.isGranted) return true;
|
|
|
|
status = await Permission.storage.request();
|
|
return status.isGranted;
|
|
}
|
|
|
|
// Android 10 and below (API 29 and below)
|
|
final status = await Permission.storage.request();
|
|
return status.isGranted;
|
|
} catch (e) {
|
|
print("❌ Permission error: $e");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// ⭐ NEW: Get TaxGlide folder in PUBLIC Downloads
|
|
static Future<Directory> getTaxGlideFolder() async {
|
|
try {
|
|
await requestStoragePermission();
|
|
|
|
if (Platform.isAndroid) {
|
|
// Use public Downloads folder for all Android versions
|
|
final publicTaxGlide = Directory(
|
|
'/storage/emulated/0/Download/TaxGlide',
|
|
);
|
|
|
|
if (!await publicTaxGlide.exists()) {
|
|
await publicTaxGlide.create(recursive: true);
|
|
print(
|
|
"✅ TaxGlide folder created in public Downloads: ${publicTaxGlide.path}",
|
|
);
|
|
}
|
|
|
|
return publicTaxGlide;
|
|
} else {
|
|
// iOS - use app documents
|
|
final baseDirectory = await getApplicationDocumentsDirectory();
|
|
final taxGlideFolder = Directory('${baseDirectory.path}/TaxGlide');
|
|
|
|
if (!await taxGlideFolder.exists()) {
|
|
await taxGlideFolder.create(recursive: true);
|
|
}
|
|
|
|
return taxGlideFolder;
|
|
}
|
|
} catch (e) {
|
|
print("❌ Error creating TaxGlide folder: $e");
|
|
// Fallback to app-specific storage if public storage fails
|
|
final fallbackDir = await getExternalStorageDirectory();
|
|
final taxGlideFolder = Directory('${fallbackDir!.path}/TaxGlide');
|
|
|
|
if (!await taxGlideFolder.exists()) {
|
|
await taxGlideFolder.create(recursive: true);
|
|
print("⚠️ Using app-specific storage as fallback");
|
|
}
|
|
|
|
return taxGlideFolder;
|
|
}
|
|
}
|
|
|
|
/// ⭐ Get Received folder in PUBLIC Downloads
|
|
static Future<Directory> getReceivedFolder() async {
|
|
final taxGlideFolder = await getTaxGlideFolder();
|
|
final receivedFolder = Directory('${taxGlideFolder.path}/Received');
|
|
|
|
if (!await receivedFolder.exists()) {
|
|
await receivedFolder.create(recursive: true);
|
|
print("✅ Received folder created: ${receivedFolder.path}");
|
|
}
|
|
|
|
return receivedFolder;
|
|
}
|
|
|
|
/// ⭐ Get Send folder in PUBLIC Downloads
|
|
static Future<Directory> getSendFolder() async {
|
|
final taxGlideFolder = await getTaxGlideFolder();
|
|
final sendFolder = Directory('${taxGlideFolder.path}/Send');
|
|
|
|
if (!await sendFolder.exists()) {
|
|
await sendFolder.create(recursive: true);
|
|
print("✅ Send folder created: ${sendFolder.path}");
|
|
}
|
|
|
|
return sendFolder;
|
|
}
|
|
|
|
/// Save file to Gallery (for images/videos only)
|
|
static Future<bool> saveToGallery(String filePath) async {
|
|
try {
|
|
final file = File(filePath);
|
|
if (!await file.exists()) {
|
|
print("❌ File does not exist: $filePath");
|
|
return false;
|
|
}
|
|
|
|
final fileName = filePath.split('/').last;
|
|
|
|
// Request permissions
|
|
final hasPermission = await requestStoragePermission();
|
|
if (!hasPermission) {
|
|
print("⚠️ No permission to save to gallery");
|
|
return false;
|
|
}
|
|
|
|
// Only save images/videos to Gallery
|
|
if (isImageFile(filePath)) {
|
|
await Gal.putImage(filePath, album: 'TaxGlide');
|
|
print("✅ Image saved to Gallery: $fileName");
|
|
return true;
|
|
} else if (isVideoFile(filePath)) {
|
|
await Gal.putVideo(filePath, album: 'TaxGlide');
|
|
print("✅ Video saved to Gallery: $fileName");
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
} catch (e) {
|
|
print("❌ Error saving to gallery: $e");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Build download URL
|
|
static String buildDownloadUrl(String filePath) {
|
|
if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
|
|
return filePath;
|
|
}
|
|
return '${API_BASE_URL}chat/download/$filePath';
|
|
}
|
|
|
|
/// Check if file exists in Received folder
|
|
static Future<String?> checkFileExistsInReceived(String url) async {
|
|
try {
|
|
final fileName = url.contains('/')
|
|
? url.split('/').last.split('?').first
|
|
: url.split('?').first;
|
|
|
|
final receivedFolder = await getReceivedFolder();
|
|
final filePath = '${receivedFolder.path}/$fileName';
|
|
final file = File(filePath);
|
|
|
|
if (await file.exists()) {
|
|
print("✅ File already exists in Received: $filePath");
|
|
return filePath;
|
|
}
|
|
return null;
|
|
} catch (e) {
|
|
print("❌ Error checking file existence: $e");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Backward compatibility
|
|
static Future<String?> checkFileExists(String url) async {
|
|
return await checkFileExistsInReceived(url);
|
|
}
|
|
|
|
/// ⭐ Download file to Received folder (now in public Downloads) and Gallery
|
|
static Future<Map<String, dynamic>> downloadFileToReceived(String url) async {
|
|
try {
|
|
final fullUrl = buildDownloadUrl(url);
|
|
print("🔗 Downloading from: $fullUrl");
|
|
|
|
// Check if file already exists
|
|
final existingFilePath = await checkFileExistsInReceived(fullUrl);
|
|
if (existingFilePath != null) {
|
|
print("📂 File already exists, opening it...");
|
|
await openDownloadedFile(existingFilePath);
|
|
return {
|
|
'filePath': existingFilePath,
|
|
'isNewDownload': false,
|
|
'success': true,
|
|
};
|
|
}
|
|
|
|
// Request permissions
|
|
await requestStoragePermission();
|
|
|
|
// Get filename and prepare save path
|
|
String fileName = fullUrl.split('/').last.split('?').first;
|
|
fileName = fileName.replaceAll(RegExp(r'[^\w\s\-\.]'), '_');
|
|
|
|
final receivedFolder = await getReceivedFolder();
|
|
final savePath = "${receivedFolder.path}/$fileName";
|
|
|
|
print("Download Started: Downloading $fileName...");
|
|
|
|
// Download file to Received folder (now in public Downloads)
|
|
Dio dio = Dio();
|
|
await dio.download(
|
|
fullUrl,
|
|
savePath,
|
|
onReceiveProgress: (received, total) {
|
|
if (total != -1) {
|
|
final progress = (received / total * 100).toStringAsFixed(0);
|
|
print("📊 Download progress: $progress%");
|
|
}
|
|
},
|
|
);
|
|
|
|
// Also save images/videos to Gallery
|
|
bool savedToGallery = false;
|
|
try {
|
|
savedToGallery = await saveToGallery(savePath);
|
|
} catch (e) {
|
|
print("⚠️ Could not save to gallery: $e");
|
|
}
|
|
|
|
if (savedToGallery) {
|
|
print(
|
|
"Download Complete: $fileName saved to Gallery & Downloads/TaxGlide/Received",
|
|
);
|
|
} else {
|
|
print(
|
|
"Download Complete: $fileName saved to Downloads/TaxGlide/Received",
|
|
);
|
|
}
|
|
|
|
print("✅ File downloaded to: $savePath");
|
|
|
|
// ⭐ Show Download Notification
|
|
try {
|
|
Get.find<NotificationService>().showDownloadNotification(fileName, savePath);
|
|
} catch (e) {
|
|
debugPrint('⚠️ Could not show download notification: $e');
|
|
}
|
|
|
|
// Open the downloaded file
|
|
await openDownloadedFile(savePath);
|
|
|
|
return {
|
|
'filePath': savePath,
|
|
'isNewDownload': true,
|
|
'success': true,
|
|
'savedToGallery': savedToGallery,
|
|
};
|
|
} catch (e) {
|
|
print("❌ Download error: $e");
|
|
print("Download Failed: Error: ${e.toString()}");
|
|
return {'success': false, 'error': e.toString()};
|
|
}
|
|
}
|
|
|
|
/// Backward compatibility
|
|
static Future<Map<String, dynamic>> downloadFile(String url) async {
|
|
return await downloadFileToReceived(url);
|
|
}
|
|
|
|
/// ⭐ Save file to Send folder (now in public Downloads)
|
|
static Future<String> saveToSendFolder(File file) async {
|
|
try {
|
|
if (!await file.exists()) {
|
|
throw Exception("Source file does not exist: ${file.path}");
|
|
}
|
|
|
|
final sendFolder = await getSendFolder();
|
|
final originalFileName = file.path.split('/').last;
|
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
|
final fileName = '${timestamp}_$originalFileName';
|
|
final savePath = '${sendFolder.path}/$fileName';
|
|
|
|
final savedFile = await file.copy(savePath);
|
|
|
|
print("✅ File saved to Send folder (Downloads/TaxGlide/Send): $savePath");
|
|
|
|
// Also save images/videos to Gallery
|
|
try {
|
|
await saveToGallery(savePath);
|
|
} catch (e) {
|
|
print("⚠️ Could not save to gallery: $e");
|
|
}
|
|
|
|
return savedFile.path;
|
|
} catch (e) {
|
|
print("❌ Error saving to Send folder: $e");
|
|
print("⚠️ Using original file path: ${file.path}");
|
|
return file.path;
|
|
}
|
|
}
|
|
|
|
/// Save multiple files to Send folder
|
|
static Future<List<File>> saveMultipleToSendFolder(List<File> files) async {
|
|
List<File> savedFiles = [];
|
|
|
|
for (File file in files) {
|
|
try {
|
|
final savedPath = await saveToSendFolder(file);
|
|
savedFiles.add(File(savedPath));
|
|
} catch (e) {
|
|
print("❌ Error saving file: ${file.path}, using original");
|
|
savedFiles.add(file);
|
|
}
|
|
}
|
|
|
|
return savedFiles;
|
|
}
|
|
|
|
/// Open file with appropriate viewer
|
|
static Future<void> openDownloadedFile(String path) async {
|
|
print("🔓 Opening file: $path");
|
|
|
|
try {
|
|
final file = File(path);
|
|
if (!await file.exists()) {
|
|
print("❌ File not found at $path");
|
|
print("Error: File not found");
|
|
return;
|
|
}
|
|
|
|
final result = await OpenFilex.open(path);
|
|
|
|
if (result.type != ResultType.done) {
|
|
print("⚠️ Could not open file: ${result.message}");
|
|
print("Notice: File saved but no app found to open it");
|
|
}
|
|
} catch (e) {
|
|
print("❌ Error opening file: $e");
|
|
print("Error: Could not open file");
|
|
}
|
|
}
|
|
|
|
/// ⭐ NEW: Open folder in system file manager
|
|
static Future<void> openFolder(String path) async {
|
|
print("📂 Opening folder: $path");
|
|
|
|
try {
|
|
final dir = Directory(path);
|
|
if (!await dir.exists()) {
|
|
print("❌ Folder not found at $path");
|
|
return;
|
|
}
|
|
|
|
// On Android, most file managers respond well to OpenFilex on a directory
|
|
final result = await OpenFilex.open(path);
|
|
|
|
if (result.type != ResultType.done) {
|
|
print("⚠️ Could not open folder: ${result.message}");
|
|
}
|
|
} catch (e) {
|
|
print("❌ Error opening folder: $e");
|
|
}
|
|
}
|
|
|
|
/// Get all files from Send folder
|
|
static Future<List<File>> getSentFiles() async {
|
|
try {
|
|
final sendFolder = await getSendFolder();
|
|
if (await sendFolder.exists()) {
|
|
return sendFolder.listSync().whereType<File>().toList()..sort(
|
|
(a, b) => b.lastModifiedSync().compareTo(a.lastModifiedSync()),
|
|
);
|
|
}
|
|
return [];
|
|
} catch (e) {
|
|
print("❌ Error getting sent files: $e");
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/// Get all files from Received folder
|
|
static Future<List<File>> getReceivedFiles() async {
|
|
try {
|
|
final receivedFolder = await getReceivedFolder();
|
|
if (await receivedFolder.exists()) {
|
|
return receivedFolder.listSync().whereType<File>().toList()..sort(
|
|
(a, b) => b.lastModifiedSync().compareTo(a.lastModifiedSync()),
|
|
);
|
|
}
|
|
return [];
|
|
} catch (e) {
|
|
print("❌ Error getting received files: $e");
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/// Delete a file
|
|
static Future<bool> deleteFile(String filePath) async {
|
|
try {
|
|
final file = File(filePath);
|
|
if (await file.exists()) {
|
|
await file.delete();
|
|
print("✅ File deleted: $filePath");
|
|
print("File Deleted: File removed successfully");
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch (e) {
|
|
print("❌ Error deleting file: $e");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Clear all files from Send folder
|
|
static Future<void> clearSendFolder() async {
|
|
try {
|
|
final files = await getSentFiles();
|
|
for (final file in files) {
|
|
await file.delete();
|
|
}
|
|
print("✅ Send folder cleared: ${files.length} files deleted");
|
|
} catch (e) {
|
|
print("❌ Error clearing Send folder: $e");
|
|
}
|
|
}
|
|
|
|
/// Clear all files from Received folder
|
|
static Future<void> clearReceivedFolder() async {
|
|
try {
|
|
final files = await getReceivedFiles();
|
|
for (final file in files) {
|
|
await file.delete();
|
|
}
|
|
print("✅ Received folder cleared: ${files.length} files deleted");
|
|
} catch (e) {
|
|
print("❌ Error clearing Received folder: $e");
|
|
}
|
|
}
|
|
|
|
/// Get file size in human-readable format
|
|
static Future<String> getFileSize(File file) async {
|
|
try {
|
|
final bytes = await file.length();
|
|
if (bytes < 1024) return '$bytes B';
|
|
if (bytes < 1024 * 1024) {
|
|
return '${(bytes / 1024).toStringAsFixed(2)} KB';
|
|
}
|
|
if (bytes < 1024 * 1024 * 1024) {
|
|
return '${(bytes / (1024 * 1024)).toStringAsFixed(2)} MB';
|
|
}
|
|
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB';
|
|
} catch (e) {
|
|
return 'Unknown';
|
|
}
|
|
}
|
|
|
|
/// Get file extension
|
|
static String getFileExtension(String path) {
|
|
return path.split('.').last.toLowerCase();
|
|
}
|
|
|
|
/// Check if file is image
|
|
static bool isImageFile(String path) {
|
|
final ext = getFileExtension(path);
|
|
return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].contains(ext);
|
|
}
|
|
|
|
/// Check if file is document
|
|
static bool isDocumentFile(String path) {
|
|
final ext = getFileExtension(path);
|
|
return [
|
|
'pdf',
|
|
'doc',
|
|
'docx',
|
|
'xls',
|
|
'xlsx',
|
|
'txt',
|
|
'ppt',
|
|
'pptx',
|
|
'rtf',
|
|
'odt',
|
|
'rar',
|
|
'zip',
|
|
'7z',
|
|
].contains(ext);
|
|
}
|
|
|
|
/// Check if file is an archive
|
|
static bool isArchiveFile(String path) {
|
|
final ext = getFileExtension(path);
|
|
return ['zip', 'rar', '7z', 'tar', 'gz'].contains(ext);
|
|
}
|
|
|
|
/// Check if file is video
|
|
static bool isVideoFile(String path) {
|
|
final ext = getFileExtension(path);
|
|
return ['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm'].contains(ext);
|
|
}
|
|
|
|
/// Check if file is audio
|
|
static bool isAudioFile(String path) {
|
|
final ext = getFileExtension(path);
|
|
return ['mp3', 'wav', 'aac', 'flac', 'ogg', 'm4a', 'wma'].contains(ext);
|
|
}
|
|
|
|
/// Get file icon based on type
|
|
static String getFileIcon(String path) {
|
|
if (isImageFile(path)) return '🖼️';
|
|
if (isVideoFile(path)) return '🎥';
|
|
if (isAudioFile(path)) return '🎵';
|
|
if (isDocumentFile(path)) {
|
|
final ext = getFileExtension(path);
|
|
if (ext == 'pdf') return '📄';
|
|
if (['doc', 'docx'].contains(ext)) return '📝';
|
|
if (['xls', 'xlsx'].contains(ext)) return '📊';
|
|
if (['ppt', 'pptx'].contains(ext)) return '📽️';
|
|
if (isArchiveFile(path)) return '📦';
|
|
}
|
|
return '📎';
|
|
}
|
|
|
|
/// Get storage info
|
|
static Future<Map<String, String>> getStorageInfo() async {
|
|
try {
|
|
final taxGlideFolder = await getTaxGlideFolder();
|
|
final sentFiles = await getSentFiles();
|
|
final receivedFiles = await getReceivedFiles();
|
|
|
|
int totalSentSize = 0;
|
|
int totalReceivedSize = 0;
|
|
|
|
for (final file in sentFiles) {
|
|
totalSentSize += await file.length();
|
|
}
|
|
|
|
for (final file in receivedFiles) {
|
|
totalReceivedSize += await file.length();
|
|
}
|
|
|
|
return {
|
|
'taxGlidePath': taxGlideFolder.path,
|
|
'sentFiles': sentFiles.length.toString(),
|
|
'receivedFiles': receivedFiles.length.toString(),
|
|
'sentSize': _formatBytes(totalSentSize),
|
|
'receivedSize': _formatBytes(totalReceivedSize),
|
|
'totalSize': _formatBytes(totalSentSize + totalReceivedSize),
|
|
};
|
|
} catch (e) {
|
|
print("❌ Error getting storage info: $e");
|
|
return {};
|
|
}
|
|
}
|
|
|
|
static String _formatBytes(int bytes) {
|
|
if (bytes < 1024) return '$bytes B';
|
|
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(2)} KB';
|
|
if (bytes < 1024 * 1024 * 1024) {
|
|
return '${(bytes / (1024 * 1024)).toStringAsFixed(2)} MB';
|
|
}
|
|
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB';
|
|
}
|
|
}
|