Compare commits
No commits in common. "d5cf2478d8051fb93c006cba5c392f4aaf7207b3" and "a75083d916dd246ccc691142bb2408d66cf9ad1b" have entirely different histories.
d5cf2478d8
...
a75083d916
@ -249,11 +249,8 @@ class SnNetworkProvider {
|
||||
return null;
|
||||
}
|
||||
|
||||
String getAttachmentUrl(String ky, {bool preview = true}) {
|
||||
String getAttachmentUrl(String ky) {
|
||||
if (ky.startsWith("http")) return ky;
|
||||
if (!preview) {
|
||||
return '${client.options.baseUrl}/cgi/uc/attachments/$ky?preview=false';
|
||||
}
|
||||
return '${client.options.baseUrl}/cgi/uc/attachments/$ky';
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,6 @@ import 'package:surface/providers/experience.dart';
|
||||
import 'package:surface/providers/relationship.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/screens/abuse_report.dart';
|
||||
import 'package:surface/screens/account/punishments.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/types/check_in.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
@ -458,7 +457,7 @@ class _UserScreenState extends State<UserScreen>
|
||||
],
|
||||
).padding(right: 8),
|
||||
if (_account!.profile!.description.isNotEmpty)
|
||||
const Gap(4)
|
||||
const Gap(12)
|
||||
else
|
||||
const Gap(8),
|
||||
if (_account!.profile!.description.isNotEmpty)
|
||||
@ -504,15 +503,14 @@ class _UserScreenState extends State<UserScreen>
|
||||
],
|
||||
).padding(vertical: 8, horizontal: 12),
|
||||
),
|
||||
if (_account!.badges.isNotEmpty) const Gap(8),
|
||||
if (_account!.badges.isNotEmpty)
|
||||
Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: _account!.badges
|
||||
.map((ele) => AccountBadge(badge: ele))
|
||||
.toList(),
|
||||
).padding(horizontal: 8),
|
||||
const Gap(8),
|
||||
Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: _account!.badges
|
||||
.map((ele) => AccountBadge(badge: ele))
|
||||
.toList(),
|
||||
).padding(horizontal: 8),
|
||||
const Gap(8),
|
||||
Column(
|
||||
children: [
|
||||
@ -621,17 +619,6 @@ class _UserScreenState extends State<UserScreen>
|
||||
],
|
||||
).padding(all: 16),
|
||||
),
|
||||
if (_account?.punishments.isNotEmpty ?? false)
|
||||
SliverToBoxAdapter(child: const Divider()),
|
||||
if (_account?.punishments.isNotEmpty ?? false)
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
children: [
|
||||
for (final ele in _account!.punishments)
|
||||
PunishmentInfoCard(ele: ele),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_account?.profile?.links.isNotEmpty ?? false)
|
||||
SliverToBoxAdapter(child: const Divider()),
|
||||
if (_account?.profile?.links.isNotEmpty ?? false)
|
||||
|
@ -107,7 +107,74 @@ class _PunishmentsScreenState extends State<PunishmentsScreen> {
|
||||
itemCount: _punishments?.length ?? 0,
|
||||
itemBuilder: (context, index) {
|
||||
final ele = _punishments![index];
|
||||
return PunishmentInfoCard(ele: ele);
|
||||
return Card(
|
||||
margin: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(kPunishmentIcons[ele.type], size: 20),
|
||||
const Gap(6),
|
||||
Expanded(
|
||||
child: Text('punishmentType${ele.type}')
|
||||
.tr()
|
||||
.fontSize(16)
|
||||
.bold(),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(ele.reason),
|
||||
const Gap(4),
|
||||
Text(
|
||||
'punishmentCreatedAt'.tr(args: [
|
||||
DateFormat().format(
|
||||
ele.createdAt.toLocal(),
|
||||
)
|
||||
]),
|
||||
).opacity(0.8),
|
||||
Text(
|
||||
ele.expiredAt == null
|
||||
? 'punishmentExpiredNever'.tr()
|
||||
: 'punishmentExpiredAt'.tr(args: [
|
||||
DateFormat().format(
|
||||
ele.expiredAt!.toLocal(),
|
||||
)
|
||||
]),
|
||||
).opacity(0.8),
|
||||
const Gap(8),
|
||||
if (ele.moderator != null)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('punishmentModerator').tr().opacity(0.75),
|
||||
InkWell(
|
||||
child: Row(
|
||||
children: [
|
||||
AccountImage(
|
||||
content: ele.moderator!.avatar,
|
||||
radius: 8,
|
||||
),
|
||||
const Gap(4),
|
||||
Text(ele.moderator?.nick ?? 'unknown'),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'accountProfilePage',
|
||||
pathParameters: {
|
||||
'name': ele.moderator!.name,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Text('punishmentMadeBySystem').tr().opacity(0.75),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 16),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Gap(8),
|
||||
),
|
||||
@ -118,82 +185,3 @@ class _PunishmentsScreenState extends State<PunishmentsScreen> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PunishmentInfoCard extends StatelessWidget {
|
||||
const PunishmentInfoCard({
|
||||
super.key,
|
||||
required this.ele,
|
||||
});
|
||||
|
||||
final SnPunishment ele;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(kPunishmentIcons[ele.type], size: 20),
|
||||
const Gap(6),
|
||||
Expanded(
|
||||
child:
|
||||
Text('punishmentType${ele.type}').tr().fontSize(16).bold(),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(ele.reason),
|
||||
const Gap(4),
|
||||
Text(
|
||||
'punishmentCreatedAt'.tr(args: [
|
||||
DateFormat().format(
|
||||
ele.createdAt.toLocal(),
|
||||
)
|
||||
]),
|
||||
).opacity(0.8),
|
||||
Text(
|
||||
ele.expiredAt == null
|
||||
? 'punishmentExpiredNever'.tr()
|
||||
: 'punishmentExpiredAt'.tr(args: [
|
||||
DateFormat().format(
|
||||
ele.expiredAt!.toLocal(),
|
||||
)
|
||||
]),
|
||||
).opacity(0.8),
|
||||
const Gap(8),
|
||||
if (ele.moderator != null)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('punishmentModerator').tr().opacity(0.75),
|
||||
InkWell(
|
||||
child: Row(
|
||||
children: [
|
||||
AccountImage(
|
||||
content: ele.moderator!.avatar,
|
||||
radius: 8,
|
||||
),
|
||||
const Gap(4),
|
||||
Text(ele.moderator?.nick ?? 'unknown'),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'accountProfilePage',
|
||||
pathParameters: {
|
||||
'name': ele.moderator!.name,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Text('punishmentMadeBySystem').tr().opacity(0.75),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 16),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,6 @@ abstract class SnAccount with _$SnAccount {
|
||||
required String language,
|
||||
required SnAccountProfile? profile,
|
||||
@Default([]) List<SnAccountBadge> badges,
|
||||
@Default([]) List<SnPunishment> punishments,
|
||||
required DateTime? suspendedAt,
|
||||
required int? affiliatedId,
|
||||
required int? affiliatedTo,
|
||||
|
@ -29,7 +29,6 @@ mixin _$SnAccount {
|
||||
String get language;
|
||||
SnAccountProfile? get profile;
|
||||
List<SnAccountBadge> get badges;
|
||||
List<SnPunishment> get punishments;
|
||||
DateTime? get suspendedAt;
|
||||
int? get affiliatedId;
|
||||
int? get affiliatedTo;
|
||||
@ -70,8 +69,6 @@ mixin _$SnAccount {
|
||||
other.language == language) &&
|
||||
(identical(other.profile, profile) || other.profile == profile) &&
|
||||
const DeepCollectionEquality().equals(other.badges, badges) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.punishments, punishments) &&
|
||||
(identical(other.suspendedAt, suspendedAt) ||
|
||||
other.suspendedAt == suspendedAt) &&
|
||||
(identical(other.affiliatedId, affiliatedId) ||
|
||||
@ -102,7 +99,6 @@ mixin _$SnAccount {
|
||||
language,
|
||||
profile,
|
||||
const DeepCollectionEquality().hash(badges),
|
||||
const DeepCollectionEquality().hash(punishments),
|
||||
suspendedAt,
|
||||
affiliatedId,
|
||||
affiliatedTo,
|
||||
@ -112,7 +108,7 @@ mixin _$SnAccount {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, name: $name, nick: $nick, permNodes: $permNodes, language: $language, profile: $profile, badges: $badges, punishments: $punishments, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)';
|
||||
return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, name: $name, nick: $nick, permNodes: $permNodes, language: $language, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)';
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,7 +132,6 @@ abstract mixin class $SnAccountCopyWith<$Res> {
|
||||
String language,
|
||||
SnAccountProfile? profile,
|
||||
List<SnAccountBadge> badges,
|
||||
List<SnPunishment> punishments,
|
||||
DateTime? suspendedAt,
|
||||
int? affiliatedId,
|
||||
int? affiliatedTo,
|
||||
@ -172,7 +167,6 @@ class _$SnAccountCopyWithImpl<$Res> implements $SnAccountCopyWith<$Res> {
|
||||
Object? language = null,
|
||||
Object? profile = freezed,
|
||||
Object? badges = null,
|
||||
Object? punishments = null,
|
||||
Object? suspendedAt = freezed,
|
||||
Object? affiliatedId = freezed,
|
||||
Object? affiliatedTo = freezed,
|
||||
@ -236,10 +230,6 @@ class _$SnAccountCopyWithImpl<$Res> implements $SnAccountCopyWith<$Res> {
|
||||
? _self.badges
|
||||
: badges // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnAccountBadge>,
|
||||
punishments: null == punishments
|
||||
? _self.punishments
|
||||
: punishments // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnPunishment>,
|
||||
suspendedAt: freezed == suspendedAt
|
||||
? _self.suspendedAt
|
||||
: suspendedAt // ignore: cast_nullable_to_non_nullable
|
||||
@ -296,7 +286,6 @@ class _SnAccount extends SnAccount {
|
||||
required this.language,
|
||||
required this.profile,
|
||||
final List<SnAccountBadge> badges = const [],
|
||||
final List<SnPunishment> punishments = const [],
|
||||
required this.suspendedAt,
|
||||
required this.affiliatedId,
|
||||
required this.affiliatedTo,
|
||||
@ -305,7 +294,6 @@ class _SnAccount extends SnAccount {
|
||||
: _contacts = contacts,
|
||||
_permNodes = permNodes,
|
||||
_badges = badges,
|
||||
_punishments = punishments,
|
||||
super._();
|
||||
factory _SnAccount.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnAccountFromJson(json);
|
||||
@ -362,15 +350,6 @@ class _SnAccount extends SnAccount {
|
||||
return EqualUnmodifiableListView(_badges);
|
||||
}
|
||||
|
||||
final List<SnPunishment> _punishments;
|
||||
@override
|
||||
@JsonKey()
|
||||
List<SnPunishment> get punishments {
|
||||
if (_punishments is EqualUnmodifiableListView) return _punishments;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_punishments);
|
||||
}
|
||||
|
||||
@override
|
||||
final DateTime? suspendedAt;
|
||||
@override
|
||||
@ -422,8 +401,6 @@ class _SnAccount extends SnAccount {
|
||||
other.language == language) &&
|
||||
(identical(other.profile, profile) || other.profile == profile) &&
|
||||
const DeepCollectionEquality().equals(other._badges, _badges) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._punishments, _punishments) &&
|
||||
(identical(other.suspendedAt, suspendedAt) ||
|
||||
other.suspendedAt == suspendedAt) &&
|
||||
(identical(other.affiliatedId, affiliatedId) ||
|
||||
@ -454,7 +431,6 @@ class _SnAccount extends SnAccount {
|
||||
language,
|
||||
profile,
|
||||
const DeepCollectionEquality().hash(_badges),
|
||||
const DeepCollectionEquality().hash(_punishments),
|
||||
suspendedAt,
|
||||
affiliatedId,
|
||||
affiliatedTo,
|
||||
@ -464,7 +440,7 @@ class _SnAccount extends SnAccount {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, name: $name, nick: $nick, permNodes: $permNodes, language: $language, profile: $profile, badges: $badges, punishments: $punishments, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)';
|
||||
return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, name: $name, nick: $nick, permNodes: $permNodes, language: $language, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)';
|
||||
}
|
||||
}
|
||||
|
||||
@ -491,7 +467,6 @@ abstract mixin class _$SnAccountCopyWith<$Res>
|
||||
String language,
|
||||
SnAccountProfile? profile,
|
||||
List<SnAccountBadge> badges,
|
||||
List<SnPunishment> punishments,
|
||||
DateTime? suspendedAt,
|
||||
int? affiliatedId,
|
||||
int? affiliatedTo,
|
||||
@ -528,7 +503,6 @@ class __$SnAccountCopyWithImpl<$Res> implements _$SnAccountCopyWith<$Res> {
|
||||
Object? language = null,
|
||||
Object? profile = freezed,
|
||||
Object? badges = null,
|
||||
Object? punishments = null,
|
||||
Object? suspendedAt = freezed,
|
||||
Object? affiliatedId = freezed,
|
||||
Object? affiliatedTo = freezed,
|
||||
@ -592,10 +566,6 @@ class __$SnAccountCopyWithImpl<$Res> implements _$SnAccountCopyWith<$Res> {
|
||||
? _self._badges
|
||||
: badges // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnAccountBadge>,
|
||||
punishments: null == punishments
|
||||
? _self._punishments
|
||||
: punishments // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnPunishment>,
|
||||
suspendedAt: freezed == suspendedAt
|
||||
? _self.suspendedAt
|
||||
: suspendedAt // ignore: cast_nullable_to_non_nullable
|
||||
|
@ -32,10 +32,6 @@ _SnAccount _$SnAccountFromJson(Map<String, dynamic> json) => _SnAccount(
|
||||
?.map((e) => SnAccountBadge.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
punishments: (json['punishments'] as List<dynamic>?)
|
||||
?.map((e) => SnPunishment.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
suspendedAt: json['suspended_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['suspended_at'] as String),
|
||||
@ -61,7 +57,6 @@ Map<String, dynamic> _$SnAccountToJson(_SnAccount instance) =>
|
||||
'language': instance.language,
|
||||
'profile': instance.profile?.toJson(),
|
||||
'badges': instance.badges.map((e) => e.toJson()).toList(),
|
||||
'punishments': instance.punishments.map((e) => e.toJson()).toList(),
|
||||
'suspended_at': instance.suspendedAt?.toIso8601String(),
|
||||
'affiliated_id': instance.affiliatedId,
|
||||
'affiliated_to': instance.affiliatedTo,
|
||||
|
@ -22,151 +22,142 @@ class AccountPopoverCard extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (data.banner.isNotEmpty)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(data.banner),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (data.banner.isNotEmpty)
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(data.banner),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
).padding(all: 16)
|
||||
else
|
||||
const Gap(16),
|
||||
// Top padding
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AccountImage(
|
||||
content: data.avatar,
|
||||
radius: 20,
|
||||
),
|
||||
Gap(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(data.nick).bold(),
|
||||
Text('@${data.name}').fontSize(13).opacity(0.75),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
GoRouter.of(context).pushReplacementNamed(
|
||||
'accountProfilePage',
|
||||
pathParameters: {'name': data.name},
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.chevron_right),
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity:
|
||||
const VisualDensity(horizontal: -4, vertical: -4),
|
||||
),
|
||||
const Gap(8)
|
||||
],
|
||||
).padding(horizontal: 16),
|
||||
if (data.badges.isNotEmpty)
|
||||
Wrap(
|
||||
spacing: 4,
|
||||
children: data.badges
|
||||
.map(
|
||||
(ele) => AccountBadge(badge: ele),
|
||||
)
|
||||
.toList(),
|
||||
).padding(horizontal: 24, bottom: 12, top: 12),
|
||||
if (data.profile?.description.isNotEmpty ?? false)
|
||||
Text(
|
||||
data.profile?.description ?? '',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).padding(horizontal: 26, bottom: 8)
|
||||
else
|
||||
const Gap(12),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.star),
|
||||
const Gap(8),
|
||||
Text('Lv${getLevelFromExp(data.profile?.experience ?? 0)}'),
|
||||
const Gap(8),
|
||||
Text(calcLevelUpProgressLevel(data.profile?.experience ?? 0))
|
||||
.fontSize(11)
|
||||
.opacity(0.5),
|
||||
const Gap(8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
constraints: const BoxConstraints(maxWidth: 160),
|
||||
child: LinearProgressIndicator(
|
||||
value: calcLevelUpProgress(data.profile?.experience ?? 0),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainer,
|
||||
).alignment(Alignment.centerLeft),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24),
|
||||
FutureBuilder(
|
||||
future: sn.client.get('/cgi/id/users/${data.name}/status'),
|
||||
builder: (context, snapshot) {
|
||||
final SnAccountStatusInfo? status = snapshot.hasData
|
||||
? SnAccountStatusInfo.fromJson(snapshot.data!.data)
|
||||
: null;
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
(status?.isDisturbable ?? true)
|
||||
? Symbols.circle
|
||||
: Symbols.do_not_disturb_on,
|
||||
fill: (status?.isOnline ?? false) ? 1 : 0,
|
||||
size: 16,
|
||||
color: (status?.isOnline ?? false)
|
||||
? (status?.isDisturbable ?? true)
|
||||
? Colors.green
|
||||
: Colors.red
|
||||
: Colors.grey,
|
||||
).padding(all: 4),
|
||||
const Gap(8),
|
||||
Text(
|
||||
status != null
|
||||
? (status.status?.label.isNotEmpty ?? false)
|
||||
? status.status!.label
|
||||
: status.isOnline
|
||||
? 'accountStatusOnline'.tr()
|
||||
: 'accountStatusOffline'.tr()
|
||||
: 'loading'.tr(),
|
||||
),
|
||||
if (status != null &&
|
||||
!status.isOnline &&
|
||||
status.lastSeenAt != null)
|
||||
Text(
|
||||
'accountStatusLastSeen'.tr(args: [
|
||||
status.lastSeenAt != null
|
||||
? RelativeTime(context).format(
|
||||
status.lastSeenAt!.toLocal(),
|
||||
)
|
||||
: 'unknown',
|
||||
]),
|
||||
).padding(left: 6).opacity(0.75),
|
||||
],
|
||||
).padding(horizontal: 24);
|
||||
},
|
||||
),
|
||||
),
|
||||
// Bottom padding
|
||||
const Gap(64),
|
||||
],
|
||||
),
|
||||
// Top padding
|
||||
Gap(16),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AccountImage(
|
||||
content: data.avatar,
|
||||
radius: 20,
|
||||
),
|
||||
Gap(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(data.nick).bold(),
|
||||
Text('@${data.name}').fontSize(13).opacity(0.75),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
GoRouter.of(context).pushReplacementNamed(
|
||||
'accountProfilePage',
|
||||
pathParameters: {'name': data.name},
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.chevron_right),
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
),
|
||||
const Gap(8)
|
||||
],
|
||||
).padding(horizontal: 16),
|
||||
if (data.badges.isNotEmpty)
|
||||
Wrap(
|
||||
spacing: 4,
|
||||
children: data.badges
|
||||
.map(
|
||||
(ele) => AccountBadge(badge: ele),
|
||||
)
|
||||
.toList(),
|
||||
).padding(horizontal: 24, bottom: 12, top: 12),
|
||||
if (data.profile?.description.isNotEmpty ?? false)
|
||||
Text(
|
||||
data.profile?.description ?? '',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).padding(horizontal: 26, bottom: 8),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.star),
|
||||
const Gap(8),
|
||||
Text('Lv${getLevelFromExp(data.profile?.experience ?? 0)}'),
|
||||
const Gap(8),
|
||||
Text(calcLevelUpProgressLevel(data.profile?.experience ?? 0))
|
||||
.fontSize(11)
|
||||
.opacity(0.5),
|
||||
const Gap(8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
constraints: const BoxConstraints(maxWidth: 160),
|
||||
child: LinearProgressIndicator(
|
||||
value: calcLevelUpProgress(data.profile?.experience ?? 0),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
).alignment(Alignment.centerLeft),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24),
|
||||
FutureBuilder(
|
||||
future: sn.client.get('/cgi/id/users/${data.name}/status'),
|
||||
builder: (context, snapshot) {
|
||||
final SnAccountStatusInfo? status = snapshot.hasData
|
||||
? SnAccountStatusInfo.fromJson(snapshot.data!.data)
|
||||
: null;
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
(status?.isDisturbable ?? true)
|
||||
? Symbols.circle
|
||||
: Symbols.do_not_disturb_on,
|
||||
fill: (status?.isOnline ?? false) ? 1 : 0,
|
||||
size: 16,
|
||||
color: (status?.isOnline ?? false)
|
||||
? (status?.isDisturbable ?? true)
|
||||
? Colors.green
|
||||
: Colors.red
|
||||
: Colors.grey,
|
||||
).padding(all: 4),
|
||||
const Gap(8),
|
||||
Text(
|
||||
status != null
|
||||
? (status.status?.label.isNotEmpty ?? false)
|
||||
? status.status!.label
|
||||
: status.isOnline
|
||||
? 'accountStatusOnline'.tr()
|
||||
: 'accountStatusOffline'.tr()
|
||||
: 'loading'.tr(),
|
||||
),
|
||||
if (status != null &&
|
||||
!status.isOnline &&
|
||||
status.lastSeenAt != null)
|
||||
Text(
|
||||
'accountStatusLastSeen'.tr(args: [
|
||||
status.lastSeenAt != null
|
||||
? RelativeTime(context).format(
|
||||
status.lastSeenAt!.toLocal(),
|
||||
)
|
||||
: 'unknown',
|
||||
]),
|
||||
).padding(left: 6).opacity(0.75),
|
||||
],
|
||||
).padding(horizontal: 24);
|
||||
},
|
||||
),
|
||||
// Bottom padding
|
||||
const Gap(16),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -170,30 +170,17 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
child: Column(
|
||||
children: widget.data
|
||||
.mapIndexed(
|
||||
(idx, ele) => AspectRatio(
|
||||
aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
|
||||
child: Container(
|
||||
constraints: constraints,
|
||||
child: AttachmentItem(
|
||||
data: ele,
|
||||
heroTag: heroTags[idx],
|
||||
fit: BoxFit.cover,
|
||||
filterQuality: widget.filterQuality,
|
||||
onZoom: () {
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data: widget.data
|
||||
.where((ele) =>
|
||||
ele != null &&
|
||||
ele.mediaType == SnMediaType.image)
|
||||
.cast(),
|
||||
initialIndex: idx,
|
||||
heroTags: heroTags,
|
||||
),
|
||||
backgroundColor: Colors.black.withOpacity(0.7),
|
||||
rootNavigator: true,
|
||||
);
|
||||
},
|
||||
(idx, ele) => GestureDetector(
|
||||
child: AspectRatio(
|
||||
aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
|
||||
child: Container(
|
||||
constraints: constraints,
|
||||
child: AttachmentItem(
|
||||
data: ele,
|
||||
heroTag: heroTags[idx],
|
||||
fit: BoxFit.cover,
|
||||
filterQuality: widget.filterQuality,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -224,52 +211,56 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
child: AspectRatio(
|
||||
aspectRatio:
|
||||
(widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (widget.data[idx]?.mediaType !=
|
||||
SnMediaType.image) {
|
||||
return;
|
||||
}
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data: widget.data
|
||||
.where((ele) =>
|
||||
ele != null &&
|
||||
ele.mediaType == SnMediaType.image)
|
||||
.cast(),
|
||||
initialIndex: idx,
|
||||
heroTags: heroTags,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
child: AttachmentItem(
|
||||
data: widget.data[idx],
|
||||
heroTag: heroTags[idx],
|
||||
filterQuality: widget.filterQuality,
|
||||
onZoom: () {
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data: widget.data
|
||||
.where((ele) =>
|
||||
ele != null &&
|
||||
ele.mediaType ==
|
||||
SnMediaType.image)
|
||||
.cast(),
|
||||
initialIndex: idx,
|
||||
heroTags: heroTags,
|
||||
),
|
||||
backgroundColor:
|
||||
Colors.black.withOpacity(0.7),
|
||||
rootNavigator: true,
|
||||
);
|
||||
},
|
||||
backgroundColor: Colors.black.withOpacity(0.7),
|
||||
rootNavigator: true,
|
||||
);
|
||||
},
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
child: AttachmentItem(
|
||||
data: widget.data[idx],
|
||||
heroTag: heroTags[idx],
|
||||
filterQuality: widget.filterQuality,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
child: Chip(
|
||||
label: Text('${idx + 1}/${widget.data.length}'),
|
||||
Positioned(
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
child: Chip(
|
||||
label: Text('${idx + 1}/${widget.data.length}'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -181,10 +181,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
scaleState == PhotoViewScaleState.initial);
|
||||
},
|
||||
imageProvider: UniversalImage.provider(
|
||||
sn.getAttachmentUrl(
|
||||
widget.data.first.rid,
|
||||
preview: false,
|
||||
),
|
||||
sn.getAttachmentUrl(widget.data.first.rid),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -202,10 +199,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
widget.heroTags?.elementAt(idx) ?? uuid.v4();
|
||||
return PhotoViewGalleryPageOptions(
|
||||
imageProvider: UniversalImage.provider(
|
||||
sn.getAttachmentUrl(
|
||||
widget.data.elementAt(idx).rid,
|
||||
preview: false,
|
||||
),
|
||||
sn.getAttachmentUrl(widget.data.elementAt(idx).rid),
|
||||
),
|
||||
heroAttributes: PhotoViewHeroAttributes(
|
||||
tag: 'attachment-${widget.data.first.rid}-$heroTag',
|
||||
|
@ -1,8 +1,11 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:popover/popover.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
@ -117,9 +120,20 @@ class ChatMessage extends StatelessWidget {
|
||||
),
|
||||
onTap: () {
|
||||
if (user == null) return;
|
||||
showModalBottomSheet(
|
||||
showPopover(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surface,
|
||||
context: context,
|
||||
builder: (context) => AccountPopoverCard(data: user),
|
||||
transition: PopoverTransition.other,
|
||||
bodyBuilder: (context) => SizedBox(
|
||||
width: math.min(
|
||||
400, MediaQuery.of(context).size.width - 10),
|
||||
child: AccountPopoverCard(data: user),
|
||||
),
|
||||
direction: PopoverDirection.bottom,
|
||||
arrowHeight: 5,
|
||||
arrowWidth: 15,
|
||||
arrowDxOffset: -190,
|
||||
);
|
||||
},
|
||||
)
|
||||
|
@ -12,6 +12,7 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:popover/popover.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
@ -1273,11 +1274,20 @@ class _PostAvatar extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
showPopover(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
context: context,
|
||||
builder: (context) => PublisherPopoverCard(
|
||||
data: data.publisher,
|
||||
transition: PopoverTransition.other,
|
||||
bodyBuilder: (context) => SizedBox(
|
||||
width: math.min(400, MediaQuery.of(context).size.width - 10),
|
||||
child: PublisherPopoverCard(
|
||||
data: data.publisher,
|
||||
),
|
||||
),
|
||||
direction: PopoverDirection.bottom,
|
||||
arrowHeight: 5,
|
||||
arrowWidth: 15,
|
||||
arrowDxOffset: -190,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -24,137 +24,125 @@ class PublisherPopoverCard extends StatelessWidget {
|
||||
|
||||
final user = data.type == 0 ? ud.getFromCache(data.accountId) : null;
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (data.banner.isNotEmpty)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(data.banner),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (data.banner.isNotEmpty)
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(data.banner),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
).padding(all: 16)
|
||||
else
|
||||
// Top padding
|
||||
),
|
||||
),
|
||||
// Top padding
|
||||
Gap(16),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AccountImage(
|
||||
content: data.avatar,
|
||||
radius: 20,
|
||||
borderRadius: data.type == 1 ? 8 : 20,
|
||||
),
|
||||
Gap(16),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AccountImage(
|
||||
content: data.avatar,
|
||||
radius: 20,
|
||||
borderRadius: data.type == 1 ? 8 : 20,
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(data.nick).bold(),
|
||||
Text('@${data.name}').fontSize(13).opacity(0.75),
|
||||
],
|
||||
),
|
||||
Gap(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(data.nick).bold(),
|
||||
Text('@${data.name}').fontSize(13).opacity(0.75),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postPublisher',
|
||||
pathParameters: {'name': data.name},
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.chevron_right),
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
),
|
||||
const Gap(8)
|
||||
],
|
||||
).padding(horizontal: 16),
|
||||
if (user != null && user.badges.isNotEmpty)
|
||||
Wrap(
|
||||
spacing: 4,
|
||||
children: user.badges
|
||||
.map(
|
||||
(ele) => AccountBadge(badge: ele),
|
||||
)
|
||||
.toList(),
|
||||
).padding(horizontal: 24, top: 16),
|
||||
const Gap(16),
|
||||
if (data.description.isNotEmpty)
|
||||
Text(
|
||||
data.description,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).padding(horizontal: 26, bottom: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text('publisherSocialPoint').tr().fontSize(13).opacity(0.75),
|
||||
Text((data.totalUpvote - data.totalDownvote).toString()),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postPublisher',
|
||||
pathParameters: {'name': data.name},
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.chevron_right),
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity:
|
||||
const VisualDensity(horizontal: -4, vertical: -4),
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
child: const VerticalDivider(
|
||||
thickness: 1,
|
||||
),
|
||||
const Gap(8)
|
||||
],
|
||||
).padding(horizontal: 16),
|
||||
if (user != null && user.badges.isNotEmpty)
|
||||
Wrap(
|
||||
spacing: 4,
|
||||
children: user.badges
|
||||
.map(
|
||||
(ele) => AccountBadge(badge: ele),
|
||||
)
|
||||
.toList(),
|
||||
).padding(horizontal: 24, top: 16),
|
||||
const Gap(16),
|
||||
if (data.description.isNotEmpty)
|
||||
Text(
|
||||
data.description,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).padding(horizontal: 26, bottom: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text('publisherSocialPoint')
|
||||
.tr()
|
||||
.fontSize(13)
|
||||
.opacity(0.75),
|
||||
Text((data.totalUpvote - data.totalDownvote).toString()),
|
||||
],
|
||||
),
|
||||
).padding(horizontal: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text('publisherTotalUpvote').tr().fontSize(13).opacity(0.75),
|
||||
Text(data.totalUpvote.toString()),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
child: const VerticalDivider(
|
||||
thickness: 1,
|
||||
),
|
||||
).padding(horizontal: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text('publisherTotalUpvote')
|
||||
.tr()
|
||||
.fontSize(13)
|
||||
.opacity(0.75),
|
||||
Text(data.totalUpvote.toString()),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
child: const VerticalDivider(
|
||||
thickness: 1,
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
child: const VerticalDivider(
|
||||
thickness: 1,
|
||||
),
|
||||
).padding(horizontal: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text('publisherTotalDownvote')
|
||||
.tr()
|
||||
.fontSize(13)
|
||||
.opacity(0.75),
|
||||
Text(data.totalDownvote.toString()),
|
||||
],
|
||||
),
|
||||
).padding(horizontal: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text('publisherTotalDownvote')
|
||||
.tr()
|
||||
.fontSize(13)
|
||||
.opacity(0.75),
|
||||
Text(data.totalDownvote.toString()),
|
||||
],
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16),
|
||||
// Bottom padding
|
||||
const Gap(64),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16),
|
||||
// Bottom padding
|
||||
const Gap(16),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user