add import export and items
This commit is contained in:
@@ -36,13 +36,22 @@ class AnwendungController extends GetxController {
|
||||
filament.value = filamente.firstWhereOrNull(
|
||||
(item) => item.filamentId == filamentId,
|
||||
);
|
||||
anwendungen.assignAll(
|
||||
storage
|
||||
.getModelList(storageKey)
|
||||
.map(AnwendungModel.fromJson)
|
||||
.where((item) => item.filamentId == filamentId),
|
||||
);
|
||||
anwendungen.sort((a, b) => b.erstelltAm.compareTo(a.erstelltAm));
|
||||
final profiles =
|
||||
storage
|
||||
.getModelList(storageKey)
|
||||
.map(AnwendungModel.fromJson)
|
||||
.where((item) => item.filamentId == filamentId)
|
||||
.toList()
|
||||
..sort((a, b) => b.erstelltAm.compareTo(a.erstelltAm));
|
||||
if (profiles.isNotEmpty) {
|
||||
final profile = profiles.first;
|
||||
anwendungen.assignAll([profile]);
|
||||
druckbettController.text = profile.druckbettTemperatur.toString();
|
||||
druckController.text = profile.druckTemperatur.toString();
|
||||
bauraumController.text = profile.bauraumTemperatur?.toString() ?? '';
|
||||
sonstigesController.text = profile.sonstiges;
|
||||
bauraumGeschlossen.value = profile.bauraumGeschlossen;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveAnwendung() async {
|
||||
@@ -51,10 +60,11 @@ class AnwendungController extends GetxController {
|
||||
isSaving.value = true;
|
||||
message.value = null;
|
||||
try {
|
||||
final existing = anwendungen.firstOrNull;
|
||||
final entry = AnwendungModel(
|
||||
anwendungId: const Uuid().v4(),
|
||||
anwendungId: existing?.anwendungId ?? const Uuid().v4(),
|
||||
filamentId: filamentId,
|
||||
erstelltAm: DateTime.now(),
|
||||
erstelltAm: existing?.erstelltAm ?? DateTime.now(),
|
||||
druckbettTemperatur: InputValidators.number(druckbettController.text)!,
|
||||
druckTemperatur: InputValidators.number(druckController.text)!,
|
||||
bauraumGeschlossen: bauraumGeschlossen.value,
|
||||
@@ -62,9 +72,11 @@ class AnwendungController extends GetxController {
|
||||
sonstiges: sonstigesController.text.trim(),
|
||||
);
|
||||
final entries = storage.getModelList(storageKey);
|
||||
await storage.saveModelList(storageKey, [...entries, entry.toJson()]);
|
||||
anwendungen.insert(0, entry);
|
||||
resetForm();
|
||||
await storage.saveModelList(storageKey, [
|
||||
...entries.where((item) => item['filamentId'] != filamentId),
|
||||
entry.toJson(),
|
||||
]);
|
||||
anwendungen.assignAll([entry]);
|
||||
hasError.value = false;
|
||||
message.value = 'Verarbeitungsprofil gespeichert.';
|
||||
} catch (_) {
|
||||
@@ -75,15 +87,6 @@ class AnwendungController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
void resetForm() {
|
||||
formKey.currentState?.reset();
|
||||
druckbettController.clear();
|
||||
druckController.clear();
|
||||
bauraumController.clear();
|
||||
sonstigesController.clear();
|
||||
bauraumGeschlossen.value = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
druckbettController.dispose();
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../models/backup_model.dart';
|
||||
import '../services/backup_file_service.dart';
|
||||
import '../services/get_storage_services.dart';
|
||||
import 'home_controller.dart';
|
||||
|
||||
class ExportController extends GetxController {
|
||||
ExportController({BackupFileService? fileService})
|
||||
: fileService = fileService ?? BackupFileService();
|
||||
|
||||
final BackupFileService fileService;
|
||||
final storage = Get.find<GetStorageService>();
|
||||
final isBusy = false.obs;
|
||||
final message = RxnString();
|
||||
final hasError = false.obs;
|
||||
final pendingBackup = Rxn<BackupModel>();
|
||||
|
||||
Future<void> exportData() async {
|
||||
if (isBusy.value) return;
|
||||
isBusy.value = true;
|
||||
message.value = null;
|
||||
try {
|
||||
final backup = BackupModel({
|
||||
for (final key in BackupModel.keys) key: storage.getModelList(key),
|
||||
});
|
||||
final name =
|
||||
'filament_backup_${DateFormat('yyyy-MM-dd_HH-mm-ss').format(DateTime.now())}.json';
|
||||
if (await fileService.save(backup.encode(), name)) {
|
||||
hasError.value = false;
|
||||
message.value = 'Datensicherung als JSON-Datei exportiert.';
|
||||
}
|
||||
} catch (_) {
|
||||
_error('Export fehlgeschlagen. Bitte erneut versuchen.');
|
||||
} finally {
|
||||
isBusy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> selectImport() async {
|
||||
if (isBusy.value) return;
|
||||
isBusy.value = true;
|
||||
message.value = null;
|
||||
pendingBackup.value = null;
|
||||
try {
|
||||
final source = await fileService.open();
|
||||
if (source != null) pendingBackup.value = BackupModel.decode(source);
|
||||
} on FormatException catch (error) {
|
||||
_error(error.message);
|
||||
} catch (_) {
|
||||
_error('Datei konnte nicht gelesen werden. Bitte erneut versuchen.');
|
||||
} finally {
|
||||
isBusy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> importData() async {
|
||||
final backup = pendingBackup.value;
|
||||
if (isBusy.value || backup == null) return;
|
||||
isBusy.value = true;
|
||||
message.value = null;
|
||||
final previous = <String, List<Map<String, dynamic>>>{};
|
||||
final attempted = <String>[];
|
||||
try {
|
||||
for (final key in BackupModel.keys) {
|
||||
previous[key] = storage.getModelList(key);
|
||||
}
|
||||
for (final key in BackupModel.keys) {
|
||||
attempted.add(key);
|
||||
await storage.saveModelList(key, backup.data[key]!);
|
||||
}
|
||||
if (Get.isRegistered<HomeController>()) {
|
||||
final home = Get.find<HomeController>();
|
||||
home.loadFilamente();
|
||||
home.loadMaterialien();
|
||||
home.resetForm();
|
||||
}
|
||||
pendingBackup.value = null;
|
||||
hasError.value = false;
|
||||
message.value = 'Alle Daten wurden importiert.';
|
||||
} catch (_) {
|
||||
var restored = true;
|
||||
for (final key in attempted.reversed) {
|
||||
try {
|
||||
await storage.saveModelList(key, previous[key]!);
|
||||
} catch (_) {
|
||||
restored = false;
|
||||
}
|
||||
}
|
||||
_error(
|
||||
restored
|
||||
? 'Import fehlgeschlagen. Die bisherigen Daten wurden beibehalten.'
|
||||
: 'Import und Wiederherstellung fehlgeschlagen. Bitte die Datensicherung erneut importieren.',
|
||||
);
|
||||
} finally {
|
||||
isBusy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void _error(String text) {
|
||||
hasError.value = true;
|
||||
message.value = text;
|
||||
}
|
||||
}
|
||||
@@ -43,17 +43,17 @@ class HomeController extends GetxController {
|
||||
final preisController = TextEditingController();
|
||||
final markeController = TextEditingController();
|
||||
final gewichtController = TextEditingController();
|
||||
final materialController = TextEditingController(text: 'PLA');
|
||||
|
||||
final kaufdatum = DateTime.now().obs;
|
||||
final material = 'PLA'.obs;
|
||||
final editingId = RxnString();
|
||||
|
||||
final materialien = [
|
||||
static const defaultMaterialien = [
|
||||
'PLA',
|
||||
'PLA+',
|
||||
'PLA light',
|
||||
'PLA-CF',
|
||||
'PLA+',
|
||||
'PETG',
|
||||
'PETG-CF',
|
||||
'ABS',
|
||||
@@ -61,11 +61,74 @@ class HomeController extends GetxController {
|
||||
'TPU',
|
||||
'Sonstiges',
|
||||
];
|
||||
final materialien = defaultMaterialien.toList().obs;
|
||||
final materialMessage = RxnString();
|
||||
final isSavingMaterial = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
loadFilamente();
|
||||
loadMaterialien();
|
||||
selectMaterial(materialien.firstOrNull ?? '');
|
||||
}
|
||||
|
||||
void loadMaterialien() {
|
||||
final saved = storage.getModelList(StorageKeys.materialien);
|
||||
if (saved.isNotEmpty) {
|
||||
materialien.assignAll((saved.single['werte'] as List).cast<String>());
|
||||
} else {
|
||||
materialien.assignAll({
|
||||
...defaultMaterialien,
|
||||
...filamente.map((filament) => filament.material),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void selectMaterial(String value) {
|
||||
material.value = value;
|
||||
materialController.text = value;
|
||||
}
|
||||
|
||||
Future<bool> addMaterial() async {
|
||||
final value = materialController.text.trim();
|
||||
if (value.isEmpty) {
|
||||
materialMessage.value = 'Bitte eine Materialart eingeben.';
|
||||
return false;
|
||||
}
|
||||
final existing = materialien.firstWhereOrNull(
|
||||
(item) => item.toLowerCase() == value.toLowerCase(),
|
||||
);
|
||||
if (existing != null) {
|
||||
materialMessage.value = null;
|
||||
selectMaterial(existing);
|
||||
return true;
|
||||
}
|
||||
final saved = await _saveMaterialien([...materialien, value]);
|
||||
if (saved) selectMaterial(value);
|
||||
return saved;
|
||||
}
|
||||
|
||||
Future<bool> removeMaterial(String value) =>
|
||||
_saveMaterialien(materialien.where((item) => item != value).toList());
|
||||
|
||||
Future<bool> _saveMaterialien(List<String> values) async {
|
||||
if (isSavingMaterial.value) return false;
|
||||
isSavingMaterial.value = true;
|
||||
materialMessage.value = null;
|
||||
try {
|
||||
await storage.saveModelList(StorageKeys.materialien, [
|
||||
{'werte': values},
|
||||
]);
|
||||
materialien.assignAll(values);
|
||||
return true;
|
||||
} catch (_) {
|
||||
materialMessage.value =
|
||||
'Artenliste konnte nicht gespeichert werden. Bitte erneut versuchen.';
|
||||
return false;
|
||||
} finally {
|
||||
isSavingMaterial.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void loadFilamente() {
|
||||
@@ -116,6 +179,8 @@ class HomeController extends GetxController {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await addMaterial()) return;
|
||||
material.value = materialController.text.trim();
|
||||
final filament = KaufFilamentModel(
|
||||
filamentId: editingId.value ?? uuid.v4(),
|
||||
marke: markeController.text.trim(),
|
||||
@@ -152,7 +217,7 @@ class HomeController extends GetxController {
|
||||
kaufdatum.value = filament.kaufdatum;
|
||||
markeController.text = filament.marke;
|
||||
haendlerController.text = filament.haendler;
|
||||
material.value = filament.material;
|
||||
selectMaterial(filament.material);
|
||||
preisController.text = filament.preis.toStringAsFixed(2);
|
||||
farbeController.text = filament.farbe;
|
||||
gewichtController.text = filament.gewichtGramm == null
|
||||
@@ -185,15 +250,16 @@ class HomeController extends GetxController {
|
||||
}
|
||||
|
||||
void resetForm() {
|
||||
formKey.currentState?.reset();
|
||||
editingId.value = null;
|
||||
haendlerController.clear();
|
||||
markeController.clear();
|
||||
farbeController.clear();
|
||||
preisController.clear();
|
||||
kaufdatum.value = DateTime.now();
|
||||
material.value = 'PLA';
|
||||
selectMaterial(materialien.firstOrNull ?? '');
|
||||
materialMessage.value = null;
|
||||
gewichtController.clear();
|
||||
formKey.currentState?.reset();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -205,6 +271,7 @@ class HomeController extends GetxController {
|
||||
farbeController.dispose();
|
||||
preisController.dispose();
|
||||
gewichtController.dispose();
|
||||
materialController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import 'package:get/get.dart';
|
||||
|
||||
import '../controller/anwendung_controller.dart';
|
||||
import '../controller/bauteil_controller.dart';
|
||||
import '../controller/export_controller.dart';
|
||||
import '../pages/export_view.dart';
|
||||
import '../pages/anwendung_view.dart';
|
||||
import '../pages/bauteil_view.dart';
|
||||
import '../pages/home_view.dart';
|
||||
@@ -10,6 +12,13 @@ import 'sample_bindings.dart';
|
||||
class SampleRouts {
|
||||
static final sampleBindings = SampleBindings();
|
||||
static List<GetPage<dynamic>> samplePages = [
|
||||
GetPage(
|
||||
name: ExportView.routeName,
|
||||
page: () => const ExportView(),
|
||||
binding: BindingsBuilder(
|
||||
() => Get.lazyPut<ExportController>(() => ExportController()),
|
||||
),
|
||||
),
|
||||
GetPage(
|
||||
name: '${AnwendungView.routeName}/:filamentId',
|
||||
page: () => const AnwendungView(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class StorageKeys {
|
||||
static const filamente = 'kauf_filamente';
|
||||
static const materialien = 'filament_materialien';
|
||||
static const anwendungen = 'filament_anwendungen';
|
||||
static const bauteile = 'gedruckte_bauteile';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../helper/storage_keys.dart';
|
||||
import 'anwendung_model.dart';
|
||||
import 'bauteil_model.dart';
|
||||
import 'kauf_filament_model.dart';
|
||||
|
||||
class BackupModel {
|
||||
static const keys = [
|
||||
StorageKeys.filamente,
|
||||
StorageKeys.anwendungen,
|
||||
StorageKeys.bauteile,
|
||||
StorageKeys.materialien,
|
||||
];
|
||||
final Map<String, List<Map<String, dynamic>>> data;
|
||||
|
||||
const BackupModel(this.data);
|
||||
|
||||
String encode() => const JsonEncoder.withIndent(' ').convert({
|
||||
'format': 'ertl_filament_verwaltung',
|
||||
'version': 1,
|
||||
'exportiertAm': DateTime.now().toUtc().toIso8601String(),
|
||||
'daten': data,
|
||||
});
|
||||
|
||||
factory BackupModel.decode(String source) {
|
||||
try {
|
||||
final json =
|
||||
jsonDecode(source.replaceFirst(RegExp(r'^\uFEFF'), '')) as Map;
|
||||
if (json['format'] != 'ertl_filament_verwaltung' ||
|
||||
json['version'] != 1) {
|
||||
throw const FormatException();
|
||||
}
|
||||
final raw = json['daten'] as Map;
|
||||
final data = <String, List<Map<String, dynamic>>>{
|
||||
for (final key in keys)
|
||||
key: (raw[key] as List)
|
||||
.map((item) => Map<String, dynamic>.from(item as Map))
|
||||
.toList(),
|
||||
};
|
||||
final filamentIds = <String>{};
|
||||
for (final json in data[StorageKeys.filamente]!) {
|
||||
final item = KaufFilamentModel.fromJson(json);
|
||||
if (item.filamentId.isEmpty ||
|
||||
!filamentIds.add(item.filamentId) ||
|
||||
item.material.trim().isEmpty ||
|
||||
!_positive(item.gewichtGramm) ||
|
||||
!_nonNegative(item.preis)) {
|
||||
throw const FormatException();
|
||||
}
|
||||
}
|
||||
final profileIds = <String>{};
|
||||
for (final json in data[StorageKeys.anwendungen]!) {
|
||||
final item = AnwendungModel.fromJson(json);
|
||||
if (item.anwendungId.isEmpty ||
|
||||
!profileIds.add(item.anwendungId) ||
|
||||
item.filamentId.isEmpty ||
|
||||
!_positive(item.druckTemperatur) ||
|
||||
!_nonNegative(item.druckbettTemperatur) ||
|
||||
(item.bauraumTemperatur != null &&
|
||||
!_nonNegative(item.bauraumTemperatur!))) {
|
||||
throw const FormatException();
|
||||
}
|
||||
}
|
||||
final partIds = <String>{};
|
||||
for (final json in data[StorageKeys.bauteile]!) {
|
||||
final item = BauteilModel.fromJson(json);
|
||||
if (item.bauteilId.isEmpty ||
|
||||
!partIds.add(item.bauteilId) ||
|
||||
item.name.trim().isEmpty ||
|
||||
item.druckdauerMinuten <= 0 ||
|
||||
!_positive(item.gewichtGramm) ||
|
||||
!_nonNegative(item.abfallGramm) ||
|
||||
item.filamente.isEmpty ||
|
||||
item.filamente.any(
|
||||
(entry) =>
|
||||
entry.filamentId.isEmpty ||
|
||||
!_nonNegative(entry.verbrauchGramm),
|
||||
)) {
|
||||
throw const FormatException();
|
||||
}
|
||||
if (item.fotoBase64 != null) base64Decode(item.fotoBase64!);
|
||||
}
|
||||
final materials = data[StorageKeys.materialien]!;
|
||||
if (materials.isNotEmpty) {
|
||||
if (materials.length != 1) throw const FormatException();
|
||||
final values = (materials.single['werte'] as List).cast<String>();
|
||||
if (values.any((value) => value.trim().isEmpty) ||
|
||||
values.map((value) => value.toLowerCase()).toSet().length !=
|
||||
values.length) {
|
||||
throw const FormatException();
|
||||
}
|
||||
}
|
||||
return BackupModel(data);
|
||||
} catch (_) {
|
||||
throw const FormatException(
|
||||
'Ungültige oder nicht unterstützte Datensicherung. Bitte eine vollständige JSON-Datei dieser App auswählen.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static bool _nonNegative(num value) => value.isFinite && value >= 0;
|
||||
static bool _positive(num? value) =>
|
||||
value != null && value.isFinite && value > 0;
|
||||
}
|
||||
@@ -2,9 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controller/anwendung_controller.dart';
|
||||
import '../helper/display_formats.dart';
|
||||
import '../widgets/anwendung_form.dart';
|
||||
import '../widgets/entry_panel.dart';
|
||||
|
||||
class AnwendungView extends GetView<AnwendungController> {
|
||||
static const routeName = '/anwendung-view';
|
||||
@@ -39,32 +37,6 @@ class AnwendungView extends GetView<AnwendungController> {
|
||||
child: AnwendungForm(controller: controller),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 24),
|
||||
sliver: Obx(() {
|
||||
final entries = controller.anwendungen.toList();
|
||||
return SavedEntries(
|
||||
title: 'Gespeicherte Profile',
|
||||
count: entries.length,
|
||||
emptyText: 'Noch keine Verarbeitungsprofile für dieses Filament gespeichert.',
|
||||
itemBuilder: (context, index) {
|
||||
final entry = entries[index];
|
||||
return SavedEntryCard(
|
||||
key: ValueKey(entry.anwendungId),
|
||||
title:
|
||||
'Druck ${DisplayFormats.number(entry.druckTemperatur)} °C · Bett ${DisplayFormats.number(entry.druckbettTemperatur)} °C',
|
||||
subtitle: DisplayFormats.date(entry.erstelltAm),
|
||||
details: [
|
||||
'Bauraum geschlossen: ${entry.bauraumGeschlossen ? "Ja" : "Nein"}',
|
||||
if (entry.bauraumTemperatur != null)
|
||||
'Bauraum ${DisplayFormats.number(entry.bauraumTemperatur!)} °C',
|
||||
],
|
||||
notes: entry.sonstiges,
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controller/export_controller.dart';
|
||||
import '../helper/storage_keys.dart';
|
||||
import '../widgets/entry_panel.dart';
|
||||
|
||||
class ExportView extends GetView<ExportController> {
|
||||
static const routeName = '/export-view';
|
||||
const ExportView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(title: const Text('Datensicherung')),
|
||||
body: SafeArea(
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 760),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Obx(() {
|
||||
final busy = controller.isBusy.value;
|
||||
final backup = controller.pendingBackup.value;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
EntryPanel(
|
||||
title: 'Daten exportieren',
|
||||
subtitle: 'Sichere Filamente, Verarbeitungsprofile, Bauteile inklusive Fotos und Verbrauch sowie deine Materialarten in einer JSON-Datei.\n\nSpeichere die Datei außerhalb der App, zum Beispiel in Dokumente oder in deiner Cloud. So kannst du sie nach einer Neuinstallation wieder importieren.',
|
||||
child: FilledButton.icon(
|
||||
onPressed: busy ? null : controller.exportData,
|
||||
icon: const Icon(Icons.file_download_outlined),
|
||||
label: const Text('Als JSON exportieren'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
EntryPanel(
|
||||
title: 'Daten importieren',
|
||||
subtitle: 'Wähle eine JSON-Datensicherung dieser App. Beim Import werden alle aktuell gespeicherten Daten durch den Inhalt der Datei ersetzt. Exportiere bei Bedarf vorher deinen aktuellen Bestand.',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: busy ? null : controller.selectImport,
|
||||
icon: const Icon(Icons.file_upload_outlined),
|
||||
label: const Text('JSON-Datei auswählen'),
|
||||
),
|
||||
if (backup != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
const Text('Geprüfte Datensicherung:'),
|
||||
Text(
|
||||
'${backup.data[StorageKeys.filamente]!.length} Filamente',
|
||||
),
|
||||
Text(
|
||||
'${backup.data[StorageKeys.anwendungen]!.length} Profile',
|
||||
),
|
||||
Text(
|
||||
'${backup.data[StorageKeys.bauteile]!.length} Bauteile',
|
||||
),
|
||||
Text(
|
||||
backup.data[StorageKeys.materialien]!.isEmpty
|
||||
? 'Materialarten: Standardliste'
|
||||
: '${(backup.data[StorageKeys.materialien]!.single['werte'] as List).length} Materialarten',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton.icon(
|
||||
onPressed: busy
|
||||
? null
|
||||
: () => _confirmImport(context),
|
||||
icon: const Icon(Icons.restore),
|
||||
label: const Text('Daten ersetzen und importieren'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (busy) ...[
|
||||
const SizedBox(height: 20),
|
||||
const LinearProgressIndicator(),
|
||||
],
|
||||
EntryMessage(
|
||||
message: controller.message.value,
|
||||
hasError: controller.hasError.value,
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> _confirmImport(BuildContext context) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Alle Daten ersetzen?'),
|
||||
content: const Text(
|
||||
'Der aktuelle Bestand wird vollständig durch die ausgewählte Datensicherung ersetzt. Auch eine leere Sicherung ersetzt den Bestand.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Abbrechen'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('Daten ersetzen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed == true) await controller.importData();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import '../widgets/filament_form.dart';
|
||||
import '../widgets/filament_inventory.dart';
|
||||
import 'anwendung_view.dart';
|
||||
import 'bauteil_view.dart';
|
||||
import 'export_view.dart';
|
||||
|
||||
class HomeView extends GetView<HomeController> {
|
||||
static const routeName = '/home-view';
|
||||
@@ -39,6 +40,24 @@ class HomeView extends GetView<HomeController> {
|
||||
],
|
||||
),
|
||||
),
|
||||
drawer: Drawer(
|
||||
child: SafeArea(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
const ListTile(title: Text('Filamentverwaltung')),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.import_export),
|
||||
title: const Text('Daten exportieren / importieren'),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Get.toNamed(ExportView.routeName);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Align(
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
|
||||
class BackupFileService {
|
||||
Future<bool> save(String json, String name) async =>
|
||||
await FilePicker.saveFile(
|
||||
dialogTitle: 'Datensicherung speichern',
|
||||
fileName: name,
|
||||
bytes: Uint8List.fromList(utf8.encode(json)),
|
||||
) !=
|
||||
null;
|
||||
|
||||
Future<String?> open() async {
|
||||
final file = await FilePicker.pickFile(
|
||||
dialogTitle: 'Datensicherung auswählen',
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['json'],
|
||||
);
|
||||
if (file == null) return null;
|
||||
return utf8.decode(await file.readAsBytes());
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ class AnwendungForm extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final filament = controller.filament.value!;
|
||||
return EntryPanel(
|
||||
title: 'Verarbeitung erfassen',
|
||||
title: 'Verarbeitungsprofil bearbeiten',
|
||||
subtitle:
|
||||
'${filament.marke} · ${filament.material} · ${filament.farbe}\n'
|
||||
'Speichere die Einstellungen für dieses Filament als Profil.',
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:intl/intl.dart';
|
||||
import '../controller/home_controller.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import '../helper/input_validators.dart';
|
||||
import 'filament_material_field.dart';
|
||||
|
||||
class FilamentForm extends StatelessWidget {
|
||||
const FilamentForm({super.key, required this.controller});
|
||||
@@ -130,29 +131,7 @@ class FilamentForm extends StatelessWidget {
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Obx(
|
||||
() => DropdownButtonFormField<String>(
|
||||
key: ValueKey(
|
||||
'${controller.editingId.value ?? "new"}_${controller.material.value}',
|
||||
),
|
||||
initialValue: controller.material.value,
|
||||
isExpanded: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Material',
|
||||
prefixIcon: Icon(Icons.layers_outlined),
|
||||
),
|
||||
items: controller.materialien
|
||||
.toSet()
|
||||
.map(
|
||||
(item) =>
|
||||
DropdownMenuItem(value: item, child: Text(item)),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) controller.material.value = value;
|
||||
},
|
||||
),
|
||||
),
|
||||
FilamentMaterialField(controller: controller),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: controller.farbeController,
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controller/home_controller.dart';
|
||||
|
||||
class FilamentMaterialField extends StatelessWidget {
|
||||
const FilamentMaterialField({super.key, required this.controller});
|
||||
|
||||
final HomeController controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: controller.materialController,
|
||||
onChanged: (value) => controller.material.value = value,
|
||||
validator: (value) => value == null || value.trim().isEmpty
|
||||
? 'Materialart eingeben oder auswählen'
|
||||
: null,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Material',
|
||||
helperText: 'Auswählen oder eigene Art eingeben',
|
||||
prefixIcon: const Icon(Icons.layers_outlined),
|
||||
suffixIcon: Obx(
|
||||
() => PopupMenuButton<String>(
|
||||
tooltip: 'Material auswählen',
|
||||
enabled: controller.materialien.isNotEmpty,
|
||||
icon: const Icon(Icons.expand_more),
|
||||
onSelected: controller.selectMaterial,
|
||||
itemBuilder: (context) => controller.materialien
|
||||
.map((item) => PopupMenuItem(value: item, child: Text(item)))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: controller.isSavingMaterial.value
|
||||
? null
|
||||
: () async {
|
||||
if (await controller.addMaterial() && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Art ist in der Liste.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Art hinzufügen'),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () => _manage(context),
|
||||
icon: const Icon(Icons.tune),
|
||||
label: const Text('Arten verwalten'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_message(),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _message() => Obx(() {
|
||||
final message = controller.materialMessage.value;
|
||||
return message == null
|
||||
? const SizedBox.shrink()
|
||||
: Text(message, style: const TextStyle(color: Colors.red));
|
||||
});
|
||||
|
||||
Future<void> _manage(BuildContext context) => showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Materialarten verwalten'),
|
||||
content: SizedBox(
|
||||
width: 360,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text(
|
||||
'Entfernte Arten verschwinden aus der Auswahl. Gespeicherte Filamente bleiben erhalten.',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Flexible(
|
||||
child: Obx(() {
|
||||
final values = controller.materialien.toList();
|
||||
final busy = controller.isSavingMaterial.value;
|
||||
if (values.isEmpty) {
|
||||
return const Text('Keine Materialarten in der Liste.');
|
||||
}
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: values.length,
|
||||
itemBuilder: (context, index) => ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(values[index]),
|
||||
trailing: IconButton(
|
||||
tooltip: '${values[index]} aus Liste entfernen',
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
onPressed: busy
|
||||
? null
|
||||
: () => controller.removeMaterial(values[index]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
_message(),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Schließen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user