home,signin,login view and ol logik
This commit is contained in:
@@ -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';
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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': ''});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 = '';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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!!'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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!!'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user