diff --git a/analysis_options.yaml b/analysis_options.yaml index 61b6c4de1..7e05ff2cb 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -9,6 +9,13 @@ # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml +analyzer: + exclude: + - "**/*.g.dart" + - "**/*.freezed.dart" + errors: + invalid_annotation_target: ignore + 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` diff --git a/build.yaml b/build.yaml index cf533e1d4..cd027361e 100644 --- a/build.yaml +++ b/build.yaml @@ -5,3 +5,6 @@ targets: generate_for: include: - lib/**.codegen.dart + json_serializable: + options: + create_to_json: false diff --git a/lib/src/app.dart b/lib/src/app.dart index f35db54d1..5e55815ec 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -12,7 +12,7 @@ class MyApp extends StatelessWidget { initialPath: '/puzzles', locationBuilder: RoutesLocationBuilder( routes: { - '*': (context, state, data) => const AppScaffold(), + '*': (_, __, ___) => const AppScaffold(), }, ), ); diff --git a/lib/src/features/authentication/domain/account.codegen.dart b/lib/src/features/authentication/domain/account.codegen.dart index 15a0afb9c..13458a706 100644 --- a/lib/src/features/authentication/domain/account.codegen.dart +++ b/lib/src/features/authentication/domain/account.codegen.dart @@ -3,13 +3,32 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'account.codegen.freezed.dart'; part 'account.codegen.g.dart'; -@freezed +@Freezed(toJson: false) class Account with _$Account { factory Account({ required String id, required String username, String? title, + required bool patron, + @JsonKey(fromJson: DateTime.fromMillisecondsSinceEpoch) required DateTime createdAt, + @JsonKey(fromJson: DateTime.fromMillisecondsSinceEpoch) required DateTime seenAt, + required Profile profile, }) = _Account; factory Account.fromJson(Map json) => _$AccountFromJson(json); } + +@Freezed(toJson: false) +class Profile with _$Profile { + factory Profile({ + String? country, + String? location, + String? bio, + String? firstName, + String? lastName, + int? fideRating, + String? links, + }) = _Profile; + + factory Profile.fromJson(Map json) => _$ProfileFromJson(json); +} diff --git a/lib/src/features/authentication/domain/account.codegen.freezed.dart b/lib/src/features/authentication/domain/account.codegen.freezed.dart index d830e310f..1ccf5129a 100644 --- a/lib/src/features/authentication/domain/account.codegen.freezed.dart +++ b/lib/src/features/authentication/domain/account.codegen.freezed.dart @@ -23,8 +23,13 @@ mixin _$Account { String get id => throw _privateConstructorUsedError; String get username => throw _privateConstructorUsedError; String? get title => throw _privateConstructorUsedError; + bool get patron => throw _privateConstructorUsedError; + @JsonKey(fromJson: DateTime.fromMillisecondsSinceEpoch) + DateTime get createdAt => throw _privateConstructorUsedError; + @JsonKey(fromJson: DateTime.fromMillisecondsSinceEpoch) + DateTime get seenAt => throw _privateConstructorUsedError; + Profile get profile => throw _privateConstructorUsedError; - Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) $AccountCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -34,7 +39,18 @@ abstract class $AccountCopyWith<$Res> { factory $AccountCopyWith(Account value, $Res Function(Account) then) = _$AccountCopyWithImpl<$Res, Account>; @useResult - $Res call({String id, String username, String? title}); + $Res call( + {String id, + String username, + String? title, + bool patron, + @JsonKey(fromJson: DateTime.fromMillisecondsSinceEpoch) + DateTime createdAt, + @JsonKey(fromJson: DateTime.fromMillisecondsSinceEpoch) + DateTime seenAt, + Profile profile}); + + $ProfileCopyWith<$Res> get profile; } /// @nodoc @@ -53,6 +69,10 @@ class _$AccountCopyWithImpl<$Res, $Val extends Account> Object? id = null, Object? username = null, Object? title = freezed, + Object? patron = null, + Object? createdAt = null, + Object? seenAt = null, + Object? profile = null, }) { return _then(_value.copyWith( id: null == id @@ -67,8 +87,32 @@ class _$AccountCopyWithImpl<$Res, $Val extends Account> ? _value.title : title // ignore: cast_nullable_to_non_nullable as String?, + patron: null == patron + ? _value.patron + : patron // ignore: cast_nullable_to_non_nullable + as bool, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + seenAt: null == seenAt + ? _value.seenAt + : seenAt // ignore: cast_nullable_to_non_nullable + as DateTime, + profile: null == profile + ? _value.profile + : profile // ignore: cast_nullable_to_non_nullable + as Profile, ) as $Val); } + + @override + @pragma('vm:prefer-inline') + $ProfileCopyWith<$Res> get profile { + return $ProfileCopyWith<$Res>(_value.profile, (value) { + return _then(_value.copyWith(profile: value) as $Val); + }); + } } /// @nodoc @@ -78,7 +122,19 @@ abstract class _$$_AccountCopyWith<$Res> implements $AccountCopyWith<$Res> { __$$_AccountCopyWithImpl<$Res>; @override @useResult - $Res call({String id, String username, String? title}); + $Res call( + {String id, + String username, + String? title, + bool patron, + @JsonKey(fromJson: DateTime.fromMillisecondsSinceEpoch) + DateTime createdAt, + @JsonKey(fromJson: DateTime.fromMillisecondsSinceEpoch) + DateTime seenAt, + Profile profile}); + + @override + $ProfileCopyWith<$Res> get profile; } /// @nodoc @@ -94,6 +150,10 @@ class __$$_AccountCopyWithImpl<$Res> Object? id = null, Object? username = null, Object? title = freezed, + Object? patron = null, + Object? createdAt = null, + Object? seenAt = null, + Object? profile = null, }) { return _then(_$_Account( id: null == id @@ -108,14 +168,39 @@ class __$$_AccountCopyWithImpl<$Res> ? _value.title : title // ignore: cast_nullable_to_non_nullable as String?, + patron: null == patron + ? _value.patron + : patron // ignore: cast_nullable_to_non_nullable + as bool, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + seenAt: null == seenAt + ? _value.seenAt + : seenAt // ignore: cast_nullable_to_non_nullable + as DateTime, + profile: null == profile + ? _value.profile + : profile // ignore: cast_nullable_to_non_nullable + as Profile, )); } } /// @nodoc -@JsonSerializable() +@JsonSerializable(createToJson: false) class _$_Account implements _Account { - _$_Account({required this.id, required this.username, this.title}); + _$_Account( + {required this.id, + required this.username, + this.title, + required this.patron, + @JsonKey(fromJson: DateTime.fromMillisecondsSinceEpoch) + required this.createdAt, + @JsonKey(fromJson: DateTime.fromMillisecondsSinceEpoch) + required this.seenAt, + required this.profile}); factory _$_Account.fromJson(Map json) => _$$_AccountFromJson(json); @@ -126,10 +211,20 @@ class _$_Account implements _Account { final String username; @override final String? title; + @override + final bool patron; + @override + @JsonKey(fromJson: DateTime.fromMillisecondsSinceEpoch) + final DateTime createdAt; + @override + @JsonKey(fromJson: DateTime.fromMillisecondsSinceEpoch) + final DateTime seenAt; + @override + final Profile profile; @override String toString() { - return 'Account(id: $id, username: $username, title: $title)'; + return 'Account(id: $id, username: $username, title: $title, patron: $patron, createdAt: $createdAt, seenAt: $seenAt, profile: $profile)'; } @override @@ -140,32 +235,37 @@ class _$_Account implements _Account { (identical(other.id, id) || other.id == id) && (identical(other.username, username) || other.username == username) && - (identical(other.title, title) || other.title == title)); + (identical(other.title, title) || other.title == title) && + (identical(other.patron, patron) || other.patron == patron) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.seenAt, seenAt) || other.seenAt == seenAt) && + (identical(other.profile, profile) || other.profile == profile)); } @JsonKey(ignore: true) @override - int get hashCode => Object.hash(runtimeType, id, username, title); + int get hashCode => Object.hash( + runtimeType, id, username, title, patron, createdAt, seenAt, profile); @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') _$$_AccountCopyWith<_$_Account> get copyWith => __$$_AccountCopyWithImpl<_$_Account>(this, _$identity); - - @override - Map toJson() { - return _$$_AccountToJson( - this, - ); - } } abstract class _Account implements Account { factory _Account( {required final String id, required final String username, - final String? title}) = _$_Account; + final String? title, + required final bool patron, + @JsonKey(fromJson: DateTime.fromMillisecondsSinceEpoch) + required final DateTime createdAt, + @JsonKey(fromJson: DateTime.fromMillisecondsSinceEpoch) + required final DateTime seenAt, + required final Profile profile}) = _$_Account; factory _Account.fromJson(Map json) = _$_Account.fromJson; @@ -176,7 +276,269 @@ abstract class _Account implements Account { @override String? get title; @override + bool get patron; + @override + @JsonKey(fromJson: DateTime.fromMillisecondsSinceEpoch) + DateTime get createdAt; + @override + @JsonKey(fromJson: DateTime.fromMillisecondsSinceEpoch) + DateTime get seenAt; + @override + Profile get profile; + @override @JsonKey(ignore: true) _$$_AccountCopyWith<_$_Account> get copyWith => throw _privateConstructorUsedError; } + +Profile _$ProfileFromJson(Map json) { + return _Profile.fromJson(json); +} + +/// @nodoc +mixin _$Profile { + String? get country => throw _privateConstructorUsedError; + String? get location => throw _privateConstructorUsedError; + String? get bio => throw _privateConstructorUsedError; + String? get firstName => throw _privateConstructorUsedError; + String? get lastName => throw _privateConstructorUsedError; + int? get fideRating => throw _privateConstructorUsedError; + String? get links => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $ProfileCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ProfileCopyWith<$Res> { + factory $ProfileCopyWith(Profile value, $Res Function(Profile) then) = + _$ProfileCopyWithImpl<$Res, Profile>; + @useResult + $Res call( + {String? country, + String? location, + String? bio, + String? firstName, + String? lastName, + int? fideRating, + String? links}); +} + +/// @nodoc +class _$ProfileCopyWithImpl<$Res, $Val extends Profile> + implements $ProfileCopyWith<$Res> { + _$ProfileCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? country = freezed, + Object? location = freezed, + Object? bio = freezed, + Object? firstName = freezed, + Object? lastName = freezed, + Object? fideRating = freezed, + Object? links = freezed, + }) { + return _then(_value.copyWith( + country: freezed == country + ? _value.country + : country // ignore: cast_nullable_to_non_nullable + as String?, + location: freezed == location + ? _value.location + : location // ignore: cast_nullable_to_non_nullable + as String?, + bio: freezed == bio + ? _value.bio + : bio // ignore: cast_nullable_to_non_nullable + as String?, + firstName: freezed == firstName + ? _value.firstName + : firstName // ignore: cast_nullable_to_non_nullable + as String?, + lastName: freezed == lastName + ? _value.lastName + : lastName // ignore: cast_nullable_to_non_nullable + as String?, + fideRating: freezed == fideRating + ? _value.fideRating + : fideRating // ignore: cast_nullable_to_non_nullable + as int?, + links: freezed == links + ? _value.links + : links // ignore: cast_nullable_to_non_nullable + as String?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_ProfileCopyWith<$Res> implements $ProfileCopyWith<$Res> { + factory _$$_ProfileCopyWith( + _$_Profile value, $Res Function(_$_Profile) then) = + __$$_ProfileCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String? country, + String? location, + String? bio, + String? firstName, + String? lastName, + int? fideRating, + String? links}); +} + +/// @nodoc +class __$$_ProfileCopyWithImpl<$Res> + extends _$ProfileCopyWithImpl<$Res, _$_Profile> + implements _$$_ProfileCopyWith<$Res> { + __$$_ProfileCopyWithImpl(_$_Profile _value, $Res Function(_$_Profile) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? country = freezed, + Object? location = freezed, + Object? bio = freezed, + Object? firstName = freezed, + Object? lastName = freezed, + Object? fideRating = freezed, + Object? links = freezed, + }) { + return _then(_$_Profile( + country: freezed == country + ? _value.country + : country // ignore: cast_nullable_to_non_nullable + as String?, + location: freezed == location + ? _value.location + : location // ignore: cast_nullable_to_non_nullable + as String?, + bio: freezed == bio + ? _value.bio + : bio // ignore: cast_nullable_to_non_nullable + as String?, + firstName: freezed == firstName + ? _value.firstName + : firstName // ignore: cast_nullable_to_non_nullable + as String?, + lastName: freezed == lastName + ? _value.lastName + : lastName // ignore: cast_nullable_to_non_nullable + as String?, + fideRating: freezed == fideRating + ? _value.fideRating + : fideRating // ignore: cast_nullable_to_non_nullable + as int?, + links: freezed == links + ? _value.links + : links // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc +@JsonSerializable(createToJson: false) +class _$_Profile implements _Profile { + _$_Profile( + {this.country, + this.location, + this.bio, + this.firstName, + this.lastName, + this.fideRating, + this.links}); + + factory _$_Profile.fromJson(Map json) => + _$$_ProfileFromJson(json); + + @override + final String? country; + @override + final String? location; + @override + final String? bio; + @override + final String? firstName; + @override + final String? lastName; + @override + final int? fideRating; + @override + final String? links; + + @override + String toString() { + return 'Profile(country: $country, location: $location, bio: $bio, firstName: $firstName, lastName: $lastName, fideRating: $fideRating, links: $links)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_Profile && + (identical(other.country, country) || other.country == country) && + (identical(other.location, location) || + other.location == location) && + (identical(other.bio, bio) || other.bio == bio) && + (identical(other.firstName, firstName) || + other.firstName == firstName) && + (identical(other.lastName, lastName) || + other.lastName == lastName) && + (identical(other.fideRating, fideRating) || + other.fideRating == fideRating) && + (identical(other.links, links) || other.links == links)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, country, location, bio, + firstName, lastName, fideRating, links); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_ProfileCopyWith<_$_Profile> get copyWith => + __$$_ProfileCopyWithImpl<_$_Profile>(this, _$identity); +} + +abstract class _Profile implements Profile { + factory _Profile( + {final String? country, + final String? location, + final String? bio, + final String? firstName, + final String? lastName, + final int? fideRating, + final String? links}) = _$_Profile; + + factory _Profile.fromJson(Map json) = _$_Profile.fromJson; + + @override + String? get country; + @override + String? get location; + @override + String? get bio; + @override + String? get firstName; + @override + String? get lastName; + @override + int? get fideRating; + @override + String? get links; + @override + @JsonKey(ignore: true) + _$$_ProfileCopyWith<_$_Profile> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/src/features/authentication/domain/account.codegen.g.dart b/lib/src/features/authentication/domain/account.codegen.g.dart index 6ed7f42b4..910460642 100644 --- a/lib/src/features/authentication/domain/account.codegen.g.dart +++ b/lib/src/features/authentication/domain/account.codegen.g.dart @@ -10,11 +10,18 @@ _$_Account _$$_AccountFromJson(Map json) => _$_Account( id: json['id'] as String, username: json['username'] as String, title: json['title'] as String?, + patron: json['patron'] as bool, + createdAt: DateTime.fromMillisecondsSinceEpoch(json['createdAt'] as int), + seenAt: DateTime.fromMillisecondsSinceEpoch(json['seenAt'] as int), + profile: Profile.fromJson(json['profile'] as Map), ); -Map _$$_AccountToJson(_$_Account instance) => - { - 'id': instance.id, - 'username': instance.username, - 'title': instance.title, - }; +_$_Profile _$$_ProfileFromJson(Map json) => _$_Profile( + country: json['country'] as String?, + location: json['location'] as String?, + bio: json['bio'] as String?, + firstName: json['firstName'] as String?, + lastName: json['lastName'] as String?, + fideRating: json['fideRating'] as int?, + links: json['links'] as String?, + ); diff --git a/lib/src/features/authentication/presentation/auth_widget.dart b/lib/src/features/authentication/presentation/auth_widget.dart index 9afeda7a7..903f4a7ee 100644 --- a/lib/src/features/authentication/presentation/auth_widget.dart +++ b/lib/src/features/authentication/presentation/auth_widget.dart @@ -5,6 +5,8 @@ import '../../../utils/extensions.dart'; import '../data/auth_repository.dart'; import './auth_widget_controller.dart'; +import '../../profile/presentation/profile_screen.dart'; + enum AccountMenu { logout } class AuthWidget extends ConsumerWidget { @@ -20,25 +22,16 @@ class AuthWidget extends ConsumerWidget { ); return authState.maybeWhen( data: (account) => account != null - ? PopupMenuButton( + ? IconButton( icon: const Icon(Icons.person), - onSelected: (AccountMenu item) { - switch (item) { - case AccountMenu.logout: - if (!uiState.isLoading) { - ref.read(authControllerProvider.notifier).signOut(); - } - break; - } + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const ProfileScreen(), + fullscreenDialog: true, + ), + ); }, - itemBuilder: (BuildContext context) => >[ - PopupMenuItem( - value: AccountMenu.logout, - child: uiState.isLoading - ? const CircularProgressIndicator() - : const Text('Sign out'), - ), - ], ) : TextButton( onPressed: uiState.isLoading diff --git a/lib/src/features/profile/presentation/profile_screen.dart b/lib/src/features/profile/presentation/profile_screen.dart new file mode 100644 index 000000000..57d35d796 --- /dev/null +++ b/lib/src/features/profile/presentation/profile_screen.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import './profile_screen_controller.dart'; + +class ProfileScreen extends ConsumerWidget { + const ProfileScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(profileScreenControllerProvider); + + return Scaffold( + appBar: AppBar(), + body: Center( + child: ElevatedButton( + onPressed: state.isLoading + ? null + : () { + ref.read(profileScreenControllerProvider.notifier).signOut(); + Navigator.of(context).pop(); + }, + child: const Text('Sign out'), + ), + ), + ); + } +} diff --git a/lib/src/features/profile/presentation/profile_screen_controller.dart b/lib/src/features/profile/presentation/profile_screen_controller.dart new file mode 100644 index 000000000..4639a3330 --- /dev/null +++ b/lib/src/features/profile/presentation/profile_screen_controller.dart @@ -0,0 +1,22 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../authentication/data/auth_repository.dart'; + +class ProfileScreenController extends StateNotifier> { + ProfileScreenController({required this.authRepository}) : super(const AsyncData(null)); + + final AuthRepository authRepository; + + Future signOut() async { + state = const AsyncLoading(); + state = (await authRepository.signOut().run()) + .match((error) => AsyncValue.error(error.message, error.stackTrace), AsyncValue.data); + } +} + +final profileScreenControllerProvider = + StateNotifierProvider.autoDispose>((ref) { + return ProfileScreenController( + authRepository: ref.watch(authRepositoryProvider), + ); +}); diff --git a/lib/src/routing.dart b/lib/src/routing.dart index 080797cf6..88b09216b 100644 --- a/lib/src/routing.dart +++ b/lib/src/routing.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'features/authentication/data/auth_repository.dart'; import 'features/authentication/presentation/auth_widget.dart'; +import 'features/profile/presentation/profile_screen.dart'; class PuzzlesLocation extends BeamLocation { PuzzlesLocation(RouteInformation routeInformation) : super(routeInformation);