Compare commits

...

15 Commits

Author SHA1 Message Date
LittleSheep
a127b5bace Social credits 2025-08-22 19:55:26 +08:00
LittleSheep
b2097cf044 🐛 Fix chat summary failed when lastMessage is null 2025-08-22 19:00:06 +08:00
LittleSheep
701f29748d Debug set access token 2025-08-22 18:59:40 +08:00
LittleSheep
9e40ed4600 Show leveling BonusMultiplier 2025-08-22 17:58:28 +08:00
LittleSheep
c90e6fe661 Experience records & refine leveling page 2025-08-22 17:53:07 +08:00
LittleSheep
569483300d Card shuffle 2025-08-22 16:20:13 +08:00
LittleSheep
bab602d98b Shuffle post 2025-08-22 01:41:25 +08:00
LittleSheep
b4f2bb803a ⬆️ Upgrade deps 2025-08-22 00:22:06 +08:00
LittleSheep
03bfed6f46 💄 Optimize cloud file info 2025-08-22 00:17:13 +08:00
LittleSheep
f98e5a0aec Post browse by categories, tags 2025-08-21 23:21:30 +08:00
LittleSheep
3d473e2fec 🐛 Replace the push with go to view posts in creator centre in order to fix #172 2025-08-21 20:09:49 +08:00
LittleSheep
0b6efa373a 💄 Optimize realm list 2025-08-21 18:49:42 +08:00
LittleSheep
9b60e96cde 💄 Optimize post detail error page 2025-08-21 02:25:13 +08:00
LittleSheep
81cd9b2082 🐛 Fix issues when saving image to gallery without ext name 2025-08-21 02:22:21 +08:00
LittleSheep
923d5d7514 👽 Update stickers api call 2025-08-20 14:26:37 +08:00
39 changed files with 2040 additions and 261 deletions

View File

@@ -854,5 +854,26 @@
"failedToLoadUserInfo": "Failed to load user info",
"failedToLoadUserInfoNetwork": "It seems be network issue, you can tap the button below to try again.",
"failedToLoadUserInfoUnauthorized": "It seems your session has been logged out or not available anymore, you can still try agian to fetch the user info if you want.",
"okay": "Okay"
"okay": "Okay",
"postDetails": "Post Details",
"postCount": {
"zero": "No posts",
"one": "{} post",
"other": "{} posts"
},
"mimeType": "MIME Type",
"fileSize": "File Size",
"fileHash": "File Hash",
"exifData": "EXIF Data",
"postShuffle": "Shuffle Posts",
"leveling": "Leveling",
"levelingHistory": "Leveling History",
"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"
}

View File

@@ -828,5 +828,20 @@
"failedToLoadUserInfo": "加载用户信息失败",
"failedToLoadUserInfoNetwork": "这看起来是个网络问题,你可以按下面的按钮来重试",
"failedToLoadUserInfoUnauthorized": "看来您的会话已被注销或不再可用,如果您愿意,您仍然可以再次尝试获取用户信息。",
"okay": "了解"
"okay": "了解",
"postDetails": "帖子详情",
"mimeType": "类型",
"fileSize": "大小",
"fileHash": "哈希",
"exifData": "EXIF 数据",
"leveling": "等级",
"levelingHistory": "经验记录",
"stellarProgram": "恒星计划",
"socialCredits": "社会信用点",
"credits": "信用",
"socialCreditsDescription": "社会信用是 Solar Network 评价用户的一种方式。它基于用户的行为和互动来计算。以 100 分为基准,分数越高表示用户在社区中的信誉越好。分数会随着时间的推移而变化,反映用户的最新行为。信用等级高的用户可以享受到更多的福利,反之的用户部份功能可能受到限制。",
"socialCreditsLevelPoor": "糟糕",
"socialCreditsLevelNormal": "正常",
"socialCreditsLevelGood": "良好",
"socialCreditsLevelExcellent": "优秀"
}

View File

@@ -208,3 +208,37 @@ sealed class SnAuthDeviceWithChallenge with _$SnAuthDeviceWithChallenge {
factory SnAuthDeviceWithChallenge.fromJson(Map<String, dynamic> json) =>
_$SnAuthDeviceWithChallengeFromJson(json);
}
@freezed
sealed class SnExperienceRecord with _$SnExperienceRecord {
const factory SnExperienceRecord({
required String id,
required int delta,
required String reasonType,
required String reason,
@Default(1.0) double? bonusMultiplier,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
}) = _SnExperienceRecord;
factory SnExperienceRecord.fromJson(Map<String, dynamic> 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<String, dynamic> json) =>
_$SnSocialCreditRecordFromJson(json);
}

View File

