Update memory: Camie Tagger v2 upgrade results
This commit is contained in:
784
lib/main.dart
Normal file
784
lib/main.dart
Normal file
@@ -0,0 +1,784 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Anime Uploader',
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.pink,
|
||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||
),
|
||||
home: const MainScreen(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Класс для хранения информации о фото
|
||||
class PhotoItem {
|
||||
final String filePath;
|
||||
final String? uploadStatus;
|
||||
final double? progress;
|
||||
bool isSelected; // Для автозагрузки
|
||||
|
||||
PhotoItem({
|
||||
required this.filePath,
|
||||
this.uploadStatus,
|
||||
this.progress,
|
||||
this.isSelected = true, // По умолчанию выбранные
|
||||
});
|
||||
}
|
||||
|
||||
// Главный экран с вкладками
|
||||
class MainScreen extends StatefulWidget {
|
||||
const MainScreen({super.key});
|
||||
|
||||
@override
|
||||
State<MainScreen> createState() => _MainScreenState();
|
||||
}
|
||||
|
||||
class _MainScreenState extends State<MainScreen> {
|
||||
int _currentIndex = 0; // 0 - ручной, 1 - авто
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: IndexedStack(
|
||||
index: _currentIndex,
|
||||
children: [
|
||||
const ManualUploadScreen(), // Ручной режим
|
||||
const AutoUploadScreen(), // Авто режим
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
type: BottomNavigationBarType.fixed,
|
||||
currentIndex: _currentIndex,
|
||||
onTap: (index) {
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
});
|
||||
},
|
||||
items: const [
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.manual),
|
||||
label: 'Ручной',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.auto_mode),
|
||||
label: 'Авто',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Экран ручной загрузки (оригинальный функционал)
|
||||
class ManualUploadScreen extends StatefulWidget {
|
||||
const ManualUploadScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ManualUploadScreen> createState() => _ManualUploadScreenState();
|
||||
}
|
||||
|
||||
class _ManualUploadScreenState extends State<ManualUploadScreen> {
|
||||
// Список выбранных фото
|
||||
List<PhotoItem> selectedPhotos = [];
|
||||
// Список истории загрузок
|
||||
List<PhotoItem> uploadHistory = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadUploadHistory();
|
||||
}
|
||||
|
||||
// Загружаем историю загрузок из SharedPreferences
|
||||
Future<void> _loadUploadHistory() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final historyJson = prefs.getStringList('upload_history') ?? [];
|
||||
|
||||
setState(() {
|
||||
uploadHistory = historyJson.map((json) {
|
||||
final parts = json.split('|');
|
||||
return PhotoItem(filePath: parts[0], uploadStatus: parts[1]);
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
|
||||
// Сохраняем историю загрузок в SharedPreferences
|
||||
Future<void> _saveUploadHistory() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final historyJson = uploadHistory.map((item) => '${item.filePath}|${item.uploadStatus}').toList();
|
||||
await prefs.setStringList('upload_history', historyJson);
|
||||
}
|
||||
|
||||
// Выбор фото из галереи
|
||||
Future<void> _selectImages() async {
|
||||
final picker = ImagePicker();
|
||||
final pickedFile = await picker.pickImage(source: ImageSource.gallery);
|
||||
|
||||
if (pickedFile != null) {
|
||||
setState(() {
|
||||
selectedPhotos.add(PhotoItem(
|
||||
filePath: pickedFile.path,
|
||||
uploadStatus: 'готово',
|
||||
progress: 0.0,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка всех выбранных фото на сервер
|
||||
Future<void> _uploadImages() async {
|
||||
if (selectedPhotos.isEmpty) {
|
||||
_showSnackBar('Нет выбранных фото для загрузки');
|
||||
return;
|
||||
}
|
||||
|
||||
// Обновляем статус перед началом загрузки
|
||||
setState(() {
|
||||
for (var i = 0; i < selectedPhotos.length; i++) {
|
||||
selectedPhotos[i] = PhotoItem(
|
||||
filePath: selectedPhotos[i].filePath,
|
||||
uploadStatus: 'загружается',
|
||||
progress: 0.0,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
bool allSuccessful = true;
|
||||
|
||||
for (int i = 0; i < selectedPhotos.length; i++) {
|
||||
final photo = selectedPhotos[i];
|
||||
|
||||
// Обновляем статус перед загрузкой
|
||||
setState(() {
|
||||
selectedPhotos[i] = PhotoItem(
|
||||
filePath: photo.filePath,
|
||||
uploadStatus: 'загружается',
|
||||
progress: 0.0,
|
||||
);
|
||||
});
|
||||
|
||||
try {
|
||||
await _uploadSingleImage(photo.filePath, i);
|
||||
} catch (e) {
|
||||
allSuccessful = false;
|
||||
setState(() {
|
||||
selectedPhotos[i] = PhotoItem(
|
||||
filePath: photo.filePath,
|
||||
uploadStatus: 'ошибка: ${e.toString()}',
|
||||
progress: 0.0,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Перемещаем успешно загруженные фото в историю
|
||||
for (int i = selectedPhotos.length - 1; i >= 0; i--) {
|
||||
final photo = selectedPhotos[i];
|
||||
if (photo.uploadStatus == 'успешно') {
|
||||
uploadHistory.insert(0, photo);
|
||||
selectedPhotos.removeAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
await _saveUploadHistory();
|
||||
|
||||
if (allSuccessful) {
|
||||
_showSnackBar('Все фото успешно загружены!');
|
||||
} else {
|
||||
_showSnackBar('Часть фото загружена с ошибками');
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка одного фото на сервер
|
||||
Future<void> _uploadSingleImage(String imagePath, int index) async {
|
||||
final request = http.MultipartRequest(
|
||||
'POST',
|
||||
Uri.parse('http://192.168.1.246:5002/upload'),
|
||||
);
|
||||
|
||||
request.headers['X-API-Key'] = 'your-secret-api-key-here';
|
||||
|
||||
final file = await http.MultipartFile.fromPath(
|
||||
'images',
|
||||
imagePath,
|
||||
);
|
||||
|
||||
request.files.add(file);
|
||||
|
||||
final streamedResponse = await request.send();
|
||||
|
||||
// Отслеживаем прогресс загрузки
|
||||
int totalBytes = 0;
|
||||
int uploadedBytes = 0;
|
||||
|
||||
streamedResponse.stream.listen(
|
||||
(bytes) {
|
||||
uploadedBytes += bytes.length;
|
||||
final progress = uploadedBytes / (streamedResponse.contentLength?.toDouble() ?? 1.0);
|
||||
|
||||
setState(() {
|
||||
selectedPhotos[index] = PhotoItem(
|
||||
filePath: selectedPhotos[index].filePath,
|
||||
uploadStatus: 'загружается',
|
||||
progress: progress,
|
||||
);
|
||||
});
|
||||
},
|
||||
onError: (error) {
|
||||
throw Exception(error.toString());
|
||||
},
|
||||
).onDone(() {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
selectedPhotos[index] = PhotoItem(
|
||||
filePath: selectedPhotos[index].filePath,
|
||||
uploadStatus: 'успешно',
|
||||
progress: 1.0,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
final response = await http.Response.fromStream(streamedResponse);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Ошибка сервера: ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
// Показываем сообщение через SnackBar
|
||||
void _showSnackBar(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(message)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Anime Uploader - Ручной режим'),
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Секция текущих фото для загрузки
|
||||
Expanded(
|
||||
child: selectedPhotos.isEmpty
|
||||
? const Center(
|
||||
child: Text(
|
||||
'Нет выбранных фото\nНажмите кнопку "+" чтобы выбрать фото',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey),
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: selectedPhotos.length,
|
||||
itemBuilder: (context, index) {
|
||||
final photo = selectedPhotos[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(8.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
child: Image.file(
|
||||
File(photo.filePath),
|
||||
width: 80,
|
||||
height: 80,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Фото ${index + 1}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
photo.uploadStatus ?? 'готово',
|
||||
style: TextStyle(
|
||||
color: photo.uploadStatus == 'успешно'
|
||||
? Colors.green
|
||||
: photo.uploadStatus?.startsWith('ошибка')
|
||||
? Colors.red
|
||||
: Colors.orange,
|
||||
),
|
||||
),
|
||||
if (photo.progress != null && photo.uploadStatus == 'загружается')
|
||||
Column(
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
LinearProgressIndicator(value: photo.progress),
|
||||
Text('${(photo.progress! * 100).round()}%'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
// Разделитель
|
||||
Container(
|
||||
height: 1,
|
||||
color: Colors.grey[300],
|
||||
),
|
||||
// Секция истории загрузок
|
||||
Container(
|
||||
height: 150,
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'История загрузок',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: uploadHistory.isEmpty
|
||||
? const Text(
|
||||
'История пуста',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
)
|
||||
: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: uploadHistory.length,
|
||||
itemBuilder: (context, index) {
|
||||
final photo = uploadHistory[index];
|
||||
return Container(
|
||||
width: 80,
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
child: Image.file(
|
||||
File(photo.filePath),
|
||||
width: 80,
|
||||
height: 80,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
photo.uploadStatus == 'успешно' ? '✓' : '✗',
|
||||
style: TextStyle(
|
||||
color: photo.uploadStatus == 'успешно' ? Colors.green : Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
FloatingActionButton(
|
||||
onPressed: _selectImages,
|
||||
tooltip: 'Выбрать фото',
|
||||
child: const Icon(Icons.add_a_photo),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FloatingActionButton(
|
||||
onPressed: _uploadImages,
|
||||
tooltip: 'Загрузить все фото',
|
||||
child: const Icon(Icons.upload),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Экран автоматической загрузки
|
||||
class AutoUploadScreen extends StatefulWidget {
|
||||
const AutoUploadScreen({super.key});
|
||||
|
||||
@override
|
||||
State<AutoUploadScreen> createState() => _AutoUploadScreenState();
|
||||
}
|
||||
|
||||
class _AutoUploadScreenState extends State<AutoUploadScreen> {
|
||||
String? _selectedFolderPath;
|
||||
List<PhotoItem> _foundFiles = [];
|
||||
bool _isScanning = false;
|
||||
bool _isUploading = false;
|
||||
double _overallProgress = 0.0;
|
||||
int _uploadedCount = 0;
|
||||
int _totalCount = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadLastFolder();
|
||||
}
|
||||
|
||||
// Загружаем последнюю выбранную папку
|
||||
Future<void> _loadLastFolder() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final lastFolder = prefs.getString('last_selected_folder');
|
||||
if (lastFolder != null) {
|
||||
setState(() {
|
||||
_selectedFolderPath = lastFolder;
|
||||
});
|
||||
// Автоматически сканируем папку, если она существует
|
||||
if (Directory(lastFolder).existsSync()) {
|
||||
_scanFolder(lastFolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Сохраняем выбранную папку
|
||||
Future<void> _saveLastFolder(String folderPath) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('last_selected_folder', folderPath);
|
||||
}
|
||||
|
||||
// Выбор папки с помощью file_picker
|
||||
Future<void> _selectFolder() async {
|
||||
String? selectedDirectory = await FilePicker.platform.getDirectoryPath();
|
||||
|
||||
if (selectedDirectory != null) {
|
||||
setState(() {
|
||||
_selectedFolderPath = selectedDirectory;
|
||||
});
|
||||
|
||||
// Сохраняем папку
|
||||
_saveLastFolder(selectedDirectory);
|
||||
|
||||
// Сканируем папку
|
||||
_scanFolder(selectedDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
// Сканирование папки рекурсивно на наличие изображений
|
||||
Future<void> _scanFolder(String folderPath) async {
|
||||
setState(() {
|
||||
_isScanning = true;
|
||||
_foundFiles = [];
|
||||
});
|
||||
|
||||
try {
|
||||
final folder = Directory(folderPath);
|
||||
final imageExtensions = ['.jpg', '.jpeg', '.png', '.webp'];
|
||||
|
||||
// Рекурсивный поиск файлов
|
||||
final files = folder.list(recursive: true).where((entity) {
|
||||
if (entity is File) {
|
||||
final extension = entity.path.toLowerCase().split('.').last;
|
||||
return imageExtensions.contains('.$extension');
|
||||
}
|
||||
return false;
|
||||
}).cast<File>();
|
||||
|
||||
final fileList = await files.toList();
|
||||
|
||||
setState(() {
|
||||
_foundFiles = fileList.map((file) => PhotoItem(
|
||||
filePath: file.path,
|
||||
isSelected: true, // Все файлы по умолчанию отмечены
|
||||
)).toList();
|
||||
_isScanning = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isScanning = false;
|
||||
});
|
||||
_showSnackBar('Ошибка при сканировании папки: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Переключение выбора файла
|
||||
void _toggleFileSelection(int index) {
|
||||
setState(() {
|
||||
_foundFiles[index].isSelected = !_foundFiles[index].isSelected;
|
||||
});
|
||||
}
|
||||
|
||||
// Выбрать все/снять выбор со всех
|
||||
void _toggleSelectAll() {
|
||||
bool allSelected = _foundFiles.every((item) => item.isSelected);
|
||||
setState(() {
|
||||
for (var item in _foundFiles) {
|
||||
item.isSelected = !allSelected;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Загрузка выбранных файлов
|
||||
Future<void> _uploadSelectedFiles() async {
|
||||
final selectedFiles = _foundFiles.where((item) => item.isSelected).toList();
|
||||
|
||||
if (selectedFiles.isEmpty) {
|
||||
_showSnackBar('Не выбрано ни одного файла для загрузки');
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isUploading = true;
|
||||
_uploadedCount = 0;
|
||||
_totalCount = selectedFiles.length;
|
||||
_overallProgress = 0.0;
|
||||
|
||||
// Сброс прогресса для всех выбранных файлов
|
||||
for (var item in selectedFiles) {
|
||||
item.progress = 0.0;
|
||||
item.uploadStatus = 'ожидает';
|
||||
}
|
||||
});
|
||||
|
||||
// Загружаем каждый выбранный файл
|
||||
for (int i = 0; i < selectedFiles.length; i++) {
|
||||
final fileItem = selectedFiles[i];
|
||||
|
||||
setState(() {
|
||||
fileItem.uploadStatus = 'загружается';
|
||||
});
|
||||
|
||||
try {
|
||||
await _uploadSingleImage(fileItem.filePath, fileItem);
|
||||
setState(() {
|
||||
fileItem.uploadStatus = 'успешно';
|
||||
fileItem.progress = 1.0;
|
||||
_uploadedCount++;
|
||||
_overallProgress = _uploadedCount / _totalCount;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
fileItem.uploadStatus = 'ошибка: ${e.toString()}';
|
||||
_uploadedCount++;
|
||||
_overallProgress = _uploadedCount / _totalCount;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isUploading = false;
|
||||
});
|
||||
|
||||
_showSnackBar('Загрузка завершена. Успешно: ${selectedFiles.length - selectedFiles.where((item) => item.uploadStatus!.contains('ошибка')).length}, с ошибками: ${selectedFiles.where((item) => item.uploadStatus!.contains('ошибка')).length}');
|
||||
}
|
||||
|
||||
// Загрузка одного файла на сервер
|
||||
Future<void> _uploadSingleImage(String imagePath, PhotoItem item) async {
|
||||
final request = http.MultipartRequest(
|
||||
'POST',
|
||||
Uri.parse('http://192.168.1.246:5002/upload'),
|
||||
);
|
||||
|
||||
request.headers['X-API-Key'] = 'your-secret-api-key-here';
|
||||
|
||||
final file = await http.MultipartFile.fromPath(
|
||||
'images',
|
||||
imagePath,
|
||||
);
|
||||
|
||||
request.files.add(file);
|
||||
|
||||
final streamedResponse = await request.send();
|
||||
|
||||
// Отслеживаем прогресс загрузки
|
||||
int totalBytes = streamedResponse.contentLength ?? 0;
|
||||
int uploadedBytes = 0;
|
||||
|
||||
streamedResponse.stream.listen(
|
||||
(bytes) {
|
||||
uploadedBytes += bytes.length;
|
||||
final progress = totalBytes > 0 ? uploadedBytes / totalBytes : 0.0;
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
item.progress = progress;
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
throw Exception(error.toString());
|
||||
},
|
||||
);
|
||||
|
||||
final response = await http.Response.fromStream(streamedResponse);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Ошибка сервера: ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
// Показываем сообщение через SnackBar
|
||||
void _showSnackBar(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(message)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Anime Uploader - Авто режим'),
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: _isScanning || _isUploading ? null : _selectFolder,
|
||||
icon: const Icon(Icons.folder_open),
|
||||
label: const Text('Выбрать папку'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Показываем выбранную папку
|
||||
if (_selectedFolderPath != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Выбранная папка:', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text(_selectedFolderPath!, overflow: TextOverflow.ellipsis),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Прогресс-бар при сканировании
|
||||
if (_isScanning)
|
||||
const LinearProgressIndicator()
|
||||
else if (_isUploading)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Загрузка...'),
|
||||
LinearProgressIndicator(value: _overallProgress),
|
||||
Text('$_uploadedCount / $_totalCount'),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Кнопка "Выбрать все" / "Снять выбор"
|
||||
if (_foundFiles.isNotEmpty)
|
||||
Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _toggleSelectAll,
|
||||
child: Text(_foundFiles.every((item) => item.isSelected)
|
||||
? 'Снять выбор со всех'
|
||||
: 'Выбрать все'),
|
||||
),
|
||||
const Spacer(),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _isUploading ? null : _uploadSelectedFiles,
|
||||
icon: const Icon(Icons.upload),
|
||||
label: const Text('Загрузить выбранное'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Список найденных файлов
|
||||
if (_foundFiles.isNotEmpty)
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: _foundFiles.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = _foundFiles[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: CheckboxListTile(
|
||||
title: Text(
|
||||
item.filePath.split('/').last, // Только имя файла
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: item.uploadStatus != null
|
||||
? Text(item.uploadStatus!)
|
||||
: null,
|
||||
value: item.isSelected,
|
||||
onChanged: _isUploading
|
||||
? null
|
||||
: (bool? value) {
|
||||
_toggleFileSelection(index);
|
||||
},
|
||||
secondary: item.progress != null && item.uploadStatus == 'загружается'
|
||||
? SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: CircularProgressIndicator(
|
||||
value: item.progress,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
)
|
||||
: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Image.file(
|
||||
File(item.filePath),
|
||||
width: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
else if (!_isScanning && _selectedFolderPath != null)
|
||||
const Center(
|
||||
child: Text(
|
||||
'В выбранной папке нет изображений\n(.jpg, .jpeg, .png, .webp)',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user