Friend list

This commit is contained in:
LittleSheep 2024-05-23 21:12:47 +08:00
parent 05f88fe3f3
commit d98d167f4c
8 changed files with 315 additions and 1 deletions

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/attachment.dart'; import 'package:solian/providers/content/attachment.dart';
import 'package:solian/providers/friend.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/translations.dart'; import 'package:solian/translations.dart';
@ -28,6 +29,7 @@ class SolianApp extends StatelessWidget {
fallbackLocale: const Locale('en', 'US'), fallbackLocale: const Locale('en', 'US'),
onInit: () { onInit: () {
Get.lazyPut(() => AuthProvider()); Get.lazyPut(() => AuthProvider());
Get.lazyPut(() => FriendProvider());
Get.lazyPut(() => AttachmentProvider()); Get.lazyPut(() => AttachmentProvider());
}, },
builder: (context, child) { builder: (context, child) {

View File

@ -109,7 +109,6 @@ class AuthProvider extends GetConnect {
} }
final resp = await get('/api/users/me'); final resp = await get('/api/users/me');
print(resp.body['picture']);
_cacheUserProfileResponse = resp; _cacheUserProfileResponse = resp;
return resp; return resp;
} }

40
lib/providers/friend.dart Normal file
View File

@ -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<Response> listFriendship() => get('/api/users/me/friends');
Future<Response> 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<Response> 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;
}
}

View File

@ -1,8 +1,10 @@
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:solian/screens/account.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/account/personalize.dart';
import 'package:solian/screens/auth/signin.dart'; import 'package:solian/screens/auth/signin.dart';
import 'package:solian/screens/auth/signup.dart'; import 'package:solian/screens/auth/signup.dart';
import 'package:solian/screens/contact.dart';
import 'package:solian/screens/social.dart'; import 'package:solian/screens/social.dart';
import 'package:solian/screens/posts/publish.dart'; import 'package:solian/screens/posts/publish.dart';
import 'package:solian/shells/basic_shell.dart'; import 'package:solian/shells/basic_shell.dart';
@ -20,6 +22,11 @@ abstract class AppRouter {
name: 'social', name: 'social',
builder: (context, state) => const SocialScreen(), builder: (context, state) => const SocialScreen(),
), ),
GoRoute(
path: '/contact',
name: 'contact',
builder: (context, state) => const ContactScreen(),
),
GoRoute( GoRoute(
path: '/account', path: '/account',
name: 'account', name: 'account',
@ -31,6 +38,11 @@ abstract class AppRouter {
builder: (context, state, child) => builder: (context, state, child) =>
BasicShell(state: state, child: child), BasicShell(state: state, child: child),
routes: [ routes: [
GoRoute(
path: '/account/friend',
name: 'accountFriend',
builder: (context, state) => const FriendScreen(),
),
GoRoute( GoRoute(
path: '/account/personalize', path: '/account/personalize',
name: 'accountPersonalize', name: 'accountPersonalize',

View File

@ -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<FriendScreen> createState() => _FriendScreenState();
}
class _FriendScreenState extends State<FriendScreen> {
bool _isBusy = false;
int? _accountId;
List<Friendship> _friendships = List.empty();
List<Friendship> 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<void> 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<Friendship>();
_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: <Widget>[
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<AuthProvider>().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,
),
),
),
],
),
),
),
);
}
}

12
lib/screens/contact.dart Normal file
View File

@ -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,
);
}
}

View File

@ -10,6 +10,7 @@ class SolianMessages extends Translations {
'reset': 'Reset', 'reset': 'Reset',
'page': 'Page', 'page': 'Page',
'social': 'Social', 'social': 'Social',
'contact': 'Contact',
'apply': 'Apply', 'apply': 'Apply',
'cancel': 'Cancel', 'cancel': 'Cancel',
'confirm': 'Confirm', 'confirm': 'Confirm',
@ -31,6 +32,12 @@ class SolianMessages extends Translations {
'accountPersonalizeApplied': 'accountPersonalizeApplied':
'Account personalize settings has been saved.', 'Account personalize settings has been saved.',
'accountFriend': 'Friend', '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', 'aspectRatio': 'Aspect Ratio',
'aspectRatioSquare': 'Square', 'aspectRatioSquare': 'Square',
'aspectRatioPortrait': 'Portrait', 'aspectRatioPortrait': 'Portrait',
@ -85,6 +92,7 @@ class SolianMessages extends Translations {
'delete': '删除', 'delete': '删除',
'page': '页面', 'page': '页面',
'social': '社交', 'social': '社交',
'contact': '联系',
'apply': '应用', 'apply': '应用',
'reply': '回复', 'reply': '回复',
'repost': '转帖', 'repost': '转帖',
@ -101,6 +109,11 @@ class SolianMessages extends Translations {
'accountPersonalize': '个性化', 'accountPersonalize': '个性化',
'accountPersonalizeApplied': '账户的个性化设置已保存。', 'accountPersonalizeApplied': '账户的个性化设置已保存。',
'accountFriend': '好友', 'accountFriend': '好友',
'accountFriendNew': '添加好友',
'accountFriendNewHint': '使用他人的用户名来发送一个好友请求吧!',
'accountFriendPending': '好友请求',
'accountFriendBlocked': '好友黑名单',
'accountFriendListHint': '左滑来拒绝,右滑来接受',
'aspectRatio': '纵横比', 'aspectRatio': '纵横比',
'aspectRatioSquare': '方型', 'aspectRatioSquare': '方型',
'aspectRatioPortrait': '竖型', 'aspectRatioPortrait': '竖型',

View File

@ -8,6 +8,11 @@ abstract class AppNavigation {
label: 'social'.tr, label: 'social'.tr,
page: 'social', page: 'social',
), ),
AppNavigationDestination(
icon: const Icon(Icons.contacts),
label: 'contact'.tr,
page: 'contact',
),
AppNavigationDestination( AppNavigationDestination(
icon: const Icon(Icons.account_circle), icon: const Icon(Icons.account_circle),
label: 'account'.tr, label: 'account'.tr,