@@ -3018,6 +3018,562 @@ as bool,
}
}
/// @nodoc
mixin _$SnExperienceRecord {
String get id; int get delta; String get reasonType; String get reason; double? get bonusMultiplier; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnExperienceRecord
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnExperienceRecordCopyWith<SnExperienceRecord> get copyWith => _$SnExperienceRecordCopyWithImpl<SnExperienceRecord>(this as SnExperienceRecord, _$identity);
/// Serializes this SnExperienceRecord to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnExperienceRecord&&(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.bonusMultiplier, bonusMultiplier) || other.bonusMultiplier == bonusMultiplier)&&(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,bonusMultiplier,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnExperienceRecord(id: $id, delta: $delta, reasonType: $reasonType, reason: $reason, bonusMultiplier: $bonusMultiplier, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnExperienceRecordCopyWith<$Res> {
factory $SnExperienceRecordCopyWith(SnExperienceRecord value, $Res Function(SnExperienceRecord) _then) = _$SnExperienceRecordCopyWithImpl;
@useResult
$Res call({
String id, int delta, String reasonType, String reason, double? bonusMultiplier, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class _$SnExperienceRecordCopyWithImpl<$Res>
implements $SnExperienceRecordCopyWith<$Res> {
_$SnExperienceRecordCopyWithImpl(this._self, this._then);
final SnExperienceRecord _self;
final $Res Function(SnExperienceRecord) _then;
/// Create a copy of SnExperienceRecord
/// 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? bonusMultiplier = 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 int,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,bonusMultiplier: freezed == bonusMultiplier ? _self.bonusMultiplier : bonusMultiplier // ignore: cast_nullable_to_non_nullable
as double?,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 [SnExperienceRecord].
extension SnExperienceRecordPatterns on SnExperienceRecord {
/// 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 extends Object?>(TResult Function( _SnExperienceRecord value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnExperienceRecord() 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 extends Object?>(TResult Function( _SnExperienceRecord value) $default,){
final _that = this;
switch (_that) {
case _SnExperienceRecord():
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 extends Object?>(TResult? Function( _SnExperienceRecord value)? $default,){
final _that = this;
switch (_that) {
case _SnExperienceRecord() 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 extends Object?>(TResult Function( String id, int delta, String reasonType, String reason, double? bonusMultiplier, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnExperienceRecord() when $default != null:
return $default(_that.id,_that.delta,_that.reasonType,_that.reason,_that.bonusMultiplier,_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 extends Object?>(TResult Function( String id, int delta, String reasonType, String reason, double? bonusMultiplier, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) {
case _SnExperienceRecord():
return $default(_that.id,_that.delta,_that.reasonType,_that.reason,_that.bonusMultiplier,_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 extends Object?>(TResult? Function( String id, int delta, String reasonType, String reason, double? bonusMultiplier, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) {
case _SnExperienceRecord() when $default != null:
return $default(_that.id,_that.delta,_that.reasonType,_that.reason,_that.bonusMultiplier,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnExperienceRecord implements SnExperienceRecord {
const _SnExperienceRecord({required this.id, required this.delta, required this.reasonType, required this.reason, this.bonusMultiplier = 1.0, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnExperienceRecord.fromJson(Map<String, dynamic> json) => _$SnExperienceRecordFromJson(json);
@override final String id;
@override final int delta;
@override final String reasonType;
@override final String reason;
@override@JsonKey() final double? bonusMultiplier;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnExperienceRecord
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnExperienceRecordCopyWith<_SnExperienceRecord> get copyWith => __$SnExperienceRecordCopyWithImpl<_SnExperienceRecord>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnExperienceRecordToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnExperienceRecord&&(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.bonusMultiplier, bonusMultiplier) || other.bonusMultiplier == bonusMultiplier)&&(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,bonusMultiplier,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnExperienceRecord(id: $id, delta: $delta, reasonType: $reasonType, reason: $reason, bonusMultiplier: $bonusMultiplier, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnExperienceRecordCopyWith<$Res> implements $SnExperienceRecordCopyWith<$Res> {
factory _$SnExperienceRecordCopyWith(_SnExperienceRecord value, $Res Function(_SnExperienceRecord) _then) = __$SnExperienceRecordCopyWithImpl;
@override @useResult
$Res call({
String id, int delta, String reasonType, String reason, double? bonusMultiplier, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class __$SnExperienceRecordCopyWithImpl<$Res>
implements _$SnExperienceRecordCopyWith<$Res> {
__$SnExperienceRecordCopyWithImpl(this._self, this._then);
final _SnExperienceRecord _self;
final $Res Function(_SnExperienceRecord) _then;
/// Create a copy of SnExperienceRecord
/// 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? bonusMultiplier = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnExperienceRecord(
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 int,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,bonusMultiplier: freezed == bonusMultiplier ? _self.bonusMultiplier : bonusMultiplier // ignore: cast_nullable_to_non_nullable
as double?,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
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<SnSocialCreditRecord> get copyWith => _$SnSocialCreditRecordCopyWithImpl<SnSocialCreditRecord>(this as SnSocialCreditRecord, _$identity);
/// Serializes this SnSocialCreditRecord to a JSON map.
Map<String, dynamic> 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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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<String, dynamic> 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<String, dynamic> 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

View File

@@ -348,3 +348,62 @@ Map<String, dynamic> _$SnAuthDeviceWithChallengeeToJson(
'challenges': instance.challenges.map((e) => e.toJson()).toList(),
'is_current': instance.isCurrent,
};
_SnExperienceRecord _$SnExperienceRecordFromJson(Map<String, dynamic> json) =>
_SnExperienceRecord(
id: json['id'] as String,
delta: (json['delta'] as num).toInt(),
reasonType: json['reason_type'] as String,
reason: json['reason'] as String,
bonusMultiplier: (json['bonus_multiplier'] as num?)?.toDouble() ?? 1.0,
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> _$SnExperienceRecordToJson(_SnExperienceRecord instance) =>
<String, dynamic>{
'id': instance.id,
'delta': instance.delta,
'reason_type': instance.reasonType,
'reason': instance.reason,
'bonus_multiplier': instance.bonusMultiplier,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};
_SnSocialCreditRecord _$SnSocialCreditRecordFromJson(
Map<String, dynamic> 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<String, dynamic> _$SnSocialCreditRecordToJson(
_SnSocialCreditRecord instance,
) => <String, dynamic>{
'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(),
};

View File

@@ -104,7 +104,7 @@ sealed class SnChatMember with _$SnChatMember {
sealed class SnChatSummary with _$SnChatSummary {
const factory SnChatSummary({
required int unreadCount,
required SnChatMessage lastMessage,
required SnChatMessage? lastMessage,
}) = _SnChatSummary;
factory SnChatSummary.fromJson(Map<String, dynamic> json) =>

View File

@@ -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 extends Object?>(TResult Function( int unreadCount, SnChatMessage lastMessage)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(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 extends Object?>(TResult Function( int unreadCount, SnChatMessage lastMessage) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(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 extends Object?>(TResult? Function( int unreadCount, SnChatMessage lastMessage)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(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<String, dynamic> 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));
});
}

View File

@@ -213,15 +213,18 @@ Map<String, dynamic> _$SnChatMemberToJson(_SnChatMember instance) =>
_SnChatSummary _$SnChatSummaryFromJson(Map<String, dynamic> json) =>
_SnChatSummary(
unreadCount: (json['unread_count'] as num).toInt(),
lastMessage: SnChatMessage.fromJson(
json['last_message'] as Map<String, dynamic>,
),
lastMessage:
json['last_message'] == null
? null
: SnChatMessage.fromJson(
json['last_message'] as Map<String, dynamic>,
),
);
Map<String, dynamic> _$SnChatSummaryToJson(_SnChatSummary instance) =>
<String, dynamic>{
'unread_count': instance.unreadCount,
'last_message': instance.lastMessage.toJson(),
'last_message': instance.lastMessage?.toJson(),
};
_MessageChange _$MessageChangeFromJson(Map<String, dynamic> json) =>

View File

@@ -15,6 +15,7 @@ sealed class SnPostCategory with _$SnPostCategory {
required String slug,
String? name,
@Default([]) List<SnPost> posts,
@Default(0) int usage,
}) = _SnPostCategory;
factory SnPostCategory.fromJson(Map<String, dynamic> json) =>

View File

@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnPostCategory {
String get id; String get slug; String? get name; List<SnPost> get posts;
String get id; String get slug; String? get name; List<SnPost> get posts; int get usage;
/// Create a copy of SnPostCategory
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $SnPostCategoryCopyWith<SnPostCategory> get copyWith => _$SnPostCategoryCopyWith
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPostCategory&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other.posts, posts));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPostCategory&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other.posts, posts)&&(identical(other.usage, usage) || other.usage == usage));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEquality().hash(posts));
int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEquality().hash(posts),usage);
@override
String toString() {
return 'SnPostCategory(id: $id, slug: $slug, name: $name, posts: $posts)';
return 'SnPostCategory(id: $id, slug: $slug, name: $name, posts: $posts, usage: $usage)';
}
@@ -48,7 +48,7 @@ abstract mixin class $SnPostCategoryCopyWith<$Res> {
factory $SnPostCategoryCopyWith(SnPostCategory value, $Res Function(SnPostCategory) _then) = _$SnPostCategoryCopyWithImpl;
@useResult
$Res call({
String id, String slug, String? name, List<SnPost> posts
String id, String slug, String? name, List<SnPost> posts, int usage
});
@@ -65,13 +65,14 @@ class _$SnPostCategoryCopyWithImpl<$Res>
/// Create a copy of SnPostCategory
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,Object? usage = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String?,posts: null == posts ? _self.posts : posts // ignore: cast_nullable_to_non_nullable
as List<SnPost>,
as List<SnPost>,usage: null == usage ? _self.usage : usage // ignore: cast_nullable_to_non_nullable
as int,
));
}
@@ -153,10 +154,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String slug, String? name, List<SnPost> posts)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String slug, String? name, List<SnPost> posts, int usage)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnPostCategory() when $default != null:
return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
return $default(_that.id,_that.slug,_that.name,_that.posts,_that.usage);case _:
return orElse();
}
@@ -174,10 +175,10 @@ return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String slug, String? name, List<SnPost> posts) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String slug, String? name, List<SnPost> posts, int usage) $default,) {final _that = this;
switch (_that) {
case _SnPostCategory():
return $default(_that.id,_that.slug,_that.name,_that.posts);}
return $default(_that.id,_that.slug,_that.name,_that.posts,_that.usage);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -191,10 +192,10 @@ return $default(_that.id,_that.slug,_that.name,_that.posts);}
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String slug, String? name, List<SnPost> posts)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String slug, String? name, List<SnPost> posts, int usage)? $default,) {final _that = this;
switch (_that) {
case _SnPostCategory() when $default != null:
return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
return $default(_that.id,_that.slug,_that.name,_that.posts,_that.usage);case _:
return null;
}
@@ -206,7 +207,7 @@ return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
@JsonSerializable()
class _SnPostCategory extends SnPostCategory {
const _SnPostCategory({required this.id, required this.slug, this.name, final List<SnPost> posts = const []}): _posts = posts,super._();
const _SnPostCategory({required this.id, required this.slug, this.name, final List<SnPost> posts = const [], this.usage = 0}): _posts = posts,super._();
factory _SnPostCategory.fromJson(Map<String, dynamic> json) => _$SnPostCategoryFromJson(json);
@override final String id;
@@ -219,6 +220,7 @@ class _SnPostCategory extends SnPostCategory {
return EqualUnmodifiableListView(_posts);
}
@override@JsonKey() final int usage;
/// Create a copy of SnPostCategory
/// with the given fields replaced by the non-null parameter values.
@@ -233,16 +235,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPostCategory&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other._posts, _posts));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPostCategory&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other._posts, _posts)&&(identical(other.usage, usage) || other.usage == usage));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEquality().hash(_posts));
int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEquality().hash(_posts),usage);
@override
String toString() {
return 'SnPostCategory(id: $id, slug: $slug, name: $name, posts: $posts)';
return 'SnPostCategory(id: $id, slug: $slug, name: $name, posts: $posts, usage: $usage)';
}
@@ -253,7 +255,7 @@ abstract mixin class _$SnPostCategoryCopyWith<$Res> implements $SnPostCategoryCo
factory _$SnPostCategoryCopyWith(_SnPostCategory value, $Res Function(_SnPostCategory) _then) = __$SnPostCategoryCopyWithImpl;
@override @useResult
$Res call({
String id, String slug, String? name, List<SnPost> posts
String id, String slug, String? name, List<SnPost> posts, int usage
});
@@ -270,13 +272,14 @@ class __$SnPostCategoryCopyWithImpl<$Res>
/// Create a copy of SnPostCategory
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,Object? usage = null,}) {
return _then(_SnPostCategory(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String?,posts: null == posts ? _self._posts : posts // ignore: cast_nullable_to_non_nullable
as List<SnPost>,
as List<SnPost>,usage: null == usage ? _self.usage : usage // ignore: cast_nullable_to_non_nullable
as int,
));
}

View File

@@ -16,6 +16,7 @@ _SnPostCategory _$SnPostCategoryFromJson(Map<String, dynamic> json) =>
?.map((e) => SnPost.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
usage: (json['usage'] as num?)?.toInt() ?? 0,
);
Map<String, dynamic> _$SnPostCategoryToJson(_SnPostCategory instance) =>
@@ -24,4 +25,5 @@ Map<String, dynamic> _$SnPostCategoryToJson(_SnPostCategory instance) =>
'slug': instance.slug,
'name': instance.name,
'posts': instance.posts.map((e) => e.toJson()).toList(),
'usage': instance.usage,
};

View File

@@ -11,6 +11,7 @@ sealed class SnPostTag with _$SnPostTag {
required String slug,
String? name,
@Default([]) List<SnPost> posts,
@Default(0) int usage,
}) = _SnPostTag;
factory SnPostTag.fromJson(Map<String, dynamic> json) =>

View File

@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnPostTag {
String get id; String get slug; String? get name; List<SnPost> get posts;
String get id; String get slug; String? get name; List<SnPost> get posts; int get usage;
/// Create a copy of SnPostTag
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $SnPostTagCopyWith<SnPostTag> get copyWith => _$SnPostTagCopyWithImpl<SnPostTag>
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPostTag&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other.posts, posts));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPostTag&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other.posts, posts)&&(identical(other.usage, usage) || other.usage == usage));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEquality().hash(posts));
int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEquality().hash(posts),usage);
@override
String toString() {
return 'SnPostTag(id: $id, slug: $slug, name: $name, posts: $posts)';
return 'SnPostTag(id: $id, slug: $slug, name: $name, posts: $posts, usage: $usage)';
}
@@ -48,7 +48,7 @@ abstract mixin class $SnPostTagCopyWith<$Res> {
factory $SnPostTagCopyWith(SnPostTag value, $Res Function(SnPostTag) _then) = _$SnPostTagCopyWithImpl;
@useResult
$Res call({
String id, String slug, String? name, List<SnPost> posts
String id, String slug, String? name, List<SnPost> posts, int usage
});
@@ -65,13 +65,14 @@ class _$SnPostTagCopyWithImpl<$Res>
/// Create a copy of SnPostTag
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,Object? usage = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String?,posts: null == posts ? _self.posts : posts // ignore: cast_nullable_to_non_nullable
as List<SnPost>,
as List<SnPost>,usage: null == usage ? _self.usage : usage // ignore: cast_nullable_to_non_nullable
as int,
));
}
@@ -153,10 +154,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String slug, String? name, List<SnPost> posts)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String slug, String? name, List<SnPost> posts, int usage)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnPostTag() when $default != null:
return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
return $default(_that.id,_that.slug,_that.name,_that.posts,_that.usage);case _:
return orElse();
}
@@ -174,10 +175,10 @@ return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String slug, String? name, List<SnPost> posts) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String slug, String? name, List<SnPost> posts, int usage) $default,) {final _that = this;
switch (_that) {
case _SnPostTag():
return $default(_that.id,_that.slug,_that.name,_that.posts);}
return $default(_that.id,_that.slug,_that.name,_that.posts,_that.usage);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -191,10 +192,10 @@ return $default(_that.id,_that.slug,_that.name,_that.posts);}
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String slug, String? name, List<SnPost> posts)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String slug, String? name, List<SnPost> posts, int usage)? $default,) {final _that = this;
switch (_that) {
case _SnPostTag() when $default != null:
return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
return $default(_that.id,_that.slug,_that.name,_that.posts,_that.usage);case _:
return null;
}
@@ -206,7 +207,7 @@ return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
@JsonSerializable()
class _SnPostTag implements SnPostTag {
const _SnPostTag({required this.id, required this.slug, this.name, final List<SnPost> posts = const []}): _posts = posts;
const _SnPostTag({required this.id, required this.slug, this.name, final List<SnPost> posts = const [], this.usage = 0}): _posts = posts;
factory _SnPostTag.fromJson(Map<String, dynamic> json) => _$SnPostTagFromJson(json);
@override final String id;
@@ -219,6 +220,7 @@ class _SnPostTag implements SnPostTag {
return EqualUnmodifiableListView(_posts);
}
@override@JsonKey() final int usage;
/// Create a copy of SnPostTag
/// with the given fields replaced by the non-null parameter values.
@@ -233,16 +235,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPostTag&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other._posts, _posts));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPostTag&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other._posts, _posts)&&(identical(other.usage, usage) || other.usage == usage));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEquality().hash(_posts));
int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEquality().hash(_posts),usage);
@override
String toString() {
return 'SnPostTag(id: $id, slug: $slug, name: $name, posts: $posts)';
return 'SnPostTag(id: $id, slug: $slug, name: $name, posts: $posts, usage: $usage)';
}
@@ -253,7 +255,7 @@ abstract mixin class _$SnPostTagCopyWith<$Res> implements $SnPostTagCopyWith<$Re
factory _$SnPostTagCopyWith(_SnPostTag value, $Res Function(_SnPostTag) _then) = __$SnPostTagCopyWithImpl;
@override @useResult
$Res call({
String id, String slug, String? name, List<SnPost> posts
String id, String slug, String? name, List<SnPost> posts, int usage
});
@@ -270,13 +272,14 @@ class __$SnPostTagCopyWithImpl<$Res>
/// Create a copy of SnPostTag
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,Object? usage = null,}) {
return _then(_SnPostTag(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String?,posts: null == posts ? _self._posts : posts // ignore: cast_nullable_to_non_nullable
as List<SnPost>,
as List<SnPost>,usage: null == usage ? _self.usage : usage // ignore: cast_nullable_to_non_nullable
as int,
));
}

