269 lines
10 KiB
Dart
269 lines
10 KiB
Dart
import 'package:drift/drift.dart' show Value;
|
|
import 'package:flutter/material.dart';
|
|
import '../database/database.dart' as db; // Import database with prefix 'db'
|
|
import '../utils/category_utils.dart'; // Import CategoryUtils
|
|
|
|
class AddCategoryDialog extends StatefulWidget {
|
|
final db.AppDatabase database;
|
|
|
|
const AddCategoryDialog({Key? key, required this.database}) : super(key: key);
|
|
|
|
@override
|
|
State<AddCategoryDialog> createState() => _AddCategoryDialogState();
|
|
}
|
|
|
|
class _AddCategoryDialogState extends State<AddCategoryDialog> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
final _nameController = TextEditingController();
|
|
// State for selected icon
|
|
String _selectedIconName = 'label_outline'; // Default icon name (string)
|
|
IconData _selectedIconData = Icons.label_outline; // Default icon data
|
|
// State for selected color
|
|
Color _selectedColor = CategoryUtils.availableColors[9]; // Default to green
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// Initialize icon data based on the default name
|
|
_selectedIconData = CategoryUtils.getIconFromString(_selectedIconName);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_nameController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
// --- Function to show the icon picker dialog ---
|
|
Future<void> _showIconPicker() async {
|
|
final Map<String, IconData> availableIcons = CategoryUtils.getAvailableIcons();
|
|
final List<String> iconNames = availableIcons.keys.toList();
|
|
final List<IconData> iconDatas = availableIcons.values.toList();
|
|
print("Number of available icons: ${availableIcons.length}"); // Debug print
|
|
|
|
final String? chosenIconName = await showDialog<String>(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: const Text('Выберите иконку'),
|
|
contentPadding: const EdgeInsets.all(10.0), // Adjust padding
|
|
content: SizedBox( // Constrain the size of the dialog content
|
|
width: double.maxFinite, // Use maximum width available
|
|
height: 300, // <--- ЗАДАЕМ ВЫСОТУ ДЛЯ ОБЛАСТИ ПРОКРУТКИ
|
|
child: GridView.builder(
|
|
// shrinkWrap: true, // <--- УБИРАЕМ SHRINKWRAP
|
|
itemCount: iconNames.length,
|
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
|
crossAxisCount: 5, // Adjust number of columns
|
|
crossAxisSpacing: 10.0,
|
|
mainAxisSpacing: 10.0,
|
|
),
|
|
itemBuilder: (context, index) {
|
|
final bool isSelected = _selectedIconName == iconNames[index];
|
|
return InkWell(
|
|
onTap: () {
|
|
Navigator.pop(context, iconNames[index]); // Return the selected icon name (String)
|
|
},
|
|
borderRadius: BorderRadius.circular(8.0), // Ripple effect matches border
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
border: Border.all(
|
|
color: isSelected
|
|
? Theme.of(context).colorScheme.primary // Highlight selected
|
|
: Colors.grey.withOpacity(0.3), // Subtle border for all
|
|
width: isSelected ? 2.0 : 1.0, // Thicker border if selected
|
|
),
|
|
borderRadius: BorderRadius.circular(8.0),
|
|
color: isSelected ? Theme.of(context).colorScheme.primary.withOpacity(0.1) : null,
|
|
),
|
|
child: Icon(
|
|
iconDatas[index],
|
|
size: 30.0, // Adjust icon size
|
|
color: Theme.of(context).brightness == Brightness.dark
|
|
? Colors.white70
|
|
: Colors.black87,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, null), // Close without selection
|
|
child: const Text('Отмена'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
|
|
// Update state if an icon was chosen
|
|
if (chosenIconName != null) {
|
|
setState(() {
|
|
_selectedIconName = chosenIconName;
|
|
_selectedIconData = CategoryUtils.getIconFromString(chosenIconName);
|
|
});
|
|
}
|
|
}
|
|
// --- End of Icon Picker Function ---
|
|
|
|
|
|
Future<void> _saveCategory() async {
|
|
if (_formKey.currentState!.validate()) {
|
|
final name = _nameController.text;
|
|
// Use the selected icon name
|
|
final icon = _selectedIconName;
|
|
// Use the selected color value
|
|
final color = _selectedColor.value;
|
|
|
|
final newCategoryCompanion = db.CategoriesCompanion(
|
|
name: Value(name),
|
|
icon: Value(icon), // Use selected icon name
|
|
color: Value(color), // Use selected color value
|
|
);
|
|
|
|
try {
|
|
// TODO: Add check for duplicate category name before inserting (more robustly)
|
|
final newCategoryId = await widget.database.addCategory(newCategoryCompanion);
|
|
final newCategory = await widget.database.getCategoryById(newCategoryId);
|
|
if (mounted) {
|
|
Navigator.pop(context, newCategory); // Return the newly created CategoryDb object
|
|
}
|
|
} catch (e) {
|
|
print('Error adding category: $e');
|
|
// Handle potential duplicate name error from database (depends on DB constraints)
|
|
String errorMessage = 'Произошла ошибка при добавлении категории.';
|
|
if (e.toString().toLowerCase().contains('unique constraint failed')) {
|
|
errorMessage = 'Категория с таким именем уже существует.';
|
|
}
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(errorMessage),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
// Determine icon color based on the selected background color's brightness
|
|
final iconColor = ThemeData.estimateBrightnessForColor(_selectedColor) == Brightness.dark
|
|
? Colors.white70
|
|
: Colors.black87;
|
|
|
|
return AlertDialog(
|
|
title: const Text('Создать категорию'),
|
|
content: Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start, // Align children to the start
|
|
children: [
|
|
// --- Icon and Name Row ---
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
// --- Icon Picker ---
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: 16.0, bottom: 10.0), // Add padding
|
|
child: InkWell(
|
|
onTap: _showIconPicker, // Show picker on tap
|
|
customBorder: const CircleBorder(), // Make ripple circular
|
|
child: CircleAvatar(
|
|
radius: 24,
|
|
backgroundColor: _selectedColor, // Use selected color for background
|
|
child: Icon(
|
|
_selectedIconData, // Display selected icon
|
|
color: iconColor, // Adjust icon color based on background
|
|
size: 26,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// --- Name Field ---
|
|
Expanded(
|
|
child: TextFormField(
|
|
controller: _nameController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Название категории',
|
|
),
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'Пожалуйста, введите название';
|
|
}
|
|
// Basic length check
|
|
if (value.length > 50) {
|
|
return 'Название слишком длинное';
|
|
}
|
|
return null;
|
|
},
|
|
textCapitalization: TextCapitalization.sentences,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 20), // Space before color picker
|
|
|
|
// --- Color Picker ---
|
|
const Text('Выберите цвет:', style: TextStyle(fontSize: 16)),
|
|
const SizedBox(height: 8),
|
|
Wrap( // Use Wrap for horizontal layout with wrapping
|
|
spacing: 8.0, // Horizontal space between circles
|
|
runSpacing: 8.0, // Vertical space between rows
|
|
children: CategoryUtils.availableColors.map((color) {
|
|
final bool isSelected = _selectedColor == color;
|
|
return InkWell(
|
|
onTap: () {
|
|
setState(() {
|
|
_selectedColor = color; // Update selected color on tap
|
|
});
|
|
},
|
|
customBorder: const CircleBorder(),
|
|
child: Container(
|
|
width: 32, // Size of the color circle
|
|
height: 32,
|
|
decoration: BoxDecoration(
|
|
color: color,
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: isSelected
|
|
? theme.colorScheme.onSurface // Highlight selected
|
|
: theme.dividerColor, // Border for others
|
|
width: isSelected ? 3.0 : 1.0,
|
|
),
|
|
boxShadow: isSelected ? [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.3),
|
|
blurRadius: 3,
|
|
offset: const Offset(0, 1),
|
|
)
|
|
] : null,
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
const SizedBox(height: 16), // Add some space at the bottom
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, null), // Return null if cancelled
|
|
child: const Text('Отмена'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: _saveCategory,
|
|
child: const Text('Сохранить'),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|