file_picker: ^8.0.0
path: ^1.9.0
permission_handler: ^11.3.0
int activeDownloads = 0;
import 'dart:io';
import 'package:permission_handler/permission_handler.dart';
import 'package:file_picker/file_picker.dart';
Future pickSavePath() async {
// --- 1. Request storage permission ONLY on Android ---
if (Platform.isAndroid) {
final perm = await Permission.storage.request();
if (!perm.isGranted) {
return Future.error('Permission Denied');
}
}
// --- 2. Open folder picker (works on macOS, iOS, Windows, Android, Linux) ---
String? selectedDirectory = await FilePicker.platform.getDirectoryPath();
if (selectedDirectory == null) {
return null; // user canceled
}
return selectedDirectory;
}
import 'dart:convert';
import 'dart:io';
Future returnFileName(String url) async {
try {
final client = HttpClient();
String currentUrl = url;
int redirectCount = 0;
HttpClientResponse response;
// -------- MANUAL REDIRECT FOLLOWING --------
while (true) {
final req = await client.getUrl(Uri.parse(currentUrl));
response = await req.close();
if (response.statusCode == 301 ||
response.statusCode == 302 ||
response.statusCode == 303 ||
response.statusCode == 307 ||
response.statusCode == 308) {
if (redirectCount >= 10) break;
final location = response.headers.value("location");
if (location == null) break;
currentUrl = location;
redirectCount++;
continue;
}
break; // Got final response
}
// -------- EXTRACT CONTENT-DISPOSITION --------
final cd = response.headers.value("content-disposition");
if (cd != null) {
// filename*=UTF-8''ActualName.xyz
final utf8Match = RegExp(r"filename\*=(?:UTF-8'')?(.+)").firstMatch(cd);
if (utf8Match != null) {
return Uri.decodeFull(utf8Match.group(1)!);
}
// filename="ActualName.xyz"
final normalMatch = RegExp(r'filename="?([^\";]+)"?').firstMatch(cd);
if (normalMatch != null) {
return normalMatch.group(1);
}
}
// -------- FALLBACK: use final URL last segment --------
return Uri.parse(currentUrl).pathSegments.last;
} catch (e, st) {
print("returnFileName ERROR → $e");
print(st);
return null;
}
}
import 'dart:io';
import 'dart:async';
import 'dart:typed_data';
import 'package:tutorials/custom_code/download_globals.dart';
Future downloadFile(
String? fileId,
String? url,
String? savePath,
) async {
if (fileId == null || url == null || savePath == null) return;
print("downloadFile START fileId=$fileId");
// ---------------------------------------------
// PARALLEL DOWNLOAD LIMIT CONTROL (FIXED)
// ---------------------------------------------
final maxParallel = FFAppState().parallelDownloads ?? 0;
if (maxParallel > 0) {
while (activeDownloads >= maxParallel) {
print(
"Parallel limit reached ($activeDownloads/$maxParallel). Waiting...");
await Future.delayed(Duration(milliseconds: 300));
}
}
// Reserve slot
activeDownloads++;
print("Slot taken. Active Downloads = $activeDownloads");
// ---------------------------------------------
// LOAD ITEM
// ---------------------------------------------
var list = FFAppState().downloadsList.toList();
var index = list.indexWhere((d) => d.id == fileId);
if (index == -1) {
activeDownloads--;
return;
}
var item = list[index];
// ---------------------------------------------
// AUTO FILENAME USING item.fileName + NO DUPLICATE
// ---------------------------------------------
String fileName = item.fileName ?? url.split("/").last;
// Split base + extension
String base = fileName;
String? ext;
int dotIndex = fileName.lastIndexOf(".");
if (dotIndex != -1) {
base = fileName.substring(0, dotIndex);
ext = fileName.substring(dotIndex);
}
// Build file path
String finalPath = "$savePath/$fileName";
int counter = 1;
// If file exists → append _1, _2, _3...
while (File(finalPath).existsSync()) {
finalPath = "$savePath/${base}_$counter${ext ?? ''}";
counter++;
}
// Update state before download
item.status = "downloading";
item.downloaded = 0;
item.speed = 0;
item.savePath = finalPath;
list[index] = item;
FFAppState().downloadsList = list;
FFAppState().update(() {});
try {
final file = File(finalPath);
if (!file.parent.existsSync()) {
file.parent.createSync(recursive: true);
}
final sink = file.openWrite(mode: FileMode.write);
final client = HttpClient();
final req = await client.getUrl(Uri.parse(url));
final res = await req.close();
int totalSize = res.contentLength;
int downloaded = 0;
// REAL SPEED VARIABLES
int lastDownloaded = 0;
int lastCheck = DateTime.now().millisecondsSinceEpoch;
// ---------------------------------------------
// MAIN DOWNLOAD LOOP
// ---------------------------------------------
await for (final chunk in res) {
sink.add(chunk);
downloaded += chunk.length;
// REAL SPEED (bytes/sec)
int now = DateTime.now().millisecondsSinceEpoch;
int elapsed = now - lastCheck;
if (elapsed >= 1000) {
item.speed = downloaded - lastDownloaded;
lastDownloaded = downloaded;
lastCheck = now;
}
// SPEED LIMIT
final limit = FFAppState().downloadSpeedLimit;
if (!(limit == null || limit == 0)) {
if (item.speed > limit) {
int delayMs =
(((item.speed - limit) / limit) * 1000).clamp(5, 300).toInt();
await Future.delayed(Duration(milliseconds: delayMs));
}
}
// REFRESH STATE
list = FFAppState().downloadsList.toList();
index = list.indexWhere((d) => d.id == fileId);
if (index == -1) break;
item = list[index];
// PAUSE
if (item.status == "paused") {
await sink.flush();
await sink.close();
client.close(force: true);
activeDownloads = (activeDownloads - 1).clamp(0, 9999);
print("Paused. Active = $activeDownloads");
return;
}
// CANCEL
if (item.status == "cancelled") {
await sink.flush();
await sink.close();
client.close(force: true);
if (file.existsSync()) file.deleteSync();
activeDownloads = (activeDownloads - 1).clamp(0, 9999);
print("Cancelled. Active = $activeDownloads");
return;
}
// UPDATE
item.totalSize = totalSize;
item.downloaded = downloaded;
item.status = "downloading";
list[index] = item;
FFAppState().downloadsList = list;
FFAppState().update(() {});
}
// FINISH DOWNLOAD
await sink.flush();
await sink.close();
client.close(force: true);
list = FFAppState().downloadsList.toList();
index = list.indexWhere((d) => d.id == fileId);
if (index != -1) {
item = list[index];
item.status = "completed";
item.speed = 0;
list[index] = item;
FFAppState().downloadsList = list;
FFAppState().update(() {});
}
activeDownloads = (activeDownloads - 1).clamp(0, 9999);
print("Completed. Active = $activeDownloads");
} catch (e, st) {
print("downloadFile ERROR: $e");
print(st);
activeDownloads = (activeDownloads - 1).clamp(0, 9999);
print("Error. Active = $activeDownloads");
}
}
Future pauseDownload(String? fileId) async {
if (fileId == null) return;
var list = FFAppState().downloadsList.toList();
var index = list.indexWhere((d) => d.id == fileId);
if (index == -1) return;
var item = list[index];
item.status = "paused";
item.speed = 0;
list[index] = item;
FFAppState().downloadsList = list;
FFAppState().update(() {});
print("Paused: $fileId");
}
import 'dart:io';
import 'dart:async';
import 'dart:typed_data';
import 'package:tutorials/custom_code/download_globals.dart';
Future resumeDownload(
String? fileId,
String? url,
String? savePath,
) async {
if (fileId == null || url == null || savePath == null) return;
print("resumeDownload START → $fileId");
// ----------------------------------------------------
// PARALLEL DOWNLOAD CONTROL (FIXED)
// ----------------------------------------------------
final maxParallel = FFAppState().parallelDownloads ?? 0;
if (maxParallel > 0) {
while (activeDownloads >= maxParallel) {
print(
"Parallel limit reached ($activeDownloads/$maxParallel). Waiting...");
await Future.delayed(Duration(milliseconds: 300));
}
}
// Reserve the slot
activeDownloads++;
print("Slot taken → Active Downloads: $activeDownloads");
// ----------------------------------------------------
// LOAD ITEM
// ----------------------------------------------------
var list = FFAppState().downloadsList.toList();
var index = list.indexWhere((d) => d.id == fileId);
if (index == -1) {
activeDownloads--;
return;
}
var item = list[index];
final file = File(savePath);
final alreadyDownloaded = file.existsSync() ? file.lengthSync() : 0;
print("Resuming from $alreadyDownloaded bytes");
// Update AppState
item.status = "downloading";
item.downloaded = alreadyDownloaded;
item.speed = 0;
list[index] = item;
FFAppState().downloadsList = list;
FFAppState().update(() {});
try {
// HTTP request with Range header
final client = HttpClient();
final req = await client.getUrl(Uri.parse(url));
req.headers.add("Range", "bytes=$alreadyDownloaded-");
final res = await req.close();
final sink = file.openWrite(mode: FileMode.append);
int downloaded = alreadyDownloaded;
int totalSize = alreadyDownloaded + res.contentLength;
// REAL SPEED VARIABLES
int lastDownloaded = downloaded;
int lastCheck = DateTime.now().millisecondsSinceEpoch;
// ----------------------------------------------------
// MAIN RESUME LOOP
// ----------------------------------------------------
await for (final chunk in res) {
sink.add(chunk);
downloaded += chunk.length;
// REAL SPEED CALCULATION
final now = DateTime.now().millisecondsSinceEpoch;
final elapsed = now - lastCheck;
if (elapsed >= 1000) {
final bytesPerSecond = downloaded - lastDownloaded;
item.speed = bytesPerSecond;
lastDownloaded = downloaded;
lastCheck = now;
}
// SPEED LIMITING (if any)
final limit = FFAppState().downloadSpeedLimit;
if (!(limit == null || limit == 0)) {
if (item.speed > limit) {
int delayMs =
(((item.speed - limit) / limit) * 1000).clamp(5, 300).toInt();
await Future.delayed(Duration(milliseconds: delayMs));
}
}
// Refresh AppState
list = FFAppState().downloadsList.toList();
index = list.indexWhere((d) => d.id == fileId);
if (index == -1) break;
item = list[index];
// ---------------- PAUSE ----------------
if (item.status == "paused") {
await sink.flush();
await sink.close();
client.close(force: true);
activeDownloads = (activeDownloads - 1).clamp(0, 9999);
print("Paused → Active: $activeDownloads");
return;
}
// ---------------- CANCEL ----------------
if (item.status == "cancelled") {
await sink.flush();
await sink.close();
client.close(force: true);
if (file.existsSync()) file.deleteSync();
activeDownloads = (activeDownloads - 1).clamp(0, 9999);
print("Cancelled → Active: $activeDownloads");
return;
}
// UPDATE STATS
item.downloaded = downloaded;
item.totalSize = totalSize;
item.status = "downloading";
list[index] = item;
FFAppState().downloadsList = list;
FFAppState().update(() {});
}
// ----------------------------------------------------
// COMPLETED
// ----------------------------------------------------
await sink.flush();
await sink.close();
client.close(force: true);
list = FFAppState().downloadsList.toList();
index = list.indexWhere((d) => d.id == fileId);
if (index != -1) {
item = list[index];
item.status = "completed";
item.speed = 0;
list[index] = item;
FFAppState().downloadsList = list;
FFAppState().update(() {});
}
activeDownloads = (activeDownloads - 1).clamp(0, 9999);
print("Completed → Active: $activeDownloads");
} catch (e, st) {
print("resumeDownload ERROR: $e");
print(st);
activeDownloads = (activeDownloads - 1).clamp(0, 9999);
print("Error → Active: $activeDownloads");
}
}
Future cancelDownload(String? fileId) async {
if (fileId == null) return;
var list = FFAppState().downloadsList.toList();
var index = list.indexWhere((d) => d.id == fileId);
if (index == -1) return;
var item = list[index];
// Set status to cancelled
item.status = "cancelled";
// Update AppState
list[index] = item;
FFAppState().downloadsList = list;
FFAppState().update(() {});
print("Cancel requested for fileId=$fileId");
}
import 'dart:io';
Future deleteLocalFile(String? filePath) async {
if (filePath == null || filePath.isEmpty) {
print("deleteLocalFile: filePath is null or empty");
return false;
}
try {
final file = File(filePath);
if (!file.existsSync()) {
print("deleteLocalFile: File does not exist -> $filePath");
return false;
}
await file.delete();
print("deleteLocalFile: File deleted -> $filePath");
return true;
} catch (e) {
print("deleteLocalFile ERROR: $e");
return false;
}
}
import 'dart:io';
import 'package:path/path.dart' as p;
Future openFolderPath(String filePath) async {
try {
final directory = Directory(p.dirname(filePath));
if (!directory.existsSync()) {
print("Folder does NOT exist → ${directory.path}");
return;
}
print("Opening folder → ${directory.path}");
if (Platform.isWindows) {
// Opens Windows Explorer
await Process.run('explorer', [directory.path.replaceAll('/', '\\')]);
} else if (Platform.isMacOS) {
// Opens Finder on macOS
await Process.run('open', [directory.path]);
} else if (Platform.isLinux) {
// Opens default Linux file manager
await Process.run('xdg-open', [directory.path]);
} else {
print("Folder opening not supported on this platform.");
}
} catch (e) {
print("Error opening folder: $e");
}
}
String formatBytes(int bytes) {
/// MODIFY CODE ONLY BELOW THIS LINE
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";
/// MODIFY CODE ONLY ABOVE THIS LINE
}
String formatSpeed(int bytesPerSec) {
/// MODIFY CODE ONLY BELOW THIS LINE
if (bytesPerSec < 1024) return "$bytesPerSec B/s";
if (bytesPerSec < 1024 * 1024)
return "${(bytesPerSec / 1024).toStringAsFixed(2)} KB/s";
return "${(bytesPerSec / (1024 * 1024)).toStringAsFixed(2)} MB/s";
/// MODIFY CODE ONLY ABOVE THIS LINE
}
String formatPercentage(double value) {
/// MODIFY CODE ONLY BELOW THIS LINE
try {
// Convert decimal → percentage
double percent = value * 100;
// Format to 2 decimal places
String formatted = percent.toStringAsFixed(2);
return "$formatted%";
} catch (e) {
print("formatPercentage ERROR: $e");
return "0%";
}
/// MODIFY CODE ONLY ABOVE THIS LINE
}
String calculateRemainingTime(
double totalSize,
double downloaded,
double speed,
) {
/// MODIFY CODE ONLY BELOW THIS LINE
try {
if (speed <= 0) return "--"; // cannot calculate
double remainingBytes = totalSize - downloaded;
if (remainingBytes <= 0) return "0s";
double secondsLeft = remainingBytes / speed;
int sec = secondsLeft.round();
int hours = sec ~/ 3600;
sec %= 3600;
int minutes = sec ~/ 60;
int seconds = sec % 60;
if (hours > 0) {
return "${hours}h ${minutes}m ${seconds}s";
} else if (minutes > 0) {
return "${minutes}m ${seconds}s";
} else {
return "${seconds}s";
}
} catch (e) {
print("calculateRemainingTime ERROR: $e");
return "--";
}
/// MODIFY CODE ONLY ABOVE THIS LINE
}