View File

@@ -15,6 +15,7 @@ _SnPostTag _$SnPostTagFromJson(Map<String, dynamic> json) => _SnPostTag(
?.map((e) => SnPost.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
usage: (json['usage'] as num?)?.toInt() ?? 0,
);
Map<String, dynamic> _$SnPostTagToJson(_SnPostTag instance) =>
@@ -23,4 +24,5 @@ Map<String, dynamic> _$SnPostTagToJson(_SnPostTag instance) =>
'slug': instance.slug,
'name': instance.name,
'posts': instance.posts.map((e) => e.toJson()).toList(),
'usage': instance.usage,
};

View File

@@ -6,11 +6,13 @@ 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';
import 'package:island/screens/developers/hub.dart';
import 'package:island/screens/discovery/articles.dart';
import 'package:island/screens/posts/post_categories_list.dart';
import 'package:island/screens/posts/post_category_detail.dart';
import 'package:island/screens/posts/post_search.dart';
import 'package:island/widgets/app_wrapper.dart';
@@ -52,6 +54,7 @@ import 'package:island/screens/account/event_calendar.dart';
import 'package:island/screens/discovery/realms.dart';
import 'package:island/screens/reports/report_detail.dart';
import 'package:island/screens/reports/report_list.dart';
import 'package:island/widgets/post/post_shuffle.dart';
// Shell route keys for nested navigation
final rootNavigatorKey = GlobalKey<NavigatorState>();
@@ -376,12 +379,14 @@ final routerProvider = Provider<GoRouter>((ref) {
builder: (context, state) => const PostSearchScreen(),
),
GoRoute(
name: 'postDetail',
path: '/posts/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return PostDetailScreen(id: id);
},
name: 'postShuffle',
path: '/posts/shuffle',
builder: (context, state) => const PostShuffleScreen(),
),
GoRoute(
name: 'postCategories',
path: '/posts/categories',
builder: (context, state) => const PostCategoriesListScreen(),
),
GoRoute(
name: 'postCategoryDetail',
@@ -391,6 +396,11 @@ final routerProvider = Provider<GoRouter>((ref) {
return PostCategoryDetailScreen(slug: slug, isCategory: true);
},
),
GoRoute(
name: 'postTags',
path: '/posts/tags',
builder: (context, state) => const PostTagsListScreen(),
),
GoRoute(
name: 'postTagDetail',
path: '/posts/tags/:slug',
@@ -402,6 +412,14 @@ final routerProvider = Provider<GoRouter>((ref) {
);
},
),
GoRoute(
name: 'postDetail',
path: '/posts/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return PostDetailScreen(id: id);
},
),
GoRoute(
name: 'publisherProfile',
path: '/publishers/:name',
@@ -538,6 +556,11 @@ final routerProvider = Provider<GoRouter>((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',

View File

@@ -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');

View File

@@ -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<double> 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<SnSocialCreditRecord> {
static const int _pageSize = 20;
@override
Future<CursorPagingData<SnSocialCreditRecord>> build() => fetch(cursor: null);
@override
Future<CursorPagingData<SnSocialCreditRecord>> 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<dynamic> 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,
),
),
);
},
),
),
),
],
),
);
}
}

