add edit page and widgets

This commit is contained in:
atseirjo 2026-01-15 13:44:48 +01:00
parent b9f379ad53
commit 6df0da2776
11 changed files with 915 additions and 16 deletions

View File

@ -1,10 +1,10 @@
import 'package:get/get.dart';
import '../model/filament_model.dart';
import '../helpers/filament_repository.dart';
import '../pages/edit_view.dart';
class DetailsController extends GetxController {
Rx<FilamentModel?> filament = Rx<FilamentModel?>(null);
final isEditing = false.obs;
@override
void onInit() {
@ -14,19 +14,9 @@ class DetailsController extends GetxController {
super.onInit();
}
void toggleEdit() {
isEditing.value = !isEditing.value;
}
void updateFilament(FilamentModel updatedFilament) {
FilamentRepository.to.updateFilament(updatedFilament);
filament.value = updatedFilament;
isEditing.value = false;
Get.snackbar(
'Erfolg',
'Filament wurde aktualisiert',
snackPosition: SnackPosition.BOTTOM,
);
void onEditPressed() {
// Implement edit functionality here
Get.toNamed(EditPage.namedRoute, arguments: {'filament': filament.value});
}
void deleteFilament() {

View File

@ -0,0 +1,164 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../model/filament_model.dart';
import '../helpers/filament_repository.dart';
class EditController extends GetxController {
Rx<FilamentModel?> originalFilament = Rx<FilamentModel?>(null);
// Form Controllers
final nameController = TextEditingController();
final typeController = TextEditingController();
final colorController = TextEditingController();
final weightController = TextEditingController();
final weightUsedController = TextEditingController();
final priceController = TextEditingController();
final manufacturerController = TextEditingController();
final purchaseDateController = TextEditingController();
final notesController = TextEditingController();
final piecesController = TextEditingController();
final printingTempController = TextEditingController();
final bedTempController = TextEditingController();
// Validation
final formKey = GlobalKey<FormState>();
final isSaving = false.obs;
// Available options
final List<String> filamentTypes = [
'PLA',
'ABS',
'PETG',
'TPU',
'ASA',
'Nylon',
'PC',
'PVA',
];
final List<String> availableColors = [
'White',
'Black',
'Red',
'Blue',
'Green',
'Yellow',
'Orange',
'Purple',
'Pink',
'Grey',
'Brown',
];
@override
void onInit() {
if (Get.arguments != null && Get.arguments['filament'] != null) {
originalFilament.value = Get.arguments['filament'];
_populateForm();
}
super.onInit();
}
void _populateForm() {
final filament = originalFilament.value;
if (filament != null) {
nameController.text = filament.name;
typeController.text = filament.type;
colorController.text = filament.color;
weightController.text = filament.weight.toString();
weightUsedController.text = filament.weightUsed.toString();
priceController.text = filament.price.toString();
manufacturerController.text = filament.manufacturer ?? '';
purchaseDateController.text = filament.purchaseDate ?? '';
notesController.text = filament.notes ?? '';
piecesController.text = filament.pices?.toString() ?? '';
printingTempController.text = filament.printingTemp?.toString() ?? '';
bedTempController.text = filament.bedTemp?.toString() ?? '';
}
}
Future<void> saveFilament() async {
if (!formKey.currentState!.validate()) return;
isSaving.value = true;
try {
final updatedFilament = FilamentModel(
id: originalFilament.value?.id ?? DateTime.now().millisecondsSinceEpoch.toString(),
name: nameController.text.trim(),
type: typeController.text.trim(),
color: colorController.text.trim(),
weight: double.tryParse(weightController.text) ?? 0,
weightUsed: double.tryParse(weightUsedController.text) ?? 0,
price: double.tryParse(priceController.text) ?? 0,
manufacturer: manufacturerController.text.trim().isEmpty
? null
: manufacturerController.text.trim(),
purchaseDate: purchaseDateController.text.trim().isEmpty
? null
: purchaseDateController.text.trim(),
notes: notesController.text.trim().isEmpty
? null
: notesController.text.trim(),
pices: int.tryParse(piecesController.text),
printingTemp: int.tryParse(printingTempController.text),
bedTemp: int.tryParse(bedTempController.text),
);
bool success;
if (originalFilament.value != null) {
success = await FilamentRepository.to.updateFilament(updatedFilament);
} else {
success = await FilamentRepository.to.createFilament(updatedFilament);
}
if (success) {
Get.back(result: updatedFilament);
Get.snackbar(
'Erfolg',
originalFilament.value != null
? 'Filament wurde aktualisiert'
: 'Filament wurde erstellt',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Get.theme.colorScheme.primary.withAlpha(26),
colorText: Get.theme.colorScheme.primary,
);
} else {
Get.snackbar(
'Fehler',
'Filament konnte nicht gespeichert werden',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red.withAlpha(26),
colorText: Colors.red,
);
}
} catch (e) {
Get.snackbar(
'Fehler',
'Ein Fehler ist aufgetreten: $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red.withAlpha(26),
colorText: Colors.red,
);
} finally {
isSaving.value = false;
}
}
@override
void onClose() {
nameController.dispose();
typeController.dispose();
colorController.dispose();
weightController.dispose();
weightUsedController.dispose();
priceController.dispose();
manufacturerController.dispose();
purchaseDateController.dispose();
notesController.dispose();
piecesController.dispose();
printingTempController.dispose();
bedTempController.dispose();
super.onClose();
}
}

View File

@ -2,6 +2,7 @@ import 'package:get/get.dart';
import '../../model/filament_model.dart';
import '../../helpers/filament_repository.dart';
import '../pages/details_view.dart';
import '../pages/edit_view.dart';
class ListController extends GetxController {
final filamentList = <FilamentModel>[].obs;
@ -35,7 +36,9 @@ class ListController extends GetxController {
Get.toNamed(DetailsPage.namedRoute, arguments: {'filament': filament});
}
void editFilament(FilamentModel filament) {}
void editFilament(FilamentModel filament) {
Get.toNamed(EditPage.namedRoute, arguments: {'filament': filament});
}
void removeFilament(FilamentModel filament) {
FilamentRepository.to.deleteFilament(filament.id);

View File

@ -1,6 +1,7 @@
import 'package:get/get.dart';
import '../controllers/details_controller.dart';
import '../controllers/edit_controller.dart';
import '../controllers/home_controller.dart';
import '../controllers/list_controller.dart';
@ -13,6 +14,7 @@ class SampleBindings extends Bindings {
Get.lazyPut<HomeController>(() => HomeController());
Get.lazyPut<ListController>(() => ListController());
Get.lazyPut<DetailsController>(() => DetailsController());
Get.lazyPut<EditController>(() => EditController());
}
}

View File

@ -1,4 +1,5 @@
import 'package:get/get.dart';
import '../pages/edit_view.dart';
import 'sample_bindings.dart';
import '../pages/home_view.dart';
import '../pages/list_view.dart';
@ -25,6 +26,11 @@ class SampleRouts {
page: () => const DetailsPage(),
binding: sampleBindings,
),
GetPage(
name: EditPage.namedRoute,
page: () => const EditPage(),
binding: sampleBindings,
),
];

View File

@ -319,7 +319,7 @@ class DetailsPage extends GetView<DetailsController> {
icon: Icons.edit,
label: 'Bearbeiten',
color: Colors.blue,
onPressed: controller.toggleEdit,
onPressed: () => controller.onEditPressed(),
),
),
SizedBox(width: 12),

409
lib/pages/edit_view.dart Normal file
View File

@ -0,0 +1,409 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import '../controllers/edit_controller.dart';
import '../widgets/custom_text_field.dart';
import '../widgets/custom_dropdown.dart';
import '../widgets/section_header.dart';
import '../widgets/color_selector.dart';
import '../widgets/action_button.dart';
class EditPage extends GetView<EditController> {
static const String namedRoute = '/edit-page';
const EditPage({super.key});
@override
Widget build(BuildContext context) {
final isNewFilament = controller.originalFilament.value == null;
return Scaffold(
backgroundColor: Colors.grey.shade50,
appBar: AppBar(
title: Text(
isNewFilament ? 'Neues Filament' : 'Filament bearbeiten',
style: TextStyle(fontWeight: FontWeight.bold),
),
backgroundColor: Colors.white,
elevation: 0,
centerTitle: true,
leading: IconButton(
icon: Icon(Icons.close),
onPressed: () => Get.back(),
),
),
body: Form(
key: controller.formKey,
child: SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Header Card
Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade400, Colors.blue.shade600],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.blue.withAlpha(77),
blurRadius: 15,
offset: Offset(0, 5),
),
],
),
child: Row(
children: [
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withAlpha(51),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
isNewFilament ? Icons.add_circle : Icons.edit,
color: Colors.white,
size: 32,
),
),
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isNewFilament ? 'Neues Filament anlegen' : 'Filament bearbeiten',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
SizedBox(height: 4),
Text(
isNewFilament
? 'Fülle die Felder aus, um ein neues Filament hinzuzufügen'
: 'Aktualisiere die Informationen deines Filaments',
style: TextStyle(
fontSize: 13,
color: Colors.white.withAlpha(230),
),
),
],
),
),
],
),
),
SizedBox(height: 24),
// Basic Information
SectionHeader(
title: 'Grundinformationen',
icon: Icons.info_outline,
color: Colors.blue,
),
SizedBox(height: 16),
CustomTextField(
controller: controller.nameController,
label: 'Name *',
hint: 'z.B. 3Djake ECO Filament',
icon: Icons.label,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Bitte einen Namen eingeben';
}
return null;
},
),
SizedBox(height: 16),
CustomDropdown(
value: controller.typeController.text,
label: 'Typ *',
icon: Icons.category,
items: controller.filamentTypes,
onChanged: (value) {
if (value != null) {
controller.typeController.text = value;
}
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Bitte einen Typ auswählen';
}
return null;
},
),
SizedBox(height: 16),
ColorSelector(
selectedColor: controller.colorController.text.isNotEmpty
? controller.colorController.text
: 'White',
colors: controller.availableColors,
onColorSelected: (color) {
controller.colorController.text = color;
},
),
SizedBox(height: 24),
// Weight & Price
SectionHeader(
title: 'Gewicht & Preis',
icon: Icons.shopping_cart,
color: Colors.green,
),
SizedBox(height: 16),
Row(
children: [
Expanded(
child: CustomTextField(
controller: controller.weightController,
label: 'Gesamtgewicht (g) *',
hint: '1000',
icon: Icons.scale,
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Erforderlich';
}
if (double.tryParse(value) == null) {
return 'Ungültige Zahl';
}
return null;
},
),
),
SizedBox(width: 12),
Expanded(
child: CustomTextField(
controller: controller.weightUsedController,
label: 'Verbraucht (g)',
hint: '0',
icon: Icons.trending_down,
keyboardType: TextInputType.number,
validator: (value) {
if (value != null && value.isNotEmpty) {
if (double.tryParse(value) == null) {
return 'Ungültig';
}
}
return null;
},
),
),
],
),
SizedBox(height: 16),
Row(
children: [
Expanded(
child: CustomTextField(
controller: controller.priceController,
label: 'Preis (€) *',
hint: '19.99',
icon: Icons.euro,
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Erforderlich';
}
if (double.tryParse(value) == null) {
return 'Ungültige Zahl';
}
return null;
},
),
),
SizedBox(width: 12),
Expanded(
child: CustomTextField(
controller: controller.piecesController,
label: 'Anzahl Rollen',
hint: '1',
icon: Icons.inventory_2,
keyboardType: TextInputType.number,
),
),
],
),
SizedBox(height: 24),
// Print Settings
SectionHeader(
title: 'Druckeinstellungen',
icon: Icons.print,
color: Colors.orange,
),
SizedBox(height: 16),
Row(
children: [
Expanded(
child: CustomTextField(
controller: controller.printingTempController,
label: 'Drucktemperatur (°C)',
hint: '200',
icon: Icons.thermostat,
keyboardType: TextInputType.number,
),
),
SizedBox(width: 12),
Expanded(
child: CustomTextField(
controller: controller.bedTempController,
label: 'Betttemperatur (°C)',
hint: '60',
icon: Icons.heat_pump,
keyboardType: TextInputType.number,
),
),
],
),
SizedBox(height: 24),
// Additional Info
SectionHeader(
title: 'Zusatzinformationen',
icon: Icons.more_horiz,
color: Colors.purple,
),
SizedBox(height: 16),
CustomTextField(
controller: controller.manufacturerController,
label: 'Hersteller',
hint: 'z.B. 3Djake.at',
icon: Icons.business,
),
SizedBox(height: 16),
CustomTextField(
controller: controller.purchaseDateController,
label: 'Kaufdatum',
hint: 'TT.MM.JJJJ',
icon: Icons.calendar_today,
readOnly: true,
onTap: () async {
final date = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime.now(),
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: ColorScheme.light(
primary: Colors.blue,
),
),
child: child!,
);
},
);
if (date != null) {
final formatter = DateFormat('dd.MM.yyyy');
controller.purchaseDateController.text = formatter.format(date);
}
},
suffix: Icon(Icons.arrow_drop_down, color: Colors.blue.shade400),
),
SizedBox(height: 16),
CustomTextField(
controller: controller.notesController,
label: 'Notizen',
hint: 'Zusätzliche Informationen...',
icon: Icons.notes,
maxLines: 4,
),
SizedBox(height: 32),
// Save Button
Obx(() => AnimatedContainer(
duration: Duration(milliseconds: 300),
height: 56,
child: controller.isSaving.value
? Container(
decoration: BoxDecoration(
color: Colors.blue.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.blue,
),
),
),
SizedBox(width: 12),
Text(
'Wird gespeichert...',
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.w600,
),
),
],
),
),
)
: ActionButton(
icon: Icons.save,
label: isNewFilament ? 'Erstellen' : 'Speichern',
color: Colors.blue,
onPressed: controller.saveFilament,
),
)),
SizedBox(height: 16),
// Cancel Button
TextButton(
onPressed: () => Get.back(),
style: TextButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 14),
),
child: Text(
'Abbrechen',
style: TextStyle(
fontSize: 15,
color: Colors.grey.shade600,
fontWeight: FontWeight.w600,
),
),
),
SizedBox(height: 32),
],
),
),
),
);
}
}

View File

@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
class ColorSelector extends StatelessWidget {
final String selectedColor;
final List<String> colors;
final void Function(String) onColorSelected;
const ColorSelector({
super.key,
required this.selectedColor,
required this.colors,
required this.onColorSelected,
});
Color _getColorFromString(String colorName) {
final colorMap = {
'red': Colors.red,
'blue': Colors.blue,
'green': Colors.green,
'yellow': Colors.yellow,
'black': Colors.black,
'white': Colors.white,
'orange': Colors.orange,
'purple': Colors.purple,
'pink': Colors.pink,
'grey': Colors.grey,
'brown': Colors.brown,
};
return colorMap[colorName.toLowerCase()] ?? Colors.grey;
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Farbe',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.grey.shade700,
),
),
SizedBox(height: 12),
Wrap(
spacing: 12,
runSpacing: 12,
children: colors.map((colorName) {
final isSelected = selectedColor.toLowerCase() == colorName.toLowerCase();
final color = _getColorFromString(colorName);
return GestureDetector(
onTap: () => onColorSelected(colorName),
child: AnimatedContainer(
duration: Duration(milliseconds: 200),
width: isSelected ? 60 : 50,
height: isSelected ? 60 : 50,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(
color: isSelected ? Colors.blue : Colors.grey.shade300,
width: isSelected ? 3 : 2,
),
boxShadow: [
if (isSelected)
BoxShadow(
color: color.withAlpha(102),
blurRadius: 12,
spreadRadius: 2,
),
],
),
child: isSelected
? Icon(
Icons.check,
color: _isLightColor(color) ? Colors.black : Colors.white,
size: 28,
)
: null,
),
);
}).toList(),
),
SizedBox(height: 8),
Text(
'Ausgewählt: $selectedColor',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
fontStyle: FontStyle.italic,
),
),
],
);
}
bool _isLightColor(Color color) {
final luminance = color.computeLuminance();
return luminance > 0.5;
}
}

