✨ Badges
This commit is contained in:
parent
6090367ed6
commit
6007bdff77
@ -8,6 +8,7 @@ class Account {
|
|||||||
dynamic avatar;
|
dynamic avatar;
|
||||||
dynamic banner;
|
dynamic banner;
|
||||||
String description;
|
String description;
|
||||||
|
List<AccountBadge>? badges;
|
||||||
String? emailAddress;
|
String? emailAddress;
|
||||||
int? externalId;
|
int? externalId;
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ class Account {
|
|||||||
required this.avatar,
|
required this.avatar,
|
||||||
required this.banner,
|
required this.banner,
|
||||||
required this.description,
|
required this.description,
|
||||||
|
required this.badges,
|
||||||
required this.emailAddress,
|
required this.emailAddress,
|
||||||
this.externalId,
|
this.externalId,
|
||||||
});
|
});
|
||||||
@ -36,6 +38,10 @@ class Account {
|
|||||||
banner: json['banner'],
|
banner: json['banner'],
|
||||||
description: json['description'],
|
description: json['description'],
|
||||||
emailAddress: json['email_address'],
|
emailAddress: json['email_address'],
|
||||||
|
badges: json['badges']
|
||||||
|
?.map((e) => AccountBadge.fromJson(e))
|
||||||
|
.toList()
|
||||||
|
.cast<AccountBadge>(),
|
||||||
externalId: json['external_id'],
|
externalId: json['external_id'],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -50,6 +56,49 @@ class Account {
|
|||||||
'banner': banner,
|
'banner': banner,
|
||||||
'description': description,
|
'description': description,
|
||||||
'email_address': emailAddress,
|
'email_address': emailAddress,
|
||||||
|
'badges': badges?.map((e) => e.toJson()).toList(),
|
||||||
'external_id': externalId,
|
'external_id': externalId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AccountBadge {
|
||||||
|
int id;
|
||||||
|
DateTime createdAt;
|
||||||
|
DateTime updatedAt;
|
||||||
|
DateTime? deletedAt;
|
||||||
|
Map<String, dynamic>? metadata;
|
||||||
|
String type;
|
||||||
|
int accountId;
|
||||||
|
|
||||||
|
AccountBadge({
|
||||||
|
required this.id,
|
||||||
|
required this.accountId,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
required this.deletedAt,
|
||||||
|
required this.metadata,
|
||||||
|
required this.type,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory AccountBadge.fromJson(Map<String, dynamic> json) => AccountBadge(
|
||||||
|
id: json["id"],
|
||||||
|
accountId: json["account_id"],
|
||||||
|
updatedAt: DateTime.parse(json["updated_at"]),
|
||||||
|
createdAt: DateTime.parse(json["created_at"]),
|
||||||
|
deletedAt: json["deleted_at"] != null
|
||||||
|
? DateTime.parse(json["deleted_at"])
|
||||||
|
: null,
|
||||||
|
metadata: json["metadata"],
|
||||||
|
type: json["type"],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
"id": id,
|
||||||
|
"account_id": accountId,
|
||||||
|
"created_at": createdAt.toIso8601String(),
|
||||||
|
"updated_at": updatedAt.toIso8601String(),
|
||||||
|
"deleted_at": deletedAt?.toIso8601String(),
|
||||||
|
"metadata": metadata,
|
||||||
|
"type": type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -36,7 +36,11 @@ class AccountProvider extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void connect({noRetry = false}) async {
|
void connect({noRetry = false}) async {
|
||||||
if (isConnected.value) return;
|
if (isConnected.value) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
||||||
|
@ -17,7 +17,11 @@ class ChatProvider extends GetxController {
|
|||||||
StreamController<NetworkPackage> stream = StreamController.broadcast();
|
StreamController<NetworkPackage> stream = StreamController.broadcast();
|
||||||
|
|
||||||
void connect({noRetry = false}) async {
|
void connect({noRetry = false}) async {
|
||||||
if (isConnected.value) return;
|
if (isConnected.value) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
||||||
|
@ -50,7 +50,10 @@ class AttachmentProvider extends GetConnect {
|
|||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
||||||
|
|
||||||
final client = GetConnect(maxAuthRetries: 3);
|
final client = GetConnect(
|
||||||
|
maxAuthRetries: 3,
|
||||||
|
timeout: const Duration(minutes: 3),
|
||||||
|
);
|
||||||
client.httpClient.baseUrl = ServiceFinder.services['paperclip'];
|
client.httpClient.baseUrl = ServiceFinder.services['paperclip'];
|
||||||
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||||
|
|
||||||
@ -68,9 +71,7 @@ class AttachmentProvider extends GetConnect {
|
|||||||
if (mimetypeOverrides.keys.contains(fileExt)) {
|
if (mimetypeOverrides.keys.contains(fileExt)) {
|
||||||
mimetypeOverride = mimetypeOverrides[fileExt];
|
mimetypeOverride = mimetypeOverrides[fileExt];
|
||||||
}
|
}
|
||||||
final resp = await client.post(
|
final payload = FormData({
|
||||||
'/api/attachments',
|
|
||||||
FormData({
|
|
||||||
'alt': fileAlt,
|
'alt': fileAlt,
|
||||||
'file': filePayload,
|
'file': filePayload,
|
||||||
'hash': hash,
|
'hash': hash,
|
||||||
@ -79,10 +80,10 @@ class AttachmentProvider extends GetConnect {
|
|||||||
'metadata': jsonEncode({
|
'metadata': jsonEncode({
|
||||||
if (ratio != null) 'ratio': ratio,
|
if (ratio != null) 'ratio': ratio,
|
||||||
}),
|
}),
|
||||||
}),
|
});
|
||||||
);
|
final resp = await client.post('/api/attachments', payload);
|
||||||
if (resp.statusCode != 200) {
|
if (resp.statusCode != 200) {
|
||||||
throw Exception('${resp.statusCode}: ${resp.bodyString}');
|
throw Exception(resp.bodyString);
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:solian/models/account.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.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/services.dart';
|
import 'package:solian/widgets/account/account_heading.dart';
|
||||||
import 'package:solian/widgets/account/account_avatar.dart';
|
|
||||||
|
|
||||||
class AccountScreen extends StatefulWidget {
|
class AccountScreen extends StatefulWidget {
|
||||||
const AccountScreen({super.key});
|
const AccountScreen({super.key});
|
||||||
@ -30,6 +30,7 @@ class _AccountScreenState extends State<AccountScreen> {
|
|||||||
|
|
||||||
return Material(
|
return Material(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
child: SafeArea(
|
||||||
child: FutureBuilder(
|
child: FutureBuilder(
|
||||||
future: provider.isAuthorized,
|
future: provider.isAuthorized,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
@ -78,7 +79,7 @@ class _AccountScreenState extends State<AccountScreen> {
|
|||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
const AccountNameCard().paddingOnly(bottom: 8),
|
const AccountHeading().paddingOnly(bottom: 8),
|
||||||
...(actionItems.map(
|
...(actionItems.map(
|
||||||
(x) => ListTile(
|
(x) => ListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
||||||
@ -104,12 +105,13 @@ class _AccountScreenState extends State<AccountScreen> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccountNameCard extends StatelessWidget {
|
class AccountHeading extends StatelessWidget {
|
||||||
const AccountNameCard({super.key});
|
const AccountHeading({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -123,69 +125,16 @@ class AccountNameCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final prof = snapshot.data!;
|
final prof = snapshot.data!;
|
||||||
return Material(
|
return AccountHeadingWidget(
|
||||||
child: Column(
|
avatar: prof.body['avatar'],
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
banner: prof.body['banner'],
|
||||||
children: [
|
name: prof.body['name'],
|
||||||
AspectRatio(
|
nick: prof.body['nick'],
|
||||||
aspectRatio: 16 / 7,
|
desc: prof.body['description'],
|
||||||
child: Container(
|
badges: prof.body['badges']
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
?.map((e) => AccountBadge.fromJson(e))
|
||||||
child: Stack(
|
.toList()
|
||||||
clipBehavior: Clip.none,
|
.cast<AccountBadge>(),
|
||||||
fit: StackFit.expand,
|
|
||||||
children: [
|
|
||||||
if (prof.body['banner'] != null)
|
|
||||||
Image.network(
|
|
||||||
'${ServiceFinder.services['paperclip']}/api/attachments/${prof.body['banner']}',
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: -30,
|
|
||||||
left: 18,
|
|
||||||
child: AccountAvatar(
|
|
||||||
content: prof.body['avatar'],
|
|
||||||
radius: 48,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
|
||||||
textBaseline: TextBaseline.alphabetic,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
prof.body['nick'],
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 17,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
).paddingOnly(right: 4),
|
|
||||||
Text(
|
|
||||||
'@${prof.body['name']}',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).paddingOnly(left: 120, top: 8),
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Card(
|
|
||||||
child: ListTile(
|
|
||||||
title: Text('description'.tr),
|
|
||||||
subtitle: Text(
|
|
||||||
prof.body['description']?.isNotEmpty
|
|
||||||
? prof.body['description']
|
|
||||||
: 'No description yet.',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).paddingOnly(left: 24, right: 24, top: 8),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -156,10 +156,10 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
const double padding = 32;
|
||||||
|
|
||||||
return Material(
|
return Material(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
color: Theme.of(context).colorScheme.surface,
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||||
@ -179,7 +179,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
).paddingSymmetric(horizontal: padding),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Stack(
|
Stack(
|
||||||
children: [
|
children: [
|
||||||
@ -193,16 +193,14 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
|||||||
? Image.network(
|
? Image.network(
|
||||||
'${ServiceFinder.services['paperclip']}/api/attachments/$_banner',
|
'${ServiceFinder.services['paperclip']}/api/attachments/$_banner',
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
loadingBuilder: (BuildContext context,
|
loadingBuilder: (BuildContext context, Widget child,
|
||||||
Widget child,
|
|
||||||
ImageChunkEvent? loadingProgress) {
|
ImageChunkEvent? loadingProgress) {
|
||||||
if (loadingProgress == null) return child;
|
if (loadingProgress == null) return child;
|
||||||
return Center(
|
return Center(
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
value: loadingProgress.expectedTotalBytes !=
|
value: loadingProgress.expectedTotalBytes !=
|
||||||
null
|
null
|
||||||
? loadingProgress
|
? loadingProgress.cumulativeBytesLoaded /
|
||||||
.cumulativeBytesLoaded /
|
|
||||||
loadingProgress.expectedTotalBytes!
|
loadingProgress.expectedTotalBytes!
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@ -225,7 +223,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
).paddingSymmetric(horizontal: padding),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@ -253,7 +251,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
).paddingSymmetric(horizontal: padding),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@ -279,7 +277,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
).paddingSymmetric(horizontal: padding),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _descriptionController,
|
controller: _descriptionController,
|
||||||
@ -290,7 +288,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
|||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: 'description'.tr,
|
labelText: 'description'.tr,
|
||||||
),
|
),
|
||||||
),
|
).paddingSymmetric(horizontal: padding),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _birthdayController,
|
controller: _birthdayController,
|
||||||
@ -300,7 +298,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
|||||||
labelText: 'birthday'.tr,
|
labelText: 'birthday'.tr,
|
||||||
),
|
),
|
||||||
onTap: () => selectBirthday(),
|
onTap: () => selectBirthday(),
|
||||||
),
|
).paddingSymmetric(horizontal: padding),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
@ -314,10 +312,9 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
|||||||
child: Text('apply'.tr),
|
child: Text('apply'.tr),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
).paddingSymmetric(horizontal: padding),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -186,7 +186,12 @@ class SolianMessages extends Translations {
|
|||||||
'callParticipantVideoOn': 'Turn On Participant Video',
|
'callParticipantVideoOn': 'Turn On Participant Video',
|
||||||
'callAlreadyOngoing': 'A call is already ongoing',
|
'callAlreadyOngoing': 'A call is already ongoing',
|
||||||
'sidebarPlaceholder':
|
'sidebarPlaceholder':
|
||||||
'Your screen is really too large, so we had to leave some space here to prevent the layout from being too messy. In the future, we will add some small widgets here for wealthy users like you to enjoy, but for now, it will stay this way.'
|
'Your screen is really too large, so we had to leave some space here to prevent the layout from being too messy. In the future, we will add some small widgets here for wealthy users like you to enjoy, but for now, it will stay this way.',
|
||||||
|
'badge': 'Badge',
|
||||||
|
'badges': 'Badges',
|
||||||
|
'badgeGrantAt': 'Badge awarded on @date',
|
||||||
|
'badgeSolsynthStaff': 'Solsynth Staff',
|
||||||
|
'badgeSolarOriginalCitizen': 'Solar Network Natives',
|
||||||
},
|
},
|
||||||
'zh_CN': {
|
'zh_CN': {
|
||||||
'hide': '隐藏',
|
'hide': '隐藏',
|
||||||
@ -359,7 +364,12 @@ class SolianMessages extends Translations {
|
|||||||
'callParticipantVideoOn': '解除静音参与者',
|
'callParticipantVideoOn': '解除静音参与者',
|
||||||
'callAlreadyOngoing': '当前正在进行一则通话',
|
'callAlreadyOngoing': '当前正在进行一则通话',
|
||||||
'sidebarPlaceholder':
|
'sidebarPlaceholder':
|
||||||
'你的屏幕真的太大啦,我们只好空出一块地方好让布局不那么混乱,未来我们会在此处加入一下小挂件来供你这样的富人玩乐,但现在就这样吧。'
|
'你的屏幕真的太大啦,我们只好空出一块地方好让布局不那么混乱,未来我们会在此处加入一下小挂件来供你这样的富人玩乐,但现在就这样吧。',
|
||||||
|
'badge': '徽章',
|
||||||
|
'badges': '徽章',
|
||||||
|
'badgeGrantAt': '徽章颁发于 @date',
|
||||||
|
'badgeSolsynthStaff': 'Solsynth 工作人员',
|
||||||
|
'badgeSolarOriginalCitizen': 'Solar Network 原住民',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -50,3 +50,58 @@ class AccountAvatar extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AccountProfileImage extends StatelessWidget {
|
||||||
|
final dynamic content;
|
||||||
|
final BoxFit fit;
|
||||||
|
|
||||||
|
const AccountProfileImage({
|
||||||
|
super.key,
|
||||||
|
required this.content,
|
||||||
|
this.fit = BoxFit.cover,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
bool direct = false;
|
||||||
|
bool isEmpty = content == null;
|
||||||
|
if (content is String) {
|
||||||
|
direct = content.startsWith('http');
|
||||||
|
if (!isEmpty) isEmpty = content.isEmpty;
|
||||||
|
if (!isEmpty) isEmpty = content.endsWith('/api/attachments/0');
|
||||||
|
}
|
||||||
|
|
||||||
|
final url = direct
|
||||||
|
? content
|
||||||
|
: '${ServiceFinder.services['paperclip']}/api/attachments/$content';
|
||||||
|
|
||||||
|
if (PlatformInfo.canCacheImage) {
|
||||||
|
return CachedNetworkImage(
|
||||||
|
imageUrl: url,
|
||||||
|
fit: fit,
|
||||||
|
progressIndicatorBuilder: (context, url, downloadProgress) => Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: downloadProgress.progress,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Image.network(
|
||||||
|
url,
|
||||||
|
fit: fit,
|
||||||
|
loadingBuilder: (BuildContext context, Widget child,
|
||||||
|
ImageChunkEvent? loadingProgress) {
|
||||||
|
if (loadingProgress == null) return child;
|
||||||
|
return Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: loadingProgress.expectedTotalBytes != null
|
||||||
|
? loadingProgress.cumulativeBytesLoaded /
|
||||||
|
loadingProgress.expectedTotalBytes!
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
62
lib/widgets/account/account_badge.dart
Normal file
62
lib/widgets/account/account_badge.dart
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:solian/models/account.dart';
|
||||||
|
|
||||||
|
class AccountBadgeWidget extends StatefulWidget {
|
||||||
|
final AccountBadge item;
|
||||||
|
|
||||||
|
const AccountBadgeWidget({super.key, required this.item});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AccountBadgeWidget> createState() => _AccountBadgeWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AccountBadgeWidgetState extends State<AccountBadgeWidget> {
|
||||||
|
final Map<String, (String, Widget)> badges = {
|
||||||
|
'solsynth.staff': (
|
||||||
|
'badgeSolsynthStaff'.tr,
|
||||||
|
const FaIcon(
|
||||||
|
FontAwesomeIcons.screwdriverWrench,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.teal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'solar.originalCitizen': (
|
||||||
|
'badgeSolarOriginalCitizen'.tr,
|
||||||
|
const FaIcon(
|
||||||
|
FontAwesomeIcons.tent,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.orange,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final spec = badges[widget.item.type];
|
||||||
|
|
||||||
|
if (spec == null) return const SizedBox();
|
||||||
|
|
||||||
|
return Tooltip(
|
||||||
|
richMessage: TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(text: '${spec.$1}\n'),
|
||||||
|
if (widget.item.metadata?['title'] != null)
|
||||||
|
TextSpan(
|
||||||
|
text: '${widget.item.metadata?['title']}\n',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: 'badgeGrantAt'.trParams(
|
||||||
|
{'date': DateFormat.yMEd().format(widget.item.createdAt)}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Chip(
|
||||||
|
label: spec.$2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
105
lib/widgets/account/account_heading.dart
Normal file
105
lib/widgets/account/account_heading.dart
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:solian/models/account.dart';
|
||||||
|
import 'package:solian/widgets/account/account_avatar.dart';
|
||||||
|
import 'package:solian/widgets/account/account_badge.dart';
|
||||||
|
|
||||||
|
class AccountHeadingWidget extends StatelessWidget {
|
||||||
|
final dynamic avatar;
|
||||||
|
final dynamic banner;
|
||||||
|
final String name;
|
||||||
|
final String nick;
|
||||||
|
final String? desc;
|
||||||
|
final List<AccountBadge>? badges;
|
||||||
|
|
||||||
|
const AccountHeadingWidget({
|
||||||
|
super.key,
|
||||||
|
this.avatar,
|
||||||
|
this.banner,
|
||||||
|
required this.name,
|
||||||
|
required this.nick,
|
||||||
|
required this.desc,
|
||||||
|
required this.badges,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 16 / 7,
|
||||||
|
child: Container(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
child: banner != null
|
||||||
|
? AccountProfileImage(
|
||||||
|
content: banner,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
)
|
||||||
|
: const SizedBox(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).paddingSymmetric(horizontal: 16),
|
||||||
|
Positioned(
|
||||||
|
bottom: -30,
|
||||||
|
left: 32,
|
||||||
|
child: AccountAvatar(content: avatar, radius: 40),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||||
|
textBaseline: TextBaseline.alphabetic,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
nick,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).paddingOnly(right: 4),
|
||||||
|
Text(
|
||||||
|
'@$name',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingOnly(left: 116, top: 6),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
if (badges?.isNotEmpty ?? false)
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Card(
|
||||||
|
child: Wrap(
|
||||||
|
runSpacing: 2,
|
||||||
|
spacing: 4,
|
||||||
|
children: badges!.map((e) {
|
||||||
|
return AccountBadgeWidget(item: e);
|
||||||
|
}).toList(),
|
||||||
|
).paddingSymmetric(horizontal: 8),
|
||||||
|
),
|
||||||
|
).paddingOnly(left: 16, right: 16),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Card(
|
||||||
|
child: ListTile(
|
||||||
|
title: Text('description'.tr),
|
||||||
|
subtitle: Text(
|
||||||
|
(desc?.isNotEmpty ?? false) ? desc! : 'No description yet.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).paddingOnly(left: 16, right: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@ 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/account.dart';
|
||||||
import 'package:solian/services.dart';
|
import 'package:solian/services.dart';
|
||||||
import 'package:solian/widgets/account/account_avatar.dart';
|
import 'package:solian/widgets/account/account_heading.dart';
|
||||||
|
|
||||||
class AccountProfilePopup extends StatefulWidget {
|
class AccountProfilePopup extends StatefulWidget {
|
||||||
final Account account;
|
final Account account;
|
||||||
@ -44,7 +44,7 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_isBusy) {
|
if (_isBusy || _userinfo == null) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,64 +53,14 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
AspectRatio(
|
AccountHeadingWidget(
|
||||||
aspectRatio: 16 / 7,
|
avatar: _userinfo!.avatar,
|
||||||
child: Container(
|
banner: _userinfo!.banner,
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
name: _userinfo!.name,
|
||||||
child: Stack(
|
nick: _userinfo!.nick,
|
||||||
clipBehavior: Clip.none,
|
desc: _userinfo!.description,
|
||||||
fit: StackFit.expand,
|
badges: _userinfo!.badges,
|
||||||
children: [
|
).paddingOnly(top: 16),
|
||||||
if (_userinfo!.banner != null)
|
|
||||||
Image.network(
|
|
||||||
'${ServiceFinder.services['paperclip']}/api/attachments/${_userinfo!.banner}',
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: -30,
|
|
||||||
left: 18,
|
|
||||||
child: AccountAvatar(
|
|
||||||
content: widget.account.banner,
|
|
||||||
radius: 48,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).paddingOnly(top: 32),
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
|
||||||
textBaseline: TextBaseline.alphabetic,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
_userinfo!.nick,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 17,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
).paddingOnly(right: 4),
|
|
||||||
Text(
|
|
||||||
'@${_userinfo!.name}',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).paddingOnly(left: 120, top: 8),
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Card(
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
||||||
child: ListTile(
|
|
||||||
title: Text('description'.tr),
|
|
||||||
subtitle: Text(
|
|
||||||
_userinfo!.description.isNotEmpty
|
|
||||||
? widget.account.description
|
|
||||||
: 'No description yet.',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).paddingOnly(left: 24, right: 24, top: 8),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -65,6 +65,7 @@ class _AttachmentItemState extends State<AttachmentItem> {
|
|||||||
children: [
|
children: [
|
||||||
if (PlatformInfo.canCacheImage)
|
if (PlatformInfo.canCacheImage)
|
||||||
CachedNetworkImage(
|
CachedNetworkImage(
|
||||||
|
fit: widget.fit,
|
||||||
imageUrl:
|
imageUrl:
|
||||||
'${ServiceFinder.services['paperclip']}/api/attachments/${widget.item.id}',
|
'${ServiceFinder.services['paperclip']}/api/attachments/${widget.item.id}',
|
||||||
progressIndicatorBuilder: (context, url, downloadProgress) =>
|
progressIndicatorBuilder: (context, url, downloadProgress) =>
|
||||||
@ -73,7 +74,6 @@ class _AttachmentItemState extends State<AttachmentItem> {
|
|||||||
value: downloadProgress.progress,
|
value: downloadProgress.progress,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
fit: widget.fit,
|
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
Image.network(
|
Image.network(
|
||||||
|
@ -165,9 +165,11 @@ class _PostItemState extends State<PostItem> {
|
|||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
useRootNavigator: true,
|
useRootNavigator: true,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) =>
|
builder: (context) => AccountProfilePopup(
|
||||||
AccountProfilePopup(account: item.author),
|
account: item.author,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
// This is a basic Flutter widget test.
|
|
||||||
//
|
|
||||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
|
||||||
// utility in the flutter_test package. For example, you can send tap and scroll
|
|
||||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
|
||||||
// tree, read text, and verify that the values of widget properties are correct.
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
|
|
||||||
import 'package:solian/main.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
|
||||||
// Build our app and trigger a frame.
|
|
||||||
await tester.pumpWidget(const SolianApp());
|
|
||||||
|
|
||||||
// Verify that our counter starts at 0.
|
|
||||||
expect(find.text('0'), findsOneWidget);
|
|
||||||
expect(find.text('1'), findsNothing);
|
|
||||||
|
|
||||||
// Tap the '+' icon and trigger a frame.
|
|
||||||
await tester.tap(find.byIcon(Icons.add));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
// Verify that our counter has incremented.
|
|
||||||
expect(find.text('0'), findsNothing);
|
|
||||||
expect(find.text('1'), findsOneWidget);
|
|
||||||
});
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user