Realm members & edit

This commit is contained in:
LittleSheep 2024-05-05 23:51:46 +08:00
parent 384d861d56
commit 0b9439262c
6 changed files with 317 additions and 36 deletions

View File

@ -1,3 +1,5 @@
import 'package:solian/models/account.dart';
class Realm { class Realm {
int id; int id;
DateTime createdAt; DateTime createdAt;
@ -6,7 +8,6 @@ class Realm {
String alias; String alias;
String name; String name;
String description; String description;
dynamic members;
bool isPublic; bool isPublic;
bool isCommunity; bool isCommunity;
int accountId; int accountId;
@ -19,37 +20,78 @@ class Realm {
required this.alias, required this.alias,
required this.name, required this.name,
required this.description, required this.description,
required this.members,
required this.isPublic, required this.isPublic,
required this.isCommunity, required this.isCommunity,
required this.accountId, required this.accountId,
}); });
factory Realm.fromJson(Map<String, dynamic> json) => Realm( factory Realm.fromJson(Map<String, dynamic> json) => Realm(
id: json['id'], id: json['id'],
createdAt: DateTime.parse(json['created_at']), createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']), updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null ? DateTime.parse(json['deleted_at']) : null, deletedAt: json['deleted_at'] != null ? DateTime.parse(json['deleted_at']) : null,
alias: json['alias'], alias: json['alias'],
name: json['name'], name: json['name'],
description: json['description'], description: json['description'],
members: json['members'], isPublic: json['is_public'],
isPublic: json['is_public'], isCommunity: json['is_community'],
isCommunity: json['is_community'], accountId: json['account_id'],
accountId: json['account_id'], );
);
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'id': id, 'id': id,
'created_at': createdAt.toIso8601String(), 'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt, 'deleted_at': deletedAt,
'alias': alias, 'alias': alias,
'name': name, 'name': name,
'description': description, 'description': description,
'members': members, 'is_public': isPublic,
'is_public': isPublic, 'is_community': isCommunity,
'is_community': isCommunity, 'account_id': accountId,
'account_id': accountId, };
}; }
}
class RealmMember {
int id;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
int realmId;
int accountId;
Account account;
int powerLevel;
RealmMember({
required this.id,
required this.createdAt,
required this.updatedAt,
this.deletedAt,
required this.realmId,
required this.accountId,
required this.account,
required this.powerLevel,
});
factory RealmMember.fromJson(Map<String, dynamic> json) => RealmMember(
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null ? DateTime.parse(json['deleted_at']) : null,
realmId: json['realm_id'],
accountId: json['account_id'],
account: Account.fromJson(json['account']),
powerLevel: json['power_level'],
);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt,
'realm_id': realmId,
'account_id': accountId,
'account': account.toJson(),
'power_level': powerLevel,
};
}

View File

@ -39,4 +39,9 @@ class RealmProvider with ChangeNotifier {
throw Exception(message); throw Exception(message);
} }
} }
void clearFocus() {
focusRealm = null;
notifyListeners();
}
} }

View File

@ -60,7 +60,7 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> {
return; return;
} }
var uri = getRequestUri('messaging', '/api/channels/global/${widget.channel.alias}'); var uri = getRequestUri('messaging', '/api/channels/${widget.realm}/${widget.channel.alias}/members');
var res = await auth.client!.delete( var res = await auth.client!.delete(
uri, uri,
@ -68,7 +68,7 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: jsonEncode({ body: jsonEncode({
'account_name': item.account.name, 'target': item.account.name,
}), }),
); );
if (res.statusCode == 200) { if (res.statusCode == 200) {
@ -90,7 +90,7 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> {
return; return;
} }
var uri = getRequestUri('messaging', '/api/channels/${widget.realm}/${widget.channel.alias}'); var uri = getRequestUri('messaging', '/api/channels/${widget.realm}/${widget.channel.alias}/members');
var res = await auth.client!.post( var res = await auth.client!.post(
uri, uri,
@ -98,7 +98,7 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: jsonEncode({ body: jsonEncode({
'account_name': username, 'target': username,
}), }),
); );
if (res.statusCode == 200) { if (res.statusCode == 200) {
@ -111,7 +111,7 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> {
setState(() => _isSubmitting = false); setState(() => _isSubmitting = false);
} }
void promptInviteMember() async { void promptAddMember() async {
final input = await showModalBottomSheet( final input = await showModalBottomSheet(
context: context, context: context,
builder: (context) { builder: (context) {
@ -146,7 +146,7 @@ class _ChatMemberScreenState extends State<ChatMemberScreen> {
appBarActions: [ appBarActions: [
IconButton( IconButton(
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
onPressed: () => promptInviteMember(), onPressed: () => promptAddMember(),
), ),
], ],
child: RefreshIndicator( child: RefreshIndicator(

View File

@ -1,9 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:solian/models/realm.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/realm.dart'; import 'package:solian/providers/realm.dart';
import 'package:solian/router.dart';
import 'package:solian/screens/chat/chat_list.dart'; import 'package:solian/screens/chat/chat_list.dart';
import 'package:solian/screens/explore.dart'; import 'package:solian/screens/explore.dart';
import 'package:solian/screens/realms/realm_member.dart';
import 'package:solian/utils/theme.dart'; import 'package:solian/utils/theme.dart';
import 'package:solian/widgets/scaffold.dart'; import 'package:solian/widgets/scaffold.dart';
@ -14,13 +17,30 @@ class RealmScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final auth = context.read<AuthProvider>();
final realm = context.watch<RealmProvider>(); final realm = context.watch<RealmProvider>();
return IndentScaffold( return IndentScaffold(
title: realm.focusRealm?.name ?? 'Loading...', title: realm.focusRealm?.name ?? 'Loading...',
hideDrawer: !SolianTheme.isLargeScreen(context), hideDrawer: true,
noSafeArea: true, noSafeArea: true,
fixedAppBarColor: SolianTheme.isLargeScreen(context), fixedAppBarColor: SolianTheme.isLargeScreen(context),
appBarActions: realm.focusRealm != null
? [
RealmManageAction(
realm: realm.focusRealm!,
onUpdate: () => realm.fetchSingle(auth, alias),
),
]
: [],
appBarLeading: SolianTheme.isLargeScreen(context)
? IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
realm.clearFocus();
},
)
: null,
child: RealmWidget( child: RealmWidget(
alias: alias, alias: alias,
), ),
@ -62,7 +82,7 @@ class _RealmWidgetState extends State<RealmWidget> {
} }
return DefaultTabController( return DefaultTabController(
length: 2, length: 3,
child: Column( child: Column(
children: [ children: [
TabBar( TabBar(
@ -70,6 +90,7 @@ class _RealmWidgetState extends State<RealmWidget> {
tabs: const [ tabs: const [
Tab(icon: Icon(Icons.newspaper)), Tab(icon: Icon(Icons.newspaper)),
Tab(icon: Icon(Icons.message)), Tab(icon: Icon(Icons.message)),
Tab(icon: Icon(Icons.supervisor_account))
], ],
), ),
Expanded( Expanded(
@ -77,6 +98,11 @@ class _RealmWidgetState extends State<RealmWidget> {
children: [ children: [
ExplorePostWidget(realm: widget.alias), ExplorePostWidget(realm: widget.alias),
ChatListWidget(realm: widget.alias), ChatListWidget(realm: widget.alias),
_realm.focusRealm != null
? RealmMemberWidget(realm: _realm.focusRealm!)
: const Center(
child: CircularProgressIndicator(),
),
], ],
), ),
) )
@ -85,3 +111,28 @@ class _RealmWidgetState extends State<RealmWidget> {
); );
} }
} }
class RealmManageAction extends StatelessWidget {
final Realm realm;
final Function onUpdate;
const RealmManageAction({
super.key,
required this.realm,
required this.onUpdate,
});
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: () async {
final did = await SolianRouter.router.pushNamed(
'realms.editor',
extra: realm,
);
if (did == true) onUpdate();
},
icon: const Icon(Icons.settings),
);
}
}

View File

@ -0,0 +1,183 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:provider/provider.dart';
import 'package:solian/models/account.dart';
import 'package:solian/models/realm.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/utils/service_url.dart';
import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/friend_picker.dart';
import 'package:solian/widgets/exts.dart';
class RealmMemberWidget extends StatefulWidget {
final Realm realm;
const RealmMemberWidget({super.key, required this.realm});
@override
State<RealmMemberWidget> createState() => _RealmMemberWidgetState();
}
class _RealmMemberWidgetState extends State<RealmMemberWidget> {
bool _isSubmitting = false;
List<RealmMember> _members = List.empty();
int _selfId = 0;
Future<void> fetchMemberships() async {
final auth = context.read<AuthProvider>();
final prof = await auth.getProfiles();
if (!await auth.isAuthorized()) return;
_selfId = prof['id'];
var uri = getRequestUri('passport', '/api/realms/${widget.realm.alias}/members');
var res = await auth.client!.get(uri);
if (res.statusCode == 200) {
final result = jsonDecode(utf8.decode(res.bodyBytes)) as List<dynamic>;
setState(() {
_members = result.map((x) => RealmMember.fromJson(x)).toList();
});
} else {
var message = utf8.decode(res.bodyBytes);
context.showErrorDialog(message);
}
}
Future<void> removeMember(RealmMember item) async {
setState(() => _isSubmitting = true);
final auth = context.read<AuthProvider>();
if (!await auth.isAuthorized()) {
setState(() => _isSubmitting = false);
return;
}
var uri = getRequestUri('passport', '/api/realms/${widget.realm.alias}/members');
var res = await auth.client!.delete(
uri,
headers: {
'Content-Type': 'application/json',
},
body: jsonEncode({
'target': item.account.name,
}),
);
if (res.statusCode == 200) {
await fetchMemberships();
} else {
var message = utf8.decode(res.bodyBytes);
context.showErrorDialog(message);
}
setState(() => _isSubmitting = false);
}
Future<void> addMember(String username) async {
setState(() => _isSubmitting = true);
final auth = context.read<AuthProvider>();
if (!await auth.isAuthorized()) {
setState(() => _isSubmitting = false);
return;
}
var uri = getRequestUri('passport', '/api/realms/${widget.realm.alias}/members');
var res = await auth.client!.post(
uri,
headers: {
'Content-Type': 'application/json',
},
body: jsonEncode({
'target': username,
}),
);
if (res.statusCode == 200) {
await fetchMemberships();
} else {
var message = utf8.decode(res.bodyBytes);
context.showErrorDialog(message);
}
setState(() => _isSubmitting = false);
}
void promptAddMember() async {
final input = await showModalBottomSheet(
context: context,
builder: (context) {
return const FriendPicker();
},
);
if (input == null) return;
await addMember((input as Account).name);
}
bool getRemovable(RealmMember item) {
if (_selfId != widget.realm.accountId) return false;
if (item.accountId == widget.realm.accountId) return false;
if (item.account.id == _selfId) return false;
return true;
}
@override
void initState() {
super.initState();
Future.delayed(Duration.zero, () => fetchMemberships());
}
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () => promptAddMember(),
),
body: RefreshIndicator(
onRefresh: () => fetchMemberships(),
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(),
),
SliverList.builder(
itemCount: _members.length,
itemBuilder: (context, index) {
final element = _members[index];
final randomId = DateTime.now().microsecondsSinceEpoch >> 10;
return Dismissible(
key: Key(randomId.toString()),
direction: getRemovable(element) ? DismissDirection.startToEnd : DismissDirection.none,
background: Container(
color: Colors.red,
padding: const EdgeInsets.symmetric(horizontal: 20),
alignment: Alignment.centerLeft,
child: const Icon(Icons.remove, color: Colors.white),
),
child: ListTile(
leading: AccountAvatar(source: element.account.avatar),
title: Text(element.account.nick),
subtitle: Text(element.account.name),
),
onDismissed: (_) {
removeMember(element);
},
);
},
)
],
),
),
);
}
}

View File

@ -59,7 +59,7 @@ class _ChannelDeletionState extends State<ChannelDeletion> {
} }
var res = await auth.client!.delete( var res = await auth.client!.delete(
getRequestUri('messaging', '/api/channels/${widget.realm}/${widget.channel.alias}/me'), getRequestUri('messaging', '/api/channels/${widget.realm}/${widget.channel.alias}/members/me'),
); );
if (res.statusCode != 200) { if (res.statusCode != 200) {
var message = utf8.decode(res.bodyBytes); var message = utf8.decode(res.bodyBytes);