Badges

This commit is contained in:
LittleSheep 2025-05-13 00:36:48 +08:00
parent ee8d502fc6
commit 610b924daf
10 changed files with 624 additions and 161 deletions

View File

@ -182,5 +182,29 @@
"uploadAll": "Upload All", "uploadAll": "Upload All",
"stickerCopyPlaceholder": "Copy Placeholder", "stickerCopyPlaceholder": "Copy Placeholder",
"realmSelection": "Select a Realm", "realmSelection": "Select a Realm",
"publisherIndividual": "Individual Publishers" "publisherIndividual": "Individual Publishers",
"firstPostBadgeName": "First Post",
"firstPostBadgeDescription": "Created your first post on Solar Network",
"popularPostBadgeName": "Popular Post",
"popularPostBadgeDescription": "Your post received significant engagement from the community",
"viralPostBadgeName": "Viral Post",
"viralPostBadgeDescription": "Your post went viral and reached a wide audience",
"helpfulCommentBadgeName": "Helpful Comment",
"helpfulCommentBadgeDescription": "Your comment was marked as helpful by others",
"newcomerBadgeName": "Newcomer",
"newcomerBadgeDescription": "Welcome to Solar Network! Start exploring and connecting",
"contributorBadgeName": "Contributor",
"contributorBadgeDescription": "Actively contributing to the Solar Network community",
"expertBadgeName": "Expert",
"expertBadgeDescription": "Recognized for your expertise and valuable contributions",
"founderBadgeName": "Founder",
"founderBadgeDescription": "One of the earliest members of Solar Network",
"betaTesterBadgeName": "Beta Tester",
"betaTesterBadgeDescription": "Helped test and improve Solar Network during beta",
"moderatorBadgeName": "Moderator",
"moderatorBadgeDescription": "Helping maintain and moderate the community",
"developerBadgeName": "Developer",
"developerBadgeDescription": "Contributing to Solar Network's development",
"translatorBadgeName": "Translator",
"translatorBadgeDescription": "Helping translate Solar Network into different languages"
} }

104
lib/models/badge.dart Normal file
View File

@ -0,0 +1,104 @@
import 'package:flutter/material.dart';
class BadgeInfo {
final String type;
final String name;
final String description;
final IconData icon;
final Color color;
const BadgeInfo({
required this.type,
required this.name,
required this.description,
this.icon = Icons.star,
this.color = Colors.blue,
});
}
const Map<String, BadgeInfo> kBadgeTemplates = {
'achievements.post.first': BadgeInfo(
type: 'achievements.post.first',
name: 'firstPostBadgeName',
description: 'firstPostBadgeDescription',
icon: Icons.create,
color: Colors.green,
),
'achievements.post.popular': BadgeInfo(
type: 'achievements.post.popular',
name: 'popularPostBadgeName',
description: 'popularPostBadgeDescription',
icon: Icons.trending_up,
color: Colors.orange,
),
'achievements.post.viral': BadgeInfo(
type: 'achievements.post.viral',
name: 'viralPostBadgeName',
description: 'viralPostBadgeDescription',
icon: Icons.whatshot,
color: Colors.red,
),
'achievements.comment.helpful': BadgeInfo(
type: 'achievements.comment.helpful',
name: 'helpfulCommentBadgeName',
description: 'helpfulCommentBadgeDescription',
icon: Icons.thumb_up,
color: Colors.lightBlue,
),
'ranks.newcomer': BadgeInfo(
type: 'ranks.newcomer',
name: 'newcomerBadgeName',
description: 'newcomerBadgeDescription',
icon: Icons.person_outline,
color: Colors.blue,
),
'ranks.contributor': BadgeInfo(
type: 'ranks.contributor',
name: 'contributorBadgeName',
description: 'contributorBadgeDescription',
icon: Icons.stars,
color: Colors.purple,
),
'ranks.expert': BadgeInfo(
type: 'ranks.expert',
name: 'expertBadgeName',
description: 'expertBadgeDescription',
icon: Icons.workspace_premium,
color: Colors.amber,
),
'event.founder': BadgeInfo(
type: 'event.founder',
name: 'founderBadgeName',
description: 'founderBadgeDescription',
icon: Icons.foundation,
color: Colors.deepPurple,
),
'event.beta.tester': BadgeInfo(
type: 'event.beta.tester',
name: 'betaTesterBadgeName',
description: 'betaTesterBadgeDescription',
icon: Icons.bug_report,
color: Colors.teal,
),
'special.moderator': BadgeInfo(
type: 'special.moderator',
name: 'moderatorBadgeName',
description: 'moderatorBadgeDescription',
icon: Icons.construction,
color: Colors.indigo,
),
'special.developer': BadgeInfo(
type: 'special.developer',
name: 'developerBadgeName',
description: 'developerBadgeDescription',
icon: Icons.code,
color: Colors.indigo,
),
'special.translator': BadgeInfo(
type: 'special.translator',
name: 'translatorBadgeName',
description: 'translatorBadgeDescription',
icon: Icons.code,
color: Colors.grey,
),
};

