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