View File

@@ -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<double>.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<double>;
String _$socialCreditHistoryNotifierHash() =>
r'950db020754160f835c64cedf3fa2175e61e4d64';
/// See also [SocialCreditHistoryNotifier].
@ProviderFor(SocialCreditHistoryNotifier)
final socialCreditHistoryNotifierProvider = AutoDisposeAsyncNotifierProvider<
SocialCreditHistoryNotifier,
CursorPagingData<SnSocialCreditRecord>
>.internal(
SocialCreditHistoryNotifier.new,
name: r'socialCreditHistoryNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$socialCreditHistoryNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SocialCreditHistoryNotifier =
AutoDisposeAsyncNotifier<CursorPagingData<SnSocialCreditRecord>>;
// 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

View File

@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/account.dart';
import 'package:island/models/wallet.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
@@ -19,6 +20,7 @@ import 'package:island/widgets/payment/payment_overlay.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:material_symbols_icons/symbols.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 'leveling.g.dart';
@@ -35,13 +37,49 @@ Future<SnWalletSubscription?> accountStellarSubscription(Ref ref) async {
}
}
@riverpod
class LevelingHistoryNotifier extends _$LevelingHistoryNotifier
with CursorPagingNotifierMixin<SnExperienceRecord> {
static const int _pageSize = 20;
@override
Future<CursorPagingData<SnExperienceRecord>> build() => fetch(cursor: null);
@override
Future<CursorPagingData<SnExperienceRecord>> 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/leveling',
queryParameters: queryParams,
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final records =
data.map((json) => SnExperienceRecord.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 LevelingScreen extends HookConsumerWidget {
const LevelingScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userInfoProvider);
final stellarSubscription = ref.watch(accountStellarSubscriptionProvider);
if (user.value == null) {
return AppScaffold(
@@ -50,47 +88,150 @@ class LevelingScreen extends HookConsumerWidget {
);
}
final currentLevel = user.value!.profile.level;
final currentExp = user.value!.profile.experience;
final progress = user.value!.profile.levelingProgress;
return DefaultTabController(
length: 2,
child: AppScaffold(
appBar: AppBar(
title: Text('levelingProgress'.tr()),
bottom: TabBar(
tabs: [
Tab(text: 'leveling'.tr()),
Tab(text: 'stellarProgram'.tr()),
],
),
),
body: TabBarView(
children: [
_buildLevelingTab(context, ref, user.value!),
_buildStellarProgramTab(context, ref),
],
),
),
);
}
return AppScaffold(
appBar: AppBar(title: Text('levelingProgress'.tr())),
body: SingleChildScrollView(
padding: getTabbedPadding(context, horizontal: 20, vertical: 20),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 480),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Current Progress Card
LevelingProgressCard(
level: currentLevel,
experience: currentExp,
progress: progress,
),
const Gap(24),
Widget _buildLevelingTab(
BuildContext context,
WidgetRef ref,
SnAccount user,
) {
final currentLevel = user.profile.level;
final currentExp = user.profile.experience;
final progress = user.profile.levelingProgress;
// Level Stairs Graph
Text(
'levelProgress'.tr(),
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const Gap(16),
return Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20),
constraints: const BoxConstraints(maxWidth: 480),
child: CustomScrollView(
slivers: [
const SliverGap(20),
// Stairs visualization with fixed height and horizontal scroll
_buildLevelStairs(context, currentLevel),
const Gap(24),
// Membership section
_buildMembershipSection(context, ref, stellarSubscription),
const Gap(16),
],
// Current Progress Card
SliverToBoxAdapter(
child: LevelingProgressCard(
level: currentLevel,
experience: currentExp,
progress: progress,
),
),
const SliverGap(24),
// Level Stairs Graph
SliverToBoxAdapter(
child: Text(
'levelProgress'.tr(),
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
const SliverGap(16),
// Stairs visualization with fixed height and horizontal scroll
SliverToBoxAdapter(child: _buildLevelStairs(context, currentLevel)),
const SliverGap(24),
// Leveling History
SliverToBoxAdapter(
child: Text(
'levelingHistory'.tr(),
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
const SliverGap(8),
PagingHelperSliverView(
provider: levelingHistoryNotifierProvider,
futureRefreshable: levelingHistoryNotifierProvider.future,
notifierRefreshable: levelingHistoryNotifierProvider.notifier,
contentBuilder:
(data, widgetCount, endItemView) => SliverList.builder(
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
final record = data.items[index];
return ListTile(
title: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(record.reason),
Row(
spacing: 4,
children: [
Text(
record.createdAt.formatRelative(context),
).fontSize(13),
Text('·').fontSize(13).bold(),
Text(
record.createdAt.formatSystem(),
).fontSize(13),
],
).opacity(0.8),
],
),
subtitle: Row(
spacing: 8,
children: [
Text(
'${record.delta > 0 ? '+' : ''}${record.delta} EXP',
),
if (record.bonusMultiplier != 1.0)
Text('x${record.bonusMultiplier}'),
],
),
minTileHeight: 56,
contentPadding: EdgeInsets.symmetric(horizontal: 4),
);
},
),
),
SliverGap(getTabbedPadding(context, vertical: 20).vertical),
],
),
),
);
}
Widget _buildStellarProgramTab(BuildContext context, WidgetRef ref) {
final stellarSubscription = ref.watch(accountStellarSubscriptionProvider);
return SingleChildScrollView(
padding: getTabbedPadding(context, horizontal: 20, vertical: 20),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 480),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildMembershipSection(context, ref, stellarSubscription),
const Gap(16),
],
),
),
),

View File

