home,signin,login view and ol logik

This commit is contained in:
2026-09-30 20:31:01 +02:00
parent 63d03baac9
commit df71b15ff9
27 changed files with 1654 additions and 127 deletions
+6
View File
@@ -0,0 +1,6 @@
class EnvDirectus {
static const String directusPublicEndpoint = 'https://directus.joshihomeserver.ipv64.net';
static const String directusApiTokenBearer = '6mqGYbOCQAg7UAhIubb87wXcJVHTAsv9';
static const String directusTankUsersRest = 'TankUsers';
static const String directusFilamentRest = 'Filament';
}
+45
View File
@@ -0,0 +1,45 @@
import 'package:get/get.dart';
import '../models/filament_model.dart';
import '../pages/login_view.dart';
import '../services/directus_service.dart';
class HomeController extends GetxController {
final identId = Get.arguments['userIdentId'] as String? ?? '';
final directusService = DirectusService();
final isLoading = false.obs;
final filamentModelList = <FilamentModel>[].obs;
@override
void onReady() {
loadFilamentListPerUser();
}
@override
void onClose() {}
void logout() {
Get.offAllNamed(LoginPage.namedRoute);
}
void addFilament() {}
Future<void> loadFilamentListPerUser() async {
isLoading.value = true;
if (filamentModelList.isNotEmpty) {
filamentModelList.clear();
}
if (identId != '') {
var resultList = await directusService.getListByUserId(identId);
if (resultList.isEmpty) {
print('Dokumente wurden nicht geladen: ${resultList.length}');
} else {
print('Filament List: $resultList');
filamentModelList.addAll(
resultList.map((json) => FilamentModel.fromJson(json)).toList(),
);
}
}
isLoading.value = false;
update();
}
}
+90
View File
@@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../helper/helper.dart' as helper;
import '../models/tank_user_model.dart';
import '../pages/home_view.dart';
import '../pages/signin_view.dart';
import '../services/directus_service.dart';
import '../helper/password_helper.dart';
class LoginController extends GetxController {
final emailController = TextEditingController();
final passwordController = TextEditingController();
final directusService = DirectusService();
@override
void onReady() {}
@override
void onClose() {
emailController.dispose();
passwordController.dispose();
}
void newLoginDirectus() async {
var email = emailController.text.trim().toLowerCase();
var password = passwordController.text;
var result = await directusService.getTankUserList();
if (result.isEmpty) {
Get.snackbar(
'Login fehlgeschlagen',
'Fehler beim Abrufen der Benutzerdaten. Bitte versuchen Sie es später erneut.',
snackPosition: SnackPosition.BOTTOM,
);
return;
}
var tankUserList = result
.map((userJson) => TankUser.fromJson(userJson))
.toList();
var identId =
tankUserList
.firstWhereOrNull((user) => user.email == email)
?.userIdentId ??
'';
if (identId.isEmpty) {
Get.snackbar(
'Login fehlgeschlagen',
'Benutzer mit dieser E-Mail-Adresse wurde nicht gefunden.',
snackPosition: SnackPosition.BOTTOM,
);
return;
}
var storedHash =
tankUserList
.firstWhereOrNull((user) => user.email == email)
?.passwordHash ??
'';
if (storedHash.isEmpty) {
Get.snackbar(
'Login fehlgeschlagen',
'Kein gespeicherter Passwort-Hash für diesen Benutzer gefunden.',
snackPosition: SnackPosition.BOTTOM,
);
return;
}
var isPassCorrect = await PasswordHelper.verifyPassword(
password,
storedHash,
);
if (isPassCorrect && identId.isNotEmpty) {
Get.snackbar(
'Login erfolgreich',
'Willkommen zurück!',
snackPosition: SnackPosition.BOTTOM,
);
print('Login erfolgreich für Benutzer mit IdentId: $identId');
helper.kCurrentUserIdentId = identId; // Set the current user identId
Get.offAllNamed(HomePage.namedRoute, arguments: {'userIdentId': identId});
} else {
Get.snackbar(
'Login fehlgeschlagen',
'Bitte überprüfen Sie Ihre E-Mail und Ihr Passwort und versuchen Sie es erneut\noder registrieren Sie sich, wenn Sie noch kein Konto haben.',
snackPosition: SnackPosition.BOTTOM,
);
}
}
void goToSignInPage() {
Get.offAllNamed(SigninPage.namedRoute, arguments: {'userIdentId': ''});
}
}
+91
View File
@@ -0,0 +1,91 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../pages/home_view.dart';
import '../pages/login_view.dart';
import '../services/directus_service.dart';
class SigninController extends GetxController {
final userNameController = TextEditingController();
final emailController = TextEditingController();
final passwordController = TextEditingController();
final cityController = TextEditingController();
final countryController = TextEditingController();
final directusService = DirectusService();
@override
void onReady() {}
@override
void onClose() {
userNameController.dispose();
emailController.dispose();
passwordController.dispose();
cityController.dispose();
countryController.dispose();
super.onClose();
}
void register() async {
// Eingaben überprüfen
if (!_checkInputs()) return;
// Registrierungslogik
var userName = userNameController.text.trim();
var email = emailController.text.trim().toLowerCase();
var password = passwordController.text;
var city = cityController.text.trim();
var country = countryController.text.trim();
print(
'Registrieren mit Benutzername: $userName, E-Mail: $email, Stadt: $city, Land: $country',
);
bool registrationSuccess = await directusService.register(userName, email, password, city, country);
if (registrationSuccess) {
Get.snackbar(
'Registrierung erfolgreich',
'Willkommen, $userName!',
snackPosition: SnackPosition.BOTTOM,
);
var userIdentId = base64Encode(utf8.encode('$email:$password'));
Get.offAllNamed(HomePage.namedRoute, arguments: {'userIdentId': userIdentId});
} else {
Get.snackbar(
'Registrierung fehlgeschlagen',
'Bitte überprüfen Sie Ihre Eingaben und versuchen Sie es erneut.',
snackPosition: SnackPosition.BOTTOM,
);
}
}
void goToLoginPage() {
FocusManager.instance.primaryFocus?.unfocus();
Get.offAllNamed(LoginPage.namedRoute);
}
bool _checkInputs() {
if (userNameController.text.trim().isEmpty ||
emailController.text.trim().isEmpty ||
passwordController.text.isEmpty) {
Get.snackbar(
'Fehler',
'Bitte alle erforderlichen Felder ausfüllen',
snackPosition: SnackPosition.BOTTOM,
);
return false;
}
if (passwordController.text.length < 6) {
Get.snackbar(
'Fehler',
'Passwort muss mindestens 6 Zeichen lang sein',
snackPosition: SnackPosition.BOTTOM,
);
return false;
}
return true;
}
}
+16
View File
@@ -0,0 +1,16 @@
import 'package:intl/intl.dart';
/// Retrieves the current date in the format "yyyy-MM-dd".
///
/// @return [String] A formatted date.
String getCurrentDate(DateTime date) {
return DateFormat("yyyy-MM-dd").format(date);
}
var kListDropDownYear = <int>[
for (var year = DateTime.now().year; year >= 2020; year--) year
];
var kCurrentUserIdentId = '';
+10
View File
@@ -0,0 +1,10 @@
import 'dart:io';
class MyHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext? context) {
return super.createHttpClient(context)
..badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
}
}
+120
View File
@@ -0,0 +1,120 @@
import 'dart:convert';
import 'dart:math';
import 'package:cryptography/cryptography.dart';
class PasswordHelper {
static const int _saltLength = 16;
static const int _hashLength = 32;
// 19 MiB, laut aktueller cryptography-Doku ein sinnvoller Startwert.
static const int _memory = 19 * 1024;
static const int _iterations = 2;
static const int _parallelism = 1;
static final Argon2id _algorithm = Argon2id(
memory: _memory,
iterations: _iterations,
parallelism: _parallelism,
hashLength: _hashLength,
);
/// Erstellt einen neuen Passwort-Hash inklusive zufälligem Salt.
///
/// Rückgabeformat:
/// argon2id$memory$iterations$parallelism$saltBase64$hashBase64
static Future<String> hashPassword(String password) async {
final salt = _generateSalt(_saltLength);
final secretKey = await _algorithm.deriveKeyFromPassword(
password: password,
nonce: salt,
);
final hashBytes = await secretKey.extractBytes();
final saltBase64 = base64UrlEncode(salt);
final hashBase64 = base64UrlEncode(hashBytes);
return 'argon2id'
'\$$_memory'
'\$$_iterations'
'\$$_parallelism'
'\$$saltBase64'
'\$$hashBase64';
}
/// Prüft ein Klartext-Passwort gegen den gespeicherten Hash.
static Future<bool> verifyPassword(
String password,
String storedHash,
) async {
try {
final parts = storedHash.split('\$');
if (parts.length != 6) {
return false;
}
if (parts[0] != 'argon2id') {
return false;
}
final memory = int.parse(parts[1]);
final iterations = int.parse(parts[2]);
final parallelism = int.parse(parts[3]);
final salt = base64Url.decode(parts[4]);
final expectedHash = base64Url.decode(parts[5]);
final algorithm = Argon2id(
memory: memory,
iterations: iterations,
parallelism: parallelism,
hashLength: expectedHash.length,
);
final secretKey = await algorithm.deriveKeyFromPassword(
password: password,
nonce: salt,
);
final calculatedHash = await secretKey.extractBytes();
return _constantTimeEquals(
calculatedHash,
expectedHash,
);
} catch (_) {
return false;
}
}
/// Erzeugt kryptographisch zufälligen Salt.
static List<int> _generateSalt(int length) {
final random = Random.secure();
return List<int>.generate(
length,
(_) => random.nextInt(256),
);
}
/// Vergleich ohne vorzeitigen Abbruch.
static bool _constantTimeEquals(
List<int> a,
List<int> b,
) {
if (a.length != b.length) {
return false;
}
var difference = 0;
for (var i = 0; i < a.length; i++) {
difference |= a[i] ^ b[i];
}
return difference == 0;
}
}
+21
View File
@@ -0,0 +1,21 @@
import 'package:get/get.dart';
import '../controller/home_controller.dart';
import '../controller/login_controller.dart';
import '../controller/signin_controller.dart';
class SampleBindings extends Bindings {
@override
void dependencies() {
// Define your dependencies here no permanent Binding
Get.lazyPut<LoginController>(() => LoginController());
Get.lazyPut<SigninController>(() => SigninController());
Get.lazyPut<HomeController>(() => HomeController());
}
}
+26
View File
@@ -0,0 +1,26 @@
import 'package:get/get.dart';
import '../pages/home_view.dart';
import '../pages/login_view.dart';
import '../pages/signin_view.dart';
import 'sample_bindings.dart';
class SampleRouts {
static final sampleBindings = SampleBindings();
static List<GetPage<dynamic>> samplePages = [
GetPage(
name: LoginPage.namedRoute,
page: () => const LoginPage(),
binding: sampleBindings,
),
GetPage(
name: SigninPage.namedRoute,
page: () => const SigninPage(),
binding: sampleBindings,
),
GetPage(
name: HomePage.namedRoute,
page: () => const HomePage(),
binding: sampleBindings,
),
];
}
+17 -107
View File
@@ -1,122 +1,32 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'helper/http_override.dart';
//import 'helper/sample_bindings.dart';
import 'helper/sample_routes.dart';
import 'pages/login_view.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
HttpOverrides.global = MyHttpOverrides();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a purple toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: .fromSeed(seedColor: Colors.deepPurple),
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
return GetMaterialApp(
title: 'Flutter Filament',
debugShowCheckedModeBanner: false,
theme: ThemeData(colorScheme: .fromSeed(seedColor: Colors.deepPurple)),
//initialBinding: SampleBindings(),
initialRoute: LoginPage.namedRoute,
getPages: SampleRouts.samplePages,
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
//
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
// action in the IDE, or press "p" in the console), to see the
// wireframe for each widget.
mainAxisAlignment: .center,
children: [
const Text('You have pushed the button this many times:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
+56
View File
@@ -0,0 +1,56 @@
class FilamentModel {
int? iD;
String? userIdentId;
String? art;
String? color;
String? duckTemperatur;
String? menge;
String? nozzelTemperatur;
String? bettTemperatur;
String? restMengeAktuelleRolleGramm;
String? updatedAt;
String? createdAt;
FilamentModel(
{this.iD,
this.userIdentId,
this.art,
this.color,
this.duckTemperatur,
this.menge,
this.nozzelTemperatur,
this.bettTemperatur,
this.restMengeAktuelleRolleGramm,
this.updatedAt,
this.createdAt});
FilamentModel.fromJson(Map<String, dynamic> json) {
iD = json['ID'];
userIdentId = json['UserIdentId'];
art = json['Art'];
color = json['Color'];
duckTemperatur = json['DuckTemperatur'];
menge = json['Menge'];
nozzelTemperatur = json['NozzelTemperatur'];
bettTemperatur = json['BettTemperatur'];
restMengeAktuelleRolleGramm = json['RestMengeAktuelleRolleGramm'];
updatedAt = json['UpdatedAt'];
createdAt = json['CreatedAt'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['ID'] = iD;
data['UserIdentId'] = userIdentId;
data['Art'] = art;
data['Color'] = color;
data['DuckTemperatur'] = duckTemperatur;
data['Menge'] = menge;
data['NozzelTemperatur'] = nozzelTemperatur;
data['BettTemperatur'] = bettTemperatur;
data['RestMengeAktuelleRolleGramm'] = restMengeAktuelleRolleGramm;
data['UpdatedAt'] = updatedAt;
data['CreatedAt'] = createdAt;
return data;
}
}
+44
View File
@@ -0,0 +1,44 @@
class TankUser {
int? iD;
String? userIdentId;
String? userName;
String? city;
String? country;
String? email;
String? passwordHash;
String? createdAt;
TankUser(
{this.iD,
this.userIdentId,
this.userName,
this.city,
this.country,
this.email,
this.passwordHash,
this.createdAt});
TankUser.fromJson(Map<String, dynamic> json) {
iD = json['ID'];
userIdentId = json['UserIdentId'];
userName = json['UserName'];
city = json['City'];
country = json['Country'];
email = json['Email'];
passwordHash = json['PasswordHash'];
createdAt = json['CreatedAt'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['ID'] = iD;
data['UserIdentId'] = userIdentId;
data['UserName'] = userName;
data['City'] = city;
data['Country'] = country;
data['Email'] = email;
data['PasswordHash'] = passwordHash;
data['CreatedAt'] = createdAt;
return data;
}
}
+78
View File
@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controller/home_controller.dart';
import '../widgets/filament_item_widget.dart';
class HomePage extends GetView<HomeController> {
static const String namedRoute = '/home-page';
const HomePage({super.key});
@override
Widget build(BuildContext context) {
var hCtrl = controller;
return Scaffold(
backgroundColor: Colors.grey.shade50,
appBar: AppBar(
centerTitle: true,
elevation: 0,
backgroundColor: Colors.teal.shade400,
foregroundColor: Colors.white,
title: const Text(
'Filament Lager',
style: TextStyle(fontWeight: FontWeight.bold),
),
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: hCtrl.logout,
),
],
),
floatingActionButton: FloatingActionButton.extended(
backgroundColor: Colors.teal.shade400.withValues(alpha: 0.4),
foregroundColor: Colors.grey.shade200,
onPressed: hCtrl.addFilament,
icon: const Icon(Icons.add),
label: const Text('Filament'),
),
body: Obx(() {
if (hCtrl.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
if (hCtrl.filamentModelList.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inventory_2_outlined, size: 64, color: Colors.grey.shade400),
const SizedBox(height: 16),
Text(
'Keine Filamente vorhanden',
style: TextStyle(fontSize: 16, color: Colors.grey.shade600),
),
],
),
);
}
return RefreshIndicator(
onRefresh: hCtrl.loadFilamentListPerUser,
child: ListView.builder(
padding: const EdgeInsets.only(top: 8, bottom: 80),
itemCount: hCtrl.filamentModelList.length,
itemBuilder: (context, index) {
final filament = hCtrl.filamentModelList[index];
return FilamentItemWidget(
filament: filament,
onTap: () {
// Optionale Detailansicht oder Bearbeitung
},
);
},
),
);
}),
);
}
}
+40
View File
@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controller/login_controller.dart';
import '../widgets/my_login_widget.dart';
class LoginPage extends GetView<LoginController> {
static const String namedRoute = '/login-page';
const LoginPage({super.key});
@override
Widget build(BuildContext context) {
var logCtrl = controller;
var displayWidth = MediaQuery.of(context).size.width;
var displayHeight = MediaQuery.of(context).size.height;
return PopScope(
canPop: false, // Verhindert das Zurücknavigieren über die System-Geste / den Back-Button
child: SafeArea(
child: Scaffold(
resizeToAvoidBottomInset: true,
body: Container(
height: displayHeight,
width: displayWidth,
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 50.0,
),
color: Colors.blue.shade100,
child: SingleChildScrollView(
child: MyLoginWidget(
onButtonPressed: () => logCtrl.newLoginDirectus(),
signInOnTab: () => logCtrl.goToSignInPage(),
logCtrl: logCtrl,
),
),
),
),
),
);
}
}
+41
View File
@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controller/signin_controller.dart';
import '../widgets/my_signin_widget.dart';
class SigninPage extends GetView<SigninController> {
static const String namedRoute = '/signin-page';
const SigninPage({super.key});
@override
Widget build(BuildContext context) {
var sigCtrl = controller;
var displayWidth = MediaQuery.of(context).size.width;
var displayHeight = MediaQuery.of(context).size.height;
return SafeArea(
child: PopScope(
canPop: false, // Verhindert das Zurücknavigieren über die System-Geste / den Back-Button
child: Scaffold(
resizeToAvoidBottomInset: true,
body: Container(
height: displayHeight,
width: displayWidth,
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 90.0,
),
color: Colors.blue.shade100,
child: SingleChildScrollView(
child: MySigninWidget(
signCtrl: sigCtrl,
onButtonPressed: () => sigCtrl.register(),
logInOnTab: () => sigCtrl.goToLoginPage(),
),
),
),
),
),
);
}
}
+134
View File
@@ -0,0 +1,134 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../config/env_directus.dart';
import '../helper/password_helper.dart';
class DirectusService {
static final String endpoint = EnvDirectus.directusPublicEndpoint;
static final String apiTokenBearer = EnvDirectus.directusApiTokenBearer;
static final String tankUsersRest = EnvDirectus.directusTankUsersRest;
static final String filamentRest = EnvDirectus.directusFilamentRest;
static Map<String, String> get headers => {
'Authorization': 'Bearer $apiTokenBearer',
'Content-Type': 'application/json',
};
Future<List<dynamic>> getTankUserList() async {
var client = http.Client();
String apiUrl = '$endpoint/items/$tankUsersRest';
var response = await client.get(Uri.parse(apiUrl), headers: headers);
if(response.statusCode != 200) {
print('Fehler beim Abrufen der TankUser-Liste: ${response.statusCode}');
client.close();
return [];
}
final json = jsonDecode(
utf8.decode(response.bodyBytes),
) as Map<String, dynamic>;
//print('$json}');
client.close();
return json['data'] as List<dynamic>;
}
Future<List<dynamic>> getListByUserId(String identId) async {
var client = http.Client();
//String apiUrl = '$endpoint/items/$filamentRest?filter[UserIdentId][_eq]=$userIdentId';
String apiUrl = '$endpoint/items/$filamentRest?filter[UserIdentId][_eq]=$identId';
var response = await client.get(Uri.parse(apiUrl), headers: headers);
if(response.statusCode != 200) {
print('Fehler beim Abrufen der Dokumente aus der Collection: ${response.statusCode}');
client.close();
return [];
}
final json = jsonDecode(
utf8.decode(response.bodyBytes),
) as Map<String, dynamic>;
//print('$json}');
client.close();
return json['data'] as List<dynamic>;
}
Future<void> deleteDocumentFromTable(int iD) async {
var client = http.Client();
String apiUrl = '$endpoint/items/$filamentRest/$iD';
var response = await client.delete(Uri.parse(apiUrl), headers: headers);
if(response.statusCode < 200 || response.statusCode >= 300) {
print('Fehler beim Löschen des Dokuments aus der Collection: ${response.statusCode}');
client.close();
throw Exception('Fehler beim Löschen des Dokuments aus der Collection: ${response.statusCode}');
}
client.close();
}
Future<bool> createDocumentInTable(Map<String, String> data) async {
return await http.post(
Uri.parse('$endpoint/items/$filamentRest'),
headers: headers,
body: jsonEncode(data),
).then((response) {
if (response.statusCode == 200 || response.statusCode == 201) {
print('Dokument erfolgreich erstellt: ${response.body}');
return true;
} else {
print('Fehler beim Erstellen des Dokuments: ${response.statusCode} - ${response.body}');
return false;
}
}).catchError((error) {
print('Fehler beim Erstellen des Dokuments: $error');
return false;
});
}
Future<bool> updateDocumentInTable(int? iD, Map<String, String> data) async {
return iD != null
? http.patch(
Uri.parse('$endpoint/items/$filamentRest/$iD'),
headers: headers,
body: jsonEncode(data),
).then((response) {
if (response.statusCode == 200) {
print('Dokument erfolgreich aktualisiert: ${response.body}');
return true;
} else {
print('Fehler beim Aktualisieren des Dokuments: ${response.statusCode} - ${response.body}');
return false;
}
}).catchError((error) {
print('Fehler beim Aktualisieren des Dokuments: $error');
return false;
})
: Future.value(false);
}
Future<bool> register(String userName, String email, String password, String city, String country) async {
var client = http.Client();
String apiUrl = '$endpoint/items/$tankUsersRest';
Map<String, String> data = {
'UserIdentId': base64Encode(utf8.encode('$email:$password')),
'UserName': userName,
'City': city,
'Country': country,
'Email': email,
'PasswordHash': await PasswordHelper.hashPassword(password),
};
var response = await client.post(
Uri.parse(apiUrl),
headers: headers,
body: jsonEncode(data),
);
if (response.statusCode == 200 || response.statusCode == 201) {
print('Benutzer erfolgreich registriert: ${response.body}');
client.close();
return true;
} else {
print('Fehler bei der Registrierung des Benutzers: ${response.statusCode} - ${response.body}');
client.close();
return false;
}
}
}
+203
View File
@@ -0,0 +1,203 @@
import 'package:flutter/material.dart';
import '../models/filament_model.dart';
class FilamentItemWidget extends StatelessWidget {
final FilamentModel filament;
final VoidCallback? onTap;
const FilamentItemWidget({
super.key,
required this.filament,
this.onTap,
});
// Hilfsmethode zur Bestimmung der Farbe basierend auf dem String "Color"
Color _parseColor(String colorName) {
switch (colorName.toLowerCase().trim()) {
case 'weiß':
return Colors.white;
case 'schwarz':
return Colors.black;
case 'hell braun':
case 'braun':
return Colors.brown.shade400;
case 'knochenfarbe':
return const Color(0xFFE3DAC9); // Bone / Beige
default:
return Colors.grey;
}
}
// Hilfsmethode zur Umwandlung von "1kg", "7kg" etc. in Gramm
double _parseTotalWeightInGramm(String mengeStr) {
final cleaned = mengeStr.toLowerCase().replaceAll('kg', '').trim();
final kg = double.tryParse(cleaned) ?? 1.0;
return kg * 1000.0;
}
@override
Widget build(BuildContext context) {
final parsedColor = _parseColor(filament.color!);
final restGramm = double.tryParse(filament.restMengeAktuelleRolleGramm!) ?? 0.0;
final totalGramm = _parseTotalWeightInGramm(filament.menge!);
final progress = (restGramm / totalGramm).clamp(0.0, 1.0);
return Card(
elevation: 0,
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: Colors.grey.shade200),
),
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header: Typ, Farbindikator, Spule/Menge
Row(
children: [
// Farbpunkte mit Schatten für weiße/helle Farben
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: parsedColor,
shape: BoxShape.circle,
border: Border.all(color: Colors.grey.shade300, width: 1),
boxShadow: [
BoxShadow(
color: parsedColor.withValues(alpha: 0.4),
blurRadius: 6,
spreadRadius: 1,
)
],
),
),
const SizedBox(width: 12),
// Material & Farbe
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
filament.art!,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
filament.color!,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
],
),
),
// Spulengröße Badge
Chip(
padding: EdgeInsets.zero,
labelStyle: const TextStyle(fontSize: 12),
label: Text(filament.menge!),
backgroundColor: Colors.teal.shade50,
side: BorderSide.none,
),
],
),
const SizedBox(height: 16),
// Temperaturen (Nozzle & Bed)
Row(
children: [
_buildIconText(
icon: Icons.thermostat,
label: 'Nozzle',
value: filament.nozzelTemperatur!,
color: Colors.orange.shade700,
),
const SizedBox(width: 24),
_buildIconText(
icon: Icons.hot_tub,
label: 'Bett',
value: filament.bettTemperatur!,
color: Colors.blue.shade700,
),
],
),
const SizedBox(height: 16),
// Restmengen Progress Indicator
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Restmenge',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
Text(
'${restGramm.toInt()}g / ${totalGramm.toInt()}g',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 6),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: progress,
minHeight: 6,
backgroundColor: Colors.grey.shade200,
color: progress < 0.2 ? Colors.red : Colors.teal.shade400,
),
),
],
),
],
),
),
),
);
}
Widget _buildIconText({
required IconData icon,
required String label,
required String value,
required Color color,
}) {
return Row(
children: [
Icon(icon, size: 18, color: color),
const SizedBox(width: 4),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(fontSize: 10, color: Colors.grey.shade600),
),
Text(
value,
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
),
],
),
],
);
}
}
+134
View File
@@ -0,0 +1,134 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart' show GoogleFonts;
import '../controller/login_controller.dart';
class MyLoginWidget extends StatelessWidget {
final void Function()? onButtonPressed;
final void Function()? signInOnTab;
final LoginController logCtrl;
const MyLoginWidget({
super.key,
this.onButtonPressed,
this.signInOnTab,
required this.logCtrl,
});
@override
Widget build(BuildContext context) {
final googleFont = GoogleFonts.righteous().fontFamily;
return Column(
children: [
Stack(
children: [
// Umriss (Outline)
Text(
"Login Page",
style: TextStyle(
fontSize: 44,
fontFamily: googleFont,
fontStyle: FontStyle.normal,
letterSpacing: 5.0,
foreground: Paint()
..style = PaintingStyle.stroke
..strokeWidth = 5
..color = Colors.blue.shade900,
),
),
// Füllung
Text(
"Login Page",
style: TextStyle(
fontSize: 44,
fontFamily: googleFont,
fontStyle: FontStyle.normal,
color: Colors.orange.shade300,
letterSpacing: 5.0,
),
),
],
),
SizedBox(height: 20),
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.orange.shade400, width: 8),
borderRadius: BorderRadius.circular(30),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(18),
child: Image.asset(
'assets/images/filamentTransparent.png',
width: 300,
height: 300,
),
),
),
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 50.0),
child: TextField(
controller: logCtrl.emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'E-Mail',
),
),
),
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 50.0),
child: TextField(
obscureText: true,
controller: logCtrl.passwordController,
keyboardType: TextInputType.visiblePassword,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Password',
),
),
),
SizedBox(height: 30),
ElevatedButton(
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll<Color>(
Colors.orange.shade500,
),
foregroundColor: WidgetStatePropertyAll<Color>(
Colors.blue.shade900,
),
minimumSize: WidgetStatePropertyAll<Size>(Size(430, 60)),
),
onPressed: onButtonPressed,
child: Text(
'Login',
style: TextStyle(fontSize: 34, fontFamily: googleFont),
),
),
SizedBox(height: 10),
RichText(
text: TextSpan(
style: TextStyle(fontSize: 16, color: Colors.black),
children: [
TextSpan(text: 'Zum registrieren '),
WidgetSpan(
child: GestureDetector(
onTap: signInOnTab,
child: Text(
'Sign in',
style: TextStyle(
fontSize: 16,
color: Colors.blue.shade700,
decoration: TextDecoration.underline,
),
),
),
),
TextSpan(text: ' clicken!!'),
],
),
),
],
);
}
}
+162
View File
@@ -0,0 +1,162 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart' show GoogleFonts;
import '../controller/signin_controller.dart';
class MySigninWidget extends StatelessWidget {
final void Function()? onButtonPressed;
final void Function()? logInOnTab;
final SigninController signCtrl;
const MySigninWidget({
super.key,
this.onButtonPressed,
this.logInOnTab,
required this.signCtrl,
});
@override
Widget build(BuildContext context) {
final googleFont = GoogleFonts.righteous().fontFamily;
return Column(
children: [
Stack(
children: [
// Umriss (Outline)
Text(
"Signin Page",
style: TextStyle(
fontSize: 44,
fontFamily: googleFont,
fontStyle: FontStyle.normal,
letterSpacing: 5.0,
foreground: Paint()
..style = PaintingStyle.stroke
..strokeWidth = 5
..color = Colors.blue.shade900,
),
),
// Füllung
Text(
"Signin Page",
style: TextStyle(
fontSize: 44,
fontFamily: googleFont,
fontStyle: FontStyle.normal,
color: Colors.orange.shade300,
letterSpacing: 5.0,
),
),
],
),
SizedBox(height: 20),
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.orange.shade400, width: 8),
borderRadius: BorderRadius.circular(30),
),
),
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 50.0),
child: TextField(
controller: signCtrl.userNameController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Username',
),
),
),
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 50.0),
child: TextField(
controller: signCtrl.emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Email',
),
),
),
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 50.0),
child: TextField(
obscureText: true,
controller: signCtrl.passwordController,
keyboardType: TextInputType.visiblePassword,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Password',
),
),
),
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 50.0),
child: TextField(
controller: signCtrl.cityController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'City',
),
),
),
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 50.0),
child: TextField(
controller: signCtrl.countryController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Country',
),
),
),
SizedBox(height: 30),
ElevatedButton(
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll<Color>(
Colors.orange.shade500,
),
foregroundColor: WidgetStatePropertyAll<Color>(
Colors.blue.shade900,
),
minimumSize: WidgetStatePropertyAll<Size>(Size(430, 60)),
),
onPressed: onButtonPressed,
child: Text(
'Register',
style: TextStyle(fontSize: 34, fontFamily: googleFont),
),
),
SizedBox(height: 10),
RichText(
text: TextSpan(
style: TextStyle(fontSize: 16, color: Colors.black),
children: [
TextSpan(text: 'Zum login '),
WidgetSpan(
child: GestureDetector(
onTap: logInOnTab,
child: Text(
'Log in',
style: TextStyle(
fontSize: 16,
color: Colors.blue.shade700,
decoration: TextDecoration.underline,
),
),
),
),
TextSpan(text: ' clicken!!'),
],
),
),
],
);
}
}