349 lines
12 KiB
Dart
349 lines
12 KiB
Dart
import 'dart:io' as io;
|
|
import 'package:flutter/material.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:open_file/open_file.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
class DownloadsScreen extends StatefulWidget {
|
|
const DownloadsScreen({super.key});
|
|
|
|
@override
|
|
State<DownloadsScreen> createState() => _DownloadsScreenState();
|
|
}
|
|
|
|
class _DownloadsScreenState extends State<DownloadsScreen> {
|
|
List<io.FileSystemEntity> _files = [];
|
|
bool _isLoading = true;
|
|
String? _downloadsPath;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadDownloads();
|
|
}
|
|
|
|
Future<void> _loadDownloads() async {
|
|
setState(() {
|
|
_isLoading = true;
|
|
});
|
|
|
|
try {
|
|
io.Directory? downloadDir;
|
|
|
|
if (io.Platform.isAndroid) {
|
|
downloadDir = await getExternalStorageDirectory();
|
|
} else if (io.Platform.isIOS) {
|
|
final directory = await getApplicationDocumentsDirectory();
|
|
downloadDir = directory;
|
|
} else if (io.Platform.isWindows || io.Platform.isLinux) {
|
|
final homeDir =
|
|
io.Platform.environment['HOME'] ??
|
|
io.Platform.environment['USERPROFILE'] ??
|
|
'';
|
|
downloadDir = io.Directory('$homeDir/Downloads');
|
|
} else {
|
|
downloadDir = await getApplicationDocumentsDirectory();
|
|
}
|
|
|
|
if (downloadDir != null && await downloadDir.exists()) {
|
|
_downloadsPath = downloadDir.path;
|
|
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final List<String> downloadedFilePaths =
|
|
prefs.getStringList('downloaded_files') ?? [];
|
|
|
|
|
|
final files =
|
|
downloadedFilePaths
|
|
.map((path) => io.File(path))
|
|
.where((file) => file.existsSync())
|
|
.toList()
|
|
..sort((a, b) {
|
|
final aStat = a.statSync();
|
|
final bStat = b.statSync();
|
|
return bStat.modified.compareTo(aStat.modified);
|
|
});
|
|
|
|
|
|
final existingPaths = files.map((f) => f.path).toSet();
|
|
final cleanPaths = downloadedFilePaths
|
|
.where((path) => existingPaths.contains(path))
|
|
.toList();
|
|
await prefs.setStringList('downloaded_files', cleanPaths);
|
|
|
|
setState(() {
|
|
_files = files;
|
|
_isLoading = false;
|
|
});
|
|
} else {
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
String _formatFileSize(int bytes) {
|
|
if (bytes < 1024) {
|
|
return '$bytes B';
|
|
} else if (bytes < 1024 * 1024) {
|
|
return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
|
} else if (bytes < 1024 * 1024 * 1024) {
|
|
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
|
} else {
|
|
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
|
|
}
|
|
}
|
|
|
|
IconData _getFileIcon(String fileName) {
|
|
final extension = fileName.split('.').last.toLowerCase();
|
|
switch (extension) {
|
|
case 'pdf':
|
|
return Icons.picture_as_pdf;
|
|
case 'doc':
|
|
case 'docx':
|
|
return Icons.description;
|
|
case 'xls':
|
|
case 'xlsx':
|
|
return Icons.table_chart;
|
|
case 'txt':
|
|
return Icons.text_snippet;
|
|
case 'zip':
|
|
case 'rar':
|
|
case '7z':
|
|
return Icons.archive;
|
|
case 'mp3':
|
|
case 'wav':
|
|
case 'flac':
|
|
return Icons.audiotrack;
|
|
case 'mp4':
|
|
case 'avi':
|
|
case 'mov':
|
|
return Icons.video_file;
|
|
case 'jpg':
|
|
case 'jpeg':
|
|
case 'png':
|
|
case 'gif':
|
|
return Icons.image;
|
|
default:
|
|
return Icons.insert_drive_file;
|
|
}
|
|
}
|
|
|
|
Future<void> _deleteFile(io.File file) async {
|
|
final confirmed = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Удалить файл?'),
|
|
content: Text(
|
|
'Вы уверены, что хотите удалить ${file.path.split('/').last}?',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, false),
|
|
child: const Text('Отмена'),
|
|
),
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, true),
|
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
|
child: const Text('Удалить'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (confirmed == true) {
|
|
try {
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final List<String> downloadedFilePaths =
|
|
prefs.getStringList('downloaded_files') ?? [];
|
|
downloadedFilePaths.remove(file.path);
|
|
await prefs.setStringList('downloaded_files', downloadedFilePaths);
|
|
|
|
|
|
await file.delete();
|
|
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(const SnackBar(content: Text('Файл удален')));
|
|
_loadDownloads();
|
|
} catch (e) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Ошибка при удалении файла: $e'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final isDark = theme.brightness == Brightness.dark;
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Загрузки'),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.refresh),
|
|
onPressed: _loadDownloads,
|
|
tooltip: 'Обновить',
|
|
),
|
|
],
|
|
),
|
|
body: _isLoading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: _files.isEmpty
|
|
? Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.download_outlined,
|
|
size: 64,
|
|
color: Colors.grey[400],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Нет скачанных файлов',
|
|
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
|
),
|
|
if (_downloadsPath != null) ...[
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Файлы сохраняются в:\n$_downloadsPath',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
)
|
|
: Column(
|
|
children: [
|
|
if (_downloadsPath != null)
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
color: isDark ? Colors.grey[850] : Colors.grey[200],
|
|
child: Row(
|
|
children: [
|
|
const Icon(Icons.folder, size: 16),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
_downloadsPath!,
|
|
style: const TextStyle(fontSize: 12),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Expanded(
|
|
child: ListView.builder(
|
|
itemCount: _files.length,
|
|
itemBuilder: (context, index) {
|
|
final file = _files[index];
|
|
if (file is! io.File) return const SizedBox.shrink();
|
|
|
|
final fileName = file.path
|
|
.split(io.Platform.pathSeparator)
|
|
.last;
|
|
final fileStat = file.statSync();
|
|
final fileSize = fileStat.size;
|
|
final modifiedDate = fileStat.modified;
|
|
|
|
return ListTile(
|
|
leading: Container(
|
|
width: 48,
|
|
height: 48,
|
|
decoration: BoxDecoration(
|
|
color: theme.primaryColor.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(
|
|
_getFileIcon(fileName),
|
|
color: theme.primaryColor,
|
|
),
|
|
),
|
|
title: Text(
|
|
fileName,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
subtitle: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(_formatFileSize(fileSize)),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
DateFormat(
|
|
'dd.MM.yyyy HH:mm',
|
|
).format(modifiedDate),
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
trailing: IconButton(
|
|
icon: const Icon(Icons.more_vert),
|
|
onPressed: () {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
builder: (context) => SafeArea(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
ListTile(
|
|
leading: const Icon(Icons.open_in_new),
|
|
title: const Text('Открыть'),
|
|
onTap: () async {
|
|
Navigator.pop(context);
|
|
await OpenFile.open(file.path);
|
|
},
|
|
),
|
|
ListTile(
|
|
leading: const Icon(
|
|
Icons.delete,
|
|
color: Colors.red,
|
|
),
|
|
title: const Text(
|
|
'Удалить',
|
|
style: TextStyle(color: Colors.red),
|
|
),
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
_deleteFile(file);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
onTap: () async {
|
|
await OpenFile.open(file.path);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|