@@ -27,5 +27,26 @@ final accountStellarSubscriptionProvider =
// ignore: unused_element
typedef AccountStellarSubscriptionRef =
AutoDisposeFutureProviderRef<SnWalletSubscription?>;
String _$levelingHistoryNotifierHash() =>
r'e795f9b7911c9e50f15c095ea237cb0e87bf1e89';
/// See also [LevelingHistoryNotifier].
@ProviderFor(LevelingHistoryNotifier)
final levelingHistoryNotifierProvider = AutoDisposeAsyncNotifierProvider<
LevelingHistoryNotifier,
CursorPagingData<SnExperienceRecord>
>.internal(
LevelingHistoryNotifier.new,
name: r'levelingHistoryNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$levelingHistoryNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$LevelingHistoryNotifier =
AutoDisposeAsyncNotifier<CursorPagingData<SnExperienceRecord>>;
// 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

View File

@@ -79,33 +79,38 @@ class ChatRoomListTile extends HookConsumerWidget {
color: Theme.of(context).colorScheme.primary,
),
),
Row(
spacing: 4,
children: [
Badge(
label: Text(data.lastMessage.sender.account.nick),
textColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).colorScheme.primary,
),
Expanded(
child: Text(
(data.lastMessage.content?.isNotEmpty ?? false)
? data.lastMessage.content!
: 'messageNone'.tr(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall,
if (data.lastMessage == null)
Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1)
else
Row(
spacing: 4,
children: [
Badge(
label: Text(data.lastMessage!.sender.account.nick),
textColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).colorScheme.primary,
),
),
Align(
alignment: Alignment.centerRight,
child: Text(
RelativeTime(context).format(data.lastMessage.createdAt),
style: Theme.of(context).textTheme.bodySmall,
Expanded(
child: Text(
(data.lastMessage!.content?.isNotEmpty ?? false)
? data.lastMessage!.content!
: 'messageNone'.tr(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall,
),
),
),
],
),
Align(
alignment: Alignment.centerRight,
child: Text(
RelativeTime(
context,
).format(data.lastMessage!.createdAt),
style: Theme.of(context).textTheme.bodySmall,
),
),
],
),
],
);
},

View File

@@ -7,7 +7,7 @@ part of 'room_detail.dart';
// **************************************************************************
String _$chatMemberListNotifierHash() =>
r'c8fbf4b95df6dae24b1ba21063e9a43351832974';
r'3ea30150278523e9f6b23f9200ea9a9fbae9c973';
/// Copied from Dart SDK
class _SystemHash {

View File

@@ -106,11 +106,7 @@ class StickerPacksNotifier extends _$StickerPacksNotifier
try {
final response = await client.get(
'/sphere/stickers',
queryParameters: {
'offset': offset,
'take': _pageSize,
'pubName': pubName,
},
queryParameters: {'offset': offset, 'take': _pageSize, 'pub': pubName},
);
final total = int.parse(response.headers.value('X-Total') ?? '0');

View File

@@ -148,7 +148,7 @@ class _StickerPackProviderElement
}
String _$stickerPacksNotifierHash() =>
r'0a8edcf9c35396c411f1214f5e77b1e8fac6a3e6';
r'30024b35235f3085a5b1ec2204d0a974ee907e22';
abstract class _$StickerPacksNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnStickerPack>> {

View File

@@ -173,12 +173,60 @@ class ExploreScreen extends HookConsumerWidget {
),
tooltip: 'webArticlesStand'.tr(),
),
IconButton(
onPressed: () {
context.pushNamed('postSearch');
},
PopupMenuButton(
itemBuilder:
(context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.category),
const Gap(12),
Text('categories').tr(),
],
),
onTap: () {
context.pushNamed('postCategories');
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.label),
const Gap(12),
Text('tags').tr(),
],
),
onTap: () {
context.pushNamed('postTags');
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.shuffle),
const Gap(12),
Text('postShuffle').tr(),
],
),
onTap: () {
context.pushNamed('postShuffle');
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.search),
const Gap(12),
Text('search').tr(),
],
),
onTap: () {
context.pushNamed('postSearch');
},
),
],
icon: Icon(
Symbols.search,
Symbols.action_key,
color: Theme.of(context).appBarTheme.foregroundColor!,
),
tooltip: 'search'.tr(),

View File

@@ -0,0 +1,242 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post_category.dart';
import 'package:island/models/post_tag.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
// Post Categories Notifier
final postCategoriesNotifierProvider = StateNotifierProvider.autoDispose<
PostCategoriesNotifier,
AsyncValue<CursorPagingData<SnPostCategory>>
>((ref) {
return PostCategoriesNotifier(ref);
});
class PostCategoriesNotifier
extends StateNotifier<AsyncValue<CursorPagingData<SnPostCategory>>> {
final AutoDisposeRef ref;
static const int _pageSize = 20;
bool _isLoading = false;
PostCategoriesNotifier(this.ref) : super(const AsyncValue.loading()) {
state = const AsyncValue.data(
CursorPagingData(items: [], hasMore: false, nextCursor: null),
);
fetch(cursor: null);
}
Future<void> fetch({String? cursor}) async {
if (_isLoading) return;
_isLoading = true;
if (cursor == null) {
state = const AsyncValue.loading();
}
try {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final response = await client.get(
'/sphere/posts/categories',
queryParameters: {
'offset': offset,
'take': _pageSize,
'order': 'usage',
},
);
final data = response.data as List;
final categories =
data.map((json) => SnPostCategory.fromJson(json)).toList();
final hasMore = categories.length == _pageSize;
final nextCursor =
hasMore ? (offset + categories.length).toString() : null;
state = AsyncValue.data(
CursorPagingData(
items: [...(state.value?.items ?? []), ...categories],
hasMore: hasMore,
nextCursor: nextCursor,
),
);
} catch (e, stack) {
state = AsyncValue.error(e, stack);
} finally {
_isLoading = false;
}
}
}
// Post Tags Notifier
final postTagsNotifierProvider = StateNotifierProvider.autoDispose<
PostTagsNotifier,
AsyncValue<CursorPagingData<SnPostTag>>
>((ref) {
return PostTagsNotifier(ref);
});
class PostTagsNotifier
extends StateNotifier<AsyncValue<CursorPagingData<SnPostTag>>> {
final AutoDisposeRef ref;
static const int _pageSize = 20;
bool _isLoading = false;
PostTagsNotifier(this.ref) : super(const AsyncValue.loading()) {
state = const AsyncValue.data(
CursorPagingData(items: [], hasMore: false, nextCursor: null),
);
fetch(cursor: null);
}
Future<void> fetch({String? cursor}) async {
if (_isLoading) return;
_isLoading = true;
if (cursor == null) {
state = const AsyncValue.loading();
}
try {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final response = await client.get(
'/sphere/posts/tags',
queryParameters: {
'offset': offset,
'take': _pageSize,
'order': 'usage',
},
);
final data = response.data as List;
final tags = data.map((json) => SnPostTag.fromJson(json)).toList();
final hasMore = tags.length == _pageSize;
final nextCursor = hasMore ? (offset + tags.length).toString() : null;
state = AsyncValue.data(
CursorPagingData(
items: [...(state.value?.items ?? []), ...tags],
hasMore: hasMore,
nextCursor: nextCursor,
),
);
} catch (e, stack) {
state = AsyncValue.error(e, stack);
} finally {
_isLoading = false;
}
}
}
class PostCategoriesListScreen extends ConsumerWidget {
const PostCategoriesListScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final categoriesState = ref.watch(postCategoriesNotifierProvider);
return AppScaffold(
appBar: AppBar(title: const Text('categories').tr()),
body: categoriesState.when(
data: (data) {
if (data.items.isEmpty) {
return const Center(child: Text('No categories found'));
}
return ListView.builder(
padding: EdgeInsets.zero,
itemCount: data.items.length + (data.hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index >= data.items.length) {
ref
.read(postCategoriesNotifierProvider.notifier)
.fetch(cursor: data.nextCursor);
return const Center(child: CircularProgressIndicator());
}
final category = data.items[index];
return ListTile(
leading: const Icon(Symbols.category),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
trailing: const Icon(Symbols.chevron_right),
title: Text(category.categoryDisplayTitle),
subtitle: Text('postCount'.plural(category.usage)),
onTap: () {
context.pushNamed(
'postCategoryDetail',
pathParameters: {'slug': category.slug},
);
},
);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, stack) => ResponseErrorWidget(
error: error,
onRetry: () => ref.invalidate(postCategoriesNotifierProvider),
),
),
);
}
}
class PostTagsListScreen extends ConsumerWidget {
const PostTagsListScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final tagsState = ref.watch(postTagsNotifierProvider);
return AppScaffold(
appBar: AppBar(title: const Text('tags').tr()),
body: tagsState.when(
data: (data) {
if (data.items.isEmpty) {
return const Center(child: Text('No tags found'));
}
return ListView.builder(
padding: EdgeInsets.zero,
itemCount: data.items.length + (data.hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index >= data.items.length) {
ref
.read(postTagsNotifierProvider.notifier)
.fetch(cursor: data.nextCursor);
return const Center(child: CircularProgressIndicator());
}
final tag = data.items[index];
return ListTile(
title: Text(tag.name ?? '#${tag.slug}'),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.label),
trailing: const Icon(Symbols.chevron_right),
subtitle: Text('postCount'.plural(tag.usage)),
onTap: () {
context.pushNamed(
'postTagDetail',
pathParameters: {'slug': tag.slug},
);
},
);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, stack) => ResponseErrorWidget(
error: error,
onRetry: () => ref.invalidate(postTagsNotifierProvider),
),
),
);
}
}

