Add view and logik
This commit is contained in:
@@ -7,6 +7,9 @@
|
|||||||
|
|
||||||
# The following line activates a set of recommended lints for Flutter apps,
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
# packages, and plugins designed to encourage good coding practices.
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
analyzer:
|
||||||
|
errors:
|
||||||
|
avoid_print: ignore
|
||||||
include: package:flutter_lints/flutter.yaml
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
|
|||||||
BIN
assets/images/fil01.jpg
Normal file
BIN
assets/images/fil01.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
58
lib/controllers/home_controller.dart
Normal file
58
lib/controllers/home_controller.dart
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_mssql_node_filament_app/helpers/services_repos.dart';
|
||||||
|
import 'package:flutter_mssql_node_filament_app/pages/list_view.dart';
|
||||||
|
import 'package:flutter_mssql_node_filament_app/pages/signin_view.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
class HomeController extends GetxController {
|
||||||
|
final benutzerController = TextEditingController();
|
||||||
|
final passwordController = TextEditingController();
|
||||||
|
final serviceRepos = ServiceRepos();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
benutzerController.dispose();
|
||||||
|
passwordController.dispose();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void navigateToSignIn() {
|
||||||
|
Get.toNamed(SignInPage.namedRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool validateLogin() {
|
||||||
|
final benutzer = benutzerController.text.trim();
|
||||||
|
final password = passwordController.text.trim();
|
||||||
|
|
||||||
|
if (benutzer.isEmpty || password.isEmpty) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Fehler',
|
||||||
|
'Bitte alle Felder ausfüllen',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Get.theme.colorScheme.error.withAlpha(26),
|
||||||
|
colorText: Get.theme.colorScheme.error,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> logInAndloadFilaments() async {
|
||||||
|
if (!validateLogin()) return;
|
||||||
|
String benutzer = benutzerController.text.trim();
|
||||||
|
String password = passwordController.text.trim();
|
||||||
|
var response = await serviceRepos.getUserIsValide(benutzer, password);
|
||||||
|
if (response.statusCode == 200 && response.body['valid'] == true) {
|
||||||
|
// Navigiere zur Listenseite
|
||||||
|
Get.offAllNamed(ListPage.namedRoute);
|
||||||
|
} else {
|
||||||
|
Get.snackbar(
|
||||||
|
'Fehler',
|
||||||
|
'Ungültiger Benutzername oder Passwort',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Get.theme.colorScheme.error.withAlpha(26),
|
||||||
|
colorText: Get.theme.colorScheme.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
lib/controllers/list_controller.dart
Normal file
18
lib/controllers/list_controller.dart
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
class ListController extends GetxController {
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
39
lib/controllers/signin_controller.dart
Normal file
39
lib/controllers/signin_controller.dart
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
class SignInController extends GetxController {
|
||||||
|
final usernameController = TextEditingController();
|
||||||
|
final passwordController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
usernameController.dispose();
|
||||||
|
passwordController.dispose();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> registerUser() async {
|
||||||
|
final username = usernameController.text.trim();
|
||||||
|
final password = passwordController.text.trim();
|
||||||
|
|
||||||
|
if (username.isEmpty || password.isEmpty) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Fehler',
|
||||||
|
'Bitte alle Felder ausfüllen',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Get.theme.colorScheme.error.withAlpha(26),
|
||||||
|
colorText: Get.theme.colorScheme.error,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Registrierungs-Logik implementieren
|
||||||
|
Get.snackbar(
|
||||||
|
'Erfolg',
|
||||||
|
'Benutzer "$username" wurde angelegt',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Get.theme.colorScheme.primary.withAlpha(26),
|
||||||
|
colorText: Get.theme.colorScheme.primary,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
lib/helpers/sampl_routes.dart
Normal file
26
lib/helpers/sampl_routes.dart
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../pages/home_view.dart';
|
||||||
|
import '../pages/list_view.dart';
|
||||||
|
import '../pages/signin_view.dart';
|
||||||
|
import 'sample_bindings.dart';
|
||||||
|
|
||||||
|
class SampleRouts {
|
||||||
|
static final sampleBindings = SampleBindings();
|
||||||
|
static List<GetPage<dynamic>> samplePages = [
|
||||||
|
GetPage(
|
||||||
|
name: HomePage.namedRoute,
|
||||||
|
page: () => const HomePage(),
|
||||||
|
binding: sampleBindings,
|
||||||
|
),
|
||||||
|
GetPage(
|
||||||
|
name: SignInPage.namedRoute,
|
||||||
|
page: () => const SignInPage(),
|
||||||
|
binding: sampleBindings,
|
||||||
|
),
|
||||||
|
GetPage(
|
||||||
|
name: ListPage.namedRoute,
|
||||||
|
page: () => const ListPage(),
|
||||||
|
binding: sampleBindings,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
17
lib/helpers/sample_bindings.dart
Normal file
17
lib/helpers/sample_bindings.dart
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../controllers/home_controller.dart';
|
||||||
|
import '../controllers/list_controller.dart';
|
||||||
|
import '../controllers/signin_controller.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class SampleBindings extends Bindings {
|
||||||
|
@override
|
||||||
|
void dependencies() {
|
||||||
|
// Define your dependencies here no permanent Binding
|
||||||
|
Get.lazyPut<HomeController>(() => HomeController());
|
||||||
|
Get.lazyPut<SignInController>(() => SignInController());
|
||||||
|
Get.lazyPut<ListController>(() => ListController());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
32
lib/helpers/services_repos.dart
Normal file
32
lib/helpers/services_repos.dart
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
class ServiceRepos extends GetConnect {
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
// Setze die Basis-URL für deine API
|
||||||
|
httpClient.baseUrl = 'https://node.joshihomeserver.ipv64.net/api';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Response> getUsersList() => get('$baseUrl/GetFilamentUsers', headers: {'Content-Type': 'application/json'});
|
||||||
|
|
||||||
|
Future<Response> getUserIsValide(String username, String password) =>
|
||||||
|
getUsersList().then((response) {
|
||||||
|
if (response.statusCode == 200 && response.body is List) {
|
||||||
|
final users = response.body as List;
|
||||||
|
final user = users.firstWhere(
|
||||||
|
(u) => u['benutzer'] == username && u['kennwort'] == password,
|
||||||
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
return Response(
|
||||||
|
statusCode: user != null ? 200 : 401,
|
||||||
|
body: user != null ? {'valid': true} : {'valid': false},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Response(statusCode: response.statusCode, body: response.body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<Response> getFilamentsList(String username) =>
|
||||||
|
post('$baseUrl/GetFilamentDataByUser', {'username': username});
|
||||||
|
}
|
||||||
116
lib/main.dart
116
lib/main.dart
@@ -1,4 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../helpers/sampl_routes.dart';
|
||||||
|
|
||||||
|
import 'pages/home_view.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
@@ -7,116 +11,16 @@ void main() {
|
|||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
const MyApp({super.key});
|
const MyApp({super.key});
|
||||||
|
|
||||||
// This widget is the root of your application.
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return GetMaterialApp(
|
||||||
title: 'Flutter Demo',
|
title: 'Flutter Demo',
|
||||||
theme: ThemeData(
|
theme: ThemeData(colorScheme: .fromSeed(seedColor: Colors.deepPurple)),
|
||||||
// This is the theme of your application.
|
initialBinding: SampleRouts.sampleBindings,
|
||||||
//
|
getPages: SampleRouts.samplePages,
|
||||||
// TRY THIS: Try running your application with "flutter run". You'll see
|
debugShowCheckedModeBanner: false,
|
||||||
// the application has a purple toolbar. Then, without quitting the app,
|
initialRoute: HomePage.namedRoute,
|
||||||
// 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'),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
157
lib/models/filament_model.dart
Normal file
157
lib/models/filament_model.dart
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
class FilamentModel {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String type; // PLA, ABS, PETG, etc.
|
||||||
|
final String color;
|
||||||
|
final int weight; // in Gramm
|
||||||
|
final int weightUsed;
|
||||||
|
final double price; // Preis
|
||||||
|
final String? manufacturer;
|
||||||
|
final String? purchaseDate;
|
||||||
|
final String? notes;
|
||||||
|
final int pices;
|
||||||
|
final int printingTemp;
|
||||||
|
final int bedTemp;
|
||||||
|
|
||||||
|
FilamentModel({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.type,
|
||||||
|
required this.color,
|
||||||
|
required this.weight,
|
||||||
|
required this.weightUsed,
|
||||||
|
required this.price,
|
||||||
|
this.manufacturer,
|
||||||
|
this.purchaseDate,
|
||||||
|
this.notes,
|
||||||
|
required this.pices,
|
||||||
|
required this.printingTemp,
|
||||||
|
required this.bedTemp,
|
||||||
|
});
|
||||||
|
|
||||||
|
// JSON Serialisierung
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'type': type,
|
||||||
|
'color': color,
|
||||||
|
'weight': weight,
|
||||||
|
'price': price,
|
||||||
|
'manufacturer': manufacturer,
|
||||||
|
'purchaseDate': purchaseDate,
|
||||||
|
'notes': notes,
|
||||||
|
'pices': pices,
|
||||||
|
'printingTemp': printingTemp,
|
||||||
|
'bedTemp': bedTemp,
|
||||||
|
'weightUsed': weightUsed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON Deserialisierung
|
||||||
|
factory FilamentModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return FilamentModel(
|
||||||
|
id: json['id'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
type: json['type'] as String,
|
||||||
|
color: json['color'] as String,
|
||||||
|
weight: json['weight'] as int? ?? 0,
|
||||||
|
weightUsed: json['weightUsed'] as int? ?? 0,
|
||||||
|
price: (json['price'] as num).toDouble(),
|
||||||
|
manufacturer: json['manufacturer'] as String?,
|
||||||
|
purchaseDate: json['purchaseDate'] as String?,
|
||||||
|
notes: json['notes'] as String?,
|
||||||
|
pices: json['pices'] as int,
|
||||||
|
printingTemp: json['printingTemp'] as int,
|
||||||
|
bedTemp: json['bedTemp'] as int,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyWith für Updates
|
||||||
|
FilamentModel copyWith({
|
||||||
|
String? id,
|
||||||
|
String? name,
|
||||||
|
String? type,
|
||||||
|
String? color,
|
||||||
|
int? weight,
|
||||||
|
int? weightUsed,
|
||||||
|
double? price,
|
||||||
|
String? manufacturer,
|
||||||
|
String? purchaseDate,
|
||||||
|
String? notes,
|
||||||
|
int? pices,
|
||||||
|
int? printingTemp,
|
||||||
|
int? bedTemp,
|
||||||
|
}) {
|
||||||
|
return FilamentModel(
|
||||||
|
id: id ?? this.id,
|
||||||
|
name: name ?? this.name,
|
||||||
|
type: type ?? this.type,
|
||||||
|
color: color ?? this.color,
|
||||||
|
weight: weight ?? this.weight,
|
||||||
|
weightUsed: weightUsed ?? this.weightUsed,
|
||||||
|
price: price ?? this.price,
|
||||||
|
manufacturer: manufacturer ?? this.manufacturer,
|
||||||
|
purchaseDate: purchaseDate ?? this.purchaseDate,
|
||||||
|
notes: notes ?? this.notes,
|
||||||
|
pices: pices ?? this.pices,
|
||||||
|
printingTemp: printingTemp ?? this.printingTemp,
|
||||||
|
bedTemp: bedTemp ?? this.bedTemp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String formatDate(DateTime date) {
|
||||||
|
final DateFormat formatter = DateFormat('dd.MM.yyyy');
|
||||||
|
return formatter.format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<FilamentModel> mockupFilamentList = [
|
||||||
|
FilamentModel(
|
||||||
|
id: '1',
|
||||||
|
name: '3Djake ECO Filament',
|
||||||
|
type: 'PLA',
|
||||||
|
color: 'White',
|
||||||
|
weight: 1000,
|
||||||
|
weightUsed: 250,
|
||||||
|
price: 19.99,
|
||||||
|
manufacturer: '3Djake.at',
|
||||||
|
purchaseDate: formatDate(DateTime(2026, 1, 10)),
|
||||||
|
notes: 'Great quality filament for everyday printing.',
|
||||||
|
pices: 1,
|
||||||
|
printingTemp: 207,
|
||||||
|
bedTemp: 55,
|
||||||
|
),
|
||||||
|
FilamentModel(
|
||||||
|
id: '2',
|
||||||
|
name: 'Geeetech',
|
||||||
|
type: 'PETG',
|
||||||
|
color: 'Black',
|
||||||
|
weight: 1000,
|
||||||
|
weightUsed: 0,
|
||||||
|
price: 9.99,
|
||||||
|
manufacturer: 'geeetech.com',
|
||||||
|
purchaseDate: formatDate(DateTime(2025, 10, 10)),
|
||||||
|
notes: 'Durable and strong, perfect for functional parts.',
|
||||||
|
pices: 8,
|
||||||
|
printingTemp: 207,
|
||||||
|
bedTemp: 55,
|
||||||
|
),
|
||||||
|
FilamentModel(
|
||||||
|
id: '3',
|
||||||
|
name: 'Tinmorry',
|
||||||
|
type: 'ASA',
|
||||||
|
color: 'Black',
|
||||||
|
weight: 1000,
|
||||||
|
weightUsed: 150,
|
||||||
|
price: 16.01,
|
||||||
|
pices: 1,
|
||||||
|
manufacturer: 'tinmorry.com',
|
||||||
|
purchaseDate: formatDate(DateTime(2026, 1, 10)),
|
||||||
|
notes: 'Weather-resistant filament for outdoor use.',
|
||||||
|
printingTemp: 265,
|
||||||
|
bedTemp: 100,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
48
lib/models/user_model.dart
Normal file
48
lib/models/user_model.dart
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class UserModel {
|
||||||
|
final String uuid;
|
||||||
|
final String username;
|
||||||
|
final String passwort;
|
||||||
|
UserModel({
|
||||||
|
required this.uuid,
|
||||||
|
required this.username,
|
||||||
|
required this.passwort,
|
||||||
|
});
|
||||||
|
|
||||||
|
UserModel copyWith({
|
||||||
|
String? uuid,
|
||||||
|
String? username,
|
||||||
|
String? passwort,
|
||||||
|
}) {
|
||||||
|
return UserModel(
|
||||||
|
uuid: uuid ?? this.uuid,
|
||||||
|
username: username ?? this.username,
|
||||||
|
passwort: passwort ?? this.passwort,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'pk_userUuid': uuid,
|
||||||
|
'benutzer': username,
|
||||||
|
'kennwort': passwort,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory UserModel.fromMap(Map<String, dynamic> map) {
|
||||||
|
return UserModel(
|
||||||
|
uuid: map['pk_userUuid'] as String,
|
||||||
|
username: map['benutzer'] as String,
|
||||||
|
passwort: map['kennwort'] as String,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory UserModel.fromJson(String source) => UserModel.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'UserModel(uuid: $uuid, username: $username, passwort: $passwort)';
|
||||||
|
}
|
||||||
143
lib/pages/home_view.dart
Normal file
143
lib/pages/home_view.dart
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../controllers/home_controller.dart';
|
||||||
|
import '../widgets/custom_text_field.dart';
|
||||||
|
import '../widgets/info_item.dart';
|
||||||
|
import '../widgets/primary_button.dart';
|
||||||
|
|
||||||
|
class HomePage extends GetView<HomeController> {
|
||||||
|
static const String namedRoute = '/home-page';
|
||||||
|
const HomePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var homeCtrl = controller;
|
||||||
|
return Scaffold(
|
||||||
|
body: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [Colors.blue.shade50, Colors.purple.shade50],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24.0,
|
||||||
|
vertical: 32.0,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// Header & Image
|
||||||
|
Hero(
|
||||||
|
tag: 'fil01.jpg',
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(50),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withAlpha(25),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: Offset(0, 10),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(50),
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/fil01.jpg',
|
||||||
|
width: 150,
|
||||||
|
height: 150,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
'Filament Verwaltung Login',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.deepPurple.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'Verwalte deine Filamente einfach und übersichtlich',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Info Card
|
||||||
|
Card(
|
||||||
|
elevation: 4,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
buildInfoItem(
|
||||||
|
icon: Icons.storage_rounded,
|
||||||
|
text: 'Speicherung in einer Datenbank',
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
buildInfoItem(
|
||||||
|
icon: Icons.list_alt_rounded,
|
||||||
|
text: 'Filament laden → Login zur Liste',
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
SizedBox(height: 24),
|
||||||
|
CustomTextField(
|
||||||
|
controller: homeCtrl.benutzerController,
|
||||||
|
label: 'Benutzername',
|
||||||
|
hint: 'Benutzername eingeben',
|
||||||
|
icon: Icons.person_rounded,
|
||||||
|
),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
CustomTextField(
|
||||||
|
controller: homeCtrl.passwordController,
|
||||||
|
label: 'Benutzer Passwort',
|
||||||
|
hint: 'Passwort eingeben',
|
||||||
|
icon: Icons.lock_rounded,
|
||||||
|
isPassword: true,
|
||||||
|
),
|
||||||
|
SizedBox(height: 4),
|
||||||
|
Center(
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () => homeCtrl.navigateToSignIn(),
|
||||||
|
child: const Text('Sign In'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Action Buttons
|
||||||
|
buildPrimaryButton(
|
||||||
|
context: context,
|
||||||
|
icon: Icons.inventory_2_rounded,
|
||||||
|
label: 'Login Filamente laden',
|
||||||
|
onPressed: () => homeCtrl.logInAndloadFilaments(),
|
||||||
|
color: Colors.deepPurple,
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
lib/pages/list_view.dart
Normal file
15
lib/pages/list_view.dart
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import '../controllers/list_controller.dart';
|
||||||
|
|
||||||
|
class ListPage extends GetView<ListController> {
|
||||||
|
static const String namedRoute = '/list-page';
|
||||||
|
const ListPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var listCtrl = controller;
|
||||||
|
return const Placeholder();
|
||||||
|
}
|
||||||
|
}
|
||||||
78
lib/pages/signin_view.dart
Normal file
78
lib/pages/signin_view.dart
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../controllers/signin_controller.dart';
|
||||||
|
import '../widgets/custom_text_field.dart';
|
||||||
|
import '../widgets/primary_button.dart';
|
||||||
|
|
||||||
|
class SignInPage extends GetView<SignInController> {
|
||||||
|
static const String namedRoute = '/signin-page';
|
||||||
|
const SignInPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var signInCtrl = controller;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Neuen Benutzer anlegen'),
|
||||||
|
centerTitle: true,
|
||||||
|
backgroundColor: Colors.deepPurple.shade700,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
body: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [Colors.blue.shade50, Colors.purple.shade50],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
Icon(
|
||||||
|
Icons.person_add_rounded,
|
||||||
|
size: 72,
|
||||||
|
color: Colors.deepPurple.shade400,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
'Konto erstellen',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.deepPurple.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
CustomTextField(
|
||||||
|
controller: signInCtrl.usernameController,
|
||||||
|
label: 'Benutzername',
|
||||||
|
hint: 'Benutzername eingeben',
|
||||||
|
icon: Icons.person_rounded,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
CustomTextField(
|
||||||
|
controller: signInCtrl.passwordController,
|
||||||
|
label: 'Passwort',
|
||||||
|
hint: 'Passwort eingeben',
|
||||||
|
icon: Icons.lock_rounded,
|
||||||
|
isPassword: true,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
buildPrimaryButton(
|
||||||
|
context: context,
|
||||||
|
icon: Icons.person_add_rounded,
|
||||||
|
label: 'Registrieren',
|
||||||
|
onPressed: () => signInCtrl.registerUser(),
|
||||||
|
color: Colors.deepPurple,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
lib/widgets/action_button.dart
Normal file
46
lib/widgets/action_button.dart
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ActionButton extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
final Color color;
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
final bool isOutlined;
|
||||||
|
|
||||||
|
const ActionButton({
|
||||||
|
super.key,
|
||||||
|
required this.icon,
|
||||||
|
required this.label,
|
||||||
|
required this.color,
|
||||||
|
required this.onPressed,
|
||||||
|
this.isOutlined = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ElevatedButton.icon(
|
||||||
|
onPressed: onPressed,
|
||||||
|
icon: Icon(icon, size: 20),
|
||||||
|
label: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: isOutlined ? Colors.white : color,
|
||||||
|
foregroundColor: isOutlined ? color : Colors.white,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
side: isOutlined
|
||||||
|
? BorderSide(color: color, width: 2)
|
||||||
|
: BorderSide.none,
|
||||||
|
),
|
||||||
|
elevation: isOutlined ? 0 : 3,
|
||||||
|
shadowColor: color.withAlpha(140),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
104
lib/widgets/color_selector.dart
Normal file
104
lib/widgets/color_selector.dart
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ColorSelector extends StatelessWidget {
|
||||||
|
final String selectedColor;
|
||||||
|
final List<String> colors;
|
||||||
|
final void Function(String) onColorSelected;
|
||||||
|
|
||||||
|
const ColorSelector({
|
||||||
|
super.key,
|
||||||
|
required this.selectedColor,
|
||||||
|
required this.colors,
|
||||||
|
required this.onColorSelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
Color _getColorFromString(String colorName) {
|
||||||
|
final colorMap = {
|
||||||
|
'red': Colors.red,
|
||||||
|
'blue': Colors.blue,
|
||||||
|
'green': Colors.green,
|
||||||
|
'yellow': Colors.yellow,
|
||||||
|
'black': Colors.black,
|
||||||
|
'white': Colors.white,
|
||||||
|
'orange': Colors.orange,
|
||||||
|
'purple': Colors.purple,
|
||||||
|
'pink': Colors.pink,
|
||||||
|
'grey': Colors.grey,
|
||||||
|
'brown': Colors.brown,
|
||||||
|
'wood' : Colors.brown[300]!,
|
||||||
|
};
|
||||||
|
return colorMap[colorName.toLowerCase()] ?? Colors.grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Farbe',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
Wrap(
|
||||||
|
spacing: 12,
|
||||||
|
runSpacing: 12,
|
||||||
|
children: colors.map((colorName) {
|
||||||
|
final isSelected = selectedColor.toLowerCase() == colorName.toLowerCase();
|
||||||
|
final color = _getColorFromString(colorName);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => onColorSelected(colorName),
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: Duration(milliseconds: 200),
|
||||||
|
width: isSelected ? 60 : 50,
|
||||||
|
height: isSelected ? 60 : 50,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: isSelected ? Colors.blue : Colors.grey.shade300,
|
||||||
|
width: isSelected ? 3 : 2,
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
if (isSelected)
|
||||||
|
BoxShadow(
|
||||||
|
color: color.withAlpha(102),
|
||||||
|
blurRadius: 12,
|
||||||
|
spreadRadius: 2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: isSelected
|
||||||
|
? Icon(
|
||||||
|
Icons.check,
|
||||||
|
color: _isLightColor(color) ? Colors.black : Colors.white,
|
||||||
|
size: 28,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Ausgewählt: $selectedColor',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isLightColor(Color color) {
|
||||||
|
final luminance = color.computeLuminance();
|
||||||
|
return luminance > 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
79
lib/widgets/custom_dropdown.dart
Normal file
79
lib/widgets/custom_dropdown.dart
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class CustomDropdown extends StatelessWidget {
|
||||||
|
final String value;
|
||||||
|
final String label;
|
||||||
|
final IconData? icon;
|
||||||
|
final List<String> items;
|
||||||
|
final void Function(String?) onChanged;
|
||||||
|
final String? Function(String?)? validator;
|
||||||
|
|
||||||
|
const CustomDropdown({
|
||||||
|
super.key,
|
||||||
|
required this.value,
|
||||||
|
required this.label,
|
||||||
|
required this.items,
|
||||||
|
required this.onChanged,
|
||||||
|
this.icon,
|
||||||
|
this.validator,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
initialValue: items.contains(value) ? value : null,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
prefixIcon: icon != null
|
||||||
|
? Icon(icon, color: Colors.blue.shade400, size: 22)
|
||||||
|
: null,
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.white,
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.blue, width: 2),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.red.shade300),
|
||||||
|
),
|
||||||
|
focusedErrorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.red, width: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
items: items.map((String item) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: item,
|
||||||
|
child: Text(item),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: onChanged,
|
||||||
|
validator: validator,
|
||||||
|
dropdownColor: Colors.white,
|
||||||
|
icon: Icon(Icons.arrow_drop_down, color: Colors.blue.shade400),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
90
lib/widgets/custom_text_field.dart
Normal file
90
lib/widgets/custom_text_field.dart
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class CustomTextField extends StatelessWidget {
|
||||||
|
final TextEditingController controller;
|
||||||
|
final String label;
|
||||||
|
final String? hint;
|
||||||
|
final IconData? icon;
|
||||||
|
final TextInputType keyboardType;
|
||||||
|
final String? Function(String?)? validator;
|
||||||
|
final int maxLines;
|
||||||
|
final bool readOnly;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
final Widget? suffix;
|
||||||
|
final bool? isPassword;
|
||||||
|
|
||||||
|
const CustomTextField({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
required this.label,
|
||||||
|
this.hint,
|
||||||
|
this.icon,
|
||||||
|
this.keyboardType = TextInputType.text,
|
||||||
|
this.validator,
|
||||||
|
this.maxLines = 1,
|
||||||
|
this.readOnly = false,
|
||||||
|
this.onTap,
|
||||||
|
this.suffix,
|
||||||
|
this.isPassword
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
TextFormField(
|
||||||
|
obscureText: isPassword ?? false,
|
||||||
|
controller: controller,
|
||||||
|
keyboardType: keyboardType,
|
||||||
|
validator: validator,
|
||||||
|
maxLines: maxLines,
|
||||||
|
readOnly: readOnly,
|
||||||
|
onTap: onTap,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: hint,
|
||||||
|
prefixIcon: icon != null
|
||||||
|
? Icon(icon, color: Colors.blue.shade400, size: 22)
|
||||||
|
: null,
|
||||||
|
suffixIcon: suffix,
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.white,
|
||||||
|
contentPadding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: maxLines > 1 ? 16 : 14,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.blue, width: 2),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.red.shade300),
|
||||||
|
),
|
||||||
|
focusedErrorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.red, width: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
104
lib/widgets/detail_header.dart
Normal file
104
lib/widgets/detail_header.dart
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class DetailHeader extends StatelessWidget {
|
||||||
|
final String name;
|
||||||
|
final String type;
|
||||||
|
final String color;
|
||||||
|
|
||||||
|
const DetailHeader({
|
||||||
|
super.key,
|
||||||
|
required this.name,
|
||||||
|
required this.type,
|
||||||
|
required this.color,
|
||||||
|
});
|
||||||
|
|
||||||
|
Color _getColorFromString(String colorName) {
|
||||||
|
final colorMap = {
|
||||||
|
'red': Colors.red,
|
||||||
|
'blue': Colors.blue,
|
||||||
|
'green': Colors.green,
|
||||||
|
'yellow': Colors.yellow,
|
||||||
|
'black': Colors.black,
|
||||||
|
'white': Colors.white,
|
||||||
|
'orange': Colors.orange,
|
||||||
|
'purple': Colors.purple,
|
||||||
|
'pink': Colors.pink,
|
||||||
|
'grey': Colors.grey,
|
||||||
|
'brown': Colors.brown,
|
||||||
|
};
|
||||||
|
return colorMap[colorName.toLowerCase()] ?? Colors.grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
_getColorFromString(color).withAlpha(100),
|
||||||
|
_getColorFromString(color).withAlpha(50),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(32),
|
||||||
|
bottomRight: Radius.circular(32),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Color circle
|
||||||
|
Container(
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _getColorFromString(color),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.white,
|
||||||
|
width: 4,
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: _getColorFromString(color).withAlpha(120),
|
||||||
|
blurRadius: 20,
|
||||||
|
spreadRadius: 5,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
// Name
|
||||||
|
Text(
|
||||||
|
name,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey.shade800,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
// Type badge
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _getColorFromString(color).withAlpha(120),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
type,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: _getColorFromString(color).withAlpha(180),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
83
lib/widgets/detail_info_card.dart
Normal file
83
lib/widgets/detail_info_card.dart
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class DetailInfoCard extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
final String value;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
const DetailInfoCard({
|
||||||
|
super.key,
|
||||||
|
required this.icon,
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
this.color = Colors.blue,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withAlpha(150),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withAlpha(10),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: color, size: 20),
|
||||||
|
),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey.shade800,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
88
lib/widgets/empty_state.dart
Normal file
88
lib/widgets/empty_state.dart
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class EmptyState extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final String message;
|
||||||
|
final IconData icon;
|
||||||
|
final VoidCallback? onActionPressed;
|
||||||
|
final String? actionLabel;
|
||||||
|
|
||||||
|
const EmptyState({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.message,
|
||||||
|
this.icon = Icons.inbox_outlined,
|
||||||
|
this.onActionPressed,
|
||||||
|
this.actionLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(32),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(32),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [Colors.blue.shade50, Colors.purple.shade50],
|
||||||
|
),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withAlpha(15),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: Offset(0, 10),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Icon(icon, size: 80, color: Colors.deepPurple.shade300),
|
||||||
|
),
|
||||||
|
SizedBox(height: 32),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.deepPurple.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
message,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (onActionPressed != null && actionLabel != null) ...[
|
||||||
|
SizedBox(height: 32),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: onActionPressed,
|
||||||
|
icon: Icon(Icons.add),
|
||||||
|
label: Text(actionLabel!),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.deepPurple.shade600,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
elevation: 4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
351
lib/widgets/filament_card.dart
Normal file
351
lib/widgets/filament_card.dart
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../models/filament_model.dart';
|
||||||
|
|
||||||
|
class FilamentCard extends StatelessWidget {
|
||||||
|
final FilamentModel filament;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
final VoidCallback? onEdit;
|
||||||
|
final VoidCallback? onDelete;
|
||||||
|
|
||||||
|
const FilamentCard({
|
||||||
|
super.key,
|
||||||
|
required this.filament,
|
||||||
|
this.onTap,
|
||||||
|
this.onEdit,
|
||||||
|
this.onDelete,
|
||||||
|
});
|
||||||
|
|
||||||
|
Color _getColorFromString(String colorName) {
|
||||||
|
final colorMap = {
|
||||||
|
'red': Colors.red,
|
||||||
|
'blue': Colors.blue,
|
||||||
|
'green': Colors.green,
|
||||||
|
'yellow': Colors.yellow,
|
||||||
|
'black': Colors.black,
|
||||||
|
'white': Colors.white,
|
||||||
|
'orange': Colors.orange,
|
||||||
|
'purple': Colors.purple,
|
||||||
|
'pink': Colors.pink,
|
||||||
|
'grey': Colors.grey,
|
||||||
|
'brown': Colors.brown,
|
||||||
|
};
|
||||||
|
|
||||||
|
return colorMap[colorName.toLowerCase()] ?? Colors.grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [Colors.white, Colors.blue.shade50],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withAlpha(15),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Header with color indicator and name
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
// Color indicator
|
||||||
|
Container(
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _getColorFromString(filament.color),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: _getColorFromString(
|
||||||
|
filament.color,
|
||||||
|
).withAlpha(100),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
// Name and Type
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
filament.name,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.deepPurple.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 4),
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.deepPurple.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
filament.type,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.deepPurple.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Action buttons
|
||||||
|
if (onEdit != null)
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.edit_outlined),
|
||||||
|
color: Colors.blue.shade600,
|
||||||
|
onPressed: onEdit,
|
||||||
|
),
|
||||||
|
if (onDelete != null)
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.delete_outline),
|
||||||
|
color: Colors.red.shade400,
|
||||||
|
onPressed: onDelete,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Details Grid
|
||||||
|
Wrap(
|
||||||
|
spacing: 12,
|
||||||
|
runSpacing: 12,
|
||||||
|
children: [
|
||||||
|
if (filament.manufacturer != null)
|
||||||
|
_buildDetailChip(
|
||||||
|
icon: Icons.business,
|
||||||
|
label: filament.manufacturer!,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
_buildDetailChip(
|
||||||
|
icon: Icons.scale,
|
||||||
|
label: '${filament.weight - filament.weightUsed}g',
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
_buildDetailChip(
|
||||||
|
icon: Icons.euro,
|
||||||
|
label: filament.price.toStringAsFixed(2),
|
||||||
|
color: Colors.orange,
|
||||||
|
),
|
||||||
|
if (filament.pices > 0)
|
||||||
|
_buildDetailChip(
|
||||||
|
icon: Icons.inventory_2,
|
||||||
|
label: '${filament.pices} Stk.',
|
||||||
|
color: Colors.purple,
|
||||||
|
),
|
||||||
|
_buildDetailChip(
|
||||||
|
icon: Icons.scale,
|
||||||
|
label: '${filament.weightUsed}g',
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Temperature info
|
||||||
|
if (filament.printingTemp != 0 || filament.bedTemp != 0)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(top: 12),
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.orange.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.orange.shade200,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
if (filament.printingTemp != 0)
|
||||||
|
_buildTempInfo(
|
||||||
|
icon: Icons.print,
|
||||||
|
label: 'Druck',
|
||||||
|
temp: '${filament.printingTemp}°C',
|
||||||
|
),
|
||||||
|
if (filament.printingTemp != 0 &&
|
||||||
|
filament.bedTemp != 0)
|
||||||
|
Container(
|
||||||
|
width: 1,
|
||||||
|
height: 30,
|
||||||
|
color: Colors.orange.shade200,
|
||||||
|
),
|
||||||
|
if (filament.bedTemp != 0)
|
||||||
|
_buildTempInfo(
|
||||||
|
icon: Icons.bed,
|
||||||
|
label: 'Bett',
|
||||||
|
temp: '${filament.bedTemp}°C',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Notes
|
||||||
|
if (filament.notes != null && filament.notes!.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(top: 12),
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.amber.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.amber.shade200,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.note_alt_outlined,
|
||||||
|
size: 18,
|
||||||
|
color: Colors.amber.shade700,
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
filament.notes!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Purchase date
|
||||||
|
if (filament.purchaseDate != null)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(top: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.calendar_today,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.grey.shade500,
|
||||||
|
),
|
||||||
|
SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
'Gekauft: ${filament.purchaseDate}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDetailChip({
|
||||||
|
required IconData icon,
|
||||||
|
required String label,
|
||||||
|
required MaterialColor color,
|
||||||
|
}) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: color.shade200, width: 1),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 16, color: color.shade700),
|
||||||
|
SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: color.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTempInfo({
|
||||||
|
required IconData icon,
|
||||||
|
required String label,
|
||||||
|
required String temp,
|
||||||
|
}) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 18, color: Colors.orange.shade700),
|
||||||
|
SizedBox(width: 6),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
temp,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.orange.shade800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
lib/widgets/info_item.dart
Normal file
28
lib/widgets/info_item.dart
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
Widget buildInfoItem({
|
||||||
|
required IconData icon,
|
||||||
|
required String text,
|
||||||
|
required Color color,
|
||||||
|
}) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withAlpha(25),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: color, size: 20),
|
||||||
|
),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.grey.shade700),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
53
lib/widgets/modern_loading_indicator.dart
Normal file
53
lib/widgets/modern_loading_indicator.dart
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ModernLoadingIndicator extends StatelessWidget {
|
||||||
|
final String? message;
|
||||||
|
|
||||||
|
const ModernLoadingIndicator({super.key, this.message});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [Colors.blue.shade50, Colors.purple.shade50],
|
||||||
|
),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withAlpha(15),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: Offset(0, 10),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 3,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
Colors.deepPurple.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (message != null) ...[
|
||||||
|
SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
message!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
lib/widgets/primary_button.dart
Normal file
32
lib/widgets/primary_button.dart
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
Widget buildPrimaryButton({
|
||||||
|
required BuildContext context,
|
||||||
|
required IconData icon,
|
||||||
|
required String label,
|
||||||
|
required VoidCallback onPressed,
|
||||||
|
required Color color,
|
||||||
|
}) {
|
||||||
|
return ElevatedButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: color,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
elevation: 4,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(icon),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
102
lib/widgets/progress_ring.dart
Normal file
102
lib/widgets/progress_ring.dart
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ProgressRing extends StatelessWidget {
|
||||||
|
final double progress; // 0.0 to 1.0
|
||||||
|
final double size;
|
||||||
|
final double strokeWidth;
|
||||||
|
final Color backgroundColor;
|
||||||
|
final Color progressColor;
|
||||||
|
final Widget? child;
|
||||||
|
|
||||||
|
const ProgressRing({
|
||||||
|
super.key,
|
||||||
|
required this.progress,
|
||||||
|
this.size = 120,
|
||||||
|
this.strokeWidth = 12,
|
||||||
|
this.backgroundColor = Colors.grey,
|
||||||
|
this.progressColor = Colors.blue,
|
||||||
|
this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: _ProgressRingPainter(
|
||||||
|
progress: progress,
|
||||||
|
strokeWidth: strokeWidth,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
progressColor: progressColor,
|
||||||
|
),
|
||||||
|
child: child != null
|
||||||
|
? Center(child: child)
|
||||||
|
: Center(
|
||||||
|
child: Text(
|
||||||
|
'${(progress * 100).toStringAsFixed(0)}%',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: size / 5,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey.shade800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProgressRingPainter extends CustomPainter {
|
||||||
|
final double progress;
|
||||||
|
final double strokeWidth;
|
||||||
|
final Color backgroundColor;
|
||||||
|
final Color progressColor;
|
||||||
|
|
||||||
|
_ProgressRingPainter({
|
||||||
|
required this.progress,
|
||||||
|
required this.strokeWidth,
|
||||||
|
required this.backgroundColor,
|
||||||
|
required this.progressColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final center = Offset(size.width / 2, size.height / 2);
|
||||||
|
final radius = (size.width - strokeWidth) / 2;
|
||||||
|
|
||||||
|
// Background circle
|
||||||
|
final backgroundPaint = Paint()
|
||||||
|
..color = backgroundColor.withAlpha(120)
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = strokeWidth
|
||||||
|
..strokeCap = StrokeCap.round;
|
||||||
|
|
||||||
|
canvas.drawCircle(center, radius, backgroundPaint);
|
||||||
|
|
||||||
|
// Progress arc
|
||||||
|
final progressPaint = Paint()
|
||||||
|
..shader = LinearGradient(
|
||||||
|
colors: [
|
||||||
|
progressColor,
|
||||||
|
progressColor.withAlpha(170),
|
||||||
|
],
|
||||||
|
).createShader(Rect.fromCircle(center: center, radius: radius))
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = strokeWidth
|
||||||
|
..strokeCap = StrokeCap.round;
|
||||||
|
|
||||||
|
final sweepAngle = 2 * pi * progress;
|
||||||
|
canvas.drawArc(
|
||||||
|
Rect.fromCircle(center: center, radius: radius),
|
||||||
|
-pi / 2, // Start from top
|
||||||
|
sweepAngle,
|
||||||
|
false,
|
||||||
|
progressPaint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||||
|
}
|
||||||
34
lib/widgets/secondary_button.dart
Normal file
34
lib/widgets/secondary_button.dart
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
Widget buildSecondaryButton({
|
||||||
|
required BuildContext context,
|
||||||
|
required IconData icon,
|
||||||
|
required String label,
|
||||||
|
required VoidCallback onPressed,
|
||||||
|
required Color color,
|
||||||
|
}) {
|
||||||
|
return ElevatedButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: color,
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
side: BorderSide(color: color.withAlpha(30)),
|
||||||
|
),
|
||||||
|
elevation: 2,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 28),
|
||||||
|
SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
56
lib/widgets/section_header.dart
Normal file
56
lib/widgets/section_header.dart
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class SectionHeader extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final IconData icon;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
const SectionHeader({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.icon,
|
||||||
|
this.color = Colors.blue,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withAlpha(26),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: color, size: 20),
|
||||||
|
),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey.shade800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
margin: EdgeInsets.only(left: 12),
|
||||||
|
height: 2,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
color.withAlpha(77),
|
||||||
|
Colors.transparent,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
pubspec.lock
24
pubspec.lock
@@ -75,6 +75,22 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
get:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: get
|
||||||
|
sha256: "5ed34a7925b85336e15d472cc4cfe7d9ebf4ab8e8b9f688585bf6b50f4c3d79a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.7.3"
|
||||||
|
intl:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: intl
|
||||||
|
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.20.2"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -208,6 +224,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.0.2"
|
version: "15.0.2"
|
||||||
|
web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web
|
||||||
|
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.11.4 <4.0.0"
|
dart: ">=3.11.4 <4.0.0"
|
||||||
flutter: ">=3.18.0-18.0.pre.54"
|
flutter: ">=3.18.0-18.0.pre.54"
|
||||||
|
|||||||
78
pubspec.yaml
78
pubspec.yaml
@@ -1,89 +1,27 @@
|
|||||||
name: flutter_mssql_node_filament_app
|
name: flutter_mssql_node_filament_app
|
||||||
description: "A new Flutter project."
|
description: "A new Flutter project."
|
||||||
# The following line prevents the package from being accidentally published to
|
|
||||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
|
||||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|
||||||
|
|
||||||
# The following defines the version and build number for your application.
|
publish_to: "none"
|
||||||
# A version number is three numbers separated by dots, like 1.2.43
|
|
||||||
# followed by an optional build number separated by a +.
|
|
||||||
# Both the version and the builder number may be overridden in flutter
|
|
||||||
# build by specifying --build-name and --build-number, respectively.
|
|
||||||
# In Android, build-name is used as versionName while build-number used as versionCode.
|
|
||||||
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
|
||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
|
||||||
# Read more about iOS versioning at
|
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
|
||||||
version: 1.0.0+1
|
version: 1.0.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.4
|
sdk: ^3.11.4
|
||||||
|
|
||||||
# Dependencies specify other packages that your package needs in order to work.
|
|
||||||
# To automatically upgrade your package dependencies to the latest versions
|
|
||||||
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
|
||||||
# dependencies can be manually updated by changing the version numbers below to
|
|
||||||
# the latest version available on pub.dev. To see which dependencies have newer
|
|
||||||
# versions available, run `flutter pub outdated`.
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
cupertino_icons: ^1.0.8
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
get: ^4.7.3
|
||||||
# The following adds the Cupertino Icons font to your application.
|
intl: ^0.20.2
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
|
||||||
cupertino_icons: ^1.0.8
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
flutter_lints: ^6.0.0
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
# The "flutter_lints" package below contains a set of recommended lints to
|
|
||||||
# encourage good coding practices. The lint set provided by the package is
|
|
||||||
# activated in the `analysis_options.yaml` file located at the root of your
|
|
||||||
# package. See that file for information about deactivating specific lint
|
|
||||||
# rules and activating additional ones.
|
|
||||||
flutter_lints: ^6.0.0
|
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
|
||||||
|
|
||||||
# The following section is specific to Flutter packages.
|
|
||||||
flutter:
|
flutter:
|
||||||
|
|
||||||
# The following line ensures that the Material Icons font is
|
|
||||||
# included with your application, so that you can use the icons in
|
|
||||||
# the material Icons class.
|
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
assets:
|
||||||
# assets:
|
- assets/images/fil01.jpg
|
||||||
# - images/a_dot_burr.jpeg
|
|
||||||
# - images/a_dot_ham.jpeg
|
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
|
||||||
# https://flutter.dev/to/resolution-aware-images
|
|
||||||
|
|
||||||
# For details regarding adding assets from package dependencies, see
|
|
||||||
# https://flutter.dev/to/asset-from-package
|
|
||||||
|
|
||||||
# To add custom fonts to your application, add a fonts section here,
|
|
||||||
# in this "flutter" section. Each entry in this list should have a
|
|
||||||
# "family" key with the font family name, and a "fonts" key with a
|
|
||||||
# list giving the asset and other descriptors for the font. For
|
|
||||||
# example:
|
|
||||||
# fonts:
|
|
||||||
# - family: Schyler
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/Schyler-Regular.ttf
|
|
||||||
# - asset: fonts/Schyler-Italic.ttf
|
|
||||||
# style: italic
|
|
||||||
# - family: Trajan Pro
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/TrajanPro.ttf
|
|
||||||
# - asset: fonts/TrajanPro_Bold.ttf
|
|
||||||
# weight: 700
|
|
||||||
#
|
|
||||||
# For details regarding fonts from package dependencies,
|
|
||||||
# see https://flutter.dev/to/font-from-package
|
|
||||||
|
|||||||
Reference in New Issue
Block a user