Update memory: Camie Tagger v2 upgrade results

This commit is contained in:
root
2026-03-30 12:27:20 +02:00
commit c3033db6a7
1082 changed files with 649864 additions and 0 deletions

784
lib/main.dart Normal file
View 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),
),
),
],
),
),
);
}
}