View File

@@ -1,3 +1,4 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -8,6 +9,7 @@ import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/post/post_quick_reply.dart';
import 'package:island/widgets/post/post_replies.dart';
import 'package:island/widgets/response.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
@@ -55,7 +57,10 @@ class PostDetailScreen extends HookConsumerWidget {
return AppScaffold(
isNoBackground: false,
appBar: AppBar(title: const Text('Post')),
appBar: AppBar(
leading: const PageBackButton(),
title: Text('postDetail').tr(),
),
body: postState.when(
data: (post) {
return Stack(
@@ -117,8 +122,12 @@ class PostDetailScreen extends HookConsumerWidget {
],
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Text('Error: $e'),
loading: () => ResponseLoadingWidget(),
error:
(e, _) => ResponseErrorWidget(
error: e,
onRetry: () => ref.invalidate(postStateProvider(id)),
),
),
);
}

View File

@@ -399,7 +399,7 @@ class _RealmChatRoomsProviderElement
}
String _$realmMemberListNotifierHash() =>
r'022bcef5a90cbae05ff23b937851afc3ef913d42';
r'2f88f803b2e61e7287ed8a43025173e56ff6ca3b';
abstract class _$RealmMemberListNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnRealmMember>> {

View File

@@ -106,7 +106,7 @@ class RealmListScreen extends HookConsumerWidget {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 540),
child: RealmListTile(realm: value[item]),
).center();
).padding(horizontal: 8).center();
},
separatorBuilder: (_, _) => const Gap(8),
),

View File

@@ -235,7 +235,11 @@ class PageBackButton extends StatelessWidget {
return IconButton(
onPressed: () {
onWillPop?.call();
context.pop();
if (context.canPop()) {
context.pop();
} else {
context.go('/');
}
},
icon: Icon(
color: color,

View File

@@ -50,6 +50,6 @@ class AppWrapper extends HookConsumerWidget {
}
}
return TourTriggerWidget(child: child);
return TourTriggerWidget(key: UniqueKey(), child: child);
}
}

View File

@@ -1,9 +1,11 @@
import 'dart:convert';
import 'dart:math' as math;
import 'dart:ui';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_blurhash/flutter_blurhash.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
@@ -326,7 +328,11 @@ class CloudFileZoomIn extends HookConsumerWidget {
// Create a temporary file to save the image
final tempDir = await getTemporaryDirectory();
final filePath = '${tempDir.path}/${item.id}.${extension(item.name)}';
var extName = extension(item.name).trim();
if (extName.isEmpty) {
extName = item.mimeType?.split('/').lastOrNull ?? 'jpeg';
}
final filePath = '${tempDir.path}/${item.id}.$extName';
await client.download(
'/drive/files/${item.id}',
@@ -342,39 +348,6 @@ class CloudFileZoomIn extends HookConsumerWidget {
}
}
Widget buildInfoRow(IconData icon, String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24),
child: Row(
children: [
Icon(
icon,
size: 20,
color: Theme.of(context).colorScheme.onSurface,
),
const SizedBox(width: 12),
Text(
label,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
const Spacer(),
Flexible(
child: Text(
value,
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500),
textAlign: TextAlign.end,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
String formatFileSize(int bytes) {
if (bytes <= 0) return '0 B';
if (bytes < 1024) return '$bytes B';
@@ -400,57 +373,247 @@ class CloudFileZoomIn extends HookConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildInfoRow(Icons.description, 'Name', item.name),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text('mimeType').tr(),
Text(
item.mimeType ?? 'unknown'.tr(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
),
SizedBox(height: 28, child: const VerticalDivider()),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text('fileSize').tr(),
Text(
formatFileSize(item.size),
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
),
if (item.hash != null)
SizedBox(height: 28, child: const VerticalDivider()),
if (item.hash != null)
Expanded(
child: GestureDetector(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text('fileHash').tr(),
Text(
'${item.hash!.substring(0, 6)}...',
style: theme.textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.bold),
),
],
),
onLongPress: () {
Clipboard.setData(
ClipboardData(text: item.hash!),
);
showSnackBar('File hash copied to clipboard');
},
),
),
],
).padding(horizontal: 24, vertical: 16),
const Divider(height: 1),
buildInfoRow(
Icons.storage,
'Size',
formatFileSize(item.size),
),
const Divider(height: 1),
buildInfoRow(
Icons.category,
'Type',
item.mimeType?.toUpperCase() ?? 'UNKNOWN',
ListTile(
leading: const Icon(Icons.file_present),
title: Text('Name').tr(),
subtitle: Text(
item.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
trailing: IconButton(
icon: const Icon(Icons.copy),
onPressed: () {
Clipboard.setData(ClipboardData(text: item.name));
showSnackBar('File name copied to clipboard');
},
),
),
if (exifData.isNotEmpty) ...[
const SizedBox(height: 16),
Text(
'EXIF Data',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
).padding(horizontal: 24),
const SizedBox(height: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...exifData.entries.map(
(entry) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${entry.key.contains('-') ? entry.key.split('-').last : entry.key}: ',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
const Divider(height: 1),
Theme(
data: theme.copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
tilePadding: const EdgeInsets.symmetric(
horizontal: 24,
),
title: Text(
'exifData'.tr(),
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...exifData.entries.map(
(entry) => ListTile(
dense: true,
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
),
Expanded(
child: Text(
title:
Text(
entry.key.contains('-')
? entry.key.split('-').last
: entry.key,
style: theme.textTheme.bodyMedium
?.copyWith(
fontWeight: FontWeight.w500,
),
).bold(),
subtitle: Text(
'${entry.value}'.isNotEmpty
? '${entry.value}'
: 'N/A',
style: theme.textTheme.bodyMedium,
),
onTap: () {
Clipboard.setData(
ClipboardData(text: '${entry.value}'),
);
showSnackBar('Value copied to clipboard');
},
),
],
),
),
],
),
],
),
),
],
if (item.fileMeta != null && item.fileMeta!.isNotEmpty) ...[
const Divider(height: 1),
Theme(
data: theme.copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
tilePadding: const EdgeInsets.symmetric(
horizontal: 24,
),
title: Text(
'File Metadata',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
).padding(horizontal: 24),
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...item.fileMeta!.entries.map(
(entry) => ListTile(
dense: true,
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
title:
Text(
entry.key,
style: theme.textTheme.bodyMedium
?.copyWith(
fontWeight: FontWeight.w500,
),
).bold(),
subtitle: Text(
jsonEncode(entry.value),
style: theme.textTheme.bodyMedium,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
onTap: () {
Clipboard.setData(
ClipboardData(
text: jsonEncode(entry.value),
),
);
showSnackBar('Value copied to clipboard');
},
),
),
],
),
],
),
),
],
if (item.userMeta != null && item.userMeta!.isNotEmpty) ...[
const Divider(height: 1),
Theme(
data: theme.copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
tilePadding: const EdgeInsets.symmetric(
horizontal: 24,
),
title: Text(
'User Metadata',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...item.userMeta!.entries.map(
(entry) => ListTile(
dense: true,
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
title:
Text(
entry.key,
style: theme.textTheme.bodyMedium
?.copyWith(
fontWeight: FontWeight.w500,
),
).bold(),
subtitle: Text(
jsonEncode(entry.value),
style: theme.textTheme.bodyMedium,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
onTap: () {
Clipboard.setData(
ClipboardData(
text: jsonEncode(entry.value),
),
);
showSnackBar('Value copied to clipboard');
},
),
),
],
),
],
),
),
],
const SizedBox(height: 16),
],

