From 122ae48754e3f2984c3f0a250847de1b4a7d1a21 Mon Sep 17 00:00:00 2001 From: josiadmin Date: Wed, 1 Apr 2026 14:28:38 +0200 Subject: [PATCH] Add view and logik --- analysis_options.yaml | 3 + assets/images/fil01.jpg | Bin 0 -> 13586 bytes lib/controllers/home_controller.dart | 58 ++++ lib/controllers/list_controller.dart | 18 ++ lib/controllers/signin_controller.dart | 39 +++ lib/helpers/sampl_routes.dart | 26 ++ lib/helpers/sample_bindings.dart | 17 ++ lib/helpers/services_repos.dart | 32 ++ lib/main.dart | 116 +------ lib/models/filament_model.dart | 157 ++++++++++ lib/models/user_model.dart | 48 +++ lib/pages/home_view.dart | 143 +++++++++ lib/pages/list_view.dart | 15 + lib/pages/signin_view.dart | 78 +++++ lib/widgets/action_button.dart | 46 +++ lib/widgets/color_selector.dart | 104 +++++++ lib/widgets/custom_dropdown.dart | 79 +++++ lib/widgets/custom_text_field.dart | 90 ++++++ lib/widgets/detail_header.dart | 104 +++++++ lib/widgets/detail_info_card.dart | 83 +++++ lib/widgets/empty_state.dart | 88 ++++++ lib/widgets/filament_card.dart | 351 ++++++++++++++++++++++ lib/widgets/info_item.dart | 28 ++ lib/widgets/modern_loading_indicator.dart | 53 ++++ lib/widgets/primary_button.dart | 32 ++ lib/widgets/progress_ring.dart | 102 +++++++ lib/widgets/secondary_button.dart | 34 +++ lib/widgets/section_header.dart | 56 ++++ pubspec.lock | 24 ++ pubspec.yaml | 78 +---- 30 files changed, 1926 insertions(+), 176 deletions(-) create mode 100644 assets/images/fil01.jpg create mode 100644 lib/controllers/home_controller.dart create mode 100644 lib/controllers/list_controller.dart create mode 100644 lib/controllers/signin_controller.dart create mode 100644 lib/helpers/sampl_routes.dart create mode 100644 lib/helpers/sample_bindings.dart create mode 100644 lib/helpers/services_repos.dart create mode 100644 lib/models/filament_model.dart create mode 100644 lib/models/user_model.dart create mode 100644 lib/pages/home_view.dart create mode 100644 lib/pages/list_view.dart create mode 100644 lib/pages/signin_view.dart create mode 100644 lib/widgets/action_button.dart create mode 100644 lib/widgets/color_selector.dart create mode 100644 lib/widgets/custom_dropdown.dart create mode 100644 lib/widgets/custom_text_field.dart create mode 100644 lib/widgets/detail_header.dart create mode 100644 lib/widgets/detail_info_card.dart create mode 100644 lib/widgets/empty_state.dart create mode 100644 lib/widgets/filament_card.dart create mode 100644 lib/widgets/info_item.dart create mode 100644 lib/widgets/modern_loading_indicator.dart create mode 100644 lib/widgets/primary_button.dart create mode 100644 lib/widgets/progress_ring.dart create mode 100644 lib/widgets/secondary_button.dart create mode 100644 lib/widgets/section_header.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index 0d29021..ace0476 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -7,6 +7,9 @@ # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. +analyzer: + errors: + avoid_print: ignore include: package:flutter_lints/flutter.yaml linter: diff --git a/assets/images/fil01.jpg b/assets/images/fil01.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9db1d980de1513717c810d62ddb0311acdfe3517 GIT binary patch literal 13586 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 literal 0 HcmV?d00001 diff --git a/lib/controllers/home_controller.dart b/lib/controllers/home_controller.dart new file mode 100644 index 0000000..5a68063 --- /dev/null +++ b/lib/controllers/home_controller.dart @@ -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 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, + ); + } + } +} diff --git a/lib/controllers/list_controller.dart b/lib/controllers/list_controller.dart new file mode 100644 index 0000000..4768cad --- /dev/null +++ b/lib/controllers/list_controller.dart @@ -0,0 +1,18 @@ + +import 'package:get/get.dart'; + +class ListController extends GetxController { + + @override + void onInit() { + super.onInit(); + } + + @override + void onClose() { + + super.onClose(); + } + + +} diff --git a/lib/controllers/signin_controller.dart b/lib/controllers/signin_controller.dart new file mode 100644 index 0000000..f5814ad --- /dev/null +++ b/lib/controllers/signin_controller.dart @@ -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 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, + ); + } +} diff --git a/lib/helpers/sampl_routes.dart b/lib/helpers/sampl_routes.dart new file mode 100644 index 0000000..54d61e3 --- /dev/null +++ b/lib/helpers/sampl_routes.dart @@ -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> 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, + ), + ]; +} diff --git a/lib/helpers/sample_bindings.dart b/lib/helpers/sample_bindings.dart new file mode 100644 index 0000000..1a4f185 --- /dev/null +++ b/lib/helpers/sample_bindings.dart @@ -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()); + Get.lazyPut(() => SignInController()); + Get.lazyPut(() => ListController()); + } + +} \ No newline at end of file diff --git a/lib/helpers/services_repos.dart b/lib/helpers/services_repos.dart new file mode 100644 index 0000000..1a333bb --- /dev/null +++ b/lib/helpers/services_repos.dart @@ -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 getUsersList() => get('$baseUrl/GetFilamentUsers', headers: {'Content-Type': 'application/json'}); + + Future 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 getFilamentsList(String username) => + post('$baseUrl/GetFilamentDataByUser', {'username': username}); +} diff --git a/lib/main.dart b/lib/main.dart index 244a702..cab8f43 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../helpers/sampl_routes.dart'; + +import 'pages/home_view.dart'; void main() { runApp(const MyApp()); @@ -7,116 +11,16 @@ void main() { class MyApp extends StatelessWidget { const MyApp({super.key}); - // This widget is the root of your application. @override Widget build(BuildContext context) { - return MaterialApp( + return GetMaterialApp( 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'), + theme: ThemeData(colorScheme: .fromSeed(seedColor: Colors.deepPurple)), + initialBinding: SampleRouts.sampleBindings, + getPages: SampleRouts.samplePages, + debugShowCheckedModeBanner: false, + initialRoute: HomePage.namedRoute, ); } } -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 createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - 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), - ), - ); - } -} diff --git a/lib/models/filament_model.dart b/lib/models/filament_model.dart new file mode 100644 index 0000000..710e066 --- /dev/null +++ b/lib/models/filament_model.dart @@ -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 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 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 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, + ), + ]; +} diff --git a/lib/models/user_model.dart b/lib/models/user_model.dart new file mode 100644 index 0000000..442b4d9 --- /dev/null +++ b/lib/models/user_model.dart @@ -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 toMap() { + return { + 'pk_userUuid': uuid, + 'benutzer': username, + 'kennwort': passwort, + }; + } + + factory UserModel.fromMap(Map 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); + + @override + String toString() => 'UserModel(uuid: $uuid, username: $username, passwort: $passwort)'; +} diff --git a/lib/pages/home_view.dart b/lib/pages/home_view.dart new file mode 100644 index 0000000..c3e2785 --- /dev/null +++ b/lib/pages/home_view.dart @@ -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 { + 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), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/list_view.dart b/lib/pages/list_view.dart new file mode 100644 index 0000000..a674610 --- /dev/null +++ b/lib/pages/list_view.dart @@ -0,0 +1,15 @@ +import 'package:flutter/widgets.dart'; +import 'package:get/get.dart'; + +import '../controllers/list_controller.dart'; + +class ListPage extends GetView { + static const String namedRoute = '/list-page'; + const ListPage({super.key}); + + @override + Widget build(BuildContext context) { + var listCtrl = controller; + return const Placeholder(); + } +} \ No newline at end of file diff --git a/lib/pages/signin_view.dart b/lib/pages/signin_view.dart new file mode 100644 index 0000000..b151da3 --- /dev/null +++ b/lib/pages/signin_view.dart @@ -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 { + 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, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/action_button.dart b/lib/widgets/action_button.dart new file mode 100644 index 0000000..d968ba5 --- /dev/null +++ b/lib/widgets/action_button.dart @@ -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), + ), + ); + } +} diff --git a/lib/widgets/color_selector.dart b/lib/widgets/color_selector.dart new file mode 100644 index 0000000..2c57269 --- /dev/null +++ b/lib/widgets/color_selector.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; + +class ColorSelector extends StatelessWidget { + final String selectedColor; + final List 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; + } +} diff --git a/lib/widgets/custom_dropdown.dart b/lib/widgets/custom_dropdown.dart new file mode 100644 index 0000000..d84a684 --- /dev/null +++ b/lib/widgets/custom_dropdown.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; + +class CustomDropdown extends StatelessWidget { + final String value; + final String label; + final IconData? icon; + final List 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( + 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( + value: item, + child: Text(item), + ); + }).toList(), + onChanged: onChanged, + validator: validator, + dropdownColor: Colors.white, + icon: Icon(Icons.arrow_drop_down, color: Colors.blue.shade400), + ), + ], + ); + } +} diff --git a/lib/widgets/custom_text_field.dart b/lib/widgets/custom_text_field.dart new file mode 100644 index 0000000..03523f9 --- /dev/null +++ b/lib/widgets/custom_text_field.dart @@ -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), + ), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/detail_header.dart b/lib/widgets/detail_header.dart new file mode 100644 index 0000000..eed31e8 --- /dev/null +++ b/lib/widgets/detail_header.dart @@ -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), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/detail_info_card.dart b/lib/widgets/detail_info_card.dart new file mode 100644 index 0000000..9bb1007 --- /dev/null +++ b/lib/widgets/detail_info_card.dart @@ -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, + ), + ], + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/widgets/empty_state.dart b/lib/widgets/empty_state.dart new file mode 100644 index 0000000..92f9a56 --- /dev/null +++ b/lib/widgets/empty_state.dart @@ -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, + ), + ), + ], + ], + ), + ), + ); + } +} diff --git a/lib/widgets/filament_card.dart b/lib/widgets/filament_card.dart new file mode 100644 index 0000000..bbdbddd --- /dev/null +++ b/lib/widgets/filament_card.dart @@ -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, + ), + ), + ], + ), + ], + ); + } +} diff --git a/lib/widgets/info_item.dart b/lib/widgets/info_item.dart new file mode 100644 index 0000000..b66faf3 --- /dev/null +++ b/lib/widgets/info_item.dart @@ -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), + ), + ), + ], + ); + } \ No newline at end of file diff --git a/lib/widgets/modern_loading_indicator.dart b/lib/widgets/modern_loading_indicator.dart new file mode 100644 index 0000000..eff8947 --- /dev/null +++ b/lib/widgets/modern_loading_indicator.dart @@ -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( + Colors.deepPurple.shade600, + ), + ), + ), + if (message != null) ...[ + SizedBox(height: 24), + Text( + message!, + style: TextStyle( + fontSize: 16, + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + ), + ), + ], + ], + ), + ); + } +} diff --git a/lib/widgets/primary_button.dart b/lib/widgets/primary_button.dart new file mode 100644 index 0000000..1608471 --- /dev/null +++ b/lib/widgets/primary_button.dart @@ -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), + ), + ], + ), + ); + } \ No newline at end of file diff --git a/lib/widgets/progress_ring.dart b/lib/widgets/progress_ring.dart new file mode 100644 index 0000000..632b4dd --- /dev/null +++ b/lib/widgets/progress_ring.dart @@ -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; +} diff --git a/lib/widgets/secondary_button.dart b/lib/widgets/secondary_button.dart new file mode 100644 index 0000000..59314e3 --- /dev/null +++ b/lib/widgets/secondary_button.dart @@ -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), + ), + ], + ), + ); +} diff --git a/lib/widgets/section_header.dart b/lib/widgets/section_header.dart new file mode 100644 index 0000000..7d1aac5 --- /dev/null +++ b/lib/widgets/section_header.dart @@ -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, + ], + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index ec64d1f..8789be8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -75,6 +75,22 @@ packages: description: flutter source: sdk 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: dependency: transitive description: @@ -208,6 +224,14 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" sdks: dart: ">=3.11.4 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/pubspec.yaml b/pubspec.yaml index 1df10be..2989466 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,89 +1,27 @@ name: flutter_mssql_node_filament_app 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. -# 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. +publish_to: "none" + version: 1.0.0+1 environment: 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: + cupertino_icons: ^1.0.8 flutter: sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.8 + get: ^4.7.3 + intl: ^0.20.2 dev_dependencies: + flutter_lints: ^6.0.0 flutter_test: 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: - - # 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 - # To add assets to your application, add an assets section, like this: - # assets: - # - 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 + assets: + - assets/images/fil01.jpg