From 001d9a8e081a0f0de1b07d5813a9b745fee0cdbc Mon Sep 17 00:00:00 2001 From: Ziedelth Date: Sun, 19 May 2024 23:51:21 +0200 Subject: [PATCH] Associate email --- lib/components/accounts/account_card.dart | 41 +++ lib/components/accounts/associate_email.dart | 156 +++++++++++ lib/components/accounts/edit_identifier.dart | 81 ++++++ lib/controllers/member_controller.dart | 34 ++- lib/dtos/member_dto.dart | 1 + lib/dtos/member_dto.freezed.dart | 23 +- lib/dtos/member_dto.g.dart | 2 + lib/l10n/app_fr.arb | 13 +- lib/main.dart | 12 + lib/utils/constant.dart | 8 +- lib/utils/http_request.dart | 22 +- lib/views/account_settings_view.dart | 114 ++++++++ lib/views/account_view.dart | 258 +++---------------- 13 files changed, 526 insertions(+), 239 deletions(-) create mode 100644 lib/components/accounts/account_card.dart create mode 100644 lib/components/accounts/associate_email.dart create mode 100644 lib/components/accounts/edit_identifier.dart create mode 100644 lib/views/account_settings_view.dart diff --git a/lib/components/accounts/account_card.dart b/lib/components/accounts/account_card.dart new file mode 100644 index 0000000..faf5d8e --- /dev/null +++ b/lib/components/accounts/account_card.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +class AccountCard extends StatelessWidget { + final String label; + final String value; + + const AccountCard({ + super.key, + required this.label, + required this.value, + }); + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Text( + value, + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + Text( + label, + style: const TextStyle(color: Colors.white), + ), + ], + ), + ), + ); + } +} diff --git a/lib/components/accounts/associate_email.dart b/lib/components/accounts/associate_email.dart new file mode 100644 index 0000000..d28996b --- /dev/null +++ b/lib/components/accounts/associate_email.dart @@ -0,0 +1,156 @@ +import 'package:application/controllers/member_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vibration/vibration.dart'; + +class AssociateEmail extends StatefulWidget { + const AssociateEmail({super.key}); + + @override + State createState() => _AssociateEmailState(); +} + +class _AssociateEmailState extends State { + final TextEditingController _emailController = TextEditingController(); + final TextEditingController _codeController = TextEditingController(); + bool _isEmailInError = false; + bool _isCodeInError = false; + bool _isLoading = false; + String? _actionUuid; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.associateEmail), + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + decoration: InputDecoration( + labelText: AppLocalizations.of(context)!.email, + errorText: _isEmailInError + ? AppLocalizations.of(context)!.invalidEmail + : null, + ), + controller: _emailController, + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: TextField( + decoration: InputDecoration( + labelText: AppLocalizations.of(context)!.code, + errorText: _isCodeInError + ? AppLocalizations.of(context)!.invalidCode + : null, + ), + controller: _codeController, + ), + ), + const SizedBox(width: 16), + ElevatedButton( + onPressed: _isLoading + ? null + : () { + saveEmail(context); + }, + child: Text(AppLocalizations.of(context)!.sendCode), + ), + ], + ), + const SizedBox(height: 32), + ElevatedButton( + onPressed: _isLoading + ? null + : () { + validateAction(context); + }, + child: Text(AppLocalizations.of(context)!.save), + ), + ], + ), + ), + ); + } + + Future saveEmail(BuildContext context) async { + if (_emailController.text.isEmpty) { + Vibration.vibrate(duration: 200, amplitude: 255); + return; + } + + try { + // Verify if the email is valid (^[A-Za-z0-9+_.-]+@(.+)$) + if (!RegExp(r'^[A-Za-z0-9+_.-]+@(.+)$').hasMatch(_emailController.text)) { + Vibration.vibrate(duration: 200, amplitude: 255); + + if (context.mounted) { + setState(() { + _isEmailInError = true; + }); + } + + return; + } + + setState(() { + _isEmailInError = false; + _isLoading = true; + }); + + _actionUuid = + await MemberController.instance.associateEmail(_emailController.text); + } catch (e) { + Vibration.vibrate(duration: 200, amplitude: 255); + + if (context.mounted) { + setState(() { + _isEmailInError = true; + }); + } + } finally { + if (context.mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + Future validateAction(BuildContext context) async { + if (_codeController.text.isEmpty || _actionUuid == null) { + Vibration.vibrate(duration: 200, amplitude: 255); + return; + } + + try { + await MemberController.instance + .validateAction(_actionUuid!, _codeController.text); + + if (context.mounted) { + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'OK', + textAlign: TextAlign.center, + ), + ), + ); + } + } catch (e) { + Vibration.vibrate(duration: 200, amplitude: 255); + + if (context.mounted) { + setState(() { + _isCodeInError = true; + }); + } + } + } +} diff --git a/lib/components/accounts/edit_identifier.dart b/lib/components/accounts/edit_identifier.dart new file mode 100644 index 0000000..243c09d --- /dev/null +++ b/lib/components/accounts/edit_identifier.dart @@ -0,0 +1,81 @@ +import 'package:application/controllers/member_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vibration/vibration.dart'; + +class EditIdentifier extends StatelessWidget { + final TextEditingController _controller = TextEditingController(); + + EditIdentifier({super.key}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + AppLocalizations.of(context)!.enterNewIdentifier, + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + TextField( + decoration: InputDecoration( + labelText: AppLocalizations.of(context)!.identifier, + ), + controller: _controller, + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(AppLocalizations.of(context)!.cancel), + ), + ElevatedButton( + onPressed: () { + saveIdentifier(context); + }, + child: Text(AppLocalizations.of(context)!.save), + ), + ], + ); + } + + Future saveIdentifier(BuildContext context) async { + if (_controller.text.isEmpty) { + Vibration.vibrate(duration: 200, amplitude: 255); + return; + } + + final oldIdentifier = MemberController.instance.identifier; + + try { + await MemberController.instance.testLogin(_controller.text); + await MemberController.instance.login(identifier: _controller.text); + } catch (e) { + Vibration.vibrate(duration: 200, amplitude: 255); + + if (MemberController.instance.identifier != oldIdentifier) { + await MemberController.instance.login(identifier: oldIdentifier); + } + + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + AppLocalizations.of(context)!.invalidIdentifier, + textAlign: TextAlign.center, + ), + ), + ); + } + } finally { + if (context.mounted) { + Navigator.of(context).pop(); + } + } + } +} diff --git a/lib/controllers/member_controller.dart b/lib/controllers/member_controller.dart index 74641e4..7ec2bb0 100644 --- a/lib/controllers/member_controller.dart +++ b/lib/controllers/member_controller.dart @@ -46,7 +46,7 @@ class MemberController { } Future register() async { - final response = await HttpRequest().post('/v1/members/private-register'); + final response = await HttpRequest().post('/v1/members/register'); if (response.statusCode != 201) { throw const HttpException('Failed to register'); @@ -60,7 +60,7 @@ class MemberController { Future testLogin(String identifier) async { final response = - await HttpRequest().post('/v1/members/private-login', body: identifier); + await HttpRequest().post('/v1/members/login', body: identifier); if (response.statusCode == 404) { throw const HttpException('Failed to login, identifier not found'); @@ -85,6 +85,36 @@ class MemberController { member = MemberDto.fromJson(json); } + Future associateEmail(String email) async { + final response = await HttpRequest().post( + '/v1/members/associate-email', + token: member!.token, + body: email, + timeout: false, + ); + + if (response.statusCode != 201) { + throw const HttpException('Failed to associate email'); + } + + final json = jsonDecode(utf8.decode(response.bodyBytes)); + return json['uuid'] as String; + } + + Future validateAction(String uuid, String code) async { + final response = await HttpRequest().post( + '/v1/member-actions/validate?uuid=$uuid', + token: member!.token, + body: code, + ); + + if (response.statusCode != 200) { + throw const HttpException('Failed to validate action'); + } + + await login(); + } + Future followAnime(AnimeDto anime) async { final response = await HttpRequest().put( '/v1/members/animes', diff --git a/lib/dtos/member_dto.dart b/lib/dtos/member_dto.dart index 933dc03..018f75c 100644 --- a/lib/dtos/member_dto.dart +++ b/lib/dtos/member_dto.dart @@ -11,6 +11,7 @@ class MemberDto with _$MemberDto { required String creationDateTime, required String lastUpdateDateTime, required bool isPrivate, + required String? email, required List followedAnimes, required List followedEpisodes, required int totalDuration, diff --git a/lib/dtos/member_dto.freezed.dart b/lib/dtos/member_dto.freezed.dart index 8cbe9eb..77aa972 100644 --- a/lib/dtos/member_dto.freezed.dart +++ b/lib/dtos/member_dto.freezed.dart @@ -25,6 +25,7 @@ mixin _$MemberDto { String get creationDateTime => throw _privateConstructorUsedError; String get lastUpdateDateTime => throw _privateConstructorUsedError; bool get isPrivate => throw _privateConstructorUsedError; + String? get email => throw _privateConstructorUsedError; List get followedAnimes => throw _privateConstructorUsedError; List get followedEpisodes => throw _privateConstructorUsedError; int get totalDuration => throw _privateConstructorUsedError; @@ -46,6 +47,7 @@ abstract class $MemberDtoCopyWith<$Res> { String creationDateTime, String lastUpdateDateTime, bool isPrivate, + String? email, List followedAnimes, List followedEpisodes, int totalDuration}); @@ -69,6 +71,7 @@ class _$MemberDtoCopyWithImpl<$Res, $Val extends MemberDto> Object? creationDateTime = null, Object? lastUpdateDateTime = null, Object? isPrivate = null, + Object? email = freezed, Object? followedAnimes = null, Object? followedEpisodes = null, Object? totalDuration = null, @@ -94,6 +97,10 @@ class _$MemberDtoCopyWithImpl<$Res, $Val extends MemberDto> ? _value.isPrivate : isPrivate // ignore: cast_nullable_to_non_nullable as bool, + email: freezed == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String?, followedAnimes: null == followedAnimes ? _value.followedAnimes : followedAnimes // ignore: cast_nullable_to_non_nullable @@ -124,6 +131,7 @@ abstract class _$$MemberDtoImplCopyWith<$Res> String creationDateTime, String lastUpdateDateTime, bool isPrivate, + String? email, List followedAnimes, List followedEpisodes, int totalDuration}); @@ -145,6 +153,7 @@ class __$$MemberDtoImplCopyWithImpl<$Res> Object? creationDateTime = null, Object? lastUpdateDateTime = null, Object? isPrivate = null, + Object? email = freezed, Object? followedAnimes = null, Object? followedEpisodes = null, Object? totalDuration = null, @@ -170,6 +179,10 @@ class __$$MemberDtoImplCopyWithImpl<$Res> ? _value.isPrivate : isPrivate // ignore: cast_nullable_to_non_nullable as bool, + email: freezed == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String?, followedAnimes: null == followedAnimes ? _value.followedAnimes : followedAnimes // ignore: cast_nullable_to_non_nullable @@ -195,6 +208,7 @@ class _$MemberDtoImpl implements _MemberDto { required this.creationDateTime, required this.lastUpdateDateTime, required this.isPrivate, + required this.email, required this.followedAnimes, required this.followedEpisodes, required this.totalDuration}); @@ -213,6 +227,8 @@ class _$MemberDtoImpl implements _MemberDto { @override final bool isPrivate; @override + final String? email; + @override final List followedAnimes; @override final List followedEpisodes; @@ -221,7 +237,7 @@ class _$MemberDtoImpl implements _MemberDto { @override String toString() { - return 'MemberDto(uuid: $uuid, token: $token, creationDateTime: $creationDateTime, lastUpdateDateTime: $lastUpdateDateTime, isPrivate: $isPrivate, followedAnimes: $followedAnimes, followedEpisodes: $followedEpisodes, totalDuration: $totalDuration)'; + return 'MemberDto(uuid: $uuid, token: $token, creationDateTime: $creationDateTime, lastUpdateDateTime: $lastUpdateDateTime, isPrivate: $isPrivate, email: $email, followedAnimes: $followedAnimes, followedEpisodes: $followedEpisodes, totalDuration: $totalDuration)'; } @override @@ -237,6 +253,7 @@ class _$MemberDtoImpl implements _MemberDto { other.lastUpdateDateTime == lastUpdateDateTime) && (identical(other.isPrivate, isPrivate) || other.isPrivate == isPrivate) && + (identical(other.email, email) || other.email == email) && const DeepCollectionEquality() .equals(other.followedAnimes, followedAnimes) && const DeepCollectionEquality() @@ -254,6 +271,7 @@ class _$MemberDtoImpl implements _MemberDto { creationDateTime, lastUpdateDateTime, isPrivate, + email, const DeepCollectionEquality().hash(followedAnimes), const DeepCollectionEquality().hash(followedEpisodes), totalDuration); @@ -279,6 +297,7 @@ abstract class _MemberDto implements MemberDto { required final String creationDateTime, required final String lastUpdateDateTime, required final bool isPrivate, + required final String? email, required final List followedAnimes, required final List followedEpisodes, required final int totalDuration}) = _$MemberDtoImpl; @@ -297,6 +316,8 @@ abstract class _MemberDto implements MemberDto { @override bool get isPrivate; @override + String? get email; + @override List get followedAnimes; @override List get followedEpisodes; diff --git a/lib/dtos/member_dto.g.dart b/lib/dtos/member_dto.g.dart index c9f0367..cdc1b46 100644 --- a/lib/dtos/member_dto.g.dart +++ b/lib/dtos/member_dto.g.dart @@ -13,6 +13,7 @@ _$MemberDtoImpl _$$MemberDtoImplFromJson(Map json) => creationDateTime: json['creationDateTime'] as String, lastUpdateDateTime: json['lastUpdateDateTime'] as String, isPrivate: json['isPrivate'] as bool, + email: json['email'] as String?, followedAnimes: (json['followedAnimes'] as List) .map((e) => e as String) .toList(), @@ -29,6 +30,7 @@ Map _$$MemberDtoImplToJson(_$MemberDtoImpl instance) => 'creationDateTime': instance.creationDateTime, 'lastUpdateDateTime': instance.lastUpdateDateTime, 'isPrivate': instance.isPrivate, + 'email': instance.email, 'followedAnimes': instance.followedAnimes, 'followedEpisodes': instance.followedEpisodes, 'totalDuration': instance.totalDuration, diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index d8f8bb8..bc9d778 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -33,9 +33,14 @@ "showLess": "Voir moins", "anonymousAccount": "Compte anonyme", - "createAccount": "Créer un compte", + "associateEmail": "Associer un email", + "invalidEmail": "Email invalide", + "code": "Code", + "invalidCode": "Code invalide", + "sendCode": "Envoyer le code", "member": "Membre", "account": "Compte", + "email": "Email", "identifier": "Identifiant", "identifierSubtitle": "Sauvegardez-le pour récupérer vos données", "noIdentifier": "Aucun identifiant", @@ -44,11 +49,13 @@ "cancel": "Annuler", "save": "Sauvegarder", "invalidIdentifier": "Identifiant invalide", + "settings": "Paramètres", + "noEmail": "Pas d'email associé", "anonymousWarningTitle": "Avertissement sur la sauvegarde de données", "anonymousWarningContent1": "Nous sauvegardons vos données de manière anonyme sur nos serveurs. Chaque compte est associé à un identifiant unique. Cependant, il est important de noter que si vous perdez cet identifiant, vous risquez de perdre l'accès à toutes vos données.", - "anonymousWarningContent2": "Vous trouverez votre identifiant unique dans la section \"Compte\" de l'application.", - "anonymousWarningContent3": "Pour éviter toute perte de données, nous vous recommandons vivement de créer un compte sécurisé en enregistrant une adresse e-mail et un mot de passe. Cela vous permettra de récupérer facilement vos données en cas de besoin et de sécuriser votre compte contre toute perte accidentelle.", + "anonymousWarningContent2": "Vous trouverez votre identifiant unique dans la section \"Compte\" dans les paramètres de l'application.", + "anonymousWarningContent3": "Pour éviter toute perte de données, nous vous recommandons vivement d'associer une adresse e-mail. Cela vous permettra de récupérer facilement vos données en cas de besoin et de sécuriser votre compte contre toute perte accidentelle.", "oops": "OOPS", "noAnimeToday": "Il n'y a pas d'animé aujourd'hui !", diff --git a/lib/main.dart b/lib/main.dart index 0e63a7a..5abc0a2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,7 @@ import 'package:application/controllers/member_controller.dart'; import 'package:application/controllers/missed_anime_controller.dart'; import 'package:application/dtos/missed_anime_dto.dart'; import 'package:application/utils/constant.dart'; +import 'package:application/views/account_settings_view.dart'; import 'package:application/views/account_view.dart'; import 'package:application/views/calendar_view.dart'; import 'package:application/views/home_view.dart'; @@ -217,6 +218,17 @@ class _MyHomePageState extends State { : Icons.filter_alt_off, ), ), + if (_currentIndex == 3) + IconButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const AccountSettingsView(), + ), + ); + }, + icon: const Icon(Icons.settings), + ), IconButton( onPressed: () { Navigator.of(context).push( diff --git a/lib/utils/constant.dart b/lib/utils/constant.dart index 3fe3036..52e2d44 100644 --- a/lib/utils/constant.dart +++ b/lib/utils/constant.dart @@ -1,7 +1,7 @@ class Constant { - static const apiUrl = 'https://api.shikkanime.fr'; - static const baseUrl = 'https://www.shikkanime.fr'; + // static const apiUrl = 'https://api.shikkanime.fr'; + // static const baseUrl = 'https://www.shikkanime.fr'; - // static const apiUrl = 'http://192.168.1.71:37100/api'; - // static const baseUrl = 'http://192.168.1.71:37100'; + static const apiUrl = 'http://192.168.1.71:37100/api'; + static const baseUrl = 'http://192.168.1.71:37100'; } diff --git a/lib/utils/http_request.dart b/lib/utils/http_request.dart index 547abe8..b2b53ef 100644 --- a/lib/utils/http_request.dart +++ b/lib/utils/http_request.dart @@ -44,6 +44,7 @@ class HttpRequest { String endpoint, { String? token, Object? body, + bool timeout = true, }) async { final headers = {}; @@ -51,16 +52,19 @@ class HttpRequest { headers['Authorization'] = 'Bearer $token'; } - final response = await http - .post( - Uri.parse( - Constant.apiUrl + endpoint, - ), - headers: headers, - body: body, - ) - .timeout(_timeout); + var request = http.post( + Uri.parse( + Constant.apiUrl + endpoint, + ), + headers: headers, + body: body, + ); + + if (timeout) { + request = request.timeout(_timeout); + } + final response = await request; return response; } diff --git a/lib/views/account_settings_view.dart b/lib/views/account_settings_view.dart new file mode 100644 index 0000000..52d7853 --- /dev/null +++ b/lib/views/account_settings_view.dart @@ -0,0 +1,114 @@ +import 'package:application/components/accounts/associate_email.dart'; +import 'package:application/components/accounts/edit_identifier.dart'; +import 'package:application/controllers/member_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vibration/vibration.dart'; + +class AccountSettingsView extends StatelessWidget { + const AccountSettingsView({super.key}); + + @override + Widget build(BuildContext context) { + final appLocalizations = AppLocalizations.of(context)!; + + return Scaffold( + appBar: AppBar( + title: Text(appLocalizations.settings), + ), + body: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + appLocalizations.account, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.grey, + ), + ), + ), + ListTile( + title: Text(appLocalizations.identifier), + subtitle: Text(appLocalizations.identifierSubtitle), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + MemberController.instance.identifier ?? + appLocalizations.noIdentifier, + style: const TextStyle(fontSize: 14), + ), + const SizedBox(width: 16), + GestureDetector( + onTap: () { + showAdaptiveDialog( + context: context, + builder: (context) => EditIdentifier(), + ); + }, + child: const Icon(Icons.edit), + ), + ], + ), + onTap: () { + if (MemberController.instance.identifier == null) { + return; + } + + // Copy to clipboard + Clipboard.setData( + ClipboardData(text: MemberController.instance.identifier!), + ); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.check, color: Colors.white), + const SizedBox(width: 8), + Text(appLocalizations.identifierCopied), + ], + ), + ), + ); + + Vibration.vibrate(duration: 200, amplitude: 255); + }, + ), + ListTile( + title: Text(appLocalizations.email), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + MemberController.instance.member?.email ?? + appLocalizations.noEmail, + style: const TextStyle(fontSize: 14), + ), + const SizedBox(width: 16), + GestureDetector( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const AssociateEmail(), + ), + ); + }, + child: const Icon(Icons.edit), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/account_view.dart b/lib/views/account_view.dart index f014d6b..e54e3d0 100644 --- a/lib/views/account_view.dart +++ b/lib/views/account_view.dart @@ -1,7 +1,7 @@ +import 'package:application/components/accounts/account_card.dart'; +import 'package:application/components/accounts/associate_email.dart'; import 'package:application/controllers/member_controller.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:vibration/vibration.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class AccountView extends StatelessWidget { @@ -9,6 +9,9 @@ class AccountView extends StatelessWidget { @override Widget build(BuildContext context) { + final appLocalizations = AppLocalizations.of(context); + final member = MemberController.instance.member; + return ListView( children: [ Row( @@ -49,7 +52,7 @@ class AccountView extends StatelessWidget { direction: Axis.horizontal, children: [ Text( - AppLocalizations.of(context)!.anonymousAccount, + appLocalizations!.anonymousAccount, style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, @@ -68,18 +71,19 @@ class AccountView extends StatelessWidget { context: context, builder: (context) { return AlertDialog( - title: Text(AppLocalizations.of(context)! - .anonymousWarningTitle), + title: Text( + appLocalizations.anonymousWarningTitle, + ), content: SingleChildScrollView( child: Column( children: [ - Text(AppLocalizations.of(context)! + Text(appLocalizations .anonymousWarningContent1), const SizedBox(height: 16), - Text(AppLocalizations.of(context)! + Text(appLocalizations .anonymousWarningContent2), const SizedBox(height: 16), - Text(AppLocalizations.of(context)! + Text(appLocalizations .anonymousWarningContent3), ], ), @@ -92,42 +96,25 @@ class AccountView extends StatelessWidget { ], ), Text( - AppLocalizations.of(context)!.member, + appLocalizations.member, style: const TextStyle( fontSize: 16, color: Colors.grey, ), ), - const SizedBox(height: 8), - ElevatedButton( - onPressed: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - content: const Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'WORK IN PROGRESS', - textAlign: TextAlign.center, - ), - ], - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text(AppLocalizations.of(context)!.cancel), - ), - ], - ); - }, - ); - }, - child: Text(AppLocalizations.of(context)!.createAccount), - ), + if (member?.email == null) ...[ + const SizedBox(height: 8), + ElevatedButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const AssociateEmail(), + ), + ); + }, + child: Text(appLocalizations.associateEmail), + ), + ], ], ), ], @@ -139,20 +126,15 @@ class AccountView extends StatelessWidget { children: [ Expanded( child: AccountCard( - label: AppLocalizations.of(context)!.animesAdded, - value: MemberController.instance.member?.followedAnimes.length - .toString() ?? - '0', + label: appLocalizations.animesAdded, + value: member?.followedAnimes.length.toString() ?? '0', ), ), const SizedBox(width: 16), Expanded( child: AccountCard( - label: AppLocalizations.of(context)!.episodesWatched, - value: MemberController - .instance.member?.followedEpisodes.length - .toString() ?? - '0', + label: appLocalizations.episodesWatched, + value: member?.followedEpisodes.length.toString() ?? '0', ), ), ], @@ -165,7 +147,7 @@ class AccountView extends StatelessWidget { children: [ Expanded( child: AccountCard( - label: AppLocalizations.of(context)!.watchTime, + label: appLocalizations.watchTime, value: MemberController.instance.buildTotalDuration(), ), ), @@ -173,186 +155,22 @@ class AccountView extends StatelessWidget { ), ), const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), child: Text( - AppLocalizations.of(context)!.account, - style: const TextStyle( + 'Nos recommandations', + style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.grey, ), ), ), - ListTile( - title: Text(AppLocalizations.of(context)!.identifier), - subtitle: Text(AppLocalizations.of(context)!.identifierSubtitle), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - MemberController.instance.identifier ?? - AppLocalizations.of(context)!.noIdentifier, - style: const TextStyle(fontSize: 14), - ), - const SizedBox(width: 16), - GestureDetector( - onTap: () { - showAdaptiveDialog( - context: context, - builder: (context) { - return EditIdentifier(); - }, - ); - }, - child: const Icon(Icons.edit), - ), - ], - ), - onTap: () { - if (MemberController.instance.identifier == null) { - return; - } - - // Copy to clipboard - Clipboard.setData( - ClipboardData(text: MemberController.instance.identifier!), - ); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.check, color: Colors.white), - const SizedBox(width: 8), - Text(AppLocalizations.of(context)!.identifierCopied), - ], - ), - ), - ); - - Vibration.vibrate(duration: 200, amplitude: 255); - }, + const ListTile( + title: Text('Bah non, toujours pas de recommandations.'), + subtitle: Text("Mais ça va venir, promis ! (Peut-être d'ici 2027)"), ), ], ); } } - -class AccountCard extends StatelessWidget { - final String label; - final String value; - - const AccountCard({ - super.key, - required this.label, - required this.value, - }); - - @override - Widget build(BuildContext context) { - return DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - borderRadius: BorderRadius.circular(8), - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Text( - value, - style: const TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - Text( - label, - style: const TextStyle(color: Colors.white), - ), - ], - ), - ), - ); - } -} - -class EditIdentifier extends StatelessWidget { - final TextEditingController _controller = TextEditingController(); - - EditIdentifier({super.key}); - - @override - Widget build(BuildContext context) { - return AlertDialog( - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - AppLocalizations.of(context)!.enterNewIdentifier, - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 16), - TextField( - decoration: InputDecoration( - labelText: AppLocalizations.of(context)!.identifier, - ), - controller: _controller, - ), - ], - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text(AppLocalizations.of(context)!.cancel), - ), - ElevatedButton( - onPressed: () { - saveIdentifier(context); - }, - child: Text(AppLocalizations.of(context)!.save), - ), - ], - ); - } - - Future saveIdentifier(BuildContext context) async { - if (_controller.text.isEmpty) { - Vibration.vibrate(duration: 200, amplitude: 255); - return; - } - - final oldIdentifier = MemberController.instance.identifier; - - try { - await MemberController.instance.testLogin(_controller.text); - await MemberController.instance.login(identifier: _controller.text); - } catch (e) { - Vibration.vibrate(duration: 200, amplitude: 255); - - if (MemberController.instance.identifier != oldIdentifier) { - await MemberController.instance.login(identifier: oldIdentifier); - } - - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - AppLocalizations.of(context)!.invalidIdentifier, - textAlign: TextAlign.center, - ), - ), - ); - } - } finally { - if (context.mounted) { - Navigator.of(context).pop(); - } - } - } -}