diff --git a/README.md b/README.md index 7c6ea38..d04c45d 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,38 @@ -[![style: effective dart](https://img.shields.io/badge/style-effective_dart-40c4ff.svg)](https://github.com/tenhobi/effective_dart) +# Flutter Profile -Short description of what your package is, why you created it. What issues it fixes and how it works. Also mention the available platforms +Flutter Profile is a package you can use to display any user data and let them alter it if desired. + +![alt text](example/image/example_profile.png) + +## Features + +Display every type of user data. +Display an image/avatar. +Enable the user to alter his data or withhold the user from doing so. ## Setup -What setup steps are neccesarry and why> +To use this package, add `flutter_profile` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). -
-PLATFORM - -specific platform steps +## How To Use -
+See the [Example Code](example/lib/main.dart) for an example on how to use this package. -## How to use +Underneath are all paramters, of the 'ProfilePage' widget, listed with an explanation. -How can we use the package descibe the most common ways with examples in - -```dart - codeblocks -``` +| Parameter | Explaination | +| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| user | The class that contains all the user data. | +| service | The service which determines what happens the user wants to update their profile, update their avatar or press the InkWell at the bottom of the page. | +| style | With the use of ProfileStyle a couple of style options can be set for the form. | +| customAvatar | An option to override the standard avatar. | +| showAvatar | The ability to disable/enable the avatar. | +| itemBuilder | The way to override the standard textfield for each standard piece of user data. | +| itemBuilderOptions | The options used by the standard itemBuilder to alter the function and style of the textfields | ## Issues -Please file any issues, bugs or feature request as an issue on our [GitHub](REPO URL) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl). +Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_profile/pulls) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl). ## Want to contribute @@ -31,4 +40,4 @@ If you would like to contribute to the plugin (e.g. by improving the documentati ## Author -This flutter_profile package for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at +This `flutter-image-picker` for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at diff --git a/example/image/example_profile.png b/example/image/example_profile.png new file mode 100644 index 0000000..a28e7cb Binary files /dev/null and b/example/image/example_profile.png differ diff --git a/example/lib/main.dart b/example/lib/main.dart index 7df78ab..d1b6562 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,29 +1,44 @@ import 'dart:typed_data'; +import 'package:example/utils/example_profile_service.dart'; import 'package:flutter/material.dart'; -import 'package:profile/profile.dart'; +import 'package:flutter_profile/flutter_profile.dart'; + +import 'utils/example_profile_data.dart'; void main() { runApp(const MyApp()); } -class MyApp extends StatefulWidget { - const MyApp({Key? key}) : super(key: key); +class MyApp extends StatelessWidget { + const MyApp({super.key}); @override - State createState() => _MyAppState(); + Widget build(BuildContext context) { + return const MaterialApp( + home: ProfileExample(), + ); + } } -class _MyAppState extends State { +class ProfileExample extends StatefulWidget { + const ProfileExample({Key? key}) : super(key: key); + + @override + State createState() => _ProfileExampleState(); +} + +class _ProfileExampleState extends State { late User _user; - MyProfileData profileData = MyProfileData(); + ProfileData profileData = + ExampleProfileData().fromMap({'email': 'example@email.com'}); @override void initState() { super.initState(); _user = User( - 'firstName', - 'lastName', + 'Firstname', + 'Lastname', Uint8List.fromList( [], ), @@ -33,125 +48,56 @@ class _MyAppState extends State { @override Widget build(BuildContext context) { - return MaterialApp( - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: ProfilePage( - service: MyProfileService(), + return Scaffold( + body: ProfilePage( + bottomActionText: 'Log out', + itemBuilderOptions: ItemBuilderOptions( + inputDecorationField: { + 'firstName': const InputDecoration( + label: Text('First name'), + ), + 'lastName': const InputDecoration( + label: Text('Last name'), + ), + 'email': const InputDecoration( + label: Text('E-mail'), + ), + }, + validators: { + 'firstName': (String? value) { + if (value == null || value.isEmpty) { + return 'Field empty'; + } + return null; + }, + 'lastName': (String? value) { + if (value == null || value.isEmpty) { + return 'Field empty'; + } + return null; + }, + 'email': (String? value) { + if (value == null || value.isEmpty) { + return 'Field empty'; + } + return null; + }, + }, + ), user: _user, - ), - ); - } -} - -class MyProfileService extends ProfileService { - @override - deleteProfile() { - return super.deleteProfile(); - } - - @override - editProfile( - User user, String key, String value) { - return super.editProfile(user, key, value); - } - - @override - uploadImage() { - return super.uploadImage(); - } -} - -class MyProfileData extends ProfileData { - MyProfileData({ - this.justMyNumber = '1', - this.justMyString = 2, - }); - - final String justMyNumber; - int justMyString; - - @override - Map mapWidget(Function update) { - return { - 'justMyString': Container( - height: 100, - width: 300, - child: Row( - children: [ - ElevatedButton( - style: ElevatedButton.styleFrom( - primary: justMyString == 1 ? Colors.green : Colors.blue, - ), - onPressed: () { - justMyString = 1; - update(); - print(justMyString); - }, - child: const Text('1'), - ), - const Spacer(), - ElevatedButton( - style: ElevatedButton.styleFrom( - primary: justMyString == 2 ? Colors.green : Colors.blue, - ), - onPressed: () { - justMyString = 2; - update(); - print(justMyString); - }, - child: const Text('2'), - ), - const Spacer(), - ElevatedButton( - style: ElevatedButton.styleFrom( - primary: justMyString == 3 ? Colors.green : Colors.blue, - ), - onPressed: () { - justMyString = 3; - update(); - }, - child: const Text('3'), - ), - const Spacer(), - ElevatedButton( - style: ElevatedButton.styleFrom( - primary: justMyString == 4 ? Colors.green : Colors.blue, - ), - onPressed: () { - justMyString = 4; - update(); - }, - child: const Text('4'), - ), - ], + service: ExampleProfileService(), + style: ProfileStyle( + avatarStyle: const AvatarStyle( + displayNameStyle: TextStyle(fontSize: 20), + ), + pagePadding: EdgeInsets.only( + top: 50, + bottom: 50, + left: MediaQuery.of(context).size.width * 0.35, + right: MediaQuery.of(context).size.width * 0.35, + ), ), ), - 'justMyNumber': null, - }; - } - - @override - ProfileData fromMap(Map data) { - return MyProfileData( - justMyNumber: data['justMyNumber'], - justMyString: int.parse( - data['justMyString'].toString(), - ), ); } - - @override - Map toMap() { - return { - 'justMyNumber': justMyNumber, - 'justMyString': justMyString, - }; - } - - @override - ProfileData create() { - return MyProfileData(); - } } diff --git a/example/lib/utils/example_profile_data.dart b/example/lib/utils/example_profile_data.dart new file mode 100644 index 0000000..5634051 --- /dev/null +++ b/example/lib/utils/example_profile_data.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_profile/flutter_profile.dart'; + +class ExampleProfileData extends ProfileData { + ExampleProfileData({ + this.email, + }); + + String? email; + + @override + Map mapWidget( + VoidCallback update, + BuildContext context, + ) { + return { + 'email': null, + }; + } + + @override + ProfileData fromMap(Map data) { + return ExampleProfileData( + email: data['email'], + ); + } + + @override + Map toMap() { + return { + 'email': email, + }; + } + + @override + ProfileData create() { + return ExampleProfileData(); + } +} diff --git a/example/lib/utils/example_profile_service.dart b/example/lib/utils/example_profile_service.dart new file mode 100644 index 0000000..52139bb --- /dev/null +++ b/example/lib/utils/example_profile_service.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_profile/flutter_profile.dart'; + +class ExampleProfileService extends ProfileService { + ExampleProfileService(); + + @override + void pageBottomAction() { + debugPrint('Bottom page action'); + } + + @override + void editProfile( + User user, + String key, + String value, + ) { + debugPrint('Editing key: $key with $value'); + } + + @override + Future uploadImage(BuildContext context) async { + debugPrint('Updating avatar'); + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock index a9c8f14..e78ef51 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.2" + version: "2.9.0" boolean_selector: dependency: transitive description: @@ -21,21 +21,14 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" + version: "1.2.1" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" collection: dependency: transitive description: @@ -56,7 +49,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" flutter: dependency: "direct main" description: flutter @@ -69,6 +62,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + flutter_profile: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.1" flutter_test: dependency: "direct dev" description: flutter @@ -87,35 +87,28 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.11" + version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.5" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" - profile: - dependency: "direct main" - description: - path: ".." - relative: true - source: path - version: "0.0.1" + version: "1.8.2" sky_engine: dependency: transitive description: flutter @@ -127,7 +120,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.9.0" stack_trace: dependency: transitive description: @@ -148,21 +141,21 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.12" vector_math: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 0c2ba24..f1d5956 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -34,7 +34,7 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 - profile: + flutter_profile: path: ../ dev_dependencies: diff --git a/lib/flutter_profile.dart b/lib/flutter_profile.dart new file mode 100644 index 0000000..37389fc --- /dev/null +++ b/lib/flutter_profile.dart @@ -0,0 +1,9 @@ +library flutter_profile; + +export 'src/widgets/profile/profile_page.dart'; +export 'src/widgets/profile/profile_style.dart'; +export 'src/widgets/avatar/avatar_style.dart'; +export 'src/services/profile_service.dart'; +export 'src/widgets/item_builder/item_builder.dart'; +export 'src/models/user.dart'; +export 'src/widgets/item_builder/item_builder_options.dart'; diff --git a/lib/profile.dart b/lib/profile.dart deleted file mode 100644 index a5911f2..0000000 --- a/lib/profile.dart +++ /dev/null @@ -1,6 +0,0 @@ -library profile; - -export 'src/widgets/profile/profile_page.dart'; -export 'src/services/profile_service.dart'; -export 'src/widgets/item_builder/item_builder.dart'; -export 'src/models/user.dart'; diff --git a/lib/src/models/user.dart b/lib/src/models/user.dart index 76dbc09..ef7c943 100644 --- a/lib/src/models/user.dart +++ b/lib/src/models/user.dart @@ -1,15 +1,15 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; -import 'package:profile/profile.dart'; -import 'package:profile/src/widgets/item_builder/item_builder.dart'; -import 'package:profile/src/widgets/item_builder/item_builder_options.dart'; -class User { +/// User is used to contain all user data. It consists of three standard fields: firstName, lastName and image. +/// +/// For additional data profileData can be used. +class User { String? firstName; String? lastName; Uint8List? image; - T? profileData; + ProfileData? profileData; User( this.firstName, @@ -37,6 +37,11 @@ class User { } } +/// ProfileData is used to store custom/addintional data for a user. +/// +/// The MapWidget method is used to bind a [Widget] to one of the keys. This will override the standard textfield. +/// +/// The Builditems method is used to make the list of field to house the user data. abstract class ProfileData { const ProfileData(); @@ -44,46 +49,7 @@ abstract class ProfileData { Map toMap(); - Map mapWidget(Function update); + Map mapWidget(VoidCallback update, BuildContext context); ProfileData create(); - - List buildItems( - Map items, - Map typeMap, - double spacing, - Function(String, String) updateProfile, { - ItemBuilder? itemBuilder, - ItemBuilderOptions? itemBuilderOptions, - }) { - var widgets = []; - ItemBuilder builder = ItemBuilder( - options: itemBuilderOptions ?? const ItemBuilderOptions(), - ); - for (var item in items.entries) { - itemBuilder == null - ? widgets.add( - builder.build( - item.value, - typeMap[item.key], - (value) { - updateProfile(item.key, value); - }, - ), - ) - : widgets.add( - itemBuilder.build( - item.value, - typeMap[item.key], - (value) { - updateProfile(item.key, value); - }, - ), - ); - widgets.add(SizedBox( - height: spacing, - )); - } - return widgets; - } } diff --git a/lib/src/services/profile_service.dart b/lib/src/services/profile_service.dart index a94a3e5..a200dfc 100644 --- a/lib/src/services/profile_service.dart +++ b/lib/src/services/profile_service.dart @@ -1,26 +1,21 @@ -import 'package:profile/profile.dart'; +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_profile/src/models/user.dart'; + +/// ProfileService can be extended and set for the profilePage. The following method can be overriden. +/// +/// BottompageAction is called when the [InkWell] at the bottom of the page is tapped. +/// +/// EditProfile is called when a user changes and submits a standard textfields. +/// +/// UploadImage is called when te user presses the avatar. abstract class ProfileService { const ProfileService(); - deleteProfile() { - print("Request to delete profile"); - // TODO(anyone) project specific - } + FutureOr pageBottomAction(); - editProfile(User user, String key, String value) { - if (user.profileData != null) { - var map = user.profileData!.toMap(); - if (map.containsKey(key)) { - map[key] = value; - var profile = user.profileData!.create(); - user.profileData = profile.fromMap(map); - } - } - } + FutureOr editProfile(User user, String key, String value); - uploadImage() { - print('Request to change picture'); - // TODO(anyone) open image picker and update profile - } + FutureOr uploadImage(BuildContext context); } diff --git a/lib/src/widgets/avatar/avatar.dart b/lib/src/widgets/avatar/avatar.dart index 0fb6896..6fc3eec 100644 --- a/lib/src/widgets/avatar/avatar.dart +++ b/lib/src/widgets/avatar/avatar.dart @@ -1,19 +1,21 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; -import 'package:profile/src/widgets/avatar/avatar_style.dart'; +import 'package:flutter_profile/src/widgets/avatar/avatar_style.dart'; class Avatar extends StatelessWidget { const Avatar({ Key? key, this.image, - this.name, + this.firstName, + this.lastName, this.avatar, this.style = const AvatarStyle(), }) : super(key: key); final Uint8List? image; - final String? name; + final String? firstName; + final String? lastName; final Widget? avatar; final AvatarStyle style; @@ -22,9 +24,12 @@ class Avatar extends StatelessWidget { return Column( children: [ _avatar(), - if (name != null) + const SizedBox( + height: 16, + ), + if (firstName != null || lastName != null) Text( - name!, + '${firstName ?? ''} ${lastName ?? ''}', style: style.displayNameStyle, ) ], @@ -39,43 +44,41 @@ class Avatar extends StatelessWidget { return Container( width: style.width, height: style.height, - decoration: const BoxDecoration( + decoration: BoxDecoration( shape: BoxShape.circle, + image: DecorationImage( + image: MemoryImage(image!), + fit: BoxFit.fill, + ), ), - child: Image.memory(image!), ); - } else if (name != null && name!.isNotEmpty) { + } else if (firstName != null || lastName != null) { return Container( width: style.width, height: style.height, decoration: BoxDecoration( - color: _generateColorWithIntials(name!), + color: _generateColorWithIntials(firstName, lastName), shape: BoxShape.circle, ), child: Center( child: Text( - _getInitials(name!), + style: const TextStyle(fontSize: 40), + _getInitials(firstName, lastName), ), ), ); } else { - return SizedBox( - width: style.width, - height: style.height, - // TODO(anyone) child fallback image - ); + return Container(); } } - String _getInitials(String name) { - var nameList = name.split(' '); - return nameList.first[0] + nameList.last[0]; + String _getInitials(String? firstName, String? lastName) { + return (firstName?[0] ?? '') + (lastName?[0] ?? ''); } - Color _generateColorWithIntials(String name) { - var nameList = name.split(' '); - var uniqueInitialId = nameList.first.toLowerCase().codeUnitAt(0) + - nameList.last.toLowerCase().codeUnitAt(0); + Color _generateColorWithIntials(String? firstName, String? lastName) { + var uniqueInitialId = (firstName?.toLowerCase().codeUnitAt(0) ?? 0) + + (lastName?.toLowerCase().codeUnitAt(0) ?? 0); return Colors.primaries[uniqueInitialId % Colors.primaries.length]; } diff --git a/lib/src/widgets/avatar/avatar_style.dart b/lib/src/widgets/avatar/avatar_style.dart index c944e24..3107a90 100644 --- a/lib/src/widgets/avatar/avatar_style.dart +++ b/lib/src/widgets/avatar/avatar_style.dart @@ -1,5 +1,14 @@ import 'package:flutter/material.dart'; +/// AvatarStyle is used to set the style of the avatar displayed at the top of the [ProfilePage]. +/// +/// Width is used to set the width of the avatar. Defaults to 100. +/// +/// Height is used to set the height of the avatar. Defaults to 100. +/// +/// InitialStyle sets the [TextStyle] of the initial which are shown when no image is provided. +/// +/// DisplayNameStyle sets the [TextStyle] for the displayname underneath the avatar. class AvatarStyle { const AvatarStyle({ this.width = 100, diff --git a/lib/src/widgets/item_builder/item_builder.dart b/lib/src/widgets/item_builder/item_builder.dart index 10fab7c..5a1e9fc 100644 --- a/lib/src/widgets/item_builder/item_builder.dart +++ b/lib/src/widgets/item_builder/item_builder.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:profile/src/widgets/item_builder/item_builder_options.dart'; +import 'package:flutter_profile/src/widgets/item_builder/item_builder_options.dart'; +/// ItemBuilder is used to set the standard textfield for each undefined users data item. +/// +/// Options sets options for the textfield. class ItemBuilder { ItemBuilder({ required this.options, @@ -8,19 +11,34 @@ class ItemBuilder { final ItemBuilderOptions options; - Widget build(dynamic value, Widget? widget, Function(String) updateItem) { + Widget build(String key, GlobalKey formKey, dynamic value, + Widget? widget, Function(String) updateItem) { if (widget == null) { var controller = TextEditingController( - text: '$value', + text: '${value ?? ''}', ); - return TextField( - controller: controller, - decoration: options.inputDecoration, - readOnly: options.readOnly, - onSubmitted: (s) { - updateItem(s); - }, + late InputDecoration inputDecoration; + + inputDecoration = + options.inputDecorationField?[key] ?? options.inputDecoration; + + return Form( + key: formKey, + child: TextFormField( + key: Key(key), + controller: controller, + decoration: inputDecoration, + readOnly: options.readOnly, + onFieldSubmitted: (value) { + if (formKey.currentState!.validate()) { + updateItem(value); + } + }, + validator: (value) { + return options.validators?[key]?.call(value); + }, + ), ); } return widget; diff --git a/lib/src/widgets/item_builder/item_builder_options.dart b/lib/src/widgets/item_builder/item_builder_options.dart index 616c25d..a0600f7 100644 --- a/lib/src/widgets/item_builder/item_builder_options.dart +++ b/lib/src/widgets/item_builder/item_builder_options.dart @@ -1,11 +1,22 @@ import 'package:flutter/material.dart'; +/// ItemBuilderOptions is a class to store all settings for a field in the profile page. +/// +/// InputDecoration sets the decoration for all standard textfields. This is overridden if a field specific decoration is set by inputDecorationField. +/// +/// inputDecorationField sets the inputdecoration by key of the user data field. So a field can have its own specific decoration. +/// +/// Validator can be used to set a validator for the standard textfield. class ItemBuilderOptions { - const ItemBuilderOptions({ + ItemBuilderOptions({ this.inputDecoration = const InputDecoration(), + this.inputDecorationField, this.readOnly = false, + this.validators, }); final InputDecoration inputDecoration; + final Map? inputDecorationField; final bool readOnly; + final Map? validators; } diff --git a/lib/src/widgets/item_builder/item_list.dart b/lib/src/widgets/item_builder/item_list.dart new file mode 100644 index 0000000..367639e --- /dev/null +++ b/lib/src/widgets/item_builder/item_list.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_profile/src/widgets/item_builder/item_builder.dart'; +import 'package:flutter_profile/src/widgets/item_builder/item_builder_options.dart'; + +class ItemList extends StatefulWidget { + const ItemList( + this.items, + this.typeMap, + this.spacing, + this.updateProfile, { + this.itemBuilder, + this.itemBuilderOptions, + super.key, + }); + + final Map items; + final Map typeMap; + final double spacing; + final Function(String, String) updateProfile; + final ItemBuilder? itemBuilder; + final ItemBuilderOptions? itemBuilderOptions; + + @override + State createState() => _ItemListState(); +} + +class _ItemListState extends State { + Map> formKeys = {}; + + @override + void initState() { + super.initState(); + + for (var item in widget.items.entries) { + formKeys.addAll( + {item.key: GlobalKey()}, + ); + } + } + + @override + Widget build(BuildContext context) { + var widgets = []; + ItemBuilder builder = ItemBuilder( + options: widget.itemBuilderOptions ?? ItemBuilderOptions(), + ); + for (var item in widget.items.entries) { + widget.itemBuilder == null + ? widgets.add( + builder.build( + item.key, + formKeys['item.key'] ?? GlobalKey(), + item.value, + widget.typeMap[item.key], + (value) { + widget.updateProfile(item.key, value); + }, + ), + ) + : widgets.add( + widget.itemBuilder!.build( + item.key, + formKeys['item.key'] ?? GlobalKey(), + item.value, + widget.typeMap[item.key], + (value) { + widget.updateProfile(item.key, value); + }, + ), + ); + widgets.add(SizedBox( + height: widget.spacing, + )); + } + + return Column( + children: widgets, + ); + } +} diff --git a/lib/src/widgets/profile/profile_page.dart b/lib/src/widgets/profile/profile_page.dart index 6c20331..aae0db7 100644 --- a/lib/src/widgets/profile/profile_page.dart +++ b/lib/src/widgets/profile/profile_page.dart @@ -1,10 +1,28 @@ import 'package:flutter/material.dart'; -import 'package:profile/profile.dart'; -import 'package:profile/src/widgets/avatar/avatar.dart'; -import 'package:profile/src/widgets/item_builder/item_builder_options.dart'; - -import 'package:profile/src/widgets/profile/profile_style.dart'; +import 'package:flutter_profile/src/models/user.dart'; +import 'package:flutter_profile/src/services/profile_service.dart'; +import 'package:flutter_profile/src/widgets/item_builder/item_builder.dart'; +import 'package:flutter_profile/src/widgets/item_builder/item_builder_options.dart'; +import 'package:flutter_profile/src/widgets/profile/profile_style.dart'; +import 'package:flutter_profile/src/widgets/profile/proifle_wrapper.dart'; +/// The ProfilePage widget is able to show the data of a user. By default the user is able to change this data. The widget has a couple of parameters listed below: +/// +/// User will contain the data of the user which atleast contain a first name, last name and an avatar/image. Besides this information the [ProfileData] can be used to set custom user fields. +/// +/// With the use of the service set by a [ProfileService] some actions can be determined what should occur when the user does the following actions: Deleting/editing the profile or uploading an image. +/// +/// The style can be used the set some style options regarding the whole form. This is done by setting a [ProfileStyle]. The following styling can be set: The style of the avatar, the padding of the page and default padding between items. +/// +/// CustomAvatar can be set to override the standard avatar using any [Widget]. +/// +/// ShowAvatar can be set using a [bool] to determine whether the avatar should be shown and be able to be set by the user. Default set to true. +/// +/// BottomActionText sets the text for the inkwell at the bottom of the page. If this is set the null then the [InkWell] is disabled. +/// +/// ItemBuilder is used to determine how the user data is represented. +/// +/// ItemBuilderOptions can be used to just set the settings for fields instead of defining the field itself and how it is used. This field should not be used when the itemBuilder is set. class ProfilePage extends StatefulWidget { const ProfilePage({ Key? key, @@ -15,17 +33,31 @@ class ProfilePage extends StatefulWidget { this.showAvatar = true, this.itemBuilder, this.itemBuilderOptions, - this.showDeleteProfile = true, + this.bottomActionText, }) : super(key: key); + /// User containing all the user data. final User user; + + /// The service the determine what should happen when the user takes action. final ProfileService service; + + /// Style to set some general styling parameters fot the whole page. final ProfileStyle style; + + /// The way to override the standard avatar is needed. final Widget? customAvatar; + + /// Whether to show the users avatar. final bool showAvatar; - final bool showDeleteProfile; + + /// Sets the text for the [InkWell] at the bottom of the profile page. The [InkWell] is disabled when null. + final String? bottomActionText; + + /// Itembuilder is used the build each field in the user. final ItemBuilder? itemBuilder; + /// Used to set settings of eacht field in user. final ItemBuilderOptions? itemBuilderOptions; @override @@ -44,132 +76,10 @@ class _ProfilePageState extends State { style: widget.style, customAvatar: widget.customAvatar, showAvatar: widget.showAvatar, - showDeleteProfile: widget.showDeleteProfile, + bottomActionText: widget.bottomActionText, itemBuilder: widget.itemBuilder, itemBuilderOptions: widget.itemBuilderOptions, key: UniqueKey(), ); } } - -class ProfileWrapper extends StatefulWidget { - const ProfileWrapper({ - Key? key, - required this.user, - required this.service, - required this.rebuild, - this.style = const ProfileStyle(), - this.customAvatar, - this.showAvatar = true, - this.itemBuilder, - this.itemBuilderOptions, - this.showDeleteProfile = true, - }) : super(key: key); - - final User user; - final ProfileService service; - final ProfileStyle style; - final Widget? customAvatar; - final bool showAvatar; - final bool showDeleteProfile; - final ItemBuilder? itemBuilder; - final Function rebuild; - final ItemBuilderOptions? itemBuilderOptions; - - @override - State createState() => _ProfileWrapperState(); -} - -class _ProfileWrapperState extends State { - late List profileItems; - List defaultItems = []; - - @override - void initState() { - super.initState(); - profileItems = widget.user.profileData!.buildItems( - widget.user.profileData!.toMap(), - widget.user.profileData!.mapWidget(() { - widget.rebuild(); - }), - widget.style.betweenDefaultItemPadding, - (key, value) { - widget.service.editProfile(widget.user, key, value); - }, - itemBuilder: widget.itemBuilder, - itemBuilderOptions: widget.itemBuilderOptions, - ); - if (widget.itemBuilder == null) { - ItemBuilder builder = ItemBuilder( - options: widget.itemBuilderOptions ?? const ItemBuilderOptions(), - ); - defaultItems.add(builder.build(widget.user.firstName, null, (v) { - widget.user.firstName = v; - })); - defaultItems.add( - SizedBox( - height: widget.style.betweenDefaultItemPadding, - ), - ); - defaultItems.add(builder.build(widget.user.lastName, null, (v) { - widget.user.lastName = v; - })); - } else { - defaultItems - .add(widget.itemBuilder!.build(widget.user.firstName, null, (v) { - widget.user.firstName = v; - })); - defaultItems.add( - SizedBox( - height: widget.style.betweenDefaultItemPadding, - ), - ); - defaultItems - .add(widget.itemBuilder!.build(widget.user.lastName, null, (v) { - widget.user.lastName = v; - })); - } - } - - @override - Widget build(BuildContext context) { - return Material( - child: Padding( - padding: widget.style.pagePadding, - child: Column( - children: [ - if (widget.showAvatar) - InkWell( - onTap: () { - widget.service.uploadImage(); - }, - child: Avatar( - name: '${widget.user.firstName} ${widget.user.lastName}', - style: widget.style.avatarStyle, - avatar: widget.customAvatar, - image: widget.user.image, - ), - ), - if (widget.showAvatar) - SizedBox( - height: widget.style.betweenDefaultItemPadding, - ), - ...defaultItems, - ...profileItems, - if (widget.showDeleteProfile) - SizedBox( - height: widget.style.betweenDefaultItemPadding, - ), - if (widget.showDeleteProfile) - InkWell( - onTap: () { - widget.service.deleteProfile(); - }, - child: const Text('Profiel verwijderen'), - ), - ], - ), - ), - ); - } -} diff --git a/lib/src/widgets/profile/profile_style.dart b/lib/src/widgets/profile/profile_style.dart index cbdded8..5d09954 100644 --- a/lib/src/widgets/profile/profile_style.dart +++ b/lib/src/widgets/profile/profile_style.dart @@ -1,6 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:profile/src/widgets/avatar/avatar_style.dart'; +import 'package:flutter_profile/src/widgets/avatar/avatar_style.dart'; +/// ProfielStyle is used to set a couple of style paramaters for the whole profile page. +/// +/// AvatarStyle is used to set some styling for the avatar using [AvatarStyle]. +/// +/// PagePaddign is used to set the padding around the whole profile page with its parent. +/// +/// BetweenDefaultitemPadding sets te padding between each user data item. class ProfileStyle { const ProfileStyle({ this.avatarStyle = const AvatarStyle(), @@ -8,7 +15,12 @@ class ProfileStyle { this.pagePadding = EdgeInsets.zero, }); + /// AvatarStyle can be used to set some avatar styling parameters. final AvatarStyle avatarStyle; + + /// PagePadding can be set to determine the padding of the whole page againt the profile page parent. final EdgeInsetsGeometry pagePadding; + + /// BetweenDefaultItemPadding sets the final double betweenDefaultItemPadding; } diff --git a/lib/src/widgets/profile/proifle_wrapper.dart b/lib/src/widgets/profile/proifle_wrapper.dart new file mode 100644 index 0000000..866fe49 --- /dev/null +++ b/lib/src/widgets/profile/proifle_wrapper.dart @@ -0,0 +1,180 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_profile/src/models/user.dart'; +import 'package:flutter_profile/src/services/profile_service.dart'; +import 'package:flutter_profile/src/widgets/avatar/avatar.dart'; +import 'package:flutter_profile/src/widgets/item_builder/item_builder.dart'; +import 'package:flutter_profile/src/widgets/item_builder/item_builder_options.dart'; +import 'package:flutter_profile/src/widgets/item_builder/item_list.dart'; +import 'package:flutter_profile/src/widgets/profile/profile_style.dart'; + +class ProfileWrapper extends StatefulWidget { + const ProfileWrapper({ + Key? key, + required this.user, + required this.service, + required this.rebuild, + this.style = const ProfileStyle(), + this.customAvatar, + this.showAvatar = true, + this.itemBuilder, + this.itemBuilderOptions, + this.bottomActionText, + }) : super(key: key); + + final User user; + final ProfileService service; + final ProfileStyle style; + final Widget? customAvatar; + final bool showAvatar; + final String? bottomActionText; + final ItemBuilder? itemBuilder; + final Function rebuild; + final ItemBuilderOptions? itemBuilderOptions; + + @override + State createState() => _ProfileWrapperState(); +} + +class _ProfileWrapperState extends State { + List defaultItems = []; + + GlobalKey firstNameKey = GlobalKey(); + + GlobalKey lastNameKey = GlobalKey(); + + @override + void initState() { + super.initState(); + + if (widget.itemBuilder == null) { + ItemBuilder builder = ItemBuilder( + options: widget.itemBuilderOptions ?? ItemBuilderOptions(), + ); + defaultItems.add( + builder.build( + 'firstName', + firstNameKey, + widget.user.firstName, + null, + (v) { + widget.user.firstName = v; + + widget.service.editProfile(widget.user, 'firstName', v); + }, + ), + ); + defaultItems.add( + SizedBox( + height: widget.style.betweenDefaultItemPadding, + ), + ); + defaultItems.add( + builder.build( + 'lastName', + lastNameKey, + widget.user.lastName, + null, + (v) { + widget.user.lastName = v; + + widget.service.editProfile(widget.user, 'lastName', v); + }, + ), + ); + } else { + defaultItems.add( + widget.itemBuilder!.build( + 'firstName', + firstNameKey, + widget.user.firstName, + null, + (v) { + widget.user.firstName = v; + + widget.service.editProfile(widget.user, 'firstname', v); + }, + ), + ); + defaultItems.add( + SizedBox( + height: widget.style.betweenDefaultItemPadding, + ), + ); + defaultItems.add( + widget.itemBuilder!.build( + 'lastName', + lastNameKey, + widget.user.lastName, + null, + (v) { + widget.user.lastName = v; + + widget.service.editProfile(widget.user, 'lastName', v); + }, + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: Padding( + padding: widget.style.pagePadding, + child: Column( + children: [ + if (widget.showAvatar) + InkWell( + onTap: () async { + await widget.service.uploadImage(context); + }, + child: Avatar( + firstName: widget.user.firstName, + lastName: widget.user.lastName, + style: widget.style.avatarStyle, + avatar: widget.customAvatar, + image: widget.user.image, + ), + ), + if (widget.showAvatar) + SizedBox( + height: widget.style.betweenDefaultItemPadding, + ), + ...defaultItems, + ItemList( + widget.user.profileData!.toMap(), + widget.user.profileData!.mapWidget( + () { + widget.rebuild(); + }, + context, + ), + widget.style.betweenDefaultItemPadding, + (key, value) { + widget.service.editProfile(widget.user, key, value); + }, + itemBuilder: widget.itemBuilder, + itemBuilderOptions: widget.itemBuilderOptions, + ), + if (widget.bottomActionText != null) + SizedBox( + height: widget.style.betweenDefaultItemPadding, + ), + const Spacer(), + if (widget.bottomActionText != null) + InkWell( + onTap: () { + widget.service.pageBottomAction(); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text(widget.bottomActionText!), + ), + ), + ], + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 0c7b99f..e425458 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,4 +1,4 @@ -name: profile +name: flutter_profile description: A new Flutter package project. version: 0.0.1 homepage: diff --git a/test/profile_test.dart b/test/profile_test.dart index 65b3dba..674b073 100644 --- a/test/profile_test.dart +++ b/test/profile_test.dart @@ -1 +1,94 @@ -// todo +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_profile/flutter_profile.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'test_classes/test_profile_data.dart'; +import 'test_classes/test_profile_service.dart'; + +void main() { + testWidgets('Profile page with preset values', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Material( + child: ProfilePage( + user: User( + 'Firstname', + 'Lastname', + Uint8List.fromList([]), + TestProfileData(email: 'test@email.com'), + ), + service: TestProfileService(), + ), + ), + ), + ); + + final firstNameFinder = find.text('Firstname'); + final lastNameFinder = find.text('Lastname'); + final emailFinder = find.text('test@email.com'); + + expect(firstNameFinder, findsOneWidget); + expect(lastNameFinder, findsOneWidget); + expect(emailFinder, findsOneWidget); + }); + + testWidgets('Profile page without preset value', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Material( + child: ProfilePage( + user: User( + null, + null, + null, + TestProfileData(email: null), + ), + service: TestProfileService(), + ), + ), + ), + ); + }); + + testWidgets('Profile page with preset value and changing them', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Material( + child: ProfilePage( + user: User( + 'Firstname', + 'Lastname', + null, + TestProfileData(email: 'test@email.com'), + ), + service: TestProfileService(), + ), + ), + ), + ); + + await tester.enterText(find.text('Firstname'), 'FirstEditedName'); + await tester.testTextInput.receiveAction(TextInputAction.send); + await tester.pump(); + + await tester.enterText(find.text('test@email.com'), 'edited@emial.com'); + await tester.testTextInput.receiveAction(TextInputAction.send); + await tester.pump(); + + final firstNameFinder = find.text('Firstname'); + final firstNameEditedFinder = find.text('FirstEditedName'); + + final lastNameFinder = find.text('Lastname'); + + final emailFinder = find.text('test@email.com'); + final emailEditedFinder = find.text('edited@emial.com'); + + expect(firstNameFinder, findsNothing); + expect(firstNameEditedFinder, findsOneWidget); + expect(lastNameFinder, findsOneWidget); + expect(emailFinder, findsNothing); + expect(emailEditedFinder, findsOneWidget); + }); +} diff --git a/test/test_classes/test_profile_data.dart b/test/test_classes/test_profile_data.dart new file mode 100644 index 0000000..1207ab6 --- /dev/null +++ b/test/test_classes/test_profile_data.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_profile/flutter_profile.dart'; + +class TestProfileData extends ProfileData { + TestProfileData({ + this.email, + }); + + String? email; + + @override + Map mapWidget( + VoidCallback update, + BuildContext context, + ) { + return { + 'email': null, + }; + } + + @override + ProfileData fromMap(Map data) { + return TestProfileData( + email: data['email'], + ); + } + + @override + Map toMap() { + return { + 'email': email, + }; + } + + @override + ProfileData create() { + return TestProfileData(); + } +} diff --git a/test/test_classes/test_profile_service.dart b/test/test_classes/test_profile_service.dart new file mode 100644 index 0000000..1f79a75 --- /dev/null +++ b/test/test_classes/test_profile_service.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_profile/flutter_profile.dart'; + +class TestProfileService extends ProfileService { + TestProfileService(); + + @override + void pageBottomAction() {} + + @override + void editProfile( + User user, + String key, + String value, + ) {} + + @override + Future uploadImage(BuildContext context) async {} +}