View File

@ -13,6 +13,7 @@ abstract class SnAccount with _$SnAccount {
required String language, required String language,
required bool isSuperuser, required bool isSuperuser,
required SnAccountProfile profile, required SnAccountProfile profile,
@Default([]) List<SnAccountBadge> badges,
required DateTime createdAt, required DateTime createdAt,
required DateTime updatedAt, required DateTime updatedAt,
required DateTime? deletedAt, required DateTime? deletedAt,
@ -63,3 +64,22 @@ abstract class SnAccountStatus with _$SnAccountStatus {
factory SnAccountStatus.fromJson(Map<String, dynamic> json) => factory SnAccountStatus.fromJson(Map<String, dynamic> json) =>
_$SnAccountStatusFromJson(json); _$SnAccountStatusFromJson(json);
} }
@freezed
abstract class SnAccountBadge with _$SnAccountBadge {
const factory SnAccountBadge({
required String id,
required String type,
required String? label,
required String? caption,
required Map<String, dynamic> meta,
required DateTime? expiredAt,
required int accountId,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
}) = _SnAccountBadge;
factory SnAccountBadge.fromJson(Map<String, dynamic> json) =>
_$SnAccountBadgeFromJson(json);
}

View File

@ -16,7 +16,7 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$SnAccount { mixin _$SnAccount {
int get id; String get name; String get nick; String get language; bool get isSuperuser; SnAccountProfile get profile; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; int get id; String get name; String get nick; String get language; bool get isSuperuser; SnAccountProfile get profile; List<SnAccountBadge> get badges; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnAccount /// Create a copy of SnAccount
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -29,16 +29,16 @@ $SnAccountCopyWith<SnAccount> get copyWith => _$SnAccountCopyWithImpl<SnAccount>
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccount&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.language, language) || other.language == language)&&(identical(other.isSuperuser, isSuperuser) || other.isSuperuser == isSuperuser)&&(identical(other.profile, profile) || other.profile == profile)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccount&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.language, language) || other.language == language)&&(identical(other.isSuperuser, isSuperuser) || other.isSuperuser == isSuperuser)&&(identical(other.profile, profile) || other.profile == profile)&&const DeepCollectionEquality().equals(other.badges, badges)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,id,name,nick,language,isSuperuser,profile,createdAt,updatedAt,deletedAt); int get hashCode => Object.hash(runtimeType,id,name,nick,language,isSuperuser,profile,const DeepCollectionEquality().hash(badges),createdAt,updatedAt,deletedAt);
@override @override
String toString() { String toString() {
return 'SnAccount(id: $id, name: $name, nick: $nick, language: $language, isSuperuser: $isSuperuser, profile: $profile, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; return 'SnAccount(id: $id, name: $name, nick: $nick, language: $language, isSuperuser: $isSuperuser, profile: $profile, badges: $badges, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
} }
@ -49,7 +49,7 @@ abstract mixin class $SnAccountCopyWith<$Res> {
factory $SnAccountCopyWith(SnAccount value, $Res Function(SnAccount) _then) = _$SnAccountCopyWithImpl; factory $SnAccountCopyWith(SnAccount value, $Res Function(SnAccount) _then) = _$SnAccountCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
int id, String name, String nick, String language, bool isSuperuser, SnAccountProfile profile, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt int id, String name, String nick, String language, bool isSuperuser, SnAccountProfile profile, List<SnAccountBadge> badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
}); });
@ -66,7 +66,7 @@ class _$SnAccountCopyWithImpl<$Res>
/// Create a copy of SnAccount /// Create a copy of SnAccount
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? nick = null,Object? language = null,Object? isSuperuser = null,Object? profile = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? nick = null,Object? language = null,Object? isSuperuser = null,Object? profile = null,Object? badges = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as int,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as int,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
@ -74,7 +74,8 @@ as String,nick: null == nick ? _self.nick : nick // ignore: cast_nullable_to_non
as String,language: null == language ? _self.language : language // ignore: cast_nullable_to_non_nullable as String,language: null == language ? _self.language : language // ignore: cast_nullable_to_non_nullable
as String,isSuperuser: null == isSuperuser ? _self.isSuperuser : isSuperuser // ignore: cast_nullable_to_non_nullable as String,isSuperuser: null == isSuperuser ? _self.isSuperuser : isSuperuser // ignore: cast_nullable_to_non_nullable
as bool,profile: null == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable as bool,profile: null == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable
as SnAccountProfile,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as SnAccountProfile,badges: null == badges ? _self.badges : badges // ignore: cast_nullable_to_non_nullable
as List<SnAccountBadge>,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?, as DateTime?,
@ -97,7 +98,7 @@ $SnAccountProfileCopyWith<$Res> get profile {
@JsonSerializable() @JsonSerializable()
class _SnAccount implements SnAccount { class _SnAccount implements SnAccount {
const _SnAccount({required this.id, required this.name, required this.nick, required this.language, required this.isSuperuser, required this.profile, required this.createdAt, required this.updatedAt, required this.deletedAt}); const _SnAccount({required this.id, required this.name, required this.nick, required this.language, required this.isSuperuser, required this.profile, final List<SnAccountBadge> badges = const [], required this.createdAt, required this.updatedAt, required this.deletedAt}): _badges = badges;
factory _SnAccount.fromJson(Map<String, dynamic> json) => _$SnAccountFromJson(json); factory _SnAccount.fromJson(Map<String, dynamic> json) => _$SnAccountFromJson(json);
@override final int id; @override final int id;
@ -106,6 +107,13 @@ class _SnAccount implements SnAccount {
@override final String language; @override final String language;
@override final bool isSuperuser; @override final bool isSuperuser;
@override final SnAccountProfile profile; @override final SnAccountProfile profile;
final List<SnAccountBadge> _badges;
@override@JsonKey() List<SnAccountBadge> get badges {
if (_badges is EqualUnmodifiableListView) return _badges;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_badges);
}
@override final DateTime createdAt; @override final DateTime createdAt;
@override final DateTime updatedAt; @override final DateTime updatedAt;
@override final DateTime? deletedAt; @override final DateTime? deletedAt;
@ -123,16 +131,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccount&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.language, language) || other.language == language)&&(identical(other.isSuperuser, isSuperuser) || other.isSuperuser == isSuperuser)&&(identical(other.profile, profile) || other.profile == profile)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccount&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.language, language) || other.language == language)&&(identical(other.isSuperuser, isSuperuser) || other.isSuperuser == isSuperuser)&&(identical(other.profile, profile) || other.profile == profile)&&const DeepCollectionEquality().equals(other._badges, _badges)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,id,name,nick,language,isSuperuser,profile,createdAt,updatedAt,deletedAt); int get hashCode => Object.hash(runtimeType,id,name,nick,language,isSuperuser,profile,const DeepCollectionEquality().hash(_badges),createdAt,updatedAt,deletedAt);
@override @override
String toString() { String toString() {
return 'SnAccount(id: $id, name: $name, nick: $nick, language: $language, isSuperuser: $isSuperuser, profile: $profile, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; return 'SnAccount(id: $id, name: $name, nick: $nick, language: $language, isSuperuser: $isSuperuser, profile: $profile, badges: $badges, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
} }
@ -143,7 +151,7 @@ abstract mixin class _$SnAccountCopyWith<$Res> implements $SnAccountCopyWith<$Re
factory _$SnAccountCopyWith(_SnAccount value, $Res Function(_SnAccount) _then) = __$SnAccountCopyWithImpl; factory _$SnAccountCopyWith(_SnAccount value, $Res Function(_SnAccount) _then) = __$SnAccountCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
int id, String name, String nick, String language, bool isSuperuser, SnAccountProfile profile, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt int id, String name, String nick, String language, bool isSuperuser, SnAccountProfile profile, List<SnAccountBadge> badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
}); });
@ -160,7 +168,7 @@ class __$SnAccountCopyWithImpl<$Res>
/// Create a copy of SnAccount /// Create a copy of SnAccount
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? nick = null,Object? language = null,Object? isSuperuser = null,Object? profile = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? nick = null,Object? language = null,Object? isSuperuser = null,Object? profile = null,Object? badges = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnAccount( return _then(_SnAccount(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as int,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as int,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
@ -168,7 +176,8 @@ as String,nick: null == nick ? _self.nick : nick // ignore: cast_nullable_to_non
as String,language: null == language ? _self.language : language // ignore: cast_nullable_to_non_nullable as String,language: null == language ? _self.language : language // ignore: cast_nullable_to_non_nullable
as String,isSuperuser: null == isSuperuser ? _self.isSuperuser : isSuperuser // ignore: cast_nullable_to_non_nullable as String,isSuperuser: null == isSuperuser ? _self.isSuperuser : isSuperuser // ignore: cast_nullable_to_non_nullable
as bool,profile: null == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable as bool,profile: null == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable
as SnAccountProfile,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as SnAccountProfile,badges: null == badges ? _self._badges : badges // ignore: cast_nullable_to_non_nullable
as List<SnAccountBadge>,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?, as DateTime?,
@ -565,6 +574,172 @@ as DateTime?,
} }
}
/// @nodoc
mixin _$SnAccountBadge {
String get id; String get type; String? get label; String? get caption; Map<String, dynamic> get meta; DateTime? get expiredAt; int get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnAccountBadge
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnAccountBadgeCopyWith<SnAccountBadge> get copyWith => _$SnAccountBadgeCopyWithImpl<SnAccountBadge>(this as SnAccountBadge, _$identity);
/// Serializes this SnAccountBadge to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountBadge&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.label, label) || other.label == label)&&(identical(other.caption, caption) || other.caption == caption)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,type,label,caption,const DeepCollectionEquality().hash(meta),expiredAt,accountId,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnAccountBadge(id: $id, type: $type, label: $label, caption: $caption, meta: $meta, expiredAt: $expiredAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnAccountBadgeCopyWith<$Res> {
factory $SnAccountBadgeCopyWith(SnAccountBadge value, $Res Function(SnAccountBadge) _then) = _$SnAccountBadgeCopyWithImpl;
@useResult
$Res call({
String id, String type, String? label, String? caption, Map<String, dynamic> meta, DateTime? expiredAt, int accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class _$SnAccountBadgeCopyWithImpl<$Res>
implements $SnAccountBadgeCopyWith<$Res> {
_$SnAccountBadgeCopyWithImpl(this._self, this._then);
final SnAccountBadge _self;
final $Res Function(SnAccountBadge) _then;
/// Create a copy of SnAccountBadge
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? type = null,Object? label = freezed,Object? caption = freezed,Object? meta = null,Object? expiredAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as String,label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
as String?,caption: freezed == caption ? _self.caption : caption // ignore: cast_nullable_to_non_nullable
as String?,meta: null == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as int,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
/// @nodoc
@JsonSerializable()
class _SnAccountBadge implements SnAccountBadge {
const _SnAccountBadge({required this.id, required this.type, required this.label, required this.caption, required final Map<String, dynamic> meta, required this.expiredAt, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt}): _meta = meta;
factory _SnAccountBadge.fromJson(Map<String, dynamic> json) => _$SnAccountBadgeFromJson(json);
@override final String id;
@override final String type;
@override final String? label;
@override final String? caption;
final Map<String, dynamic> _meta;
@override Map<String, dynamic> get meta {
if (_meta is EqualUnmodifiableMapView) return _meta;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_meta);
}
@override final DateTime? expiredAt;
@override final int accountId;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnAccountBadge
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnAccountBadgeCopyWith<_SnAccountBadge> get copyWith => __$SnAccountBadgeCopyWithImpl<_SnAccountBadge>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnAccountBadgeToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountBadge&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.label, label) || other.label == label)&&(identical(other.caption, caption) || other.caption == caption)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,type,label,caption,const DeepCollectionEquality().hash(_meta),expiredAt,accountId,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnAccountBadge(id: $id, type: $type, label: $label, caption: $caption, meta: $meta, expiredAt: $expiredAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnAccountBadgeCopyWith<$Res> implements $SnAccountBadgeCopyWith<$Res> {
factory _$SnAccountBadgeCopyWith(_SnAccountBadge value, $Res Function(_SnAccountBadge) _then) = __$SnAccountBadgeCopyWithImpl;
@override @useResult
$Res call({
String id, String type, String? label, String? caption, Map<String, dynamic> meta, DateTime? expiredAt, int accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class __$SnAccountBadgeCopyWithImpl<$Res>
implements _$SnAccountBadgeCopyWith<$Res> {
__$SnAccountBadgeCopyWithImpl(this._self, this._then);
final _SnAccountBadge _self;
final $Res Function(_SnAccountBadge) _then;
/// Create a copy of SnAccountBadge
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? label = freezed,Object? caption = freezed,Object? meta = null,Object? expiredAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnAccountBadge(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as String,label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
as String?,caption: freezed == caption ? _self.caption : caption // ignore: cast_nullable_to_non_nullable
as String?,meta: null == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as int,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
} }
// dart format on // dart format on

View File

@ -13,6 +13,11 @@ _SnAccount _$SnAccountFromJson(Map<String, dynamic> json) => _SnAccount(
language: json['language'] as String, language: json['language'] as String,
isSuperuser: json['is_superuser'] as bool, isSuperuser: json['is_superuser'] as bool,
profile: SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>), profile: SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>),
badges:
(json['badges'] as List<dynamic>?)
?.map((e) => SnAccountBadge.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt:
@ -29,6 +34,7 @@ Map<String, dynamic> _$SnAccountToJson(_SnAccount instance) =>
'language': instance.language, 'language': instance.language,
'is_superuser': instance.isSuperuser, 'is_superuser': instance.isSuperuser,
'profile': instance.profile.toJson(), 'profile': instance.profile.toJson(),
'badges': instance.badges.map((e) => e.toJson()).toList(),
'created_at': instance.createdAt.toIso8601String(), 'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(), 'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(), 'deleted_at': instance.deletedAt?.toIso8601String(),
@ -114,3 +120,37 @@ Map<String, dynamic> _$SnAccountStatusToJson(_SnAccountStatus instance) =>
'updated_at': instance.updatedAt.toIso8601String(), 'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(), 'deleted_at': instance.deletedAt?.toIso8601String(),
}; };
_SnAccountBadge _$SnAccountBadgeFromJson(Map<String, dynamic> json) =>
_SnAccountBadge(
id: json['id'] as String,
type: json['type'] as String,
label: json['label'] as String?,
caption: json['caption'] as String?,
meta: json['meta'] as Map<String, dynamic>,
expiredAt:
json['expired_at'] == null
? null
: DateTime.parse(json['expired_at'] as String),
accountId: (json['account_id'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);
Map<String, dynamic> _$SnAccountBadgeToJson(_SnAccountBadge instance) =>
<String, dynamic>{
'id': instance.id,
'type': instance.type,
'label': instance.label,
'caption': instance.caption,
'meta': instance.meta,
'expired_at': instance.expiredAt?.toIso8601String(),
'account_id': instance.accountId,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};

View File

@ -36,7 +36,7 @@ class AccountScreen extends HookConsumerWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (user.value?.profile.background != null) if (user.value?.profile.backgroundId != null)
ClipRRect( ClipRRect(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(8), topLeft: Radius.circular(8),
@ -44,8 +44,8 @@ class AccountScreen extends HookConsumerWidget {
), ),
child: AspectRatio( child: AspectRatio(
aspectRatio: 16 / 7, aspectRatio: 16 / 7,
child: CloudFileWidget( child: CloudImageWidget(
item: user.value!.profile.background!, fileId: user.value!.profile.backgroundId!,
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),
@ -65,22 +65,26 @@ class AccountScreen extends HookConsumerWidget {
); );
}, },
), ),
Column( Expanded(
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Row( children: [
spacing: 4, Row(
crossAxisAlignment: CrossAxisAlignment.baseline, spacing: 4,
textBaseline: TextBaseline.alphabetic, crossAxisAlignment: CrossAxisAlignment.baseline,
children: [ textBaseline: TextBaseline.alphabetic,
Text(user.value!.nick).bold().fontSize(16), children: [
Text('@${user.value!.name}'), Text(user.value!.nick).bold().fontSize(16),
], Text('@${user.value!.name}'),
), ],
Text( ),
user.value!.profile.bio ?? 'No description yet.', Text(
), user.value!.profile.bio ?? 'No description yet.',
], maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
), ),
], ],
).padding(horizontal: 16, top: 16), ).padding(horizontal: 16, top: 16),

View File

@ -143,116 +143,119 @@ class UpdateProfileScreen extends HookConsumerWidget {
title: Text('updateYourProfile').tr(), title: Text('updateYourProfile').tr(),
leading: const PageBackButton(), leading: const PageBackButton(),
), ),
body: Column( body: SingleChildScrollView(
crossAxisAlignment: CrossAxisAlignment.start, padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
children: [ child: Column(
AspectRatio( crossAxisAlignment: CrossAxisAlignment.start,
aspectRatio: 16 / 7, children: [
child: Stack( AspectRatio(
clipBehavior: Clip.none, aspectRatio: 16 / 7,
fit: StackFit.expand, child: Stack(
children: [ clipBehavior: Clip.none,
GestureDetector( fit: StackFit.expand,
child: Container( children: [
color: Theme.of(context).colorScheme.surfaceContainerHigh, GestureDetector(
child: child: Container(
user.value!.profile.background != null color: Theme.of(context).colorScheme.surfaceContainerHigh,
? CloudFileWidget( child:
item: user.value!.profile.background!, user.value!.profile.background != null
fit: BoxFit.cover, ? CloudFileWidget(
) item: user.value!.profile.background!,
: const SizedBox.shrink(), fit: BoxFit.cover,
), )
onTap: () { : const SizedBox.shrink(),
updateProfilePicture('background');
},
),
Positioned(
left: 20,
bottom: -32,
child: GestureDetector(
child: ProfilePictureWidget(
fileId: user.value!.profile.pictureId,
radius: 40,
), ),
onTap: () { onTap: () {
updateProfilePicture('picture'); updateProfilePicture('background');
}, },
), ),
), Positioned(
], left: 20,
bottom: -32,
child: GestureDetector(
child: ProfilePictureWidget(
fileId: user.value!.profile.pictureId,
radius: 40,
),
onTap: () {
updateProfilePicture('picture');
},
),
),
],
),
).padding(bottom: 32),
Text('accountBasicInfo')
.tr()
.bold()
.fontSize(18)
.padding(horizontal: 24, top: 16, bottom: 12),
Form(
key: formKeyBasicInfo,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 16,
children: [
TextFormField(
decoration: InputDecoration(
labelText: 'username'.tr(),
helperText: 'usernameCannotChangeHint'.tr(),
prefixText: '@',
),
controller: usernameController,
readOnly: true,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
TextFormField(
decoration: InputDecoration(labelText: 'nickname'.tr()),
controller: nicknameController,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: submitting.value ? null : updateBasicInfo,
label: Text('saveChanges').tr(),
icon: const Icon(Symbols.save),
),
),
],
).padding(horizontal: 24),
), ),
).padding(bottom: 32), Text('accountProfile')
Text('accountBasicInfo') .tr()
.tr() .bold()
.bold() .fontSize(18)
.fontSize(18) .padding(horizontal: 24, top: 16, bottom: 8),
.padding(horizontal: 24, top: 16, bottom: 12), Form(
Form( key: formKeyProfile,
key: formKeyBasicInfo, child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, spacing: 16,
spacing: 16, children: [
children: [ TextFormField(
TextFormField( decoration: InputDecoration(labelText: 'bio'.tr()),
decoration: InputDecoration( maxLines: null,
labelText: 'username'.tr(), minLines: 3,
helperText: 'usernameCannotChangeHint'.tr(), controller: bioController,
prefixText: '@', onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
controller: usernameController, Align(
readOnly: true, alignment: Alignment.centerRight,
onTapOutside: child: TextButton.icon(
(_) => FocusManager.instance.primaryFocus?.unfocus(), onPressed: submitting.value ? null : updateProfile,
), label: Text('saveChanges').tr(),
TextFormField( icon: const Icon(Symbols.save),
decoration: InputDecoration(labelText: 'nickname'.tr()), ),
controller: nicknameController,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: submitting.value ? null : updateBasicInfo,
label: Text('saveChanges').tr(),
icon: const Icon(Symbols.save),
), ),
), ],
], ).padding(horizontal: 24),
).padding(horizontal: 24), ),
), ],
Text('accountProfile') ),
.tr()
.bold()
.fontSize(18)
.padding(horizontal: 24, top: 16, bottom: 8),
Form(
key: formKeyProfile,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 16,
children: [
TextFormField(
decoration: InputDecoration(labelText: 'bio'.tr()),
maxLines: null,
minLines: 3,
controller: bioController,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: submitting.value ? null : updateProfile,
label: Text('saveChanges').tr(),
icon: const Icon(Symbols.save),
),
),
],
).padding(horizontal: 24),
),
],
), ),
); );
} }

View File

@ -1,12 +1,16 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart'; import 'package:island/models/user.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/widgets/account/badge.dart';
import 'package:island/widgets/account/status.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
part 'profile.g.dart'; part 'profile.g.dart';
@ -28,6 +32,13 @@ class AccountProfileScreen extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final accountAsync = ref.watch(accountProvider(name)); final accountAsync = ref.watch(accountProvider(name));
final iconShadow = Shadow(
color: Colors.black54,
blurRadius: 5.0,
offset: Offset(1.0, 1.0),
);
return accountAsync.when( return accountAsync.when(
data: data:
(data) => AppScaffold( (data) => AppScaffold(
@ -36,6 +47,7 @@ class AccountProfileScreen extends HookConsumerWidget {
SliverAppBar( SliverAppBar(
expandedHeight: 180, expandedHeight: 180,
pinned: true, pinned: true,
leading: PageBackButton(shadows: [iconShadow]),
flexibleSpace: FlexibleSpaceBar( flexibleSpace: FlexibleSpaceBar(
background: background:
data.profile.backgroundId != null data.profile.backgroundId != null
@ -47,33 +59,67 @@ class AccountProfileScreen extends HookConsumerWidget {
Theme.of(context).appBarTheme.backgroundColor, Theme.of(context).appBarTheme.backgroundColor,
), ),
title: Text( title: Text(
data.name, data.nick,
style: TextStyle( style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor, color: Theme.of(context).appBarTheme.foregroundColor,
shadows: [ shadows: [iconShadow],
Shadow(
color: Colors.black54,
blurRadius: 5.0,
offset: Offset(1.0, 1.0),
),
],
), ),
), ),
), ),
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Row(
padding: const EdgeInsets.all(16.0), crossAxisAlignment: CrossAxisAlignment.start,
child: Column( spacing: 20,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ ProfilePictureWidget(
Text( fileId: data.profile.pictureId!,
data.profile.bio ?? '', radius: 32,
style: const TextStyle(fontSize: 16), ),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
spacing: 6,
children: [
Text(data.nick).fontSize(20),
Text(
'@${data.name}',
).fontSize(14).opacity(0.85),
],
),
AccountStatusWidget(
uname: name,
padding: EdgeInsets.zero,
),
],
), ),
], ),
), ],
), ).padding(horizontal: 24, top: 24, bottom: 8),
),
if (data.badges.isNotEmpty)
SliverToBoxAdapter(
child: BadgeList(
badges: data.badges,
).padding(horizontal: 24, bottom: 24),
)
else
const Gap(16),
SliverToBoxAdapter(
child: const Divider(height: 1).padding(bottom: 24),
),
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('bio').tr().bold(),
if (data.profile.bio != null &&
data.profile.bio!.isNotEmpty)
Text(data.profile.bio!),
],
).padding(horizontal: 24),
), ),
], ],
), ),

