diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ec01400..a29f813 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -27,6 +27,12 @@ + + + .broadcast(); String? identifier; MemberDto? member; + int imageVersion = 0; Future init({bool afterDelete = false}) async { if (!afterDelete) { @@ -23,6 +25,7 @@ class MemberController { } identifier = _sharedPreferences.getString('identifier') ?? await register(); + imageVersion = _sharedPreferences.getInt('imageVersion') ?? 0; try { await login(); @@ -33,6 +36,8 @@ class MemberController { // Move the current identifier to old identifier final oldIdentifier = identifier; await _sharedPreferences.remove('identifier'); + await _sharedPreferences.remove('imageVersion'); + await _sharedPreferences.setString('oldIdentifier', oldIdentifier!); await init(afterDelete: true); } @@ -86,6 +91,33 @@ class MemberController { streamController.add(member!); } + Future changeImage(String path) async { + final croppedFile = await ImageCropper().cropImage( + sourcePath: path, + aspectRatioPresets: [CropAspectRatioPreset.square], + uiSettings: [ + AndroidUiSettings(), + IOSUiSettings(), + ], + ); + + final response = await HttpRequest().postMultipart( + '/v1/members/image', + member!.token, + croppedFile!.path, + ); + + if (response.statusCode != 200) { + throw HttpException('Failed to change image ${response.body}'); + } + + imageVersion++; + await _sharedPreferences.setInt('imageVersion', imageVersion); + Future.delayed(const Duration(seconds: 1), () { + streamController.add(member!); + }); + } + Future associateEmail(String email) async { final response = await HttpRequest().post( '/v1/members/associate-email', 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 84c488e..23ff95c 100644 --- a/lib/utils/http_request.dart +++ b/lib/utils/http_request.dart @@ -62,6 +62,30 @@ class HttpRequest { .timeout(_timeout); } + Future postMultipart( + String endpoint, + String token, + String path, + ) async { + final request = http.MultipartRequest( + 'POST', + Uri.parse( + Constant.apiUrl + endpoint, + ), + ); + + request.headers.putIfAbsent('Authorization', () => 'Bearer $token'); + request.files.add( + await http.MultipartFile.fromPath( + 'file', + path, + filename: path.split('/').last, + ), + ); + + return http.Response.fromStream(await request.send()).timeout(_timeout); + } + Future put( String endpoint, String token, diff --git a/lib/views/account_view.dart b/lib/views/account_view.dart index e54e3d0..e82b787 100644 --- a/lib/views/account_view.dart +++ b/lib/views/account_view.dart @@ -1,6 +1,10 @@ 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:application/dtos/member_dto.dart'; +import 'package:application/utils/constant.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -10,167 +14,209 @@ class AccountView extends StatelessWidget { @override Widget build(BuildContext context) { final appLocalizations = AppLocalizations.of(context); - final member = MemberController.instance.member; - return ListView( - children: [ - Row( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Stack( + return StreamBuilder( + stream: MemberController.instance.streamController.stream, + initialData: MemberController.instance.member, + builder: (context, snapshot) { + final member = snapshot.data; + + return ListView( + children: [ + Row( children: [ - const CircleAvatar( - radius: 40, - backgroundImage: AssetImage('assets/avatar.jpg'), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Stack( + children: [ + SizedBox( + width: 80, + height: 80, + child: ClipRRect( + borderRadius: BorderRadius.circular(50), + child: CachedNetworkImage( + imageUrl: + '${Constant.apiUrl}/v1/attachments?uuid=${member?.uuid}&v=${MemberController.instance.imageVersion}', + filterQuality: FilterQuality.high, + placeholder: (context, url) => + const CircularProgressIndicator(), + errorWidget: (context, url, error) => + DecoratedBox( + decoration: + BoxDecoration(color: Colors.grey[900]), + child: const Icon( + Icons.person, + size: 32, + ), + ), + ), + ), + ), + Positioned( + bottom: 0, + right: 0, + child: GestureDetector( + onTap: () async { + final result = + await FilePicker.platform.pickFiles( + allowMultiple: false, + type: FileType.image, + ); + + if (result != null) { + final path = result.files.single.path; + await MemberController.instance + .changeImage(path!); + } + }, + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: const Padding( + padding: EdgeInsets.all(4), + child: Icon( + Icons.edit, + size: 15, + color: Colors.grey, + ), + ), + ), + ), + ), + ], + ), ), - Positioned( - bottom: 0, - right: 0, - child: Container( - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flex( + direction: Axis.horizontal, + children: [ + Text( + appLocalizations!.anonymousAccount, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(width: 8), + GestureDetector( + child: const Icon( + Icons.info, + color: Colors.grey, + size: 20, + ), + onTap: () { + showAdaptiveDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text( + appLocalizations.anonymousWarningTitle, + ), + content: SingleChildScrollView( + child: Column( + children: [ + Text(appLocalizations + .anonymousWarningContent1), + const SizedBox(height: 16), + Text(appLocalizations + .anonymousWarningContent2), + const SizedBox(height: 16), + Text(appLocalizations + .anonymousWarningContent3), + ], + ), + ), + ); + }, + ); + }, + ), + ], ), - child: const Padding( - padding: EdgeInsets.all(4), - child: Icon( - Icons.edit, - size: 15, + Text( + appLocalizations.member, + style: const TextStyle( + fontSize: 16, color: Colors.grey, ), ), - ), + if (member?.email == null) ...[ + const SizedBox(height: 8), + ElevatedButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const AssociateEmail(), + ), + ); + }, + child: Text(appLocalizations.associateEmail), + ), + ], + ], ), ], ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flex( - direction: Axis.horizontal, + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( children: [ - Text( - appLocalizations!.anonymousAccount, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.white, + Expanded( + child: AccountCard( + label: appLocalizations.animesAdded, + value: member?.followedAnimes.length.toString() ?? '0', ), ), - const SizedBox(width: 8), - GestureDetector( - child: const Icon( - Icons.info, - color: Colors.grey, - size: 20, + const SizedBox(width: 16), + Expanded( + child: AccountCard( + label: appLocalizations.episodesWatched, + value: + member?.followedEpisodes.length.toString() ?? '0', + ), + ), + ], + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Expanded( + child: AccountCard( + label: appLocalizations.watchTime, + value: MemberController.instance.buildTotalDuration(), ), - onTap: () { - showAdaptiveDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text( - appLocalizations.anonymousWarningTitle, - ), - content: SingleChildScrollView( - child: Column( - children: [ - Text(appLocalizations - .anonymousWarningContent1), - const SizedBox(height: 16), - Text(appLocalizations - .anonymousWarningContent2), - const SizedBox(height: 16), - Text(appLocalizations - .anonymousWarningContent3), - ], - ), - ), - ); - }, - ); - }, ), ], ), - Text( - appLocalizations.member, - style: const TextStyle( + ), + const SizedBox(height: 16), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Text( + 'Nos recommandations', + style: TextStyle( fontSize: 16, + fontWeight: FontWeight.bold, color: Colors.grey, ), ), - if (member?.email == null) ...[ - const SizedBox(height: 8), - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const AssociateEmail(), - ), - ); - }, - child: Text(appLocalizations.associateEmail), - ), - ], - ], - ), - ], - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - Expanded( - child: AccountCard( - label: appLocalizations.animesAdded, - value: member?.followedAnimes.length.toString() ?? '0', - ), ), - const SizedBox(width: 16), - Expanded( - child: AccountCard( - label: appLocalizations.episodesWatched, - value: member?.followedEpisodes.length.toString() ?? '0', - ), - ), - ], - ), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - Expanded( - child: AccountCard( - label: appLocalizations.watchTime, - value: MemberController.instance.buildTotalDuration(), - ), + const ListTile( + title: Text('Bah non, toujours pas de recommandations.'), + subtitle: + Text("Mais ça va venir, promis ! (Peut-être d'ici 2027)"), ), ], - ), - ), - const SizedBox(height: 16), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 16), - child: Text( - 'Nos recommandations', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.grey, - ), - ), - ), - const ListTile( - title: Text('Bah non, toujours pas de recommandations.'), - subtitle: Text("Mais ça va venir, promis ! (Peut-être d'ici 2027)"), - ), - ], - ); + ); + }); } } diff --git a/pubspec.lock b/pubspec.lock index a630551..c631357 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -297,6 +297,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.4" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "29c90806ac5f5fb896547720b73b17ee9aed9bba540dc5d91fe29f8c5745b10a" + url: "https://pub.dev" + source: hosted + version: "8.0.3" firebase_core: dependency: "direct main" description: @@ -395,6 +403,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e + url: "https://pub.dev" + source: hosted + version: "2.0.20" flutter_test: dependency: transitive description: flutter @@ -485,6 +501,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.0" + image_cropper: + dependency: "direct main" + description: + name: image_cropper + sha256: db779a8b620cd509874cb0e2a8bdc8649177f8f5ca46c13273ceaffe071e3f4a + url: "https://pub.dev" + source: hosted + version: "6.0.0" + image_cropper_for_web: + dependency: transitive + description: + name: image_cropper_for_web + sha256: ba67de40a98b3294084eed0b025b557cb594356e1171c9a830b340527dbd5e5f + url: "https://pub.dev" + source: hosted + version: "4.0.0" + image_cropper_platform_interface: + dependency: transitive + description: + name: image_cropper_platform_interface + sha256: ee160d686422272aa306125f3b6fb1c1894d9b87a5e20ed33fa008e7285da11e + url: "https://pub.dev" + source: hosted + version: "5.0.0" intl: dependency: "direct main" description: @@ -641,10 +681,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.2.5" path_provider_foundation: dependency: transitive description: @@ -785,10 +825,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" + sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.3" shared_preferences_foundation: dependency: transitive description: @@ -998,10 +1038,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "17cd5e205ea615e2c6ea7a77323a11712dffa0720a8a90540db57a01347f9ad9" + sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.3" url_launcher_ios: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3c313d5..3dd1d83 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,7 @@ environment: dependencies: cached_network_image: ^3.3.1 + file_picker: ^8.0.3 firebase_core: ^2.32.0 firebase_messaging: ^14.9.4 flutter: @@ -18,6 +19,7 @@ dependencies: sdk: flutter freezed_annotation: ^2.4.1 http: ^1.2.1 + image_cropper: ^6.0.0 intl: any json_annotation: ^4.9.0 like_button: ^2.0.5 @@ -58,7 +60,6 @@ flutter: - assets/icon.png - assets/icon_128x128.png - assets/splash.png - - assets/avatar.jpg - shorebird.yaml fonts: - family: Satoshi