From a127b5bacec39595ab7ca13d9d00763a410df733 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Fri, 22 Aug 2025 19:55:26 +0800 Subject: [PATCH] :sparkles: Social credits --- assets/i18n/en-US.json | 9 +- assets/i18n/zh-CN.json | 9 +- lib/models/account.dart | 17 ++ lib/models/account.freezed.dart | 278 +++++++++++++++++++++++++++++ lib/models/account.g.dart | 32 ++++ lib/models/chat.freezed.dart | 48 ++--- lib/route.dart | 6 + lib/screens/account.dart | 19 ++ lib/screens/account/credits.dart | 152 ++++++++++++++++ lib/screens/account/credits.g.dart | 49 +++++ 10 files changed, 596 insertions(+), 23 deletions(-) create mode 100644 lib/screens/account/credits.dart create mode 100644 lib/screens/account/credits.g.dart diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 66eeaa4a..e782ae34 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -868,5 +868,12 @@ "postShuffle": "Shuffle Posts", "leveling": "Leveling", "levelingHistory": "Leveling History", - "stellarProgram": "Stellar Program" + "stellarProgram": "Stellar Program", + "socialCredits": "Social Credits", + "credits": "Credits", + "socialCreditsDescription": "Social Credit is a way for Solar Network to evaluate users. It is calculated based on their behavior and interactions. With a base score of 100, higher scores indicate a user's credibility within the community. Scores change over time to reflect a user's recent behavior. Users with higher credit ratings enjoy more benefits, while users with lower credit ratings may have some functionality restricted.", + "socialCreditsLevelPoor": "Poor", + "socialCreditsLevelNormal": "Normal", + "socialCreditsLevelGood": "Good", + "socialCreditsLevelExcellent": "Excellent" } diff --git a/assets/i18n/zh-CN.json b/assets/i18n/zh-CN.json index 81e57f7d..029a1cf1 100644 --- a/assets/i18n/zh-CN.json +++ b/assets/i18n/zh-CN.json @@ -836,5 +836,12 @@ "exifData": "EXIF 数据", "leveling": "等级", "levelingHistory": "经验记录", - "stellarProgram": "恒星计划" + "stellarProgram": "恒星计划", + "socialCredits": "社会信用点", + "credits": "信用", + "socialCreditsDescription": "社会信用是 Solar Network 评价用户的一种方式。它基于用户的行为和互动来计算。以 100 分为基准,分数越高表示用户在社区中的信誉越好。分数会随着时间的推移而变化,反映用户的最新行为。信用等级高的用户可以享受到更多的福利,反之的用户部份功能可能受到限制。", + "socialCreditsLevelPoor": "糟糕", + "socialCreditsLevelNormal": "正常", + "socialCreditsLevelGood": "良好", + "socialCreditsLevelExcellent": "优秀" } diff --git a/lib/models/account.dart b/lib/models/account.dart index b88aa96c..d44cc6db 100644 --- a/lib/models/account.dart +++ b/lib/models/account.dart @@ -225,3 +225,20 @@ sealed class SnExperienceRecord with _$SnExperienceRecord { factory SnExperienceRecord.fromJson(Map json) => _$SnExperienceRecordFromJson(json); } + +@freezed +sealed class SnSocialCreditRecord with _$SnSocialCreditRecord { + const factory SnSocialCreditRecord({ + required String id, + required double delta, + required String reasonType, + required String reason, + required DateTime? expiredAt, + required DateTime createdAt, + required DateTime updatedAt, + required DateTime? deletedAt, + }) = _SnSocialCreditRecord; + + factory SnSocialCreditRecord.fromJson(Map json) => + _$SnSocialCreditRecordFromJson(json); +} diff --git a/lib/models/account.freezed.dart b/lib/models/account.freezed.dart index 7ea48619..9a24d1df 100644 --- a/lib/models/account.freezed.dart +++ b/lib/models/account.freezed.dart @@ -3296,6 +3296,284 @@ as DateTime?, } +} + + +/// @nodoc +mixin _$SnSocialCreditRecord { + + String get id; double get delta; String get reasonType; String get reason; DateTime? get expiredAt; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; +/// Create a copy of SnSocialCreditRecord +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SnSocialCreditRecordCopyWith get copyWith => _$SnSocialCreditRecordCopyWithImpl(this as SnSocialCreditRecord, _$identity); + + /// Serializes this SnSocialCreditRecord to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SnSocialCreditRecord&&(identical(other.id, id) || other.id == id)&&(identical(other.delta, delta) || other.delta == delta)&&(identical(other.reasonType, reasonType) || other.reasonType == reasonType)&&(identical(other.reason, reason) || other.reason == reason)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(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,delta,reasonType,reason,expiredAt,createdAt,updatedAt,deletedAt); + +@override +String toString() { + return 'SnSocialCreditRecord(id: $id, delta: $delta, reasonType: $reasonType, reason: $reason, expiredAt: $expiredAt, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; +} + + +} + +/// @nodoc +abstract mixin class $SnSocialCreditRecordCopyWith<$Res> { + factory $SnSocialCreditRecordCopyWith(SnSocialCreditRecord value, $Res Function(SnSocialCreditRecord) _then) = _$SnSocialCreditRecordCopyWithImpl; +@useResult +$Res call({ + String id, double delta, String reasonType, String reason, DateTime? expiredAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt +}); + + + + +} +/// @nodoc +class _$SnSocialCreditRecordCopyWithImpl<$Res> + implements $SnSocialCreditRecordCopyWith<$Res> { + _$SnSocialCreditRecordCopyWithImpl(this._self, this._then); + + final SnSocialCreditRecord _self; + final $Res Function(SnSocialCreditRecord) _then; + +/// Create a copy of SnSocialCreditRecord +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? delta = null,Object? reasonType = null,Object? reason = null,Object? expiredAt = 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,delta: null == delta ? _self.delta : delta // ignore: cast_nullable_to_non_nullable +as double,reasonType: null == reasonType ? _self.reasonType : reasonType // ignore: cast_nullable_to_non_nullable +as String,reason: null == reason ? _self.reason : reason // ignore: cast_nullable_to_non_nullable +as String,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable +as DateTime?,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?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [SnSocialCreditRecord]. +extension SnSocialCreditRecordPatterns on SnSocialCreditRecord { +/// 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( _SnSocialCreditRecord value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _SnSocialCreditRecord() 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( _SnSocialCreditRecord value) $default,){ +final _that = this; +switch (_that) { +case _SnSocialCreditRecord(): +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( _SnSocialCreditRecord value)? $default,){ +final _that = this; +switch (_that) { +case _SnSocialCreditRecord() 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 id, double delta, String reasonType, String reason, DateTime? expiredAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _SnSocialCreditRecord() when $default != null: +return $default(_that.id,_that.delta,_that.reasonType,_that.reason,_that.expiredAt,_that.createdAt,_that.updatedAt,_that.deletedAt);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 id, double delta, String reasonType, String reason, DateTime? expiredAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this; +switch (_that) { +case _SnSocialCreditRecord(): +return $default(_that.id,_that.delta,_that.reasonType,_that.reason,_that.expiredAt,_that.createdAt,_that.updatedAt,_that.deletedAt);} +} +/// 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 id, double delta, String reasonType, String reason, DateTime? expiredAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this; +switch (_that) { +case _SnSocialCreditRecord() when $default != null: +return $default(_that.id,_that.delta,_that.reasonType,_that.reason,_that.expiredAt,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _SnSocialCreditRecord implements SnSocialCreditRecord { + const _SnSocialCreditRecord({required this.id, required this.delta, required this.reasonType, required this.reason, required this.expiredAt, required this.createdAt, required this.updatedAt, required this.deletedAt}); + factory _SnSocialCreditRecord.fromJson(Map json) => _$SnSocialCreditRecordFromJson(json); + +@override final String id; +@override final double delta; +@override final String reasonType; +@override final String reason; +@override final DateTime? expiredAt; +@override final DateTime createdAt; +@override final DateTime updatedAt; +@override final DateTime? deletedAt; + +/// Create a copy of SnSocialCreditRecord +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$SnSocialCreditRecordCopyWith<_SnSocialCreditRecord> get copyWith => __$SnSocialCreditRecordCopyWithImpl<_SnSocialCreditRecord>(this, _$identity); + +@override +Map toJson() { + return _$SnSocialCreditRecordToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnSocialCreditRecord&&(identical(other.id, id) || other.id == id)&&(identical(other.delta, delta) || other.delta == delta)&&(identical(other.reasonType, reasonType) || other.reasonType == reasonType)&&(identical(other.reason, reason) || other.reason == reason)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(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,delta,reasonType,reason,expiredAt,createdAt,updatedAt,deletedAt); + +@override +String toString() { + return 'SnSocialCreditRecord(id: $id, delta: $delta, reasonType: $reasonType, reason: $reason, expiredAt: $expiredAt, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; +} + + +} + +/// @nodoc +abstract mixin class _$SnSocialCreditRecordCopyWith<$Res> implements $SnSocialCreditRecordCopyWith<$Res> { + factory _$SnSocialCreditRecordCopyWith(_SnSocialCreditRecord value, $Res Function(_SnSocialCreditRecord) _then) = __$SnSocialCreditRecordCopyWithImpl; +@override @useResult +$Res call({ + String id, double delta, String reasonType, String reason, DateTime? expiredAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt +}); + + + + +} +/// @nodoc +class __$SnSocialCreditRecordCopyWithImpl<$Res> + implements _$SnSocialCreditRecordCopyWith<$Res> { + __$SnSocialCreditRecordCopyWithImpl(this._self, this._then); + + final _SnSocialCreditRecord _self; + final $Res Function(_SnSocialCreditRecord) _then; + +/// Create a copy of SnSocialCreditRecord +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? delta = null,Object? reasonType = null,Object? reason = null,Object? expiredAt = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { + return _then(_SnSocialCreditRecord( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,delta: null == delta ? _self.delta : delta // ignore: cast_nullable_to_non_nullable +as double,reasonType: null == reasonType ? _self.reasonType : reasonType // ignore: cast_nullable_to_non_nullable +as String,reason: null == reason ? _self.reason : reason // ignore: cast_nullable_to_non_nullable +as String,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable +as DateTime?,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 diff --git a/lib/models/account.g.dart b/lib/models/account.g.dart index 16981496..40648834 100644 --- a/lib/models/account.g.dart +++ b/lib/models/account.g.dart @@ -375,3 +375,35 @@ Map _$SnExperienceRecordToJson(_SnExperienceRecord instance) => 'updated_at': instance.updatedAt.toIso8601String(), 'deleted_at': instance.deletedAt?.toIso8601String(), }; + +_SnSocialCreditRecord _$SnSocialCreditRecordFromJson( + Map json, +) => _SnSocialCreditRecord( + id: json['id'] as String, + delta: (json['delta'] as num).toDouble(), + reasonType: json['reason_type'] as String, + reason: json['reason'] as String, + expiredAt: + json['expired_at'] == null + ? null + : DateTime.parse(json['expired_at'] as String), + 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 _$SnSocialCreditRecordToJson( + _SnSocialCreditRecord instance, +) => { + 'id': instance.id, + 'delta': instance.delta, + 'reason_type': instance.reasonType, + 'reason': instance.reason, + 'expired_at': instance.expiredAt?.toIso8601String(), + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + 'deleted_at': instance.deletedAt?.toIso8601String(), +}; diff --git a/lib/models/chat.freezed.dart b/lib/models/chat.freezed.dart index 6dd772b7..8b7fa4ca 100644 --- a/lib/models/chat.freezed.dart +++ b/lib/models/chat.freezed.dart @@ -1410,7 +1410,7 @@ $SnAccountStatusCopyWith<$Res>? get status { /// @nodoc mixin _$SnChatSummary { - int get unreadCount; SnChatMessage get lastMessage; + int get unreadCount; SnChatMessage? get lastMessage; /// Create a copy of SnChatSummary /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -1443,11 +1443,11 @@ abstract mixin class $SnChatSummaryCopyWith<$Res> { factory $SnChatSummaryCopyWith(SnChatSummary value, $Res Function(SnChatSummary) _then) = _$SnChatSummaryCopyWithImpl; @useResult $Res call({ - int unreadCount, SnChatMessage lastMessage + int unreadCount, SnChatMessage? lastMessage }); -$SnChatMessageCopyWith<$Res> get lastMessage; +$SnChatMessageCopyWith<$Res>? get lastMessage; } /// @nodoc @@ -1460,20 +1460,23 @@ class _$SnChatSummaryCopyWithImpl<$Res> /// Create a copy of SnChatSummary /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? unreadCount = null,Object? lastMessage = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? unreadCount = null,Object? lastMessage = freezed,}) { return _then(_self.copyWith( unreadCount: null == unreadCount ? _self.unreadCount : unreadCount // ignore: cast_nullable_to_non_nullable -as int,lastMessage: null == lastMessage ? _self.lastMessage : lastMessage // ignore: cast_nullable_to_non_nullable -as SnChatMessage, +as int,lastMessage: freezed == lastMessage ? _self.lastMessage : lastMessage // ignore: cast_nullable_to_non_nullable +as SnChatMessage?, )); } /// Create a copy of SnChatSummary /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') -$SnChatMessageCopyWith<$Res> get lastMessage { - - return $SnChatMessageCopyWith<$Res>(_self.lastMessage, (value) { +$SnChatMessageCopyWith<$Res>? get lastMessage { + if (_self.lastMessage == null) { + return null; + } + + return $SnChatMessageCopyWith<$Res>(_self.lastMessage!, (value) { return _then(_self.copyWith(lastMessage: value)); }); } @@ -1555,7 +1558,7 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( int unreadCount, SnChatMessage lastMessage)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( int unreadCount, SnChatMessage? lastMessage)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _SnChatSummary() when $default != null: return $default(_that.unreadCount,_that.lastMessage);case _: @@ -1576,7 +1579,7 @@ return $default(_that.unreadCount,_that.lastMessage);case _: /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( int unreadCount, SnChatMessage lastMessage) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( int unreadCount, SnChatMessage? lastMessage) $default,) {final _that = this; switch (_that) { case _SnChatSummary(): return $default(_that.unreadCount,_that.lastMessage);} @@ -1593,7 +1596,7 @@ return $default(_that.unreadCount,_that.lastMessage);} /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( int unreadCount, SnChatMessage lastMessage)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( int unreadCount, SnChatMessage? lastMessage)? $default,) {final _that = this; switch (_that) { case _SnChatSummary() when $default != null: return $default(_that.unreadCount,_that.lastMessage);case _: @@ -1612,7 +1615,7 @@ class _SnChatSummary implements SnChatSummary { factory _SnChatSummary.fromJson(Map json) => _$SnChatSummaryFromJson(json); @override final int unreadCount; -@override final SnChatMessage lastMessage; +@override final SnChatMessage? lastMessage; /// Create a copy of SnChatSummary /// with the given fields replaced by the non-null parameter values. @@ -1647,11 +1650,11 @@ abstract mixin class _$SnChatSummaryCopyWith<$Res> implements $SnChatSummaryCopy factory _$SnChatSummaryCopyWith(_SnChatSummary value, $Res Function(_SnChatSummary) _then) = __$SnChatSummaryCopyWithImpl; @override @useResult $Res call({ - int unreadCount, SnChatMessage lastMessage + int unreadCount, SnChatMessage? lastMessage }); -@override $SnChatMessageCopyWith<$Res> get lastMessage; +@override $SnChatMessageCopyWith<$Res>? get lastMessage; } /// @nodoc @@ -1664,11 +1667,11 @@ class __$SnChatSummaryCopyWithImpl<$Res> /// Create a copy of SnChatSummary /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? unreadCount = null,Object? lastMessage = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? unreadCount = null,Object? lastMessage = freezed,}) { return _then(_SnChatSummary( unreadCount: null == unreadCount ? _self.unreadCount : unreadCount // ignore: cast_nullable_to_non_nullable -as int,lastMessage: null == lastMessage ? _self.lastMessage : lastMessage // ignore: cast_nullable_to_non_nullable -as SnChatMessage, +as int,lastMessage: freezed == lastMessage ? _self.lastMessage : lastMessage // ignore: cast_nullable_to_non_nullable +as SnChatMessage?, )); } @@ -1676,9 +1679,12 @@ as SnChatMessage, /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') -$SnChatMessageCopyWith<$Res> get lastMessage { - - return $SnChatMessageCopyWith<$Res>(_self.lastMessage, (value) { +$SnChatMessageCopyWith<$Res>? get lastMessage { + if (_self.lastMessage == null) { + return null; + } + + return $SnChatMessageCopyWith<$Res>(_self.lastMessage!, (value) { return _then(_self.copyWith(lastMessage: value)); }); } diff --git a/lib/route.dart b/lib/route.dart index 5bcbde7b..fa5e92de 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart' show kIsWeb; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/screens/about.dart'; +import 'package:island/screens/account/credits.dart'; import 'package:island/screens/developers/apps.dart'; import 'package:island/screens/developers/edit_app.dart'; import 'package:island/screens/developers/new_app.dart'; @@ -555,6 +556,11 @@ final routerProvider = Provider((ref) { path: '/account/wallet', builder: (context, state) => const WalletScreen(), ), + GoRoute( + name: 'socialCredits', + path: '/account/credits', + builder: (context, state) => const SocialCreditsScreen(), + ), GoRoute( name: 'relationships', path: '/account/relationships', diff --git a/lib/screens/account.dart b/lib/screens/account.dart index d3e52c9c..1d375810 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -236,6 +236,16 @@ class AccountScreen extends HookConsumerWidget { context.pushNamed('stickerMarketplace'); }, ), + ListTile( + minTileHeight: 48, + leading: const Icon(Symbols.star), + trailing: const Icon(Symbols.chevron_right), + contentPadding: EdgeInsets.symmetric(horizontal: 24), + title: Text('credits').tr(), + onTap: () { + context.pushNamed('socialCredits'); + }, + ), ListTile( minTileHeight: 48, title: Text('abuseReport').tr(), @@ -389,6 +399,15 @@ class _UnauthorizedAccountScreen extends StatelessWidget { }, child: Text('about').tr(), ), + TextButton( + child: Text('debugOptions').tr(), + onPressed: () { + showModalBottomSheet( + context: context, + builder: (context) => DebugSheet(), + ); + }, + ), TextButton( onPressed: () { context.pushNamed('settings'); diff --git a/lib/screens/account/credits.dart b/lib/screens/account/credits.dart new file mode 100644 index 00000000..5150fd42 --- /dev/null +++ b/lib/screens/account/credits.dart @@ -0,0 +1,152 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/models/account.dart'; +import 'package:island/pods/network.dart'; +import 'package:island/widgets/app_scaffold.dart'; +import 'package:material_symbols_icons/material_symbols_icons.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; +import 'package:styled_widget/styled_widget.dart'; + +part 'credits.g.dart'; + +@riverpod +Future socialCredits(Ref ref) async { + final client = ref.watch(apiClientProvider); + final response = await client.get('/id/accounts/me/credits'); + if (response.statusCode != 200) { + throw Exception('Failed to load social credits'); + } + return response.data?.toDouble() ?? 0.0; +} + +@riverpod +class SocialCreditHistoryNotifier extends _$SocialCreditHistoryNotifier + with CursorPagingNotifierMixin { + static const int _pageSize = 20; + + @override + Future> build() => fetch(cursor: null); + + @override + Future> fetch({ + required String? cursor, + }) async { + final client = ref.read(apiClientProvider); + final offset = cursor == null ? 0 : int.parse(cursor); + + final queryParams = {'offset': offset, 'take': _pageSize}; + + final response = await client.get( + '/id/accounts/me/credits/history', + queryParameters: queryParams, + ); + final total = int.parse(response.headers.value('X-Total') ?? '0'); + final List data = response.data; + final records = + data.map((json) => SnSocialCreditRecord.fromJson(json)).toList(); + + final hasMore = offset + records.length < total; + final nextCursor = hasMore ? (offset + records.length).toString() : null; + + return CursorPagingData( + items: records, + hasMore: hasMore, + nextCursor: nextCursor, + ); + } +} + +class SocialCreditsScreen extends HookConsumerWidget { + const SocialCreditsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final socialCredits = ref.watch(socialCreditsProvider); + + return AppScaffold( + appBar: AppBar(title: Text('socialCredits').tr()), + body: Column( + children: [ + Card( + margin: EdgeInsets.only(left: 16, right: 16, top: 8), + child: socialCredits + .when( + data: + (credits) => Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + credits < 100 + ? 'socialCreditsLevelPoor'.tr() + : credits < 150 + ? 'socialCreditsLevelNormal'.tr() + : credits < 200 + ? 'socialCreditsLevelGood'.tr() + : 'socialCreditsLevelExcellent'.tr(), + ).tr().bold().fontSize(20), + Text( + '${credits.toStringAsFixed(2)} pts', + ).fontSize(14), + const Gap(8), + LinearProgressIndicator(value: credits / 200), + ], + ), + Positioned( + right: 0, + top: 0, + child: IconButton( + onPressed: () {}, + icon: const Icon(Symbols.info), + tooltip: 'socialCreditsDescription'.tr(), + ), + ), + ], + ), + error: (_, _) => Text('Error loading credits'), + loading: () => const LinearProgressIndicator(), + ) + .padding(horizontal: 20, vertical: 16), + ), + Expanded( + child: PagingHelperView( + provider: socialCreditHistoryNotifierProvider, + futureRefreshable: socialCreditHistoryNotifierProvider.future, + notifierRefreshable: socialCreditHistoryNotifierProvider.notifier, + contentBuilder: + (data, widgetCount, endItemView) => ListView.builder( + padding: EdgeInsets.zero, + itemCount: widgetCount, + itemBuilder: (context, index) { + if (index == widgetCount - 1) { + return endItemView; + } + final record = data.items[index]; + return ListTile( + contentPadding: EdgeInsets.symmetric(horizontal: 24), + title: Text(record.reason), + subtitle: Text( + DateFormat.yMMMd().format(record.createdAt), + ), + trailing: Text( + record.delta > 0 + ? '+${record.delta}' + : '${record.delta}', + style: TextStyle( + color: record.delta > 0 ? Colors.green : Colors.red, + ), + ), + ); + }, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/account/credits.g.dart b/lib/screens/account/credits.g.dart new file mode 100644 index 00000000..988167a0 --- /dev/null +++ b/lib/screens/account/credits.g.dart @@ -0,0 +1,49 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'credits.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$socialCreditsHash() => r'2599844e892127ee4d315caced5c10e4dbaea142'; + +/// See also [socialCredits]. +@ProviderFor(socialCredits) +final socialCreditsProvider = AutoDisposeFutureProvider.internal( + socialCredits, + name: r'socialCreditsProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$socialCreditsHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef SocialCreditsRef = AutoDisposeFutureProviderRef; +String _$socialCreditHistoryNotifierHash() => + r'950db020754160f835c64cedf3fa2175e61e4d64'; + +/// See also [SocialCreditHistoryNotifier]. +@ProviderFor(SocialCreditHistoryNotifier) +final socialCreditHistoryNotifierProvider = AutoDisposeAsyncNotifierProvider< + SocialCreditHistoryNotifier, + CursorPagingData +>.internal( + SocialCreditHistoryNotifier.new, + name: r'socialCreditHistoryNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$socialCreditHistoryNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$SocialCreditHistoryNotifier = + AutoDisposeAsyncNotifier>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package