View File

@ -0,0 +1,46 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:island/models/user.dart';
import 'package:island/models/badge.dart';
class BadgeList extends StatelessWidget {
final List<SnAccountBadge> badges;
const BadgeList({super.key, required this.badges});
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 8,
runSpacing: 8,
children: badges.map((badge) => BadgeItem(badge: badge)).toList(),
);
}
}
class BadgeItem extends StatelessWidget {
final SnAccountBadge badge;
const BadgeItem({super.key, required this.badge});
@override
Widget build(BuildContext context) {
final template = kBadgeTemplates[badge.type];
final name = badge.label ?? template?.name.tr() ?? 'unknown'.tr();
final description = badge.caption ?? template?.description.tr() ?? '';
return Tooltip(
message: '$name\n$description',
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: (template?.color ?? Colors.blue).withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
template?.icon ?? Icons.stars,
color: template?.color ?? Colors.orange,
size: 20,
),
),
);
}
}

View File

@ -50,7 +50,10 @@ class AccountStatusCreationWidget extends HookConsumerWidget {
data: data:
(status) => (status) =>
(status?.isCustomized ?? false) (status?.isCustomized ?? false)
? AccountStatusWidget(uname: uname) ? Padding(
padding: const EdgeInsets.only(left: 4),
child: AccountStatusWidget(uname: uname),
)
: Padding( : Padding(
padding: padding:
padding ?? padding ??
@ -112,15 +115,13 @@ class AccountStatusWidget extends HookConsumerWidget {
child: Row( child: Row(
spacing: 4, spacing: 4,
children: [ children: [
if (!(userStatus.value?.isCustomized ?? false)) if (userStatus.value!.isOnline)
Icon(Symbols.keyboard_arrow_up)
else if (userStatus.value!.isOnline)
Icon( Icon(
Symbols.circle, Symbols.circle,
fill: 1, fill: 1,
color: Colors.green, color: Colors.green,
size: 16, size: 16,
).padding(all: 4) ).padding(right: 4)
else else
Icon(Symbols.circle, color: Colors.grey, size: 16).padding(all: 4), Icon(Symbols.circle, color: Colors.grey, size: 16).padding(all: 4),
if (userStatus.value?.isCustomized ?? false) if (userStatus.value?.isCustomized ?? false)