View File

@@ -8,6 +8,52 @@ import 'package:island/pods/websocket.dart';
import 'package:island/widgets/content/network_status_sheet.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:island/pods/config.dart';
Future<void> _showSetTokenDialog(BuildContext context, WidgetRef ref) async {
final TextEditingController controller = TextEditingController();
final prefs = ref.read(sharedPreferencesProvider);
return showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Set Access Token'),
content: TextField(
controller: controller,
decoration: const InputDecoration(
hintText: 'Enter access token',
border: OutlineInputBorder(),
),
autofocus: true,
),
actions: <Widget>[
TextButton(
child: const Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Set'),
onPressed: () async {
final token = controller.text.trim();
if (token.isNotEmpty) {
await setToken(prefs, token);
ref.invalidate(tokenProvider);
// Store context in local variable to avoid async gap issue
final navigatorContext = context;
if (navigatorContext.mounted) {
Navigator.of(navigatorContext).pop();
}
}
},
),
],
);
},
);
}
class DebugSheet extends HookConsumerWidget {
const DebugSheet({super.key});
@@ -49,6 +95,17 @@ class DebugSheet extends HookConsumerWidget {
Clipboard.setData(ClipboardData(text: tk!.token));
},
),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.edit),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('Set access token'),
onTap: () async {
await _showSetTokenDialog(context, ref);
},
),
const Divider(height: 1),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.delete),

View File

@@ -94,7 +94,7 @@ class PostItemCreator extends HookConsumerWidget {
borderRadius: BorderRadius.circular(12),
onTap: () {
if (isOpenable) {
context.pushNamed('postDetail', pathParameters: {'id': item.id});
context.goNamed('postDetail', pathParameters: {'id': item.id});
}
},
child: Padding(

View File

@@ -21,6 +21,7 @@ class PostListNotifier extends _$PostListNotifier
int? type,
List<String>? categories,
List<String>? tags,
bool shuffle = false,
}) {
return fetch(cursor: null);
}
@@ -38,6 +39,7 @@ class PostListNotifier extends _$PostListNotifier
if (type != null) 'type': type,
if (tags != null) 'tags': tags,
if (categories != null) 'categories': categories,
if (shuffle) 'shuffle': true,
};
final response = await client.get(
@@ -74,6 +76,7 @@ class SliverPostList extends HookConsumerWidget {
final int? type;
final List<String>? categories;
final List<String>? tags;
final bool shuffle;
final PostItemType itemType;
final Color? backgroundColor;
final EdgeInsets? padding;
@@ -88,6 +91,7 @@ class SliverPostList extends HookConsumerWidget {
this.type,
this.categories,
this.tags,
this.shuffle = false,
this.itemType = PostItemType.regular,
this.backgroundColor,
this.padding,
@@ -105,6 +109,7 @@ class SliverPostList extends HookConsumerWidget {
type: type,
categories: categories,
tags: tags,
shuffle: shuffle,
),
futureRefreshable:
postListNotifierProvider(
@@ -113,6 +118,7 @@ class SliverPostList extends HookConsumerWidget {
type: type,
categories: categories,
tags: tags,
shuffle: shuffle,
).future,
notifierRefreshable:
postListNotifierProvider(
@@ -121,6 +127,7 @@ class SliverPostList extends HookConsumerWidget {
type: type,
categories: categories,
tags: tags,
shuffle: shuffle,
).notifier,
contentBuilder:
(data, widgetCount, endItemView) => SliverList.builder(

View File

@@ -6,7 +6,7 @@ part of 'post_list.dart';
// RiverpodGenerator
// **************************************************************************
String _$postListNotifierHash() => r'9784b282b3ee14b7109e263c5841a082cf0be78e';
String _$postListNotifierHash() => r'faa0b939fae56367ff120ce63d9deb17b1995c9c';
/// Copied from Dart SDK
class _SystemHash {
@@ -36,6 +36,7 @@ abstract class _$PostListNotifier
late final int? type;
late final List<String>? categories;
late final List<String>? tags;
late final bool shuffle;
FutureOr<CursorPagingData<SnPost>> build({
String? pubName,
@@ -43,6 +44,7 @@ abstract class _$PostListNotifier
int? type,
List<String>? categories,
List<String>? tags,
bool shuffle = false,
});
}
@@ -63,6 +65,7 @@ class PostListNotifierFamily
int? type,
List<String>? categories,
List<String>? tags,
bool shuffle = false,
}) {
return PostListNotifierProvider(
pubName: pubName,
@@ -70,6 +73,7 @@ class PostListNotifierFamily
type: type,
categories: categories,
tags: tags,
shuffle: shuffle,
);
}
@@ -83,6 +87,7 @@ class PostListNotifierFamily
type: provider.type,
categories: provider.categories,
tags: provider.tags,
shuffle: provider.shuffle,
);
}
@@ -115,6 +120,7 @@ class PostListNotifierProvider
int? type,
List<String>? categories,
List<String>? tags,
bool shuffle = false,
}) : this._internal(
() =>
PostListNotifier()
@@ -122,7 +128,8 @@ class PostListNotifierProvider
..realm = realm
..type = type
..categories = categories
..tags = tags,
..tags = tags
..shuffle = shuffle,
from: postListNotifierProvider,
name: r'postListNotifierProvider',
debugGetCreateSourceHash:
@@ -137,6 +144,7 @@ class PostListNotifierProvider
type: type,
categories: categories,
tags: tags,
shuffle: shuffle,
);
PostListNotifierProvider._internal(
@@ -151,6 +159,7 @@ class PostListNotifierProvider
required this.type,
required this.categories,
required this.tags,
required this.shuffle,
}) : super.internal();
final String? pubName;
@@ -158,6 +167,7 @@ class PostListNotifierProvider
final int? type;
final List<String>? categories;
final List<String>? tags;
final bool shuffle;
@override
FutureOr<CursorPagingData<SnPost>> runNotifierBuild(
@@ -169,6 +179,7 @@ class PostListNotifierProvider
type: type,
categories: categories,
tags: tags,
shuffle: shuffle,
);
}
@@ -183,7 +194,8 @@ class PostListNotifierProvider
..realm = realm
..type = type
..categories = categories
..tags = tags,
..tags = tags
..shuffle = shuffle,
from: from,
name: null,
dependencies: null,
@@ -194,6 +206,7 @@ class PostListNotifierProvider
type: type,
categories: categories,
tags: tags,
shuffle: shuffle,
),
);
}
@@ -214,7 +227,8 @@ class PostListNotifierProvider
other.realm == realm &&
other.type == type &&
other.categories == categories &&
other.tags == tags;
other.tags == tags &&
other.shuffle == shuffle;
}
@override
@@ -225,6 +239,7 @@ class PostListNotifierProvider
hash = _SystemHash.combine(hash, type.hashCode);
hash = _SystemHash.combine(hash, categories.hashCode);
hash = _SystemHash.combine(hash, tags.hashCode);
hash = _SystemHash.combine(hash, shuffle.hashCode);
return _SystemHash.finish(hash);
}
@@ -248,6 +263,9 @@ mixin PostListNotifierRef
/// The parameter `tags` of this provider.
List<String>? get tags;
/// The parameter `shuffle` of this provider.
bool get shuffle;
}
class _PostListNotifierProviderElement
@@ -270,6 +288,8 @@ class _PostListNotifierProviderElement
(origin as PostListNotifierProvider).categories;
@override
List<String>? get tags => (origin as PostListNotifierProvider).tags;
@override
bool get shuffle => (origin as PostListNotifierProvider).shuffle;
}
// ignore_for_file: type=lint

View File

