commit 63d1be6d9c26fa6a8c9199ed47bc41f5c9ab08e4 Author: josiadmin Date: Tue Feb 24 21:03:54 2026 +0100 Initial commit: Flutter Weight Tracker Web diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..c0fe018 --- /dev/null +++ b/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "90673a4eef275d1a6692c26ac80d6d746d41a73a" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a + base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a + - platform: android + create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a + base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a + - platform: ios + create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a + base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a + - platform: linux + create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a + base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a + - platform: macos + create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a + base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a + - platform: web + create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a + base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a + - platform: windows + create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a + base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/README.md b/README.md new file mode 100644 index 0000000..6907187 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# flutter_weight_tracker_web + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter) +- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/lib/controllers/home_controller.dart b/lib/controllers/home_controller.dart new file mode 100644 index 0000000..59889c8 --- /dev/null +++ b/lib/controllers/home_controller.dart @@ -0,0 +1,85 @@ +import 'package:get/get.dart'; + +import '../models/home_model.dart'; +import '../widgets/add_weight_dialog.dart'; + +class HomeController extends GetxController { + final isloading = false.obs; + final List weights = [].obs; + + @override + void onInit() { + _loadDataList(); + super.onInit(); + } + + @override + void onReady() {} + + @override + void onClose() {} + + void _loadDataList() { + isloading.value = true; + if (weights.isNotEmpty) { + weights.clear(); + } + // Simulate loading data from a database or API + weights.assignAll(WeightModel.sampleData); + isloading.value = false; + update(); + } + + void addWeightEntry(WeightModel entry) { + weights.add(entry); + update(); + } + + void editWeightEntry(WeightModel updated) { + final idx = weights.indexWhere((w) => w.id == updated.id); + if (idx != -1) { + weights[idx] = updated; + update(); + } + } + + /// Gewicht des Eintrags unmittelbar VOR [date] einer Person. + /// Wird für die weightChange-Berechnung beim Bearbeiten genutzt. + double getPreviousWeight(String personName, DateTime date) { + final before = weights + .where((w) => w.name == personName && w.date.isBefore(date)) + .toList(); + if (before.isEmpty) return 0.0; + before.sort((a, b) => b.date.compareTo(a.date)); + return before.first.weight; + } + + /// Gibt das zuletzt eingetragene Gewicht einer Person zurück. + /// 0.0 wenn noch kein Eintrag existiert (wird als "erster Eintrag" behandelt). + double getLastWeight(String personName) { + final personEntries = weights.where((w) => w.name == personName).toList(); + if (personEntries.isEmpty) return 0.0; + personEntries.sort((a, b) => b.date.compareTo(a.date)); + return personEntries.first.weight; + } + + void addWeight(String personName) {} + + // ── Dialog-Logik ───────────────────────────────────────────────────────── + + Future openAddDialog(String personName) async { + final entry = await AddWeightDialog.show( + personName: personName, + lastWeight: getLastWeight(personName), + ); + if (entry != null) addWeightEntry(entry); + } + + Future openEditDialog(WeightModel existing) async { + final updated = await AddWeightDialog.showEdit( + entry: existing, + previousWeight: getPreviousWeight(existing.name, existing.date), + ); + if (updated != null) editWeightEntry(updated); + } +} diff --git a/lib/helpers/sample_bindings.dart b/lib/helpers/sample_bindings.dart new file mode 100644 index 0000000..bfe8cdc --- /dev/null +++ b/lib/helpers/sample_bindings.dart @@ -0,0 +1,18 @@ + +import 'package:get/get.dart'; + +import '../controllers/home_controller.dart'; + + + + + + +class SampleBindings extends Bindings { + @override + void dependencies() { + // Define your dependencies here no permanent Binding + Get.lazyPut(() => HomeController()); + + } +} diff --git a/lib/helpers/samples_routes.dart b/lib/helpers/samples_routes.dart new file mode 100644 index 0000000..04d2ae0 --- /dev/null +++ b/lib/helpers/samples_routes.dart @@ -0,0 +1,18 @@ +import 'package:get/get.dart'; + +import '../pages/home/home_view.dart'; +import 'sample_bindings.dart'; + + +class SampleRouts { + static final sampleBindings = SampleBindings(); + static List> samplePages = [ + + GetPage( + name: HomePage.namedRoute, + page: () => const HomePage(), + binding: sampleBindings, + ), + + ]; +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..923119f --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import 'helpers/samples_routes.dart'; +import 'pages/home/home_view.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return GetMaterialApp( + title: 'Flutter Demo', + debugShowCheckedModeBanner: false, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange), + useMaterial3: true, + ), + initialRoute: HomePage.namedRoute, + getPages: SampleRouts.samplePages, + ); + } +} diff --git a/lib/models/home_model.dart b/lib/models/home_model.dart new file mode 100644 index 0000000..dc3f215 --- /dev/null +++ b/lib/models/home_model.dart @@ -0,0 +1,81 @@ +class WeightModel { + final String name; + final double weight; + final DateTime date; + final String id; + final double weightChange; + + WeightModel({ + required this.weight, + required this.date, + required this.id, + required this.weightChange, required this.name, + }); + + static WeightModel fromJson(Map json) { + return WeightModel( + weight: json['weight'], + date: DateTime.parse(json['date']), + id: json['id'], + weightChange: json['weightChange'], name: json['name'], + ); + } + + static Map toJson(WeightModel weightModel) { + return { + 'weight': weightModel.weight, + 'date': weightModel.date.toIso8601String(), + 'id': weightModel.id, + 'weightChange': weightModel.weightChange, + 'name': weightModel.name, + }; + } + + // An empty instance for initialization or default values + static final empty = WeightModel( + weight: 0.0, + date: DateTime.now(), + id: '00', + weightChange: 0.0, name: 'Joe', + ); + + // Sample data for testing and demonstration purposes + static List sampleData = [ + WeightModel( + weight: 70.0, + date: DateTime(2024, 1, 1), + id: '01', + weightChange: 0.0, name: 'Joe', + ), + WeightModel( + weight: 69.5, + date: DateTime(2024, 1, 15), + id: '02', + weightChange: -0.5, name: 'Joe', + ), + WeightModel( + weight: 68.0, + date: DateTime(2024, 2, 1), + id: '03', + weightChange: -1.5, name: 'Joe', + ), + WeightModel( + weight: 100.0, + date: DateTime(2024, 1, 1), + id: '04', + weightChange: 0.0, name: 'Karl', + ), + WeightModel( + weight: 95.5, + date: DateTime(2024, 1, 15), + id: '05', + weightChange: -4.5, name: 'Karl', + ), + WeightModel( + weight: 90.0, + date: DateTime(2024, 2, 1), + id: '06', + weightChange: -5.5, name: 'Karl', + ), + ]; +} diff --git a/lib/pages/home/home_view.dart b/lib/pages/home/home_view.dart new file mode 100644 index 0000000..b144371 --- /dev/null +++ b/lib/pages/home/home_view.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../controllers/home_controller.dart'; +import '../../models/home_model.dart'; +import '../../widgets/glass_app_bar.dart'; +import '../../widgets/person_weight_card.dart'; + +class HomePage extends GetView { + static const String namedRoute = '/home-page'; + const HomePage({super.key}); + + /// Gruppiert eine flache Liste nach `name` und gibt eine Map zurück, + /// die je Person alle zugehörigen Einträge enthält. + Map> _groupByName(List weights) { + final map = >{}; + for (final w in weights) { + map.putIfAbsent(w.name, () => []).add(w); + } + return map; + } + + @override + Widget build(BuildContext context) { + final homeCtrl = controller; + + return Scaffold( + extendBodyBehindAppBar: true, + appBar: GlassAppBar( + title: 'Weight Tracker', + subtitle: 'Verfolge dein Gewicht', + ), + body: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF0D0F14), + Color(0xFF141824), + Color(0xFF1A2035), + Color(0xFF0F1520), + ], + stops: [0.0, 0.35, 0.65, 1.0], + ), + ), + child: Obx(() { + if (homeCtrl.isloading.value) { + return const Center(child: CircularProgressIndicator()); + } + + if (homeCtrl.weights.isEmpty) { + return const Center( + child: Text( + 'Noch keine Gewichtsangaben.\nKlicke auf das "+" Symbol, um deinen ersten Eintrag hinzuzufügen.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 18, color: Colors.white70), + ), + ); + } + + final grouped = _groupByName(homeCtrl.weights); + final names = grouped.keys.toList()..sort(); + + return LayoutBuilder( + builder: (context, constraints) { + // Responsive: ab 700 px Breite → 2-spaltiges Grid + final isWide = constraints.maxWidth >= 700; + + return CustomScrollView( + slivers: [ + SliverPadding( + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top + 96, + left: isWide ? 24 : 16, + right: isWide ? 24 : 16, + bottom: 24, + ), + sliver: isWide + ? SliverGrid( + gridDelegate: + const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 600, + mainAxisSpacing: 16, + crossAxisSpacing: 16, + childAspectRatio: 0.95, + ), + delegate: SliverChildBuilderDelegate( + (context, i) => PersonWeightCard( + personName: names[i], + entries: grouped[names[i]]!, + onAddWeight: () => + homeCtrl.openAddDialog(names[i]), + onEditEntry: (entry) => + homeCtrl.openEditDialog(entry), + ), + childCount: names.length, + ), + ) + : SliverList( + delegate: SliverChildBuilderDelegate( + (context, i) => Padding( + padding: const EdgeInsets.only(bottom: 16), + child: PersonWeightCard( + personName: names[i], + entries: grouped[names[i]]!, + onAddWeight: () => + homeCtrl.openAddDialog(names[i]), + onEditEntry: (entry) => + homeCtrl.openEditDialog(entry), + ), + ), + childCount: names.length, + ), + ), + ), + ], + ); + }, + ); + }), + ), + ); + } +} + + diff --git a/lib/widgets/add_weight_dialog.dart b/lib/widgets/add_weight_dialog.dart new file mode 100644 index 0000000..3bff925 --- /dev/null +++ b/lib/widgets/add_weight_dialog.dart @@ -0,0 +1,556 @@ +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import '../models/home_model.dart'; + +class AddWeightDialog extends StatefulWidget { + final String personName; + + /// Letztes bekanntes Gewicht dieser Person – wird für weightChange benötigt. + final double lastWeight; + + /// Wenn gesetzt → Edit-Modus: Felder werden vorausgefüllt. + final WeightModel? existingEntry; + + const AddWeightDialog({ + super.key, + required this.personName, + required this.lastWeight, + this.existingEntry, + }); + + /// Öffnet den Dialog zum Hinzufügen eines neuen Eintrags. + static Future show({ + required String personName, + required double lastWeight, + }) { + return Get.dialog( + AddWeightDialog(personName: personName, lastWeight: lastWeight), + barrierColor: Colors.black.withValues(alpha: 0.55), + ); + } + + /// Öffnet den Dialog zum Bearbeiten eines vorhandenen Eintrags. + /// [previousWeight] = Gewicht des Eintrags VOR dem zu bearbeitenden. + static Future showEdit({ + required WeightModel entry, + required double previousWeight, + }) { + return Get.dialog( + AddWeightDialog( + personName: entry.name, + lastWeight: previousWeight, + existingEntry: entry, + ), + barrierColor: Colors.black.withValues(alpha: 0.55), + ); + } + + @override + State createState() => _AddWeightDialogState(); +} + +class _AddWeightDialogState extends State + with SingleTickerProviderStateMixin { + final _formKey = GlobalKey(); + final _weightController = TextEditingController(); + late final TextEditingController _nameController; + + DateTime _selectedDate = DateTime.now(); + double? _parsedWeight; + late AnimationController _anim; + late Animation _fadeScale; + + @override + void initState() { + super.initState(); + _nameController = TextEditingController(text: widget.personName); + // Edit-Modus: Felder vorausfüllen + if (widget.existingEntry != null) { + final e = widget.existingEntry!; + _weightController.text = e.weight.toString().replaceAll('.', ','); + _parsedWeight = e.weight; + _selectedDate = e.date; + } + _anim = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 220), + ); + _fadeScale = CurvedAnimation(parent: _anim, curve: Curves.easeOutBack); + _anim.forward(); + } + + @override + void dispose() { + _weightController.dispose(); + _nameController.dispose(); + _anim.dispose(); + super.dispose(); + } + + double get _weightChange { + if (_parsedWeight == null || widget.lastWeight == 0) return 0; + return double.parse( + (_parsedWeight! - widget.lastWeight).toStringAsFixed(2), + ); + } + + Color get _changeColor { + if (_weightChange < 0) return const Color(0xFF4FFFB0); + if (_weightChange > 0) return const Color(0xFFFF6B6B); + return Colors.white54; + } + + String get _changeLabel { + if (_parsedWeight == null) return ''; + if (widget.lastWeight == 0) return 'Erster Eintrag'; + final sign = _weightChange > 0 ? '+' : ''; + return '$sign${_weightChange.toStringAsFixed(1)} kg'; + } + + Future _pickDate() async { + final picked = await showDatePicker( + context: context, + initialDate: _selectedDate, + firstDate: DateTime(2000), + lastDate: DateTime(2100), + builder: (context, child) => Theme( + data: Theme.of(context).copyWith( + colorScheme: const ColorScheme.dark( + primary: Color(0xFF7B9FFF), + onSurface: Colors.white, + surface: Color(0xFF1A2035), + ), + ), + child: child!, + ), + ); + if (picked != null) setState(() => _selectedDate = picked); + } + + void _submit() { + if (!_formKey.currentState!.validate()) return; + final name = _nameController.text.trim(); + final entry = WeightModel( + // Im Edit-Modus dieselbe ID behalten + id: + widget.existingEntry?.id ?? + DateTime.now().millisecondsSinceEpoch.toString(), + name: name, + weight: _parsedWeight!, + date: _selectedDate, + weightChange: widget.lastWeight == 0 ? 0 : _weightChange, + ); + Navigator.of(context).pop(entry); + } + + @override + Widget build(BuildContext context) { + return ScaleTransition( + scale: _fadeScale, + child: FadeTransition( + opacity: _anim, + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 460), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 40), + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 24, sigmaY: 24), + child: Material( + type: MaterialType.transparency, + child: Container( + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(24), + border: Border.all( + color: Colors.white.withValues(alpha: 0.2), + width: 1, + ), + ), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // ── Titelzeile ────────────────────────── + _DialogHeader( + personName: widget.personName, + isEdit: widget.existingEntry != null, + ), + Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Name + _GlassField( + label: 'Name', + icon: Icons.person_outline_rounded, + child: TextFormField( + controller: _nameController, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + ), + decoration: _inputDecoration('z. B. Joe'), + validator: (v) => + (v == null || v.trim().isEmpty) + ? 'Name eingeben' + : null, + ), + ), + const SizedBox(height: 14), + + // Gewicht + _GlassField( + label: 'Gewicht', + icon: Icons.monitor_weight_outlined, + child: TextFormField( + controller: _weightController, + keyboardType: + const TextInputType.numberWithOptions( + decimal: true, + ), + inputFormatters: [ + FilteringTextInputFormatter.allow( + RegExp(r'^\d*[.,]?\d*'), + ), + ], + style: const TextStyle( + color: Colors.white, + fontSize: 16, + ), + decoration: _inputDecoration( + 'z. B. 72,5', + ), + onChanged: (v) { + final parsed = double.tryParse( + v.replaceAll(',', '.'), + ); + setState(() => _parsedWeight = parsed); + }, + validator: (v) { + if (v == null || v.isEmpty) { + return 'Gewicht eingeben'; + } + if (double.tryParse( + v.replaceAll(',', '.'), + ) == + null) { + return 'Ungültige Zahl'; + } + return null; + }, + ), + ), + const SizedBox(height: 14), + + // Datum + _GlassField( + label: 'Datum', + icon: Icons.calendar_today_outlined, + child: InkWell( + onTap: _pickDate, + borderRadius: BorderRadius.circular(10), + child: InputDecorator( + decoration: _inputDecoration(''), + child: Text( + DateFormat( + 'dd.MM.yyyy', + ).format(_selectedDate), + style: const TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + ), + ), + ), + const SizedBox(height: 20), + + // Vorschau weightChange + if (_parsedWeight != null) + _ChangePreview( + label: _changeLabel, + color: _changeColor, + lastWeight: widget.lastWeight, + ), + if (_parsedWeight != null) + const SizedBox(height: 20), + + // Buttons + Row( + children: [ + Expanded( + child: _GlassButton( + label: 'Abbrechen', + onPressed: () => + Navigator.of(context).pop(), + primary: false, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _GlassButton( + label: widget.existingEntry != null + ? 'Speichern' + : 'Hinzufügen', + onPressed: _submit, + primary: true, + ), + ), + ], + ), + const SizedBox(height: 20), + ], + ), + ), + ], + ), + ), // Form + ), // Container + ), // Material + ), // BackdropFilter + ), // ClipRRect + ), // Padding + ), // ConstrainedBox + ), // Center + ), // FadeTransition + ); // ScaleTransition + } + + InputDecoration _inputDecoration(String hint) => InputDecoration( + hintText: hint, + hintStyle: TextStyle( + color: Colors.white.withValues(alpha: 0.35), + fontSize: 14, + ), + filled: true, + fillColor: Colors.white.withValues(alpha: 0.07), + contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.2)), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.2)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Color(0xFF7B9FFF), width: 1.5), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Color(0xFFFF6B6B), width: 1.2), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Color(0xFFFF6B6B), width: 1.5), + ), + errorStyle: const TextStyle(color: Color(0xFFFF6B6B), fontSize: 11), + ); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Hilfs-Widgets +// ───────────────────────────────────────────────────────────────────────────── + +class _DialogHeader extends StatelessWidget { + final String personName; + final bool isEdit; + const _DialogHeader({required this.personName, this.isEdit = false}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.fromLTRB(20, 18, 12, 14), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.06), + border: Border( + bottom: BorderSide(color: Colors.white.withValues(alpha: 0.12)), + ), + ), + child: Row( + children: [ + Icon( + isEdit ? Icons.edit_outlined : Icons.add_circle_outline_rounded, + color: const Color(0xFF7B9FFF), + size: 22, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isEdit ? 'Eintrag bearbeiten' : 'Gewicht hinzufügen', + style: const TextStyle( + color: Colors.white, + fontSize: 17, + fontWeight: FontWeight.w700, + letterSpacing: 0.3, + ), + ), + Text( + personName, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.55), + fontSize: 13, + ), + ), + ], + ), + ), + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: Icon( + Icons.close_rounded, + color: Colors.white.withValues(alpha: 0.5), + size: 20, + ), + splashRadius: 18, + ), + ], + ), + ); + } +} + +class _GlassField extends StatelessWidget { + final String label; + final IconData icon; + final Widget child; + + const _GlassField({ + required this.label, + required this.icon, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, color: Colors.white.withValues(alpha: 0.5), size: 14), + const SizedBox(width: 6), + Text( + label, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.6), + fontSize: 12, + fontWeight: FontWeight.w600, + letterSpacing: 0.8, + ), + ), + ], + ), + const SizedBox(height: 6), + child, + ], + ); + } +} + +class _ChangePreview extends StatelessWidget { + final String label; + final Color color; + final double lastWeight; + + const _ChangePreview({ + required this.label, + required this.color, + required this.lastWeight, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: color.withValues(alpha: 0.35)), + ), + child: Row( + children: [ + Icon(Icons.swap_vert_rounded, color: color, size: 18), + const SizedBox(width: 8), + Text( + lastWeight == 0 ? label : 'Änderung zum letzten Eintrag: ', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.7), + fontSize: 13, + ), + ), + if (lastWeight != 0) + Text( + label, + style: TextStyle( + color: color, + fontSize: 13, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ); + } +} + +class _GlassButton extends StatelessWidget { + final String label; + final VoidCallback onPressed; + final bool primary; + + const _GlassButton({ + required this.label, + required this.onPressed, + required this.primary, + }); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 13), + decoration: BoxDecoration( + color: primary + ? const Color(0xFF7B9FFF).withValues(alpha: 0.25) + : Colors.white.withValues(alpha: 0.07), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: primary + ? const Color(0xFF7B9FFF).withValues(alpha: 0.6) + : Colors.white.withValues(alpha: 0.15), + width: 1, + ), + ), + alignment: Alignment.center, + child: Text( + label, + style: TextStyle( + color: primary ? const Color(0xFF7B9FFF) : Colors.white60, + fontWeight: FontWeight.w700, + fontSize: 14, + letterSpacing: 0.3, + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/glass_app_bar.dart b/lib/widgets/glass_app_bar.dart new file mode 100644 index 0000000..4440c37 --- /dev/null +++ b/lib/widgets/glass_app_bar.dart @@ -0,0 +1,143 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'outlined_text.dart'; + +class GlassAppBar extends StatelessWidget implements PreferredSizeWidget { + final String title; + final String? subtitle; + final List? actions; + final Widget? leading; + final double height; + final Color glassColor; + final double blurSigma; + final double borderOpacity; + + const GlassAppBar({ + super.key, + required this.title, + this.subtitle, + this.actions, + this.leading, + this.height = 80, + this.glassColor = const Color(0x22FFFFFF), + this.blurSigma = 18, + this.borderOpacity = 0.25, + }); + + @override + Size get preferredSize => Size.fromHeight(height); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: blurSigma, sigmaY: blurSigma), + child: Container( + height: height, + decoration: BoxDecoration( + color: glassColor, + border: Border( + bottom: BorderSide( + color: Colors.white.withOpacity(borderOpacity), + width: 1, + ), + ), + ), + child: SafeArea( + bottom: false, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (leading != null) ...[leading!, const SizedBox(width: 12)], + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OutlinedText( + title, + style: theme.textTheme.headlineSmall!.copyWith( + fontWeight: FontWeight.w900, + letterSpacing: 0.8, + fontSize: 26, + ), + fillColor: Colors.white, + strokeColor: Colors.black, + strokeWidth: 2.5, + ), + if (subtitle != null) ...[ + const SizedBox(height: 3), + OutlinedText( + subtitle!, + style: theme.textTheme.bodyMedium!.copyWith( + fontWeight: FontWeight.w600, + letterSpacing: 0.4, + ), + fillColor: Colors.white.withOpacity(0.95), + strokeColor: Colors.black, + strokeWidth: 1.5, + ), + ], + ], + ), + ), + if (actions != null) + Row(mainAxisSize: MainAxisSize.min, children: actions!), + ], + ), + ), + ), + ), + ), + ); + } +} + +/// Ein Icon-Button im Glas-Stil, passend zur GlassAppBar. +class GlassIconButton extends StatelessWidget { + final IconData icon; + final VoidCallback? onPressed; + final String? tooltip; + final Color? color; + + const GlassIconButton({ + super.key, + required this.icon, + this.onPressed, + this.tooltip, + this.color, + }); + + @override + Widget build(BuildContext context) { + final iconColor = + color ?? Theme.of(context).colorScheme.onSurface.withOpacity(0.85); + return Tooltip( + message: tooltip ?? '', + child: InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(50), + child: ClipRRect( + borderRadius: BorderRadius.circular(50), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 8, sigmaY: 8), + child: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.12), + shape: BoxShape.circle, + border: Border.all(color: Colors.white.withOpacity(0.25)), + ), + child: Icon(icon, color: iconColor, size: 22), + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/outlined_text.dart b/lib/widgets/outlined_text.dart new file mode 100644 index 0000000..fc15a9d --- /dev/null +++ b/lib/widgets/outlined_text.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +/// Rendert Text mit einer farbigen Kontur (Stroke) via Stack. +class OutlinedText extends StatelessWidget { + final String text; + final TextStyle style; + final Color fillColor; + final Color strokeColor; + final double strokeWidth; + + const OutlinedText( + this.text, { + super.key, + required this.style, + required this.fillColor, + this.strokeColor = Colors.black, + this.strokeWidth = 2.0, + }); + + @override + Widget build(BuildContext context) { + final strokeStyle = style.copyWith( + foreground: Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = strokeWidth + ..strokeJoin = StrokeJoin.round + ..color = strokeColor, + ); + final fillStyle = style.copyWith(color: fillColor); + + return Stack( + children: [ + Text(text, style: strokeStyle), + Text(text, style: fillStyle), + ], + ); + } +} diff --git a/lib/widgets/person_weight_card.dart b/lib/widgets/person_weight_card.dart new file mode 100644 index 0000000..2365c7e --- /dev/null +++ b/lib/widgets/person_weight_card.dart @@ -0,0 +1,434 @@ +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import '../models/home_model.dart'; +import 'outlined_text.dart'; + +class PersonWeightCard extends StatelessWidget { + /// Alle Einträge einer Person, absteigend nach Datum sortiert. + final String personName; + final List entries; + final VoidCallback? onAddWeight; + + /// Wird aufgerufen wenn ein Verlaufseintrag oder der Header-Eintrag + /// zum Bearbeiten angeklickt wird. + final void Function(WeightModel entry)? onEditEntry; + + const PersonWeightCard({ + super.key, + required this.personName, + required this.entries, + this.onAddWeight, + this.onEditEntry, + }); + + @override + Widget build(BuildContext context) { + // Defensiv: sicher sortiert (neuestes zuerst) + final sorted = [...entries]..sort((a, b) => b.date.compareTo(a.date)); + + final current = sorted.first; + final history = sorted.skip(1).toList(); + + return ClipRRect( + borderRadius: BorderRadius.circular(20), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), + child: Container( + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.07), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Colors.white.withValues(alpha: 0.15), + width: 1, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // ── Header ────────────────────────────────────────── + _CardHeader( + current: current, + name: personName, + onAddWeight: onAddWeight, + onEdit: onEditEntry != null + ? () => onEditEntry!(current) + : null, + ), + + // ── Historische Einträge ───────────────────────────── + if (history.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 6, + ), + child: Text( + 'Verlauf', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.45), + fontSize: 12, + letterSpacing: 1.2, + fontWeight: FontWeight.w600, + ), + ), + ), + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 220), + child: ListView.separated( + shrinkWrap: true, + padding: const EdgeInsets.only( + left: 12, + right: 12, + bottom: 12, + ), + itemCount: history.length, + separatorBuilder: (_, __) => Divider( + color: Colors.white.withValues(alpha: 0.08), + height: 1, + ), + itemBuilder: (context, i) => _HistoryRow( + entry: history[i], + onTap: onEditEntry != null + ? () => onEditEntry!(history[i]) + : null, + ), + ), + ), + ] else ...[ + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: Text( + 'Noch keine älteren Einträge.', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.35), + fontSize: 13, + ), + ), + ), + ], + ], + ), + ), + ), + ); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Header +// ───────────────────────────────────────────────────────────────────────────── + +class _CardHeader extends StatelessWidget { + final WeightModel current; + final String name; + final VoidCallback? onAddWeight; + final VoidCallback? onEdit; + + const _CardHeader({ + required this.current, + required this.name, + this.onAddWeight, + this.onEdit, + }); + + @override + Widget build(BuildContext context) { + final changeColor = current.weightChange < 0 + ? const Color(0xFF4FFFB0) + : current.weightChange > 0 + ? const Color(0xFFFF6B6B) + : Colors.white54; + + final changeIcon = current.weightChange < 0 + ? Icons.trending_down_rounded + : current.weightChange > 0 + ? Icons.trending_up_rounded + : Icons.remove_rounded; + + return Container( + padding: const EdgeInsets.fromLTRB(16, 18, 16, 14), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.05), + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + border: Border( + bottom: BorderSide(color: Colors.white.withValues(alpha: 0.1)), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Avatar + CircleAvatar( + radius: 26, + backgroundColor: Colors.white.withValues(alpha: 0.12), + child: OutlinedText( + name[0].toUpperCase(), + style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w900), + fillColor: Colors.white, + strokeColor: Colors.black, + strokeWidth: 1.5, + ), + ), + const SizedBox(width: 14), + // Name + Datum + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OutlinedText( + name, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.w900, + letterSpacing: 0.4, + ), + fillColor: Colors.white, + strokeColor: Colors.black, + strokeWidth: 2, + ), + const SizedBox(height: 2), + Text( + 'Zuletzt: ${DateFormat('dd.MM.yyyy').format(current.date)}', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.5), + fontSize: 12, + letterSpacing: 0.3, + ), + ), + ], + ), + ), + // Add-Button + if (onAddWeight != null) _GlassAddButton(onPressed: onAddWeight!), + if (onEdit != null) ...[ + const SizedBox(width: 6), + _GlassEditButton(onPressed: onEdit!), + ], + const SizedBox(width: 8), + // Aktuelles Gewicht + Änderung + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + OutlinedText( + '${current.weight.toStringAsFixed(1)} kg', + style: const TextStyle( + fontSize: 26, + fontWeight: FontWeight.w900, + letterSpacing: 0.2, + ), + fillColor: Colors.white, + strokeColor: Colors.black, + strokeWidth: 2, + ), + const SizedBox(height: 4), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(changeIcon, color: changeColor, size: 16), + const SizedBox(width: 3), + Text( + current.weightChange == 0 + ? '±0.0 kg' + : '${current.weightChange > 0 ? '+' : ''}${current.weightChange.toStringAsFixed(1)} kg', + style: TextStyle( + color: changeColor, + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ], + ), + ], + ), + ); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Verlaufs-Zeile +// ───────────────────────────────────────────────────────────────────────────── + +class _HistoryRow extends StatelessWidget { + final WeightModel entry; + final VoidCallback? onTap; + + const _HistoryRow({required this.entry, this.onTap}); + + @override + Widget build(BuildContext context) { + final changeColor = entry.weightChange < 0 + ? const Color(0xFF4FFFB0) + : entry.weightChange > 0 + ? const Color(0xFFFF6B6B) + : Colors.white54; + + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 6), + child: Row( + children: [ + // Datum + Expanded( + flex: 3, + child: Text( + DateFormat('dd.MM.yyyy').format(entry.date), + style: TextStyle( + color: Colors.white.withValues(alpha: 0.6), + fontSize: 14, + ), + ), + ), + // Gewicht + Expanded( + flex: 2, + child: Text( + '${entry.weight.toStringAsFixed(1)} kg', + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + // Änderung + Expanded( + flex: 2, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Icon( + entry.weightChange < 0 + ? Icons.arrow_downward_rounded + : entry.weightChange > 0 + ? Icons.arrow_upward_rounded + : Icons.remove_rounded, + color: changeColor, + size: 14, + ), + const SizedBox(width: 2), + Text( + entry.weightChange == 0 + ? '±0.0' + : '${entry.weightChange > 0 ? '+' : ''}${entry.weightChange.toStringAsFixed(1)}', + style: TextStyle( + color: changeColor, + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + // Edit-Button – gut sichtbar auf Mobile + if (onTap != null) ...[ + const SizedBox(width: 10), + Container( + padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 6), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.white.withValues(alpha: 0.25), + ), + ), + child: const Icon( + Icons.edit_outlined, + size: 16, + color: Colors.white, + ), + ), + ], + ], + ), + ), + ); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Glas-Add-Button (runder "+"-Button passend zum Karten-Header) +// ───────────────────────────────────────────────────────────────────────────── + +/// Ein runder Bearbeiten-Button im Glas-Stil. +class _GlassEditButton extends StatelessWidget { + final VoidCallback onPressed; + + const _GlassEditButton({required this.onPressed}); + + @override + Widget build(BuildContext context) { + return Tooltip( + message: 'Eintrag bearbeiten', + child: InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(50), + child: ClipRRect( + borderRadius: BorderRadius.circular(50), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 8, sigmaY: 8), + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.10), + shape: BoxShape.circle, + border: Border.all(color: Colors.white.withValues(alpha: 0.25)), + ), + child: const Icon( + Icons.edit_outlined, + color: Colors.white, + size: 18, + ), + ), + ), + ), + ), + ); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Glas-Add-Button +// ───────────────────────────────────────────────────────────────────────────── + +class _GlassAddButton extends StatelessWidget { + final VoidCallback onPressed; + + const _GlassAddButton({required this.onPressed}); + + @override + Widget build(BuildContext context) { + return Tooltip( + message: 'Gewicht hinzufügen', + child: InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(50), + child: ClipRRect( + borderRadius: BorderRadius.circular(50), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 8, sigmaY: 8), + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.14), + shape: BoxShape.circle, + border: Border.all(color: Colors.white.withValues(alpha: 0.3)), + ), + child: const Icon( + Icons.add_rounded, + color: Colors.white, + size: 20, + ), + ), + ), + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..b1b53b1 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,237 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + 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: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + url: "https://pub.dev" + source: hosted + version: "0.12.18" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + url: "https://pub.dev" + source: hosted + version: "0.7.9" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + 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.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..368506d --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,77 @@ +name: flutter_weight_tracker_web +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. +version: 1.0.0+1 + +environment: + sdk: ^3.11.0 + +# 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 + get: ^4.7.3 + intl: ^0.20.2 + +dev_dependencies: + flutter_lints: ^6.0.0 + flutter_test: + sdk: flutter + +# 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 diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..5b537b4 --- /dev/null +++ b/web/index.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + flutter_weight_tracker_web + + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..e736ddb --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "flutter_weight_tracker_web", + "short_name": "flutter_weight_tracker_web", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +}