Better DM

This commit is contained in:
LittleSheep 2024-05-29 00:14:41 +08:00
parent c50a49f37d
commit d4cbabeb31
17 changed files with 253 additions and 187 deletions

View File

@ -15,13 +15,13 @@ class Account {
required this.id, required this.id,
required this.createdAt, required this.createdAt,
required this.updatedAt, required this.updatedAt,
this.deletedAt, required this.deletedAt,
required this.name, required this.name,
required this.nick, required this.nick,
required this.avatar, required this.avatar,
required this.banner, required this.banner,
required this.description, required this.description,
this.emailAddress, required this.emailAddress,
this.externalId, this.externalId,
}); });

View File

@ -15,8 +15,8 @@ class Call {
required this.id, required this.id,
required this.createdAt, required this.createdAt,
required this.updatedAt, required this.updatedAt,
this.deletedAt, required this.deletedAt,
this.endedAt, required this.endedAt,
required this.externalId, required this.externalId,
required this.founderId, required this.founderId,
required this.channelId, required this.channelId,

View File

@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';
@ -12,6 +10,7 @@ class Channel {
String name; String name;
String description; String description;
int type; int type;
List<ChannelMember>? members;
Account account; Account account;
int accountId; int accountId;
Realm? realm; Realm? realm;
@ -24,16 +23,17 @@ class Channel {
required this.id, required this.id,
required this.createdAt, required this.createdAt,
required this.updatedAt, required this.updatedAt,
this.deletedAt, required this.deletedAt,
required this.alias, required this.alias,
required this.name, required this.name,
required this.description, required this.description,
required this.type, required this.type,
required this.members,
required this.account, required this.account,
required this.accountId, required this.accountId,
required this.isEncrypted, required this.isEncrypted,
this.realm, required this.realm,
this.realmId, required this.realmId,
}); });
factory Channel.fromJson(Map<String, dynamic> json) => Channel( factory Channel.fromJson(Map<String, dynamic> json) => Channel(
@ -45,6 +45,10 @@ class Channel {
name: json['name'], name: json['name'],
description: json['description'], description: json['description'],
type: json['type'], type: json['type'],
members: json['members']
?.map((e) => ChannelMember.fromJson(e))
.toList()
.cast<ChannelMember>(),
account: Account.fromJson(json['account']), account: Account.fromJson(json['account']),
accountId: json['account_id'], accountId: json['account_id'],
realm: json['realm'] != null ? Realm.fromJson(json['realm']) : null, realm: json['realm'] != null ? Realm.fromJson(json['realm']) : null,
@ -61,21 +65,13 @@ class Channel {
'name': name, 'name': name,
'description': description, 'description': description,
'type': type, 'type': type,
'members': members?.map((e) => e.toJson()).toList(),
'account': account.toJson(), 'account': account.toJson(),
'account_id': accountId, 'account_id': accountId,
'realm': realm?.toJson(), 'realm': realm?.toJson(),
'realm_id': realmId, 'realm_id': realmId,
'is_encrypted': isEncrypted, 'is_encrypted': isEncrypted,
}; };
IconData get icon {
switch (type) {
case 1:
return FontAwesomeIcons.userGroup;
default:
return FontAwesomeIcons.hashtag;
}
}
} }
class ChannelMember { class ChannelMember {

View File

@ -16,10 +16,10 @@ class Friendship {
required this.id, required this.id,
required this.createdAt, required this.createdAt,
required this.updatedAt, required this.updatedAt,
this.deletedAt, required this.deletedAt,
required this.accountId, required this.accountId,
required this.relatedId, required this.relatedId,
this.blockedBy, required this.blockedBy,
required this.account, required this.account,
required this.related, required this.related,
required this.status, required this.status,

View File

@ -16,25 +16,31 @@ class Notification {
required this.id, required this.id,
required this.createdAt, required this.createdAt,
required this.updatedAt, required this.updatedAt,
this.deletedAt, required this.deletedAt,
required this.subject, required this.subject,
required this.content, required this.content,
this.links, required this.links,
required this.isImportant, required this.isImportant,
required this.isRealtime, required this.isRealtime,
this.readAt, required this.readAt,
this.senderId, required this.senderId,
required this.recipientId, required this.recipientId,
}); });
factory Notification.fromJson(Map<String, dynamic> json) => Notification( factory Notification.fromJson(Map<String, dynamic> json) => Notification(
id: json['id'] ?? 0, id: json['id'] ?? 0,
createdAt: json['created_at'] == null ? DateTime.now() : DateTime.parse(json['created_at']), createdAt: json['created_at'] == null
updatedAt: json['updated_at'] == null ? DateTime.now() : DateTime.parse(json['updated_at']), ? DateTime.now()
: DateTime.parse(json['created_at']),
updatedAt: json['updated_at'] == null
? DateTime.now()
: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'], deletedAt: json['deleted_at'],
subject: json['subject'], subject: json['subject'],
content: json['content'], content: json['content'],
links: json['links'] != null ? List<Link>.from(json['links'].map((x) => Link.fromJson(x))) : List.empty(), links: json['links'] != null
? List<Link>.from(json['links'].map((x) => Link.fromJson(x)))
: List.empty(),
isImportant: json['is_important'], isImportant: json['is_important'],
isRealtime: json['is_realtime'], isRealtime: json['is_realtime'],
readAt: json['read_at'], readAt: json['read_at'],
@ -49,7 +55,9 @@ class Notification {
'deleted_at': deletedAt, 'deleted_at': deletedAt,
'subject': subject, 'subject': subject,
'content': content, 'content': content,
'links': links != null ? List<dynamic>.from(links!.map((x) => x.toJson())) : List.empty(), 'links': links != null
? List<dynamic>.from(links!.map((x) => x.toJson()))
: List.empty(),
'is_important': isImportant, 'is_important': isImportant,
'is_realtime': isRealtime, 'is_realtime': isRealtime,
'read_at': readAt, 'read_at': readAt,

View File

@ -13,11 +13,11 @@ class PersonalPage {
required this.id, required this.id,
required this.createdAt, required this.createdAt,
required this.updatedAt, required this.updatedAt,
this.deletedAt, required this.deletedAt,
required this.content, required this.content,
required this.script, required this.script,
required this.style, required this.style,
this.links, required this.links,
required this.accountId, required this.accountId,
}); });
@ -25,7 +25,9 @@ class PersonalPage {
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,
content: json['content'], content: json['content'],
script: json['script'], script: json['script'],
style: json['style'], style: json['style'],

View File

@ -1,6 +1,9 @@
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/services.dart'; import 'package:solian/services.dart';
import 'package:solian/widgets/account/friend_select.dart';
import 'package:uuid/uuid.dart';
class ChannelProvider extends GetxController { class ChannelProvider extends GetxController {
Future<Response> getChannel(String alias, {String realm = 'global'}) async { Future<Response> getChannel(String alias, {String realm = 'global'}) async {
@ -33,4 +36,70 @@ class ChannelProvider extends GetxController {
return resp; return resp;
} }
Future<Response> createChannel(String scope, dynamic payload) async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized');
final client = GetConnect(maxAuthRetries: 3);
client.httpClient.baseUrl = ServiceFinder.services['messaging'];
client.httpClient.addAuthenticator(auth.requestAuthenticator);
final resp = await client.post('/api/channels/$scope', payload);
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
Future<Response?> createDirectChannel(
BuildContext context, String scope) async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized');
final related = await showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => FriendSelect(
title: 'channelOrganizeDirectHint'.tr,
),
);
if (related == null) return null;
final prof = await auth.getProfile();
final client = GetConnect(maxAuthRetries: 3);
client.httpClient.baseUrl = ServiceFinder.services['messaging'];
client.httpClient.addAuthenticator(auth.requestAuthenticator);
final resp = await client.post('/api/channels/$scope/dm', {
'alias': const Uuid().v4().replaceAll('-', '').substring(0, 12),
'name': 'DM',
'description':
'A direct message channel between @${prof.body['name']} and @${related.name}',
'related_user': related.id,
'is_encrypted': false,
});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
Future<Response> updateChannel(String scope, int id, dynamic payload) async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized');
final client = GetConnect(maxAuthRetries: 3);
client.httpClient.baseUrl = ServiceFinder.services['messaging'];
client.httpClient.addAuthenticator(auth.requestAuthenticator);
final resp = await client.put('/api/channels/$scope/$id', payload);
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
} }

View File

@ -20,7 +20,7 @@ abstract class AppRouter {
routes: [ routes: [
ShellRoute( ShellRoute(
builder: (context, state, child) => builder: (context, state, child) =>
NavShell(state: state, child: child, showAppBar: false), NavShell(state: state, showAppBar: false, child: child),
routes: [ routes: [
GoRoute( GoRoute(
path: '/', path: '/',

View File

@ -96,11 +96,11 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row( child: Row(
children: [ children: [
CircleAvatar( const CircleAvatar(
radius: 28, radius: 28,
backgroundColor: Colors.teal, backgroundColor: Colors.teal,
child: FaIcon( child: FaIcon(
widget.channel.icon, FontAwesomeIcons.hashtag,
color: Colors.white, color: Colors.white,
size: 18, size: 18,
), ),

View File

@ -1,14 +1,12 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/account.dart';
import 'package:solian/models/channel.dart'; import 'package:solian/models/channel.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/channel.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
import 'package:solian/widgets/account/friend_select.dart';
import 'package:solian/widgets/prev_page.dart'; import 'package:solian/widgets/prev_page.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -29,11 +27,6 @@ class ChannelOrganizeScreen extends StatefulWidget {
} }
class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> { class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
static Map<int, String> channelTypes = {
0: 'channelTypeCommon'.tr,
1: 'channelTypeDirect'.tr,
};
bool _isBusy = false; bool _isBusy = false;
final _aliasController = TextEditingController(); final _aliasController = TextEditingController();
@ -41,38 +34,6 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
final _descriptionController = TextEditingController(); final _descriptionController = TextEditingController();
bool _isEncrypted = false; bool _isEncrypted = false;
int _channelType = 0;
List<Account> _initialMembers = List.empty(growable: true);
void selectInitialMembers() async {
final input = await showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
context: context,
builder: (context) => FriendSelect(
title: 'channelMember'.tr,
trailingBuilder: (item) {
if (_initialMembers.any((e) => e.id == item.id)) {
return const Icon(Icons.check);
} else {
return null;
}
},
),
);
if (input == null) return;
setState(() {
if (_initialMembers.any((e) => e.id == input.id)) {
_initialMembers = _initialMembers
.where((e) => e.id != input.id)
.toList(growable: true);
} else {
_initialMembers.add(input as Account);
}
});
}
void applyChannel() async { void applyChannel() async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
@ -82,6 +43,8 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final ChannelProvider provider = Get.find();
final client = GetConnect(maxAuthRetries: 3); final client = GetConnect(maxAuthRetries: 3);
client.httpClient.baseUrl = ServiceFinder.services['messaging']; client.httpClient.baseUrl = ServiceFinder.services['messaging'];
client.httpClient.addAuthenticator(auth.requestAuthenticator); client.httpClient.addAuthenticator(auth.requestAuthenticator);
@ -92,27 +55,21 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
'name': _nameController.value.text, 'name': _nameController.value.text,
'description': _descriptionController.value.text, 'description': _descriptionController.value.text,
'is_encrypted': _isEncrypted, 'is_encrypted': _isEncrypted,
if (_channelType == 1)
'members': _initialMembers.map((e) => e.id).toList(),
}; };
Response resp; Response? resp;
if (widget.edit != null) { try {
resp = await client.put( if (widget.edit != null) {
'/api/channels/$scope/${widget.edit!.id}', resp = await provider.updateChannel(scope!, widget.edit!.id, payload);
payload, } else {
); resp = await provider.createChannel(scope!, payload);
} else if (_channelType == 1) { }
resp = await client.post('/api/channels/$scope/dm', payload); } catch (e) {
} else { context.showErrorDialog(e);
resp = await client.post('/api/channels/$scope', payload);
}
if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString);
} else {
AppRouter.instance.pop(resp.body);
} }
AppRouter.instance.pop(resp!.body);
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
@ -127,7 +84,6 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
_nameController.text = widget.edit!.name; _nameController.text = widget.edit!.name;
_descriptionController.text = widget.edit!.description; _descriptionController.text = widget.edit!.description;
_isEncrypted = widget.edit!.isEncrypted; _isEncrypted = widget.edit!.isEncrypted;
_channelType = widget.edit!.type;
} }
} }
@ -227,55 +183,6 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
).paddingSymmetric(horizontal: 16, vertical: 12), ).paddingSymmetric(horizontal: 16, vertical: 12),
), ),
const Divider(thickness: 0.3), const Divider(thickness: 0.3),
if (_channelType == 1 && widget.edit == null)
ListTile(
leading: const Icon(Icons.supervisor_account)
.paddingSymmetric(horizontal: 8),
title: Text('channelMember'.tr),
subtitle: _initialMembers.isNotEmpty
? Text(_initialMembers.map((e) => e.name).join(' '))
: null,
trailing: const Icon(Icons.chevron_right),
onTap: () => selectInitialMembers(),
).animate().fadeIn().slideY(
begin: 1,
end: 0,
curve: Curves.fastEaseInToSlowEaseOut,
),
ListTile(
leading: const Icon(Icons.mode).paddingSymmetric(horizontal: 8),
title: Text('channelType'.tr),
trailing: DropdownButtonHideUnderline(
child: DropdownButton2<int>(
isExpanded: true,
items: channelTypes.entries
.map((item) => DropdownMenuItem<int>(
enabled: widget.edit == null ||
item.key == widget.edit?.type,
value: item.key,
child: Text(
item.value,
style: const TextStyle(
fontSize: 14,
),
),
))
.toList(),
value: _channelType,
onChanged: (int? value) {
setState(() => _channelType = value ?? 0);
},
buttonStyleData: const ButtonStyleData(
padding: EdgeInsets.only(left: 16, right: 1),
height: 40,
width: 140,
),
menuItemStyleData: const MenuItemStyleData(
height: 40,
),
),
),
),
CheckboxListTile( CheckboxListTile(
title: Text('channelEncrypted'.tr), title: Text('channelEncrypted'.tr),
value: _isEncrypted, value: _isEncrypted,

View File

@ -8,6 +8,7 @@ import 'package:solian/providers/content/channel.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/screens/account/notification.dart'; import 'package:solian/screens/account/notification.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/signin_required_overlay.dart'; import 'package:solian/widgets/account/signin_required_overlay.dart';
class ContactScreen extends StatefulWidget { class ContactScreen extends StatefulWidget {
@ -19,9 +20,16 @@ class ContactScreen extends StatefulWidget {
class _ContactScreenState extends State<ContactScreen> { class _ContactScreenState extends State<ContactScreen> {
bool _isBusy = true; bool _isBusy = true;
int? _accountId;
final List<Channel> _channels = List.empty(growable: true); final List<Channel> _channels = List.empty(growable: true);
getProfile() async {
final AuthProvider auth = Get.find();
final prof = await auth.getProfile();
_accountId = prof.body['id'];
}
getChannels() async { getChannels() async {
setState(() => _isBusy = true); setState(() => _isBusy = true);
@ -42,6 +50,7 @@ class _ContactScreenState extends State<ContactScreen> {
void initState() { void initState() {
super.initState(); super.initState();
getProfile();
getChannels(); getChannels();
} }
@ -81,17 +90,48 @@ class _ContactScreenState extends State<ContactScreen> {
forceElevated: innerBoxIsScrolled, forceElevated: innerBoxIsScrolled,
actions: [ actions: [
const NotificationButton(), const NotificationButton(),
IconButton( PopupMenuButton(
icon: const Icon(Icons.add_circle), icon: const Icon(Icons.add_circle),
onPressed: () { itemBuilder: (BuildContext context) => [
AppRouter.instance PopupMenuItem(
.pushNamed('channelOrganizing') child: ListTile(
.then( title: Text('channelOrganizeCommon'.tr),
(value) { leading: const Icon(Icons.tag),
if (value != null) getChannels(); contentPadding:
const EdgeInsets.symmetric(horizontal: 8),
),
onTap: () {
AppRouter.instance
.pushNamed('channelOrganizing')
.then(
(value) {
if (value != null) getChannels();
},
);
}, },
); ),
}, PopupMenuItem(
child: ListTile(
title: Text('channelOrganizeDirect'.tr),
leading: const FaIcon(
FontAwesomeIcons.userGroup,
size: 16,
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 8),
),
onTap: () {
final ChannelProvider provider = Get.find();
provider
.createDirectChannel(context, 'global')
.then((resp) {
if (resp != null) {
getChannels();
}
});
},
),
],
), ),
SizedBox( SizedBox(
width: SolianTheme.isLargeScreen(context) ? 8 : 16, width: SolianTheme.isLargeScreen(context) ? 8 : 16,
@ -115,30 +155,7 @@ class _ContactScreenState extends State<ContactScreen> {
itemCount: _channels.length, itemCount: _channels.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final element = _channels[index]; final element = _channels[index];
return ListTile( return buildItem(element);
leading: CircleAvatar(
backgroundColor: Colors.indigo,
child: FaIcon(
element.icon,
color: Colors.white,
size: 16,
),
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 24),
title: Text(element.name),
subtitle: Text(element.description),
onTap: () {
AppRouter.instance.pushNamed(
'channelChat',
pathParameters: {'alias': element.alias},
queryParameters: {
if (element.realmId != null)
'realm': element.realm!.alias,
},
);
},
);
}, },
), ),
), ),
@ -152,4 +169,58 @@ class _ContactScreenState extends State<ContactScreen> {
), ),
); );
} }
Widget buildItem(Channel element) {
if (element.type == 1) {
final otherside = element.members!
.where((e) => e.account.externalId != _accountId)
.first;
return ListTile(
leading: AccountAvatar(
content: otherside.account.avatar,
bgColor: Colors.indigo,
feColor: Colors.white,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
title: Text(otherside.account.name),
subtitle: Text(
'channelDirectDescription'
.trParams({'username': otherside.account.name}),
),
onTap: () {
AppRouter.instance.pushNamed(
'channelChat',
pathParameters: {'alias': element.alias},
queryParameters: {
if (element.realmId != null) 'realm': element.realm!.alias,
},
);
},
);
}
return ListTile(
leading: const CircleAvatar(
backgroundColor: Colors.indigo,
child: FaIcon(
FontAwesomeIcons.hashtag,
color: Colors.white,
size: 16,
),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
title: Text(element.name),
subtitle: Text(element.description),
onTap: () {
AppRouter.instance.pushNamed(
'channelChat',
pathParameters: {'alias': element.alias},
queryParameters: {
if (element.realmId != null) 'realm': element.realm!.alias,
},
);
},
);
}
} }

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';

View File

@ -105,10 +105,14 @@ class SolianMessages extends Translations {
'realmPublic': 'Public Realm', 'realmPublic': 'Public Realm',
'realmCommunity': 'Community Realm', 'realmCommunity': 'Community Realm',
'channelOrganizing': 'Organize a channel', 'channelOrganizing': 'Organize a channel',
'channelOrganizeCommon': 'Create regular channel',
'channelOrganizeDirect': 'Create DM',
'channelOrganizeDirectHint': 'Choose friend to create DM',
'channelEditingNotify': 'You\'re editing channel @channel', 'channelEditingNotify': 'You\'re editing channel @channel',
'channelAlias': 'Alias (Identifier)', 'channelAlias': 'Alias (Identifier)',
'channelName': 'Name', 'channelName': 'Name',
'channelDescription': 'Description', 'channelDescription': 'Description',
'channelDirectDescription': 'Direct message with @username',
'channelEncrypted': 'Encrypted Channel', 'channelEncrypted': 'Encrypted Channel',
'channelMember': 'Channel member', 'channelMember': 'Channel member',
'channelMembers': 'Channel members', 'channelMembers': 'Channel members',
@ -222,10 +226,14 @@ class SolianMessages extends Translations {
'realmPublic': '公开领域', 'realmPublic': '公开领域',
'realmCommunity': '社区领域', 'realmCommunity': '社区领域',
'channelOrganizing': '组织频道', 'channelOrganizing': '组织频道',
'channelOrganizeCommon': '创建普通频道',
'channelOrganizeDirect': '创建私信频道',
'channelOrganizeDirectHint': '选择好友来创建私信',
'channelEditingNotify': '你正在编辑频道 @channel', 'channelEditingNotify': '你正在编辑频道 @channel',
'channelAlias': '别称(标识符)', 'channelAlias': '别称(标识符)',
'channelName': '显示名称', 'channelName': '显示名称',
'channelDescription': '频道简介', 'channelDescription': '频道简介',
'channelDirectDescription': '与 @username 的私聊',
'channelEncrypted': '加密频道', 'channelEncrypted': '加密频道',
'channelMember': '频道成员', 'channelMember': '频道成员',
'channelMembers': '频道成员', 'channelMembers': '频道成员',

View File

@ -3,11 +3,17 @@ import 'package:solian/services.dart';
class AccountAvatar extends StatelessWidget { class AccountAvatar extends StatelessWidget {
final dynamic content; final dynamic content;
final Color? color; final Color? bgColor;
final Color? feColor;
final double? radius; final double? radius;
const AccountAvatar( const AccountAvatar({
{super.key, required this.content, this.color, this.radius}); super.key,
required this.content,
this.bgColor,
this.feColor,
this.radius,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -22,7 +28,7 @@ class AccountAvatar extends StatelessWidget {
return CircleAvatar( return CircleAvatar(
key: Key('a$content'), key: Key('a$content'),
radius: radius, radius: radius,
backgroundColor: color, backgroundColor: bgColor,
backgroundImage: !isEmpty backgroundImage: !isEmpty
? NetworkImage( ? NetworkImage(
direct direct
@ -34,6 +40,7 @@ class AccountAvatar extends StatelessWidget {
? Icon( ? Icon(
Icons.account_circle, Icons.account_circle,
size: radius != null ? radius! * 1.2 : 24, size: radius != null ? radius! * 1.2 : 24,
color: feColor,
) )
: null, : null,
); );

View File

@ -19,7 +19,7 @@ class FriendSelect extends StatefulWidget {
class _FriendSelectState extends State<FriendSelect> { class _FriendSelectState extends State<FriendSelect> {
int _accountId = 0; int _accountId = 0;
List<Friendship> _friends = List.empty(growable: true); final List<Friendship> _friends = List.empty(growable: true);
getFriends() async { getFriends() async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();

View File

@ -56,7 +56,7 @@ class _AttachmentPublishingPopupState extends State<AttachmentPublishingPopup> {
ratio: await calculateFileAspectRatio(file), ratio: await calculateFileAspectRatio(file),
); );
} catch (err) { } catch (err) {
this.context.showErrorDialog(err); context.showErrorDialog(err);
} }
} }
@ -79,7 +79,7 @@ class _AttachmentPublishingPopupState extends State<AttachmentPublishingPopup> {
try { try {
await uploadAttachment(file, hash, ratio: ratio); await uploadAttachment(file, hash, ratio: ratio);
} catch (err) { } catch (err) {
this.context.showErrorDialog(err); context.showErrorDialog(err);
} }
setState(() => _isBusy = false); setState(() => _isBusy = false);
@ -102,7 +102,7 @@ class _AttachmentPublishingPopupState extends State<AttachmentPublishingPopup> {
try { try {
await uploadAttachment(file, hash); await uploadAttachment(file, hash);
} catch (err) { } catch (err) {
this.context.showErrorDialog(err); context.showErrorDialog(err);
} }
} }
@ -136,7 +136,7 @@ class _AttachmentPublishingPopupState extends State<AttachmentPublishingPopup> {
try { try {
await uploadAttachment(file, hash, ratio: ratio); await uploadAttachment(file, hash, ratio: ratio);
} catch (err) { } catch (err) {
this.context.showErrorDialog(err); context.showErrorDialog(err);
} }
setState(() => _isBusy = false); setState(() => _isBusy = false);
@ -392,7 +392,7 @@ class _AttachmentEditingDialogState extends State<AttachmentEditingDialog> {
); );
return Attachment.fromJson(resp.body); return Attachment.fromJson(resp.body);
} catch (e) { } catch (e) {
this.context.showErrorDialog(e); context.showErrorDialog(e);
return null; return null;
} finally { } finally {
setState(() => _isBusy = false); setState(() => _isBusy = false);
@ -406,7 +406,7 @@ class _AttachmentEditingDialogState extends State<AttachmentEditingDialog> {
await provider.deleteAttachment(widget.item.id); await provider.deleteAttachment(widget.item.id);
widget.onDelete(); widget.onDelete();
} catch (e) { } catch (e) {
this.context.showErrorDialog(e); context.showErrorDialog(e);
} finally { } finally {
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }

View File

@ -1,4 +1,3 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';