diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 95a11a48..5d285879 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -479,7 +479,6 @@ "accountProfileView": "View Profile", "unspecified": "Unspecified", "added": "Added", - "preview": "Preview", "togglePreview": "Toggle Preview", "subscribe": "Subscribe", "unsubscribe": "Unsubscribe", @@ -1227,5 +1226,25 @@ "refresh": "Refresh", "spoiler": "Spoiler", "activityHeatmap": "Activity Heatmap", - "custom": "Custom" + "custom": "Custom", + "usernameColor": "Username Color", + "colorType": "Color Type", + "plain": "Plain", + "gradient": "Gradient", + "colorValue": "Color Value", + "gradientDirection": "Gradient Direction", + "gradientDirectionToRight": "To Right", + "gradientDirectionToLeft": "To Left", + "gradientDirectionToBottom": "To Bottom", + "gradientDirectionToTop": "To Top", + "gradientDirectionToBottomRight": "To Bottom Right", + "gradientDirectionToBottomLeft": "To Bottom Left", + "gradientDirectionToTopRight": "To Top Right", + "gradientDirectionToTopLeft": "To Top Left", + "gradientColors": "Gradient Colors", + "color": "Color", + "addColor": "Add Color", + "preview": "Preview", + "availableWithYourPlan": "Available with your plan", + "upgradeRequired": "Upgrade required" } diff --git a/lib/models/account.dart b/lib/models/account.dart index 4c95dfc2..5f6a9daf 100644 --- a/lib/models/account.dart +++ b/lib/models/account.dart @@ -55,6 +55,19 @@ class ProfileLinkConverter } } +@freezed +sealed class UsernameColor with _$UsernameColor { + const factory UsernameColor({ + @Default('plain') String type, + String? value, + String? direction, + List? colors, + }) = _UsernameColor; + + factory UsernameColor.fromJson(Map json) => + _$UsernameColorFromJson(json); +} + @freezed sealed class SnAccountProfile with _$SnAccountProfile { const factory SnAccountProfile({ @@ -79,6 +92,7 @@ sealed class SnAccountProfile with _$SnAccountProfile { required SnCloudFile? picture, required SnCloudFile? background, required SnVerificationMark? verification, + UsernameColor? usernameColor, required DateTime createdAt, required DateTime updatedAt, required DateTime? deletedAt, diff --git a/lib/models/account.freezed.dart b/lib/models/account.freezed.dart index 045de96d..edc42c3a 100644 --- a/lib/models/account.freezed.dart +++ b/lib/models/account.freezed.dart @@ -622,10 +622,284 @@ as String, } +/// @nodoc +mixin _$UsernameColor { + + String get type; String? get value; String? get direction; List? get colors; +/// Create a copy of UsernameColor +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$UsernameColorCopyWith get copyWith => _$UsernameColorCopyWithImpl(this as UsernameColor, _$identity); + + /// Serializes this UsernameColor to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is UsernameColor&&(identical(other.type, type) || other.type == type)&&(identical(other.value, value) || other.value == value)&&(identical(other.direction, direction) || other.direction == direction)&&const DeepCollectionEquality().equals(other.colors, colors)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,type,value,direction,const DeepCollectionEquality().hash(colors)); + +@override +String toString() { + return 'UsernameColor(type: $type, value: $value, direction: $direction, colors: $colors)'; +} + + +} + +/// @nodoc +abstract mixin class $UsernameColorCopyWith<$Res> { + factory $UsernameColorCopyWith(UsernameColor value, $Res Function(UsernameColor) _then) = _$UsernameColorCopyWithImpl; +@useResult +$Res call({ + String type, String? value, String? direction, List? colors +}); + + + + +} +/// @nodoc +class _$UsernameColorCopyWithImpl<$Res> + implements $UsernameColorCopyWith<$Res> { + _$UsernameColorCopyWithImpl(this._self, this._then); + + final UsernameColor _self; + final $Res Function(UsernameColor) _then; + +/// Create a copy of UsernameColor +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? value = freezed,Object? direction = freezed,Object? colors = freezed,}) { + return _then(_self.copyWith( +type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as String,value: freezed == value ? _self.value : value // ignore: cast_nullable_to_non_nullable +as String?,direction: freezed == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable +as String?,colors: freezed == colors ? _self.colors : colors // ignore: cast_nullable_to_non_nullable +as List?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [UsernameColor]. +extension UsernameColorPatterns on UsernameColor { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _UsernameColor value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _UsernameColor() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _UsernameColor value) $default,){ +final _that = this; +switch (_that) { +case _UsernameColor(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _UsernameColor value)? $default,){ +final _that = this; +switch (_that) { +case _UsernameColor() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String type, String? value, String? direction, List? colors)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _UsernameColor() when $default != null: +return $default(_that.type,_that.value,_that.direction,_that.colors);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String type, String? value, String? direction, List? colors) $default,) {final _that = this; +switch (_that) { +case _UsernameColor(): +return $default(_that.type,_that.value,_that.direction,_that.colors);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String type, String? value, String? direction, List? colors)? $default,) {final _that = this; +switch (_that) { +case _UsernameColor() when $default != null: +return $default(_that.type,_that.value,_that.direction,_that.colors);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _UsernameColor implements UsernameColor { + const _UsernameColor({this.type = 'plain', this.value, this.direction, final List? colors}): _colors = colors; + factory _UsernameColor.fromJson(Map json) => _$UsernameColorFromJson(json); + +@override@JsonKey() final String type; +@override final String? value; +@override final String? direction; + final List? _colors; +@override List? get colors { + final value = _colors; + if (value == null) return null; + if (_colors is EqualUnmodifiableListView) return _colors; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); +} + + +/// Create a copy of UsernameColor +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$UsernameColorCopyWith<_UsernameColor> get copyWith => __$UsernameColorCopyWithImpl<_UsernameColor>(this, _$identity); + +@override +Map toJson() { + return _$UsernameColorToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _UsernameColor&&(identical(other.type, type) || other.type == type)&&(identical(other.value, value) || other.value == value)&&(identical(other.direction, direction) || other.direction == direction)&&const DeepCollectionEquality().equals(other._colors, _colors)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,type,value,direction,const DeepCollectionEquality().hash(_colors)); + +@override +String toString() { + return 'UsernameColor(type: $type, value: $value, direction: $direction, colors: $colors)'; +} + + +} + +/// @nodoc +abstract mixin class _$UsernameColorCopyWith<$Res> implements $UsernameColorCopyWith<$Res> { + factory _$UsernameColorCopyWith(_UsernameColor value, $Res Function(_UsernameColor) _then) = __$UsernameColorCopyWithImpl; +@override @useResult +$Res call({ + String type, String? value, String? direction, List? colors +}); + + + + +} +/// @nodoc +class __$UsernameColorCopyWithImpl<$Res> + implements _$UsernameColorCopyWith<$Res> { + __$UsernameColorCopyWithImpl(this._self, this._then); + + final _UsernameColor _self; + final $Res Function(_UsernameColor) _then; + +/// Create a copy of UsernameColor +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? value = freezed,Object? direction = freezed,Object? colors = freezed,}) { + return _then(_UsernameColor( +type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as String,value: freezed == value ? _self.value : value // ignore: cast_nullable_to_non_nullable +as String?,direction: freezed == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable +as String?,colors: freezed == colors ? _self._colors : colors // ignore: cast_nullable_to_non_nullable +as List?, + )); +} + + +} + + /// @nodoc mixin _$SnAccountProfile { - String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday;@ProfileLinkConverter() List get links; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get socialCredits; int get socialCreditsLevel; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; + String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday;@ProfileLinkConverter() List get links; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get socialCredits; int get socialCreditsLevel; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; UsernameColor? get usernameColor; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; /// Create a copy of SnAccountProfile /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -638,16 +912,16 @@ $SnAccountProfileCopyWith get copyWith => _$SnAccountProfileCo @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other.links, links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.socialCreditsLevel, socialCreditsLevel) || other.socialCreditsLevel == socialCreditsLevel)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(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 SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other.links, links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.socialCreditsLevel, socialCreditsLevel) || other.socialCreditsLevel == socialCreditsLevel)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.usernameColor, usernameColor) || other.usernameColor == usernameColor)&&(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.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(links),lastSeenAt,activeBadge,experience,level,socialCredits,socialCreditsLevel,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]); +int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(links),lastSeenAt,activeBadge,experience,level,socialCredits,socialCreditsLevel,levelingProgress,picture,background,verification,usernameColor,createdAt,updatedAt,deletedAt]); @override String toString() { - return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, socialCredits: $socialCredits, socialCreditsLevel: $socialCreditsLevel, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; + return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, socialCredits: $socialCredits, socialCreditsLevel: $socialCreditsLevel, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, usernameColor: $usernameColor, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; } @@ -658,11 +932,11 @@ abstract mixin class $SnAccountProfileCopyWith<$Res> { factory $SnAccountProfileCopyWith(SnAccountProfile value, $Res Function(SnAccountProfile) _then) = _$SnAccountProfileCopyWithImpl; @useResult $Res call({ - String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt + String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, UsernameColor? usernameColor, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt }); -$SnAccountBadgeCopyWith<$Res>? get activeBadge;$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;$SnVerificationMarkCopyWith<$Res>? get verification; +$SnAccountBadgeCopyWith<$Res>? get activeBadge;$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;$SnVerificationMarkCopyWith<$Res>? get verification;$UsernameColorCopyWith<$Res>? get usernameColor; } /// @nodoc @@ -675,7 +949,7 @@ class _$SnAccountProfileCopyWithImpl<$Res> /// Create a copy of SnAccountProfile /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? socialCredits = null,Object? socialCreditsLevel = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? socialCredits = null,Object? socialCreditsLevel = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? usernameColor = freezed,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,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable @@ -698,7 +972,8 @@ as int,levelingProgress: null == levelingProgress ? _self.levelingProgress : lev as double,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable as SnCloudFile?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable -as SnVerificationMark?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as SnVerificationMark?,usernameColor: freezed == usernameColor ? _self.usernameColor : usernameColor // ignore: cast_nullable_to_non_nullable +as UsernameColor?,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?, @@ -752,6 +1027,18 @@ $SnVerificationMarkCopyWith<$Res>? get verification { return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) { return _then(_self.copyWith(verification: value)); }); +}/// Create a copy of SnAccountProfile +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$UsernameColorCopyWith<$Res>? get usernameColor { + if (_self.usernameColor == null) { + return null; + } + + return $UsernameColorCopyWith<$Res>(_self.usernameColor!, (value) { + return _then(_self.copyWith(usernameColor: value)); + }); } } @@ -831,10 +1118,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, UsernameColor? usernameColor, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _SnAccountProfile() when $default != null: -return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: +return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.usernameColor,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: return orElse(); } @@ -852,10 +1139,10 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, UsernameColor? usernameColor, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this; switch (_that) { case _SnAccountProfile(): -return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);} +return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.usernameColor,_that.createdAt,_that.updatedAt,_that.deletedAt);} } /// A variant of `when` that fallback to returning `null` /// @@ -869,10 +1156,10 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, UsernameColor? usernameColor, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this; switch (_that) { case _SnAccountProfile() when $default != null: -return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: +return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.usernameColor,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: return null; } @@ -884,7 +1171,7 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b @JsonSerializable() class _SnAccountProfile implements SnAccountProfile { - const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, @ProfileLinkConverter() final List links = const [], this.lastSeenAt, this.activeBadge, required this.experience, required this.level, this.socialCredits = 100, this.socialCreditsLevel = 0, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.createdAt, required this.updatedAt, required this.deletedAt}): _links = links; + const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, @ProfileLinkConverter() final List links = const [], this.lastSeenAt, this.activeBadge, required this.experience, required this.level, this.socialCredits = 100, this.socialCreditsLevel = 0, required this.levelingProgress, required this.picture, required this.background, required this.verification, this.usernameColor, required this.createdAt, required this.updatedAt, required this.deletedAt}): _links = links; factory _SnAccountProfile.fromJson(Map json) => _$SnAccountProfileFromJson(json); @override final String id; @@ -914,6 +1201,7 @@ class _SnAccountProfile implements SnAccountProfile { @override final SnCloudFile? picture; @override final SnCloudFile? background; @override final SnVerificationMark? verification; +@override final UsernameColor? usernameColor; @override final DateTime createdAt; @override final DateTime updatedAt; @override final DateTime? deletedAt; @@ -931,16 +1219,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other._links, _links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.socialCreditsLevel, socialCreditsLevel) || other.socialCreditsLevel == socialCreditsLevel)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(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 _SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other._links, _links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.socialCreditsLevel, socialCreditsLevel) || other.socialCreditsLevel == socialCreditsLevel)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.usernameColor, usernameColor) || other.usernameColor == usernameColor)&&(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.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(_links),lastSeenAt,activeBadge,experience,level,socialCredits,socialCreditsLevel,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]); +int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(_links),lastSeenAt,activeBadge,experience,level,socialCredits,socialCreditsLevel,levelingProgress,picture,background,verification,usernameColor,createdAt,updatedAt,deletedAt]); @override String toString() { - return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, socialCredits: $socialCredits, socialCreditsLevel: $socialCreditsLevel, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; + return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, socialCredits: $socialCredits, socialCreditsLevel: $socialCreditsLevel, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, usernameColor: $usernameColor, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; } @@ -951,11 +1239,11 @@ abstract mixin class _$SnAccountProfileCopyWith<$Res> implements $SnAccountProfi factory _$SnAccountProfileCopyWith(_SnAccountProfile value, $Res Function(_SnAccountProfile) _then) = __$SnAccountProfileCopyWithImpl; @override @useResult $Res call({ - String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt + String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, UsernameColor? usernameColor, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt }); -@override $SnAccountBadgeCopyWith<$Res>? get activeBadge;@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;@override $SnVerificationMarkCopyWith<$Res>? get verification; +@override $SnAccountBadgeCopyWith<$Res>? get activeBadge;@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;@override $SnVerificationMarkCopyWith<$Res>? get verification;@override $UsernameColorCopyWith<$Res>? get usernameColor; } /// @nodoc @@ -968,7 +1256,7 @@ class __$SnAccountProfileCopyWithImpl<$Res> /// Create a copy of SnAccountProfile /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? socialCredits = null,Object? socialCreditsLevel = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? socialCredits = null,Object? socialCreditsLevel = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? usernameColor = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { return _then(_SnAccountProfile( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable @@ -991,7 +1279,8 @@ as int,levelingProgress: null == levelingProgress ? _self.levelingProgress : lev as double,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable as SnCloudFile?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable -as SnVerificationMark?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as SnVerificationMark?,usernameColor: freezed == usernameColor ? _self.usernameColor : usernameColor // ignore: cast_nullable_to_non_nullable +as UsernameColor?,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?, @@ -1046,6 +1335,18 @@ $SnVerificationMarkCopyWith<$Res>? get verification { return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) { return _then(_self.copyWith(verification: value)); }); +}/// Create a copy of SnAccountProfile +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$UsernameColorCopyWith<$Res>? get usernameColor { + if (_self.usernameColor == null) { + return null; + } + + return $UsernameColorCopyWith<$Res>(_self.usernameColor!, (value) { + return _then(_self.copyWith(usernameColor: value)); + }); } } diff --git a/lib/models/account.g.dart b/lib/models/account.g.dart index 8970ff92..3f0a434e 100644 --- a/lib/models/account.g.dart +++ b/lib/models/account.g.dart @@ -63,6 +63,23 @@ _ProfileLink _$ProfileLinkFromJson(Map json) => Map _$ProfileLinkToJson(_ProfileLink instance) => {'name': instance.name, 'url': instance.url}; +_UsernameColor _$UsernameColorFromJson(Map json) => + _UsernameColor( + type: json['type'] as String? ?? 'plain', + value: json['value'] as String?, + direction: json['direction'] as String?, + colors: + (json['colors'] as List?)?.map((e) => e as String).toList(), + ); + +Map _$UsernameColorToJson(_UsernameColor instance) => + { + 'type': instance.type, + 'value': instance.value, + 'direction': instance.direction, + 'colors': instance.colors, + }; + _SnAccountProfile _$SnAccountProfileFromJson(Map json) => _SnAccountProfile( id: json['id'] as String, @@ -113,6 +130,12 @@ _SnAccountProfile _$SnAccountProfileFromJson(Map json) => : SnVerificationMark.fromJson( json['verification'] as Map, ), + usernameColor: + json['username_color'] == null + ? null + : UsernameColor.fromJson( + json['username_color'] as Map, + ), createdAt: DateTime.parse(json['created_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String), deletedAt: @@ -144,6 +167,7 @@ Map _$SnAccountProfileToJson(_SnAccountProfile instance) => 'picture': instance.picture?.toJson(), 'background': instance.background?.toJson(), 'verification': instance.verification?.toJson(), + 'username_color': instance.usernameColor?.toJson(), 'created_at': instance.createdAt.toIso8601String(), 'updated_at': instance.updatedAt.toIso8601String(), 'deleted_at': instance.deletedAt?.toIso8601String(), diff --git a/lib/pods/chat/messages_notifier.g.dart b/lib/pods/chat/messages_notifier.g.dart index 2ef628c8..61015980 100644 --- a/lib/pods/chat/messages_notifier.g.dart +++ b/lib/pods/chat/messages_notifier.g.dart @@ -6,7 +6,7 @@ part of 'messages_notifier.dart'; // RiverpodGenerator // ************************************************************************** -String _$messagesNotifierHash() => r'639286fd8e4e0cfdef5be6cf5d80eea769007cb3'; +String _$messagesNotifierHash() => r'6adefd9152cdd686c2a863964993f24c42d405b5'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/screens/account/me/profile_update.dart b/lib/screens/account/me/profile_update.dart index 1d74a884..2ee7c38e 100644 --- a/lib/screens/account/me/profile_update.dart +++ b/lib/screens/account/me/profile_update.dart @@ -14,6 +14,7 @@ import 'package:island/pods/userinfo.dart'; import 'package:island/services/file.dart'; import 'package:island/services/file_uploader.dart'; import 'package:island/services/timezone.dart'; +import 'package:island/widgets/account/account_name.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.dart'; @@ -24,6 +25,17 @@ const kServerSupportedLanguages = {'en-US': 'en-us', 'zh-CN': 'zh-hans'}; const kServerSupportedRegions = ['US', 'JP', 'CN']; class UpdateProfileScreen extends HookConsumerWidget { + bool _isValidHexColor(String color) { + if (!color.startsWith('#')) return false; + if (color.length != 7) return false; // #RRGGBB format + try { + int.parse(color.substring(1), radix: 16); + return true; + } catch (_) { + return false; + } + } + const UpdateProfileScreen({super.key}); @override @@ -148,12 +160,39 @@ class UpdateProfileScreen extends HookConsumerWidget { text: user.value!.profile.timeZone, ); + // Username color state + final usernameColorType = useState( + user.value!.profile.usernameColor?.type ?? 'plain', + ); + final usernameColorValue = useTextEditingController( + text: user.value!.profile.usernameColor?.value, + ); + final usernameColorDirection = useTextEditingController( + text: user.value!.profile.usernameColor?.direction, + ); + final usernameColorColors = useState>( + user.value!.profile.usernameColor?.colors ?? [], + ); + void updateProfile() async { if (!formKeyProfile.currentState!.validate()) return; submitting.value = true; try { final client = ref.watch(apiClientProvider); + final usernameColorData = { + 'type': usernameColorType.value, + if (usernameColorType.value == 'plain' && + usernameColorValue.text.isNotEmpty) + 'value': usernameColorValue.text, + if (usernameColorType.value == 'gradient') ...{ + if (usernameColorDirection.text.isNotEmpty) + 'direction': usernameColorDirection.text, + 'colors': + usernameColorColors.value.where((c) => c.isNotEmpty).toList(), + }, + }; + await client.patch( '/id/accounts/me/profile', data: { @@ -166,6 +205,7 @@ class UpdateProfileScreen extends HookConsumerWidget { 'location': locationController.text, 'time_zone': timeZoneController.text, 'birthday': birthday.value?.toUtc().toIso8601String(), + 'username_color': usernameColorData, 'links': links.value .where((e) => e.name.isNotEmpty && e.url.isNotEmpty) @@ -593,6 +633,320 @@ class UpdateProfileScreen extends HookConsumerWidget { ), ), ), + Text( + 'usernameColor', + ).tr().bold().fontSize(18).padding(top: 16), + Column( + spacing: 16, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Preview section + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: + Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('preview').tr().bold().fontSize(14), + const Gap(8), + // Create a preview account with the current settings + Builder( + builder: (context) { + final previewAccount = user.value!.copyWith( + profile: user.value!.profile.copyWith( + usernameColor: UsernameColor( + type: usernameColorType.value, + value: + usernameColorType.value == 'plain' && + usernameColorValue + .text + .isNotEmpty + ? usernameColorValue.text + : null, + direction: + usernameColorType.value == + 'gradient' && + usernameColorDirection + .text + .isNotEmpty + ? usernameColorDirection.text + : null, + colors: + usernameColorType.value == 'gradient' + ? usernameColorColors.value + .where((c) => c.isNotEmpty) + .toList() + : null, + ), + ), + ); + + // Check if user can use this color + final tier = + user.value!.perkSubscription?.identifier; + final canUseColor = switch (tier) { + 'solian.stellar.primary' => + usernameColorType.value == 'plain' && + (kUsernamePlainColors.containsKey( + usernameColorValue.text, + ) || + (usernameColorValue.text.startsWith( + '#', + ) && + _isValidHexColor( + usernameColorValue.text, + ))), + 'solian.stellar.nova' => + usernameColorType.value == 'plain', + 'solian.stellar.supernova' => true, + _ => false, + }; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AccountName( + account: previewAccount, + style: const TextStyle(fontSize: 18), + ignorePermissions: true, + ), + const Gap(4), + Row( + children: [ + Icon( + canUseColor + ? Symbols.check_circle + : Symbols.error, + size: 16, + color: + canUseColor + ? Colors.green + : Colors.red, + ), + const Gap(4), + Text( + canUseColor + ? 'availableWithYourPlan'.tr() + : 'upgradeRequired'.tr(), + style: TextStyle( + fontSize: 12, + color: + canUseColor + ? Colors.green + : Colors.red, + ), + ), + ], + ), + ], + ); + }, + ), + ], + ), + ), + DropdownButtonFormField2( + decoration: InputDecoration( + labelText: 'colorType'.tr(), + ), + items: [ + DropdownMenuItem( + value: 'plain', + child: Text('plain'.tr()), + ), + DropdownMenuItem( + value: 'gradient', + child: Text('gradient'.tr()), + ), + ], + value: usernameColorType.value, + onChanged: (value) { + usernameColorType.value = value ?? 'plain'; + }, + customButton: Row( + children: [ + Expanded(child: Text(usernameColorType.value).tr()), + Icon(Symbols.arrow_drop_down), + ], + ), + ), + if (usernameColorType.value == 'plain') + Autocomplete( + optionsBuilder: (TextEditingValue textEditingValue) { + final options = kUsernamePlainColors.keys.toList(); + if (textEditingValue.text == '') { + return options; + } + return options.where( + (option) => option.toLowerCase().contains( + textEditingValue.text.toLowerCase(), + ), + ); + }, + onSelected: (String selection) { + usernameColorValue.text = selection; + }, + fieldViewBuilder: ( + context, + controller, + focusNode, + onFieldSubmitted, + ) { + // Initialize the controller with the current value + if (controller.text.isEmpty && + usernameColorValue.text.isNotEmpty) { + controller.text = usernameColorValue.text; + } + + return TextFormField( + controller: controller, + focusNode: focusNode, + decoration: InputDecoration( + labelText: 'colorValue'.tr(), + hintText: 'e.g. red or #ff6600', + ), + onChanged: (value) { + usernameColorValue.text = value; + }, + onTapOutside: + (_) => + FocusManager.instance.primaryFocus + ?.unfocus(), + ); + }, + ), + if (usernameColorType.value == 'gradient') ...[ + DropdownButtonFormField2( + decoration: InputDecoration( + labelText: 'gradientDirection'.tr(), + ), + items: [ + DropdownMenuItem( + value: 'to right', + child: Text('gradientDirectionToRight'.tr()), + ), + DropdownMenuItem( + value: 'to left', + child: Text('gradientDirectionToLeft'.tr()), + ), + DropdownMenuItem( + value: 'to bottom', + child: Text('gradientDirectionToBottom'.tr()), + ), + DropdownMenuItem( + value: 'to top', + child: Text('gradientDirectionToTop'.tr()), + ), + DropdownMenuItem( + value: 'to bottom right', + child: Text( + 'gradientDirectionToBottomRight'.tr(), + ), + ), + DropdownMenuItem( + value: 'to bottom left', + child: Text('gradientDirectionToBottomLeft'.tr()), + ), + DropdownMenuItem( + value: 'to top right', + child: Text('gradientDirectionToTopRight'.tr()), + ), + DropdownMenuItem( + value: 'to top left', + child: Text('gradientDirectionToTopLeft'.tr()), + ), + ], + value: + usernameColorDirection.text.isNotEmpty + ? usernameColorDirection.text + : 'to right', + onChanged: (value) { + usernameColorDirection.text = value ?? 'to right'; + }, + customButton: Row( + children: [ + Expanded( + child: Text( + usernameColorDirection.text.isNotEmpty + ? usernameColorDirection.text + : 'to right', + ), + ), + Icon(Symbols.arrow_drop_down), + ], + ), + ), + Text( + 'gradientColors', + ).tr().bold().fontSize(14).padding(top: 8), + Column( + spacing: 8, + children: [ + for ( + var i = 0; + i < usernameColorColors.value.length; + i++ + ) + Row( + key: ValueKey( + usernameColorColors.value[i].hashCode, + ), + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: TextFormField( + initialValue: + usernameColorColors.value[i], + decoration: InputDecoration( + labelText: 'color'.tr(), + hintText: 'e.g. #ff0000', + isDense: true, + ), + onChanged: (value) { + usernameColorColors.value[i] = value; + }, + onTapOutside: + (_) => + FocusManager.instance.primaryFocus + ?.unfocus(), + ), + ), + IconButton( + icon: const Icon(Symbols.delete), + onPressed: () { + usernameColorColors.value = + usernameColorColors.value + .whereIndexed( + (idx, _) => idx != i, + ) + .toList(); + }, + ), + ], + ), + Align( + alignment: Alignment.centerRight, + child: FilledButton.icon( + onPressed: () { + usernameColorColors.value = List.from( + usernameColorColors.value, + )..add(''); + }, + label: Text('addColor').tr(), + icon: const Icon(Symbols.add), + ).padding(top: 8), + ), + ], + ), + ], + ], + ), Text('links').tr().bold().fontSize(18).padding(top: 16), Column( spacing: 8, diff --git a/lib/widgets/account/account_name.dart b/lib/widgets/account/account_name.dart index 127741f6..46616578 100644 --- a/lib/widgets/account/account_name.dart +++ b/lib/widgets/account/account_name.dart @@ -6,6 +6,25 @@ import 'package:island/models/wallet.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:styled_widget/styled_widget.dart'; +const Map kUsernamePlainColors = { + 'red': Colors.red, + 'blue': Colors.blue, + 'green': Colors.green, + 'yellow': Colors.yellow, + 'purple': Colors.purple, + 'orange': Colors.orange, + 'pink': Colors.pink, + 'cyan': Colors.cyan, + 'lime': Colors.lime, + 'indigo': Colors.indigo, + 'teal': Colors.teal, + 'amber': Colors.amber, + 'brown': Colors.brown, + 'grey': Colors.grey, + 'black': Colors.black, + 'white': Colors.white, +}; + const kVerificationMarkColors = [ Colors.teal, Colors.blue, @@ -17,12 +36,175 @@ const kVerificationMarkColors = [ class AccountName extends StatelessWidget { final SnAccount account; final TextStyle? style; - const AccountName({super.key, required this.account, this.style}); + final bool ignorePermissions; + const AccountName({ + super.key, + required this.account, + this.style, + this.ignorePermissions = false, + }); + + Alignment _parseGradientDirection(String direction) { + switch (direction) { + case 'to right': + return Alignment.centerLeft; + case 'to left': + return Alignment.centerRight; + case 'to bottom': + return Alignment.topCenter; + case 'to top': + return Alignment.bottomCenter; + case 'to bottom right': + return Alignment.topLeft; + case 'to bottom left': + return Alignment.topRight; + case 'to top right': + return Alignment.bottomLeft; + case 'to top left': + return Alignment.bottomRight; + default: + return Alignment.centerLeft; + } + } + + Alignment _parseGradientEnd(String direction) { + switch (direction) { + case 'to right': + return Alignment.centerRight; + case 'to left': + return Alignment.centerLeft; + case 'to bottom': + return Alignment.bottomCenter; + case 'to top': + return Alignment.topCenter; + case 'to bottom right': + return Alignment.bottomRight; + case 'to bottom left': + return Alignment.bottomLeft; + case 'to top right': + return Alignment.topRight; + case 'to top left': + return Alignment.topLeft; + default: + return Alignment.centerRight; + } + } @override Widget build(BuildContext context) { var nameStyle = (style ?? TextStyle()); - if (account.perkSubscription != null) { + + // Apply username color based on membership tier and custom settings + if (account.profile.usernameColor != null) { + final usernameColor = account.profile.usernameColor!; + final tier = account.perkSubscription?.identifier; + + // Check tier restrictions + final canUseCustomColor = + ignorePermissions || + switch (tier) { + 'solian.stellar.primary' => + usernameColor.type == 'plain' && + kUsernamePlainColors.containsKey(usernameColor.value), + 'solian.stellar.nova' => usernameColor.type == 'plain', + 'solian.stellar.supernova' => true, + _ => false, + }; + + if (canUseCustomColor) { + if (usernameColor.type == 'plain') { + // Plain color + Color? color; + if (kUsernamePlainColors.containsKey(usernameColor.value)) { + color = kUsernamePlainColors[usernameColor.value]; + } else if (usernameColor.value != null) { + // Try to parse hex color + try { + color = Color( + int.parse( + usernameColor.value!.replaceFirst('#', ''), + radix: 16, + ) + + 0xFF000000, + ); + } catch (_) { + // Invalid hex, ignore + } + } + if (color != null) { + nameStyle = nameStyle.copyWith(color: color); + } + } else if (usernameColor.type == 'gradient' && + usernameColor.colors != null && + usernameColor.colors!.isNotEmpty) { + // Gradient - use ShaderMask for text gradient + final colors = []; + for (final colorStr in usernameColor.colors!) { + Color? color; + if (kUsernamePlainColors.containsKey(colorStr)) { + color = kUsernamePlainColors[colorStr]; + } else { + // Try to parse hex color + try { + color = Color( + int.parse(colorStr.replaceFirst('#', ''), radix: 16) + + 0xFF000000, + ); + } catch (_) { + // Invalid hex, skip + continue; + } + } + if (color != null) { + colors.add(color); + } + } + + if (colors.isNotEmpty) { + final gradient = LinearGradient( + colors: colors, + begin: _parseGradientDirection( + usernameColor.direction ?? 'to right', + ), + end: _parseGradientEnd(usernameColor.direction ?? 'to right'), + ); + + return Row( + mainAxisSize: MainAxisSize.min, + spacing: 4, + children: [ + Flexible( + child: ShaderMask( + shaderCallback: (bounds) => gradient.createShader(bounds), + child: Text( + account.nick, + style: nameStyle.copyWith(color: Colors.white), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + if (account.perkSubscription != null) + StellarMembershipMark(membership: account.perkSubscription!), + if (account.profile.verification != null) + VerificationMark(mark: account.profile.verification!), + if (account.automatedId != null) + Tooltip( + message: 'accountAutomated'.tr(), + child: Icon( + Symbols.smart_toy, + size: 16, + color: nameStyle.color, + fill: 1, + ), + ), + ], + ); + } + } + } + } else if (account.perkSubscription != null) { + // Default membership colors if no custom color is set nameStyle = nameStyle.copyWith( color: (switch (account.perkSubscription!.identifier) { 'solian.stellar.primary' => Colors.blueAccent,