View File

@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
class CustomDropdown extends StatelessWidget {
final String value;
final String label;
final IconData? icon;
final List<String> items;
final void Function(String?) onChanged;
final String? Function(String?)? validator;
const CustomDropdown({
super.key,
required this.value,
required this.label,
required this.items,
required this.onChanged,
this.icon,
this.validator,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.grey.shade700,
),
),
SizedBox(height: 8),
DropdownButtonFormField<String>(
initialValue: items.contains(value) ? value : null,
decoration: InputDecoration(
prefixIcon: icon != null
? Icon(icon, color: Colors.blue.shade400, size: 22)
: null,
filled: true,
fillColor: Colors.white,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.blue, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.red.shade300),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.red, width: 2),
),
),
items: items.map((String item) {
return DropdownMenuItem<String>(
value: item,
child: Text(item),
);
}).toList(),
onChanged: onChanged,
validator: validator,
dropdownColor: Colors.white,
icon: Icon(Icons.arrow_drop_down, color: Colors.blue.shade400),
),
],
);
}
}

View File

@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
class CustomTextField extends StatelessWidget {
final TextEditingController controller;
final String label;
final String? hint;
final IconData? icon;
final TextInputType keyboardType;
final String? Function(String?)? validator;
final int maxLines;
final bool readOnly;
final VoidCallback? onTap;
final Widget? suffix;
const CustomTextField({
super.key,
required this.controller,
required this.label,
this.hint,
this.icon,
this.keyboardType = TextInputType.text,
this.validator,
this.maxLines = 1,
this.readOnly = false,
this.onTap,
this.suffix,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.grey.shade700,
),
),
SizedBox(height: 8),
TextFormField(
controller: controller,
keyboardType: keyboardType,
validator: validator,
maxLines: maxLines,
readOnly: readOnly,
onTap: onTap,
decoration: InputDecoration(
hintText: hint,
prefixIcon: icon != null
? Icon(icon, color: Colors.blue.shade400, size: 22)
: null,
suffixIcon: suffix,
filled: true,
fillColor: Colors.white,
contentPadding: EdgeInsets.symmetric(
horizontal: 16,
vertical: maxLines > 1 ? 16 : 14,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.blue, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.red.shade300),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.red, width: 2),
),
),
),
],
);
}
}

View File

@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
class SectionHeader extends StatelessWidget {
final String title;
final IconData icon;
final Color color;
const SectionHeader({
super.key,
required this.title,
required this.icon,
this.color = Colors.blue,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withAlpha(26),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: color, size: 20),
),
SizedBox(width: 12),
Text(
title,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.grey.shade800,
),
),
Expanded(
child: Container(
margin: EdgeInsets.only(left: 12),
height: 2,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
color.withAlpha(77),
Colors.transparent,
],
),
),
),
),
],
),
);
}
}