diff --git a/lib/main.dart b/lib/main.dart index 120b324..c4f28c4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/providers/content/attachment.dart'; +import 'package:solian/providers/friend.dart'; import 'package:solian/router.dart'; import 'package:solian/theme.dart'; import 'package:solian/translations.dart'; @@ -28,6 +29,7 @@ class SolianApp extends StatelessWidget { fallbackLocale: const Locale('en', 'US'), onInit: () { Get.lazyPut(() => AuthProvider()); + Get.lazyPut(() => FriendProvider()); Get.lazyPut(() => AttachmentProvider()); }, builder: (context, child) { diff --git a/lib/providers/auth.dart b/lib/providers/auth.dart index 0ab60c6..6f3fc71 100644 --- a/lib/providers/auth.dart +++ b/lib/providers/auth.dart @@ -109,7 +109,6 @@ class AuthProvider extends GetConnect { } final resp = await get('/api/users/me'); - print(resp.body['picture']); _cacheUserProfileResponse = resp; return resp; } diff --git a/lib/providers/friend.dart b/lib/providers/friend.dart new file mode 100644 index 0000000..f7b3384 --- /dev/null +++ b/lib/providers/friend.dart @@ -0,0 +1,40 @@ +import 'package:get/get.dart'; +import 'package:solian/models/friendship.dart'; +import 'package:solian/providers/auth.dart'; +import 'package:solian/services.dart'; + +class FriendProvider extends GetConnect { + @override + void onInit() { + final AuthProvider auth = Get.find(); + + httpClient.baseUrl = ServiceFinder.services['passport']; + httpClient.addAuthenticator(auth.reqAuthenticator); + } + + Future listFriendship() => get('/api/users/me/friends'); + + Future createFriendship(String username) async { + final resp = await post('/api/users/me/friends?related=$username', {}); + if (resp.statusCode != 200) { + throw Exception(resp.bodyString); + } + + return resp; + } + + Future updateFriendship(Friendship relationship, int status) async { + final AuthProvider auth = Get.find(); + final prof = await auth.getProfile(); + final otherside = relationship.getOtherside(prof.body['id']); + + final resp = await put('/api/users/me/friends/${otherside.id}', { + 'status': status, + }); + if (resp.statusCode != 200) { + throw Exception(resp.bodyString); + } + + return resp; + } +} diff --git a/lib/router.dart b/lib/router.dart index 882647d..c97c1ca 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -1,8 +1,10 @@ import 'package:go_router/go_router.dart'; import 'package:solian/screens/account.dart'; +import 'package:solian/screens/account/friend.dart'; import 'package:solian/screens/account/personalize.dart'; import 'package:solian/screens/auth/signin.dart'; import 'package:solian/screens/auth/signup.dart'; +import 'package:solian/screens/contact.dart'; import 'package:solian/screens/social.dart'; import 'package:solian/screens/posts/publish.dart'; import 'package:solian/shells/basic_shell.dart'; @@ -20,6 +22,11 @@ abstract class AppRouter { name: 'social', builder: (context, state) => const SocialScreen(), ), + GoRoute( + path: '/contact', + name: 'contact', + builder: (context, state) => const ContactScreen(), + ), GoRoute( path: '/account', name: 'account', @@ -31,6 +38,11 @@ abstract class AppRouter { builder: (context, state, child) => BasicShell(state: state, child: child), routes: [ + GoRoute( + path: '/account/friend', + name: 'accountFriend', + builder: (context, state) => const FriendScreen(), + ), GoRoute( path: '/account/personalize', name: 'accountPersonalize', diff --git a/lib/screens/account/friend.dart b/lib/screens/account/friend.dart new file mode 100644 index 0000000..e801998 --- /dev/null +++ b/lib/screens/account/friend.dart @@ -0,0 +1,231 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:get/get.dart'; +import 'package:solian/exts.dart'; +import 'package:solian/models/friendship.dart'; +import 'package:solian/providers/auth.dart'; +import 'package:solian/providers/friend.dart'; +import 'package:solian/widgets/account/account_avatar.dart'; + +class FriendScreen extends StatefulWidget { + const FriendScreen({super.key}); + + @override + State createState() => _FriendScreenState(); +} + +class _FriendScreenState extends State { + bool _isBusy = false; + int? _accountId; + + List _friendships = List.empty(); + + List filterWithStatus(int status) { + return _friendships.where((x) => x.status == status).toList(); + } + + DismissDirection getDismissDirection(Friendship relation) { + if (relation.status == 2) return DismissDirection.endToStart; + if (relation.status == 1) return DismissDirection.startToEnd; + if (relation.status == 0 && relation.relatedId != _accountId) { + return DismissDirection.startToEnd; + } + return DismissDirection.horizontal; + } + + Future getFriendship() async { + setState(() => _isBusy = true); + + final FriendProvider provider = Get.find(); + final resp = await provider.listFriendship(); + + setState(() { + _friendships = resp.body + .map((e) => Friendship.fromJson(e)) + .toList() + .cast(); + _isBusy = false; + }); + } + + void promptAddFriend() async { + final FriendProvider provider = Get.find(); + + final controller = TextEditingController(); + final input = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('accountFriendNew'.tr), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('accountFriendNewHint'.tr, textAlign: TextAlign.left), + const SizedBox(height: 18), + TextField( + controller: controller, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: 'username'.tr, + ), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + ], + ), + actions: [ + TextButton( + style: TextButton.styleFrom( + foregroundColor: + Theme.of(context).colorScheme.onSurface.withOpacity(0.8), + ), + onPressed: () => Navigator.pop(context), + child: Text('cancel'.tr), + ), + TextButton( + child: Text('next'.tr), + onPressed: () { + Navigator.pop(context, controller.text); + }, + ), + ], + ); + }, + ); + + WidgetsBinding.instance.addPostFrameCallback((_) => controller.dispose()); + + if (input == null || input.isEmpty) return; + + try { + setState(() => _isBusy = true); + await provider.createFriendship(input); + } catch (e) { + context.showErrorDialog(e); + } finally { + setState(() => _isBusy = false); + } + } + + @override + void initState() { + Get.find().getProfile().then((value) { + _accountId = value.body['id']; + }); + super.initState(); + + Future.delayed(Duration.zero, () => getFriendship()); + } + + Widget buildFriendshipItem(context, index, status) { + final element = filterWithStatus(status)[index]; + final otherside = element.getOtherside(_accountId!); + + final randomId = DateTime.now().microsecondsSinceEpoch >> 10; + + return Dismissible( + key: Key(randomId.toString()), + background: Container( + color: Colors.red, + padding: const EdgeInsets.symmetric(horizontal: 20), + alignment: Alignment.centerLeft, + child: const Icon(Icons.close, color: Colors.white), + ), + secondaryBackground: Container( + color: Colors.green, + padding: const EdgeInsets.symmetric(horizontal: 20), + alignment: Alignment.centerRight, + child: const Icon(Icons.check, color: Colors.white), + ), + direction: getDismissDirection(element), + child: ListTile( + title: Text(otherside.nick), + subtitle: Text(otherside.name), + leading: AccountAvatar(content: otherside.avatar), + ), + onDismissed: (direction) async { + final FriendProvider provider = Get.find(); + if (direction == DismissDirection.startToEnd) { + await provider.updateFriendship(element, 2); + await getFriendship(); + } + if (direction == DismissDirection.endToStart) { + await provider.updateFriendship(element, 1); + await getFriendship(); + } + }, + ); + } + + @override + Widget build(BuildContext context) { + return Material( + color: Theme.of(context).colorScheme.surface, + child: Scaffold( + floatingActionButton: FloatingActionButton( + child: const Icon(Icons.add), + onPressed: () => promptAddFriend(), + ), + body: RefreshIndicator( + onRefresh: () => getFriendship(), + child: CustomScrollView( + slivers: [ + if (_isBusy) + SliverToBoxAdapter( + child: const LinearProgressIndicator().animate().scaleX(), + ), + SliverToBoxAdapter( + child: ListTile( + tileColor: Theme.of(context).colorScheme.surfaceContainerLow, + contentPadding: const EdgeInsets.symmetric(horizontal: 20), + leading: const Icon(Icons.person_add), + trailing: const Icon(Icons.chevron_right), + title: Text( + '${'accountFriendPending'.tr} (${filterWithStatus(0).length})', + ), + onTap: () {}, + ), + ), + SliverToBoxAdapter( + child: ListTile( + tileColor: Theme.of(context).colorScheme.surfaceContainerLow, + contentPadding: const EdgeInsets.symmetric(horizontal: 20), + leading: const Icon(Icons.block), + trailing: const Icon(Icons.chevron_right), + title: Text( + '${'accountFriendBlocked'.tr} (${filterWithStatus(2).length})', + ), + onTap: () {}, + ), + ), + SliverList.builder( + itemCount: filterWithStatus(1).length, + itemBuilder: (_, __) => buildFriendshipItem(_, __, 1), + ), + SliverToBoxAdapter( + child: Container( + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: Theme.of(context) + .colorScheme + .surfaceVariant + .withOpacity(0.8), + width: 0.3, + )), + ), + padding: const EdgeInsets.only(top: 16, bottom: 32), + child: Text( + 'accountFriendListHint'.tr, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodySmall, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/contact.dart b/lib/screens/contact.dart new file mode 100644 index 0000000..6d73f89 --- /dev/null +++ b/lib/screens/contact.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class ContactScreen extends StatelessWidget { + const ContactScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Material( + color: Theme.of(context).colorScheme.surface, + ); + } +} diff --git a/lib/translations.dart b/lib/translations.dart index bad154a..3e18bdd 100644 --- a/lib/translations.dart +++ b/lib/translations.dart @@ -10,6 +10,7 @@ class SolianMessages extends Translations { 'reset': 'Reset', 'page': 'Page', 'social': 'Social', + 'contact': 'Contact', 'apply': 'Apply', 'cancel': 'Cancel', 'confirm': 'Confirm', @@ -31,6 +32,12 @@ class SolianMessages extends Translations { 'accountPersonalizeApplied': 'Account personalize settings has been saved.', 'accountFriend': 'Friend', + 'accountFriendNew': 'New friend', + 'accountFriendNewHint': + 'Use someone\'s username to send a request of making friends with them!', + 'accountFriendPending': 'Friend requests', + 'accountFriendBlocked': 'Friend blocklist', + 'accountFriendListHint': 'Swipe left to decline, right to approve', 'aspectRatio': 'Aspect Ratio', 'aspectRatioSquare': 'Square', 'aspectRatioPortrait': 'Portrait', @@ -85,6 +92,7 @@ class SolianMessages extends Translations { 'delete': '删除', 'page': '页面', 'social': '社交', + 'contact': '联系', 'apply': '应用', 'reply': '回复', 'repost': '转帖', @@ -101,6 +109,11 @@ class SolianMessages extends Translations { 'accountPersonalize': '个性化', 'accountPersonalizeApplied': '账户的个性化设置已保存。', 'accountFriend': '好友', + 'accountFriendNew': '添加好友', + 'accountFriendNewHint': '使用他人的用户名来发送一个好友请求吧!', + 'accountFriendPending': '好友请求', + 'accountFriendBlocked': '好友黑名单', + 'accountFriendListHint': '左滑来拒绝,右滑来接受', 'aspectRatio': '纵横比', 'aspectRatioSquare': '方型', 'aspectRatioPortrait': '竖型', diff --git a/lib/widgets/navigation/app_navigation.dart b/lib/widgets/navigation/app_navigation.dart index 55ad5a9..fad7573 100644 --- a/lib/widgets/navigation/app_navigation.dart +++ b/lib/widgets/navigation/app_navigation.dart @@ -8,6 +8,11 @@ abstract class AppNavigation { label: 'social'.tr, page: 'social', ), + AppNavigationDestination( + icon: const Icon(Icons.contacts), + label: 'contact'.tr, + page: 'contact', + ), AppNavigationDestination( icon: const Icon(Icons.account_circle), label: 'account'.tr,