@@ -0,0 +1,108 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_card_swiper/flutter_card_swiper.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/post/post_list.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
class PostShuffleScreen extends HookConsumerWidget {
const PostShuffleScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final postListState = ref.watch(postListNotifierProvider(shuffle: true));
final postListNotifier = ref.watch(
postListNotifierProvider(shuffle: true).notifier,
);
final cardSwiperController = useMemoized(() => CardSwiperController(), []);
useEffect(() {
return cardSwiperController.dispose;
}, []);
const kBottomControlHeight = 96.0;
return AppScaffold(
appBar: AppBar(title: const Text('postShuffle').tr()),
body: Stack(
children: [
Padding(
padding: EdgeInsets.only(
bottom:
kBottomControlHeight + MediaQuery.of(context).padding.bottom,
),
child:
(postListState.value?.items.length ?? 0) > 0
? CardSwiper(
controller: cardSwiperController,
cardsCount: postListState.value!.items.length,
cardBuilder: (
context,
index,
horizontalOffsetPercentage,
verticalOffsetPercentage,
) {
return Center(
child: Card(
margin: EdgeInsets.zero,
child: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
child: PostActionableItem(
item: postListState.value!.items[index],
),
),
),
);
},
onEnd: () {
if (postListState.value?.hasMore ?? true) {
postListNotifier.fetch(
cursor: postListState.value?.nextCursor,
);
}
},
)
: Center(child: CircularProgressIndicator()),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom,
),
height: kBottomControlHeight,
child:
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: () {
cardSwiperController.undo();
},
icon: const Icon(Symbols.arrow_left_alt),
),
IconButton(
onPressed: () {
cardSwiperController.swipe(CardSwiperDirection.right);
},
icon: const Icon(Symbols.arrow_right_alt),
),
],
).padding(all: 8).center(),
),
),
],
),
);
}
}

View File

@@ -565,10 +565,10 @@ packages:
dependency: "direct main"
description:
name: file_picker
sha256: ef7d2a085c1b1d69d17b6842d0734aad90156de08df6bd3c12496d0bd6ddf8e2
sha256: e7e16c9d15c36330b94ca0e2ad8cb61f93cd5282d0158c09805aed13b5452f22
url: "https://pub.dev"
source: hosted
version: "10.3.1"
version: "10.3.2"
file_selector_linux:
dependency: transitive
description:
@@ -734,6 +734,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.4.1"
flutter_card_swiper:
dependency: "direct main"
description:
name: flutter_card_swiper
sha256: "1eacbfab31b572223042e03409726553aec431abe48af48c8d591d376d070d3d"
url: "https://pub.dev"
source: hosted
version: "7.0.2"
flutter_colorpicker:
dependency: "direct main"
description:
@@ -754,10 +762,10 @@ packages:
dependency: "direct main"
description:
name: flutter_hooks
sha256: c3df76c62bb3a9f9bee75c57cdab40abab6123b734c1cd7e9b26a5dbd436eceb
sha256: "8ae1f090e5f4ef5cfa6670ce1ab5dddadd33f3533a7f9ba19d9f958aa2a89f42"
url: "https://pub.dev"
source: hosted
version: "0.21.3"
version: "0.21.3+1"
flutter_inappwebview:
dependency: "direct main"
description:
@@ -1137,10 +1145,10 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: "8b1f37dfaf6e958c6b872322db06f946509433bec3de753c3491a42ae9ec2b48"
sha256: ced3fdc143c1437234ac3b8e985f3286cf138968bb83ca9a6f94d22f2951c6b9
url: "https://pub.dev"
source: hosted
version: "16.1.0"
version: "16.2.0"
google_fonts:
dependency: "direct main"
description:
@@ -1497,10 +1505,10 @@ packages:
dependency: "direct main"
description:
name: material_symbols_icons
sha256: ef20d86fb34c2b59eb7553c4d795bb8a7ec8c890c53ffd3148c64f7adc46ae50
sha256: b1342194e859b2774f920b484c46f54a37a845488e23d570385fbe3ede92ee9f
url: "https://pub.dev"
source: hosted
version: "4.2858.1"
version: "4.2867.0"
media_kit:
dependency: "direct main"
description:
@@ -1841,10 +1849,10 @@ packages:
dependency: transitive
description:
name: provider
sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84"
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.dev"
source: hosted
version: "6.1.5"
version: "6.1.5+1"
pub_semver:
dependency: transitive
description:
@@ -1897,42 +1905,42 @@ packages:
dependency: "direct main"
description:
name: record
sha256: "3d08502b77edf2a864aa6e4cd7874b983d42a80f3689431da053cc5e85c1ad21"
sha256: "9dbc6ff3e784612f90a9b001373c45ff76b7a08abd2bd9fdf72c242320c8911c"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
version: "6.1.1"
record_android:
dependency: transitive
description:
name: record_android
sha256: "8b170e33d9866f9b51e01a767d7e1ecb97b9ecd629950bd87a47c79359ec57f8"
sha256: "8361a791c9a3fa5c065f0b8b5adb10f12531f8538c86b19474cf7b56ea80d426"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.4.1"
record_ios:
dependency: transitive
description:
name: record_ios
sha256: ad97d0a75933c44bcf5aff648e86e32fc05eb61f8fbef190f14968c8eaf86692
sha256: "895c9467faec72d8e718a3142b51114958f42f18053836a8b484a74f9372f51a"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "1.1.1"
record_linux:
dependency: transitive
description:
name: record_linux
sha256: "785e8e8d6db109aa606d0669d95aaae416458aaa39782bb0abe0bee74eee17d7"
sha256: "235b1f1fb84e810f8149cc0c2c731d7d697f8d1c333b32cb820c449bf7bb72d8"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.2.1"
record_macos:
dependency: transitive
description:
name: record_macos
sha256: f1399bca76a1634da109e5b0cba764ed8332a2b4da49c704c66d2c553405ed81
sha256: "2849068bb59072f300ad63ed146e543d66afaef8263edba4de4834fc7c8d4d35"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "1.1.1"
record_platform_interface:
dependency: transitive
description:
@@ -1953,10 +1961,10 @@ packages:
dependency: transitive
description:
name: record_windows
sha256: "85a22fc97f6d73ecd67c8ba5f2f472b74ef1d906f795b7970f771a0914167e99"
sha256: "223258060a1d25c62bae18282c16783f28581ec19401d17e56b5205b9f039d78"
url: "https://pub.dev"
source: hosted
version: "1.0.6"
version: "1.0.7"
relative_time:
dependency: "direct main"
description:

View File

@@ -36,10 +36,10 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
flutter_hooks: ^0.21.3
flutter_hooks: ^0.21.3+1
hooks_riverpod: ^2.6.1
bitsdojo_window: ^0.1.6
go_router: ^16.1.0
go_router: ^16.2.0
styled_widget: ^0.4.1
shared_preferences: ^2.5.3
flutter_riverpod: ^2.6.1
@@ -73,7 +73,7 @@ dependencies:
git: https://github.com/LittleSheep2Code/tus_client.git
cross_file: ^0.3.4+2
image_picker: ^1.2.0
file_picker: ^10.3.1
file_picker: ^10.3.2
riverpod_annotation: ^2.6.1
image_picker_platform_interface: ^2.11.0
image_picker_android: ^0.8.13
@@ -83,7 +83,7 @@ dependencies:
flutter_udid: ^4.0.0
firebase_core: ^4.0.0
web_socket_channel: ^3.0.3
material_symbols_icons: ^4.2858.1
material_symbols_icons: ^4.2867.0
drift: ^2.28.1
drift_flutter: ^0.2.5
path: ^1.9.1
@@ -107,7 +107,7 @@ dependencies:
livekit_client: ^2.5.0+hotfix.1
pasteboard: ^0.4.0
flutter_colorpicker: ^1.1.0
record: ^6.1.0
record: ^6.1.1
qr_flutter: ^4.1.0
flutter_otp_text_field: ^1.5.1+1
palette_generator: ^0.3.3+7
@@ -138,6 +138,7 @@ dependencies:
firebase_analytics: ^12.0.0
material_color_utilities: ^0.11.1
screenshot: ^3.0.0
flutter_card_swiper: ^7.0.2
dev_dependencies:
flutter_test: