Compare commits
	
		
			20 Commits
		
	
	
		
			3.2.0+127
			...
			1232318a5d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 1232318a5d | |||
|  | 56f41b6c0e | ||
|  | 3ea717d25a | ||
| 1fe4889460 | |||
| cdf2722268 | |||
| a127b5bace | |||
| b2097cf044 | |||
| 701f29748d | |||
| 9e40ed4600 | |||
| c90e6fe661 | |||
| 569483300d | |||
| bab602d98b | |||
| b4f2bb803a | |||
| 03bfed6f46 | |||
| f98e5a0aec | |||
| 3d473e2fec | |||
| 0b6efa373a | |||
| 9b60e96cde | |||
| 81cd9b2082 | |||
| 923d5d7514 | 
| @@ -854,5 +854,28 @@ | ||||
|   "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", | ||||
|   "orderByPopularity": "Sort by popularity", | ||||
|   "orderByReleaseDate": "Sort by release date" | ||||
| } | ||||
|   | ||||
| @@ -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": "优秀" | ||||
| } | ||||
|   | ||||
| @@ -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); | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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(), | ||||
| }; | ||||
|   | ||||
| @@ -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) => | ||||
|   | ||||
| @@ -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 { | ||||
| $SnChatMessageCopyWith<$Res>? get lastMessage { | ||||
|     if (_self.lastMessage == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $SnChatMessageCopyWith<$Res>(_self.lastMessage, (value) { | ||||
|   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 { | ||||
| $SnChatMessageCopyWith<$Res>? get lastMessage { | ||||
|     if (_self.lastMessage == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $SnChatMessageCopyWith<$Res>(_self.lastMessage, (value) { | ||||
|   return $SnChatMessageCopyWith<$Res>(_self.lastMessage!, (value) { | ||||
|     return _then(_self.copyWith(lastMessage: value)); | ||||
|   }); | ||||
| } | ||||
|   | ||||
| @@ -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) => | ||||
|   | ||||
| @@ -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) => | ||||
|   | ||||
| @@ -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, | ||||
|   )); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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, | ||||
|     }; | ||||
|   | ||||
| @@ -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) => | ||||
|   | ||||
| @@ -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, | ||||
|   )); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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, | ||||
|     }; | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import 'dart:convert'; | ||||
| import 'dart:developer'; | ||||
| import 'dart:io' show Platform; | ||||
|  | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| @@ -28,7 +29,10 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> { | ||||
|       final response = await client.get('/id/accounts/me'); | ||||
|       final user = SnAccount.fromJson(response.data); | ||||
|       state = AsyncValue.data(user); | ||||
|       FirebaseAnalytics.instance.setUserId(id: user.id); | ||||
|  | ||||
|       if (kIsWeb || !Platform.isLinux) { | ||||
|         FirebaseAnalytics.instance.setUserId(id: user.id); | ||||
|       } | ||||
|     } catch (error, stackTrace) { | ||||
|       if (!kIsWeb) { | ||||
|         if (error is DioException) { | ||||
| @@ -83,7 +87,9 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> { | ||||
|     final prefs = _ref.read(sharedPreferencesProvider); | ||||
|     await prefs.remove(kTokenPairStoreKey); | ||||
|     _ref.invalidate(tokenProvider); | ||||
|     FirebaseAnalytics.instance.setUserId(id: null); | ||||
|     if (kIsWeb || !Platform.isLinux) { | ||||
|       FirebaseAnalytics.instance.setUserId(id: null); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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'; | ||||
| @@ -33,8 +35,10 @@ import 'package:island/screens/creators/hub.dart'; | ||||
| import 'package:island/screens/creators/posts/post_manage_list.dart'; | ||||
| import 'package:island/screens/creators/stickers/stickers.dart'; | ||||
| import 'package:island/screens/creators/stickers/pack_detail.dart'; | ||||
| import 'package:island/screens/stickers/marketplace.dart'; | ||||
| import 'package:island/screens/stickers/sticker_marketplace.dart'; | ||||
| import 'package:island/screens/stickers/pack_detail.dart'; | ||||
| import 'package:island/screens/discovery/feeds/feed_marketplace.dart'; | ||||
| import 'package:island/screens/discovery/feeds/feed_detail.dart'; | ||||
| import 'package:island/screens/creators/poll/poll_list.dart'; | ||||
| import 'package:island/screens/creators/publishers.dart'; | ||||
| import 'package:island/screens/creators/webfeed/webfeed_list.dart'; | ||||
| @@ -52,6 +56,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 +381,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 +398,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 +414,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', | ||||
| @@ -528,6 +548,22 @@ final routerProvider = Provider<GoRouter>((ref) { | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     name: 'webFeedMarketplace', | ||||
|                     path: '/feeds', | ||||
|                     builder: | ||||
|                         (context, state) => const MarketplaceWebFeedsScreen(), | ||||
|                     routes: [ | ||||
|                       GoRoute( | ||||
|                         name: 'webFeedDetail', | ||||
|                         path: ':feedId', | ||||
|                         builder: (context, state) { | ||||
|                           final feedId = state.pathParameters['feedId']!; | ||||
|                           return MarketplaceWebFeedDetailScreen(id: feedId); | ||||
|                         }, | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     name: 'notifications', | ||||
|                     path: '/account/notifications', | ||||
| @@ -538,6 +574,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', | ||||
|   | ||||
| @@ -236,6 +236,26 @@ class AccountScreen extends HookConsumerWidget { | ||||
|                 context.pushNamed('stickerMarketplace'); | ||||
|               }, | ||||
|             ), | ||||
|             ListTile( | ||||
|               minTileHeight: 48, | ||||
|               leading: const Icon(Symbols.rss_feed), | ||||
|               trailing: const Icon(Symbols.chevron_right), | ||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|               title: Text('webFeeds').tr(), | ||||
|               onTap: () { | ||||
|                 context.pushNamed('webFeedMarketplace'); | ||||
|               }, | ||||
|             ), | ||||
|             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 +409,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'); | ||||
|   | ||||
							
								
								
									
										152
									
								
								lib/screens/account/credits.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								lib/screens/account/credits.dart
									
									
									
									
									
										Normal 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, | ||||
|                           ), | ||||
|                         ), | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										49
									
								
								lib/screens/account/credits.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								lib/screens/account/credits.g.dart
									
									
									
									
									
										Normal 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 | ||||
| @@ -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), | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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, | ||||
|                       ), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|             ], | ||||
|           ); | ||||
|         }, | ||||
|   | ||||
| @@ -7,7 +7,7 @@ part of 'room_detail.dart'; | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$chatMemberListNotifierHash() => | ||||
|     r'c8fbf4b95df6dae24b1ba21063e9a43351832974'; | ||||
|     r'3ea30150278523e9f6b23f9200ea9a9fbae9c973'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
|   | ||||
| @@ -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'); | ||||
|   | ||||
| @@ -148,7 +148,7 @@ class _StickerPackProviderElement | ||||
| } | ||||
|  | ||||
| String _$stickerPacksNotifierHash() => | ||||
|     r'0a8edcf9c35396c411f1214f5e77b1e8fac6a3e6'; | ||||
|     r'30024b35235f3085a5b1ec2204d0a974ee907e22'; | ||||
|  | ||||
| abstract class _$StickerPacksNotifier | ||||
|     extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnStickerPack>> { | ||||
|   | ||||
							
								
								
									
										189
									
								
								lib/screens/discovery/feeds/feed_detail.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								lib/screens/discovery/feeds/feed_detail.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,189 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/webfeed.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| part 'feed_detail.g.dart'; | ||||
|  | ||||
| /// Provider for web feed articles content | ||||
| @riverpod | ||||
| Future<List<SnWebArticle>> marketplaceWebFeedContent( | ||||
|   Ref ref, { | ||||
|   required String feedId, | ||||
| }) async { | ||||
|   final apiClient = ref.watch(apiClientProvider); | ||||
|   final resp = await apiClient.get('/sphere/feeds/$feedId/articles'); | ||||
|   return (resp.data as List).map((e) => SnWebArticle.fromJson(e)).toList(); | ||||
| } | ||||
|  | ||||
| /// Provider for web feed subscription status | ||||
| @riverpod | ||||
| Future<bool> marketplaceWebFeedSubscription( | ||||
|   Ref ref, { | ||||
|   required String feedId, | ||||
| }) async { | ||||
|   final api = ref.watch(apiClientProvider); | ||||
|   try { | ||||
|     await api.get('/sphere/feeds/$feedId/subscription'); | ||||
|     // If not 404, consider subscribed | ||||
|     return true; | ||||
|   } on Object catch (e) { | ||||
|     // Dio error handling agnostic: treat 404 as not-subscribed, rethrow others | ||||
|     final msg = e.toString(); | ||||
|     if (msg.contains('404')) return false; | ||||
|     rethrow; | ||||
|   } | ||||
| } | ||||
|  | ||||
| class MarketplaceWebFeedDetailScreen extends HookConsumerWidget { | ||||
|   final String id; | ||||
|   const MarketplaceWebFeedDetailScreen({super.key, required this.id}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     // TODO: Need to create a web feed provider similar to stickerPackProvider | ||||
|     // For now, we'll fetch the feed directly | ||||
|     final feedContent = ref.watch( | ||||
|       marketplaceWebFeedContentProvider(feedId: id), | ||||
|     ); | ||||
|     final subscribed = ref.watch( | ||||
|       marketplaceWebFeedSubscriptionProvider(feedId: id), | ||||
|     ); | ||||
|  | ||||
|     // Subscribe to web feed | ||||
|     Future<void> subscribeToFeed() async { | ||||
|       final apiClient = ref.watch(apiClientProvider); | ||||
|       await apiClient.post('/sphere/feeds/$id/subscribe'); | ||||
|       HapticFeedback.selectionClick(); | ||||
|       ref.invalidate(marketplaceWebFeedSubscriptionProvider(feedId: id)); | ||||
|       if (!context.mounted) return; | ||||
|       showSnackBar('feedSubscribed'.tr()); | ||||
|     } | ||||
|  | ||||
|     // Unsubscribe from web feed | ||||
|     Future<void> unsubscribeFromFeed() async { | ||||
|       final apiClient = ref.watch(apiClientProvider); | ||||
|       await apiClient.delete('/sphere/feeds/$id/subscribe'); | ||||
|       HapticFeedback.selectionClick(); | ||||
|       ref.invalidate(marketplaceWebFeedSubscriptionProvider(feedId: id)); | ||||
|       if (!context.mounted) return; | ||||
|       showSnackBar('feedUnsubscribed'.tr()); | ||||
|     } | ||||
|  | ||||
|     // TODO: Replace with actual feed data provider once created | ||||
|     final dummyFeed = SnWebFeed( | ||||
|       id: id, | ||||
|       url: 'https://example.com', | ||||
|       title: 'Loading...', | ||||
|       publisherId: 'publisher-id', | ||||
|       createdAt: DateTime.now(), | ||||
|       updatedAt: DateTime.now(), | ||||
|     ); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar(title: Text(dummyFeed.title)), | ||||
|       body: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|         children: [ | ||||
|           // Feed meta | ||||
|           Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|             children: [ | ||||
|               Text(dummyFeed.description ?? ''), | ||||
|               Row( | ||||
|                 spacing: 4, | ||||
|                 children: [ | ||||
|                   const Icon(Symbols.rss_feed, size: 16), | ||||
|                   Text('${feedContent.value?.length ?? 0} articles'), | ||||
|                 ], | ||||
|               ).opacity(0.85), | ||||
|               Row( | ||||
|                 spacing: 4, | ||||
|                 children: [ | ||||
|                   const Icon(Symbols.link, size: 16), | ||||
|                   SelectableText(dummyFeed.url), | ||||
|                 ], | ||||
|               ).opacity(0.85), | ||||
|             ], | ||||
|           ).padding(horizontal: 24, vertical: 24), | ||||
|           const Divider(height: 1), | ||||
|           // Articles list | ||||
|           Expanded( | ||||
|             child: feedContent.when( | ||||
|               data: | ||||
|                   (articles) => RefreshIndicator( | ||||
|                     onRefresh: | ||||
|                         () => ref.refresh( | ||||
|                           marketplaceWebFeedContentProvider(feedId: id).future, | ||||
|                         ), | ||||
|                     child: ListView.builder( | ||||
|                       padding: const EdgeInsets.symmetric( | ||||
|                         horizontal: 24, | ||||
|                         vertical: 20, | ||||
|                       ), | ||||
|                       itemCount: articles.length, | ||||
|                       itemBuilder: (context, index) { | ||||
|                         final article = articles[index]; | ||||
|                         return Card( | ||||
|                           child: ListTile( | ||||
|                             title: Text(article.title), | ||||
|                             subtitle: Text(article.author ?? ''), | ||||
|                             trailing: const Icon(Symbols.open_in_new), | ||||
|                             onTap: () { | ||||
|                               // TODO: Navigate to article detail or open URL | ||||
|                             }, | ||||
|                           ), | ||||
|                         ); | ||||
|                       }, | ||||
|                     ), | ||||
|                   ), | ||||
|               error: | ||||
|                   (err, _) => | ||||
|                       Text( | ||||
|                         'Error: $err', | ||||
|                       ).textAlignment(TextAlign.center).center(), | ||||
|               loading: () => const CircularProgressIndicator().center(), | ||||
|             ), | ||||
|           ), | ||||
|           Padding( | ||||
|             padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8), | ||||
|             child: subscribed.when( | ||||
|               data: | ||||
|                   (isSubscribed) => FilledButton.icon( | ||||
|                     onPressed: | ||||
|                         isSubscribed ? unsubscribeFromFeed : subscribeToFeed, | ||||
|                     icon: Icon( | ||||
|                       isSubscribed ? Symbols.remove_circle : Symbols.add_circle, | ||||
|                     ), | ||||
|                     label: Text( | ||||
|                       isSubscribed ? 'unsubscribe'.tr() : 'subscribe'.tr(), | ||||
|                     ), | ||||
|                   ), | ||||
|               loading: | ||||
|                   () => const SizedBox( | ||||
|                     height: 32, | ||||
|                     width: 32, | ||||
|                     child: CircularProgressIndicator(strokeWidth: 2), | ||||
|                   ), | ||||
|               error: | ||||
|                   (_, _) => OutlinedButton.icon( | ||||
|                     onPressed: subscribeToFeed, | ||||
|                     icon: const Icon(Symbols.add_circle), | ||||
|                     label: Text('subscribe').tr(), | ||||
|                   ), | ||||
|             ), | ||||
|           ), | ||||
|           Gap(MediaQuery.of(context).padding.bottom), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										313
									
								
								lib/screens/discovery/feeds/feed_detail.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										313
									
								
								lib/screens/discovery/feeds/feed_detail.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,313 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'feed_detail.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$marketplaceWebFeedContentHash() => | ||||
|     r'4e65350bff4055302e15ec14266cdebb1cd89bbe'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
|   _SystemHash._(); | ||||
|  | ||||
|   static int combine(int hash, int value) { | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + value); | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); | ||||
|     return hash ^ (hash >> 6); | ||||
|   } | ||||
|  | ||||
|   static int finish(int hash) { | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); | ||||
|     // ignore: parameter_assignments | ||||
|     hash = hash ^ (hash >> 11); | ||||
|     return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// Provider for web feed articles content | ||||
| /// | ||||
| /// Copied from [marketplaceWebFeedContent]. | ||||
| @ProviderFor(marketplaceWebFeedContent) | ||||
| const marketplaceWebFeedContentProvider = MarketplaceWebFeedContentFamily(); | ||||
|  | ||||
| /// Provider for web feed articles content | ||||
| /// | ||||
| /// Copied from [marketplaceWebFeedContent]. | ||||
| class MarketplaceWebFeedContentFamily | ||||
|     extends Family<AsyncValue<List<SnWebArticle>>> { | ||||
|   /// Provider for web feed articles content | ||||
|   /// | ||||
|   /// Copied from [marketplaceWebFeedContent]. | ||||
|   const MarketplaceWebFeedContentFamily(); | ||||
|  | ||||
|   /// Provider for web feed articles content | ||||
|   /// | ||||
|   /// Copied from [marketplaceWebFeedContent]. | ||||
|   MarketplaceWebFeedContentProvider call({required String feedId}) { | ||||
|     return MarketplaceWebFeedContentProvider(feedId: feedId); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   MarketplaceWebFeedContentProvider getProviderOverride( | ||||
|     covariant MarketplaceWebFeedContentProvider provider, | ||||
|   ) { | ||||
|     return call(feedId: provider.feedId); | ||||
|   } | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _dependencies = null; | ||||
|  | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get dependencies => _dependencies; | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null; | ||||
|  | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get allTransitiveDependencies => | ||||
|       _allTransitiveDependencies; | ||||
|  | ||||
|   @override | ||||
|   String? get name => r'marketplaceWebFeedContentProvider'; | ||||
| } | ||||
|  | ||||
| /// Provider for web feed articles content | ||||
| /// | ||||
| /// Copied from [marketplaceWebFeedContent]. | ||||
| class MarketplaceWebFeedContentProvider | ||||
|     extends AutoDisposeFutureProvider<List<SnWebArticle>> { | ||||
|   /// Provider for web feed articles content | ||||
|   /// | ||||
|   /// Copied from [marketplaceWebFeedContent]. | ||||
|   MarketplaceWebFeedContentProvider({required String feedId}) | ||||
|     : this._internal( | ||||
|         (ref) => marketplaceWebFeedContent( | ||||
|           ref as MarketplaceWebFeedContentRef, | ||||
|           feedId: feedId, | ||||
|         ), | ||||
|         from: marketplaceWebFeedContentProvider, | ||||
|         name: r'marketplaceWebFeedContentProvider', | ||||
|         debugGetCreateSourceHash: | ||||
|             const bool.fromEnvironment('dart.vm.product') | ||||
|                 ? null | ||||
|                 : _$marketplaceWebFeedContentHash, | ||||
|         dependencies: MarketplaceWebFeedContentFamily._dependencies, | ||||
|         allTransitiveDependencies: | ||||
|             MarketplaceWebFeedContentFamily._allTransitiveDependencies, | ||||
|         feedId: feedId, | ||||
|       ); | ||||
|  | ||||
|   MarketplaceWebFeedContentProvider._internal( | ||||
|     super._createNotifier, { | ||||
|     required super.name, | ||||
|     required super.dependencies, | ||||
|     required super.allTransitiveDependencies, | ||||
|     required super.debugGetCreateSourceHash, | ||||
|     required super.from, | ||||
|     required this.feedId, | ||||
|   }) : super.internal(); | ||||
|  | ||||
|   final String feedId; | ||||
|  | ||||
|   @override | ||||
|   Override overrideWith( | ||||
|     FutureOr<List<SnWebArticle>> Function(MarketplaceWebFeedContentRef provider) | ||||
|     create, | ||||
|   ) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: MarketplaceWebFeedContentProvider._internal( | ||||
|         (ref) => create(ref as MarketplaceWebFeedContentRef), | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         feedId: feedId, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeFutureProviderElement<List<SnWebArticle>> createElement() { | ||||
|     return _MarketplaceWebFeedContentProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is MarketplaceWebFeedContentProvider && other.feedId == feedId; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||
|     hash = _SystemHash.combine(hash, feedId.hashCode); | ||||
|  | ||||
|     return _SystemHash.finish(hash); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| mixin MarketplaceWebFeedContentRef | ||||
|     on AutoDisposeFutureProviderRef<List<SnWebArticle>> { | ||||
|   /// The parameter `feedId` of this provider. | ||||
|   String get feedId; | ||||
| } | ||||
|  | ||||
| class _MarketplaceWebFeedContentProviderElement | ||||
|     extends AutoDisposeFutureProviderElement<List<SnWebArticle>> | ||||
|     with MarketplaceWebFeedContentRef { | ||||
|   _MarketplaceWebFeedContentProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   String get feedId => (origin as MarketplaceWebFeedContentProvider).feedId; | ||||
| } | ||||
|  | ||||
| String _$marketplaceWebFeedSubscriptionHash() => | ||||
|     r'2ff06a48ed7d4236b57412ecca55e94c0a0b6330'; | ||||
|  | ||||
| /// Provider for web feed subscription status | ||||
| /// | ||||
| /// Copied from [marketplaceWebFeedSubscription]. | ||||
| @ProviderFor(marketplaceWebFeedSubscription) | ||||
| const marketplaceWebFeedSubscriptionProvider = | ||||
|     MarketplaceWebFeedSubscriptionFamily(); | ||||
|  | ||||
| /// Provider for web feed subscription status | ||||
| /// | ||||
| /// Copied from [marketplaceWebFeedSubscription]. | ||||
| class MarketplaceWebFeedSubscriptionFamily extends Family<AsyncValue<bool>> { | ||||
|   /// Provider for web feed subscription status | ||||
|   /// | ||||
|   /// Copied from [marketplaceWebFeedSubscription]. | ||||
|   const MarketplaceWebFeedSubscriptionFamily(); | ||||
|  | ||||
|   /// Provider for web feed subscription status | ||||
|   /// | ||||
|   /// Copied from [marketplaceWebFeedSubscription]. | ||||
|   MarketplaceWebFeedSubscriptionProvider call({required String feedId}) { | ||||
|     return MarketplaceWebFeedSubscriptionProvider(feedId: feedId); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   MarketplaceWebFeedSubscriptionProvider getProviderOverride( | ||||
|     covariant MarketplaceWebFeedSubscriptionProvider provider, | ||||
|   ) { | ||||
|     return call(feedId: provider.feedId); | ||||
|   } | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _dependencies = null; | ||||
|  | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get dependencies => _dependencies; | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null; | ||||
|  | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get allTransitiveDependencies => | ||||
|       _allTransitiveDependencies; | ||||
|  | ||||
|   @override | ||||
|   String? get name => r'marketplaceWebFeedSubscriptionProvider'; | ||||
| } | ||||
|  | ||||
| /// Provider for web feed subscription status | ||||
| /// | ||||
| /// Copied from [marketplaceWebFeedSubscription]. | ||||
| class MarketplaceWebFeedSubscriptionProvider | ||||
|     extends AutoDisposeFutureProvider<bool> { | ||||
|   /// Provider for web feed subscription status | ||||
|   /// | ||||
|   /// Copied from [marketplaceWebFeedSubscription]. | ||||
|   MarketplaceWebFeedSubscriptionProvider({required String feedId}) | ||||
|     : this._internal( | ||||
|         (ref) => marketplaceWebFeedSubscription( | ||||
|           ref as MarketplaceWebFeedSubscriptionRef, | ||||
|           feedId: feedId, | ||||
|         ), | ||||
|         from: marketplaceWebFeedSubscriptionProvider, | ||||
|         name: r'marketplaceWebFeedSubscriptionProvider', | ||||
|         debugGetCreateSourceHash: | ||||
|             const bool.fromEnvironment('dart.vm.product') | ||||
|                 ? null | ||||
|                 : _$marketplaceWebFeedSubscriptionHash, | ||||
|         dependencies: MarketplaceWebFeedSubscriptionFamily._dependencies, | ||||
|         allTransitiveDependencies: | ||||
|             MarketplaceWebFeedSubscriptionFamily._allTransitiveDependencies, | ||||
|         feedId: feedId, | ||||
|       ); | ||||
|  | ||||
|   MarketplaceWebFeedSubscriptionProvider._internal( | ||||
|     super._createNotifier, { | ||||
|     required super.name, | ||||
|     required super.dependencies, | ||||
|     required super.allTransitiveDependencies, | ||||
|     required super.debugGetCreateSourceHash, | ||||
|     required super.from, | ||||
|     required this.feedId, | ||||
|   }) : super.internal(); | ||||
|  | ||||
|   final String feedId; | ||||
|  | ||||
|   @override | ||||
|   Override overrideWith( | ||||
|     FutureOr<bool> Function(MarketplaceWebFeedSubscriptionRef provider) create, | ||||
|   ) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: MarketplaceWebFeedSubscriptionProvider._internal( | ||||
|         (ref) => create(ref as MarketplaceWebFeedSubscriptionRef), | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         feedId: feedId, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeFutureProviderElement<bool> createElement() { | ||||
|     return _MarketplaceWebFeedSubscriptionProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is MarketplaceWebFeedSubscriptionProvider && | ||||
|         other.feedId == feedId; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||
|     hash = _SystemHash.combine(hash, feedId.hashCode); | ||||
|  | ||||
|     return _SystemHash.finish(hash); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| mixin MarketplaceWebFeedSubscriptionRef on AutoDisposeFutureProviderRef<bool> { | ||||
|   /// The parameter `feedId` of this provider. | ||||
|   String get feedId; | ||||
| } | ||||
|  | ||||
| class _MarketplaceWebFeedSubscriptionProviderElement | ||||
|     extends AutoDisposeFutureProviderElement<bool> | ||||
|     with MarketplaceWebFeedSubscriptionRef { | ||||
|   _MarketplaceWebFeedSubscriptionProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   String get feedId => | ||||
|       (origin as MarketplaceWebFeedSubscriptionProvider).feedId; | ||||
| } | ||||
|  | ||||
| // 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 | ||||
							
								
								
									
										169
									
								
								lib/screens/discovery/feeds/feed_marketplace.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								lib/screens/discovery/feeds/feed_marketplace.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,169 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/webfeed.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'dart:async'; | ||||
|  | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | ||||
|  | ||||
| part 'feed_marketplace.g.dart'; | ||||
|  | ||||
| @riverpod | ||||
| class MarketplaceWebFeedsNotifier extends _$MarketplaceWebFeedsNotifier | ||||
|     with CursorPagingNotifierMixin<SnWebFeed> { | ||||
|   String? _query; | ||||
|  | ||||
|   @override | ||||
|   Future<CursorPagingData<SnWebFeed>> build({required String? query}) { | ||||
|     _query = query; | ||||
|     return fetch(cursor: null); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<CursorPagingData<SnWebFeed>> fetch({required String? cursor}) async { | ||||
|     final client = ref.read(apiClientProvider); | ||||
|     final offset = cursor == null ? 0 : int.parse(cursor); | ||||
|  | ||||
|     final response = await client.get( | ||||
|       '/sphere/feeds', | ||||
|       queryParameters: { | ||||
|         'offset': offset, | ||||
|         'take': 20, | ||||
|         if (_query != null && _query!.isNotEmpty) 'query': _query, | ||||
|       }, | ||||
|     ); | ||||
|  | ||||
|     final total = int.parse(response.headers.value('X-Total') ?? '0'); | ||||
|     final List<dynamic> data = response.data; | ||||
|     final feeds = data.map((e) => SnWebFeed.fromJson(e)).toList(); | ||||
|  | ||||
|     final hasMore = offset + feeds.length < total; | ||||
|     final nextCursor = hasMore ? (offset + feeds.length).toString() : null; | ||||
|  | ||||
|     return CursorPagingData( | ||||
|       items: feeds, | ||||
|       hasMore: hasMore, | ||||
|       nextCursor: nextCursor, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// Marketplace screen for browsing web feeds. | ||||
| class MarketplaceWebFeedsScreen extends HookConsumerWidget { | ||||
|   const MarketplaceWebFeedsScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final query = useState<String?>(null); | ||||
|     final searchController = useTextEditingController(); | ||||
|     final focusNode = useFocusNode(); | ||||
|     final debounceTimer = useState<Timer?>(null); | ||||
|  | ||||
|     // Clear search when query is cleared | ||||
|     useEffect(() { | ||||
|       if (query.value == null || query.value!.isEmpty) { | ||||
|         searchController.clear(); | ||||
|       } | ||||
|       return null; | ||||
|     }, [query.value]); | ||||
|  | ||||
|     // Clean up timer on dispose | ||||
|     useEffect(() { | ||||
|       return () { | ||||
|         debounceTimer.value?.cancel(); | ||||
|       }; | ||||
|     }, []); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         title: const Text('webFeeds').tr(), | ||||
|         actions: const [Gap(8)], | ||||
|       ), | ||||
|       body: PagingHelperView( | ||||
|         provider: marketplaceWebFeedsNotifierProvider(query: query.value), | ||||
|         futureRefreshable: | ||||
|             marketplaceWebFeedsNotifierProvider(query: query.value).future, | ||||
|         notifierRefreshable: | ||||
|             marketplaceWebFeedsNotifierProvider(query: query.value).notifier, | ||||
|         contentBuilder: | ||||
|             (data, widgetCount, endItemView) => Column( | ||||
|               children: [ | ||||
|                 // Search bar above the list | ||||
|                 Padding( | ||||
|                   padding: const EdgeInsets.all(16), | ||||
|                   child: SearchBar( | ||||
|                     elevation: WidgetStateProperty.all(4), | ||||
|                     controller: searchController, | ||||
|                     focusNode: focusNode, | ||||
|                     hintText: 'search'.tr(), | ||||
|                     leading: const Icon(Symbols.search), | ||||
|                     padding: WidgetStateProperty.all( | ||||
|                       const EdgeInsets.symmetric(horizontal: 24), | ||||
|                     ), | ||||
|                     onTapOutside: | ||||
|                         (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                     trailing: [ | ||||
|                       if (query.value != null && query.value!.isNotEmpty) | ||||
|                         IconButton( | ||||
|                           icon: const Icon(Symbols.close), | ||||
|                           onPressed: () { | ||||
|                             query.value = null; | ||||
|                             searchController.clear(); | ||||
|                             focusNode.unfocus(); | ||||
|                           }, | ||||
|                         ), | ||||
|                     ], | ||||
|                     onChanged: (value) { | ||||
|                       // Debounce search to avoid excessive API calls | ||||
|                       debounceTimer.value?.cancel(); | ||||
|                       debounceTimer.value = Timer( | ||||
|                         const Duration(milliseconds: 500), | ||||
|                         () { | ||||
|                           query.value = value.isEmpty ? null : value; | ||||
|                         }, | ||||
|                       ); | ||||
|                     }, | ||||
|                     onSubmitted: (value) { | ||||
|                       query.value = value.isEmpty ? null : value; | ||||
|                       focusNode.unfocus(); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ), | ||||
|                 Expanded( | ||||
|                   child: ListView.builder( | ||||
|                     padding: EdgeInsets.zero, | ||||
|                     itemCount: widgetCount, | ||||
|                     itemBuilder: (context, index) { | ||||
|                       if (index == widgetCount - 1) { | ||||
|                         return endItemView; | ||||
|                       } | ||||
|  | ||||
|                       final feed = data.items[index]; | ||||
|                       return ListTile( | ||||
|                         title: Text(feed.title), | ||||
|                         subtitle: Text(feed.description ?? ''), | ||||
|                         trailing: const Icon(Symbols.chevron_right), | ||||
|                         onTap: () { | ||||
|                           // Navigate to web feed detail page | ||||
|                           context.pushNamed( | ||||
|                             'webFeedDetail', | ||||
|                             pathParameters: {'feedId': feed.id}, | ||||
|                           ); | ||||
|                         }, | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										180
									
								
								lib/screens/discovery/feeds/feed_marketplace.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								lib/screens/discovery/feeds/feed_marketplace.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,180 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'feed_marketplace.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$marketplaceWebFeedsNotifierHash() => | ||||
|     r'dbf885d95570ca9c2259a58998975db813b18cbb'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
|   _SystemHash._(); | ||||
|  | ||||
|   static int combine(int hash, int value) { | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + value); | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); | ||||
|     return hash ^ (hash >> 6); | ||||
|   } | ||||
|  | ||||
|   static int finish(int hash) { | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); | ||||
|     // ignore: parameter_assignments | ||||
|     hash = hash ^ (hash >> 11); | ||||
|     return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); | ||||
|   } | ||||
| } | ||||
|  | ||||
| abstract class _$MarketplaceWebFeedsNotifier | ||||
|     extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnWebFeed>> { | ||||
|   late final String? query; | ||||
|  | ||||
|   FutureOr<CursorPagingData<SnWebFeed>> build({required String? query}); | ||||
| } | ||||
|  | ||||
| /// See also [MarketplaceWebFeedsNotifier]. | ||||
| @ProviderFor(MarketplaceWebFeedsNotifier) | ||||
| const marketplaceWebFeedsNotifierProvider = MarketplaceWebFeedsNotifierFamily(); | ||||
|  | ||||
| /// See also [MarketplaceWebFeedsNotifier]. | ||||
| class MarketplaceWebFeedsNotifierFamily | ||||
|     extends Family<AsyncValue<CursorPagingData<SnWebFeed>>> { | ||||
|   /// See also [MarketplaceWebFeedsNotifier]. | ||||
|   const MarketplaceWebFeedsNotifierFamily(); | ||||
|  | ||||
|   /// See also [MarketplaceWebFeedsNotifier]. | ||||
|   MarketplaceWebFeedsNotifierProvider call({required String? query}) { | ||||
|     return MarketplaceWebFeedsNotifierProvider(query: query); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   MarketplaceWebFeedsNotifierProvider getProviderOverride( | ||||
|     covariant MarketplaceWebFeedsNotifierProvider provider, | ||||
|   ) { | ||||
|     return call(query: provider.query); | ||||
|   } | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _dependencies = null; | ||||
|  | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get dependencies => _dependencies; | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null; | ||||
|  | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get allTransitiveDependencies => | ||||
|       _allTransitiveDependencies; | ||||
|  | ||||
|   @override | ||||
|   String? get name => r'marketplaceWebFeedsNotifierProvider'; | ||||
| } | ||||
|  | ||||
| /// See also [MarketplaceWebFeedsNotifier]. | ||||
| class MarketplaceWebFeedsNotifierProvider | ||||
|     extends | ||||
|         AutoDisposeAsyncNotifierProviderImpl< | ||||
|           MarketplaceWebFeedsNotifier, | ||||
|           CursorPagingData<SnWebFeed> | ||||
|         > { | ||||
|   /// See also [MarketplaceWebFeedsNotifier]. | ||||
|   MarketplaceWebFeedsNotifierProvider({required String? query}) | ||||
|     : this._internal( | ||||
|         () => MarketplaceWebFeedsNotifier()..query = query, | ||||
|         from: marketplaceWebFeedsNotifierProvider, | ||||
|         name: r'marketplaceWebFeedsNotifierProvider', | ||||
|         debugGetCreateSourceHash: | ||||
|             const bool.fromEnvironment('dart.vm.product') | ||||
|                 ? null | ||||
|                 : _$marketplaceWebFeedsNotifierHash, | ||||
|         dependencies: MarketplaceWebFeedsNotifierFamily._dependencies, | ||||
|         allTransitiveDependencies: | ||||
|             MarketplaceWebFeedsNotifierFamily._allTransitiveDependencies, | ||||
|         query: query, | ||||
|       ); | ||||
|  | ||||
|   MarketplaceWebFeedsNotifierProvider._internal( | ||||
|     super._createNotifier, { | ||||
|     required super.name, | ||||
|     required super.dependencies, | ||||
|     required super.allTransitiveDependencies, | ||||
|     required super.debugGetCreateSourceHash, | ||||
|     required super.from, | ||||
|     required this.query, | ||||
|   }) : super.internal(); | ||||
|  | ||||
|   final String? query; | ||||
|  | ||||
|   @override | ||||
|   FutureOr<CursorPagingData<SnWebFeed>> runNotifierBuild( | ||||
|     covariant MarketplaceWebFeedsNotifier notifier, | ||||
|   ) { | ||||
|     return notifier.build(query: query); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Override overrideWith(MarketplaceWebFeedsNotifier Function() create) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: MarketplaceWebFeedsNotifierProvider._internal( | ||||
|         () => create()..query = query, | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         query: query, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeAsyncNotifierProviderElement< | ||||
|     MarketplaceWebFeedsNotifier, | ||||
|     CursorPagingData<SnWebFeed> | ||||
|   > | ||||
|   createElement() { | ||||
|     return _MarketplaceWebFeedsNotifierProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is MarketplaceWebFeedsNotifierProvider && other.query == query; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||
|     hash = _SystemHash.combine(hash, query.hashCode); | ||||
|  | ||||
|     return _SystemHash.finish(hash); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| mixin MarketplaceWebFeedsNotifierRef | ||||
|     on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnWebFeed>> { | ||||
|   /// The parameter `query` of this provider. | ||||
|   String? get query; | ||||
| } | ||||
|  | ||||
| class _MarketplaceWebFeedsNotifierProviderElement | ||||
|     extends | ||||
|         AutoDisposeAsyncNotifierProviderElement< | ||||
|           MarketplaceWebFeedsNotifier, | ||||
|           CursorPagingData<SnWebFeed> | ||||
|         > | ||||
|     with MarketplaceWebFeedsNotifierRef { | ||||
|   _MarketplaceWebFeedsNotifierProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   String? get query => (origin as MarketplaceWebFeedsNotifierProvider).query; | ||||
| } | ||||
|  | ||||
| // 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 | ||||
| @@ -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(), | ||||
|   | ||||
							
								
								
									
										242
									
								
								lib/screens/posts/post_categories_list.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										242
									
								
								lib/screens/posts/post_categories_list.dart
									
									
									
									
									
										Normal 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), | ||||
|             ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -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)), | ||||
|             ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -399,7 +399,7 @@ class _RealmChatRoomsProviderElement | ||||
| } | ||||
|  | ||||
| String _$realmMemberListNotifierHash() => | ||||
|     r'022bcef5a90cbae05ff23b937851afc3ef913d42'; | ||||
|     r'2f88f803b2e61e7287ed8a43025173e56ff6ca3b'; | ||||
|  | ||||
| abstract class _$RealmMemberListNotifier | ||||
|     extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnRealmMember>> { | ||||
|   | ||||
| @@ -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), | ||||
|                     ), | ||||
|   | ||||
| @@ -1,103 +0,0 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/sticker.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | ||||
|  | ||||
| part 'marketplace.g.dart'; | ||||
|  | ||||
| @riverpod | ||||
| class MarketplaceStickerPacksNotifier extends _$MarketplaceStickerPacksNotifier | ||||
|     with CursorPagingNotifierMixin<SnStickerPack> { | ||||
|   @override | ||||
|   Future<CursorPagingData<SnStickerPack>> build() { | ||||
|     return fetch(cursor: null); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<CursorPagingData<SnStickerPack>> fetch({ | ||||
|     required String? cursor, | ||||
|   }) async { | ||||
|     final client = ref.read(apiClientProvider); | ||||
|     final offset = cursor == null ? 0 : int.parse(cursor); | ||||
|  | ||||
|     final response = await client.get( | ||||
|       '/sphere/stickers', | ||||
|       queryParameters: {'offset': offset, 'take': 20}, | ||||
|     ); | ||||
|  | ||||
|     final total = int.parse(response.headers.value('X-Total') ?? '0'); | ||||
|     final List<dynamic> data = response.data; | ||||
|     final stickers = data.map((e) => SnStickerPack.fromJson(e)).toList(); | ||||
|  | ||||
|     final hasMore = offset + stickers.length < total; | ||||
|     final nextCursor = hasMore ? (offset + stickers.length).toString() : null; | ||||
|  | ||||
|     return CursorPagingData( | ||||
|       items: stickers, | ||||
|       hasMore: hasMore, | ||||
|       nextCursor: nextCursor, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// User-facing marketplace screen for browsing sticker packs. | ||||
| /// This version does NOT rely on publisher name (no pubName). | ||||
| class MarketplaceStickersScreen extends HookConsumerWidget { | ||||
|   const MarketplaceStickersScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         title: const Text('stickers').tr(), | ||||
|         actions: const [Gap(8)], | ||||
|       ), | ||||
|       body: const SliverMarketplaceStickerPacksList(), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class SliverMarketplaceStickerPacksList extends HookConsumerWidget { | ||||
|   const SliverMarketplaceStickerPacksList({super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     return PagingHelperView( | ||||
|       provider: marketplaceStickerPacksNotifierProvider, | ||||
|       futureRefreshable: marketplaceStickerPacksNotifierProvider.future, | ||||
|       notifierRefreshable: marketplaceStickerPacksNotifierProvider.notifier, | ||||
|       contentBuilder: | ||||
|           (data, widgetCount, endItemView) => ListView.builder( | ||||
|             padding: EdgeInsets.zero, | ||||
|             itemCount: widgetCount, | ||||
|             itemBuilder: (context, index) { | ||||
|               if (index == widgetCount - 1) { | ||||
|                 return endItemView; | ||||
|               } | ||||
|  | ||||
|               final pack = data.items[index]; | ||||
|               return ListTile( | ||||
|                 title: Text(pack.name), | ||||
|                 subtitle: Text(pack.description), | ||||
|                 trailing: const Icon(Symbols.chevron_right), | ||||
|                 onTap: () { | ||||
|                   // Navigate to user-facing sticker pack detail page. | ||||
|                   // Adjust the route name/parameters if your app uses different ones. | ||||
|                   context.pushNamed( | ||||
|                     'stickerPackDetail', | ||||
|                     pathParameters: {'packId': pack.id}, | ||||
|                   ); | ||||
|                 }, | ||||
|               ); | ||||
|             }, | ||||
|           ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -1,32 +0,0 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'marketplace.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$marketplaceStickerPacksNotifierHash() => | ||||
|     r'b62ae8b7f5c4f8bb3be8c17fc005ea26da355187'; | ||||
|  | ||||
| /// See also [MarketplaceStickerPacksNotifier]. | ||||
| @ProviderFor(MarketplaceStickerPacksNotifier) | ||||
| final marketplaceStickerPacksNotifierProvider = | ||||
|     AutoDisposeAsyncNotifierProvider< | ||||
|       MarketplaceStickerPacksNotifier, | ||||
|       CursorPagingData<SnStickerPack> | ||||
|     >.internal( | ||||
|       MarketplaceStickerPacksNotifier.new, | ||||
|       name: r'marketplaceStickerPacksNotifierProvider', | ||||
|       debugGetCreateSourceHash: | ||||
|           const bool.fromEnvironment('dart.vm.product') | ||||
|               ? null | ||||
|               : _$marketplaceStickerPacksNotifierHash, | ||||
|       dependencies: null, | ||||
|       allTransitiveDependencies: null, | ||||
|     ); | ||||
|  | ||||
| typedef _$MarketplaceStickerPacksNotifier = | ||||
|     AutoDisposeAsyncNotifier<CursorPagingData<SnStickerPack>>; | ||||
| // 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 | ||||
							
								
								
									
										199
									
								
								lib/screens/stickers/sticker_marketplace.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								lib/screens/stickers/sticker_marketplace.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,199 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/sticker.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'dart:async'; | ||||
|  | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | ||||
|  | ||||
| part 'sticker_marketplace.g.dart'; | ||||
|  | ||||
| @riverpod | ||||
| class MarketplaceStickerPacksNotifier extends _$MarketplaceStickerPacksNotifier | ||||
|     with CursorPagingNotifierMixin<SnStickerPack> { | ||||
|   @override | ||||
|   Future<CursorPagingData<SnStickerPack>> build({ | ||||
|     required String? query, | ||||
|     required bool byUsage, | ||||
|   }) { | ||||
|     return fetch(cursor: null); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<CursorPagingData<SnStickerPack>> fetch({ | ||||
|     required String? cursor, | ||||
|   }) async { | ||||
|     final client = ref.read(apiClientProvider); | ||||
|     final offset = cursor == null ? 0 : int.parse(cursor); | ||||
|  | ||||
|     final response = await client.get( | ||||
|       '/sphere/stickers', | ||||
|       queryParameters: { | ||||
|         'offset': offset, | ||||
|         'take': 20, | ||||
|         'order': byUsage ? 'usage' : 'date', | ||||
|         if (query != null && query!.isNotEmpty) 'query': query, | ||||
|       }, | ||||
|     ); | ||||
|  | ||||
|     final total = int.parse(response.headers.value('X-Total') ?? '0'); | ||||
|     final List<dynamic> data = response.data; | ||||
|     final stickers = data.map((e) => SnStickerPack.fromJson(e)).toList(); | ||||
|  | ||||
|     final hasMore = offset + stickers.length < total; | ||||
|     final nextCursor = hasMore ? (offset + stickers.length).toString() : null; | ||||
|  | ||||
|     return CursorPagingData( | ||||
|       items: stickers, | ||||
|       hasMore: hasMore, | ||||
|       nextCursor: nextCursor, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// User-facing marketplace screen for browsing sticker packs. | ||||
| /// This version does NOT rely on publisher name (no pubName). | ||||
| class MarketplaceStickersScreen extends HookConsumerWidget { | ||||
|   const MarketplaceStickersScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final byUsage = useState(true); | ||||
|     final query = useState<String?>(null); | ||||
|     final searchController = useTextEditingController(); | ||||
|     final focusNode = useFocusNode(); | ||||
|     final debounceTimer = useState<Timer?>(null); | ||||
|  | ||||
|     // Clear search when query is cleared | ||||
|     useEffect(() { | ||||
|       if (query.value == null || query.value!.isEmpty) { | ||||
|         searchController.clear(); | ||||
|       } | ||||
|       return null; | ||||
|     }, [query.value]); | ||||
|  | ||||
|     // Clean up timer on dispose | ||||
|     useEffect(() { | ||||
|       return () { | ||||
|         debounceTimer.value?.cancel(); | ||||
|       }; | ||||
|     }, []); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         title: const Text('stickers').tr(), | ||||
|         actions: [ | ||||
|           IconButton( | ||||
|             onPressed: () { | ||||
|               byUsage.value = !byUsage.value; | ||||
|             }, | ||||
|             icon: | ||||
|                 byUsage.value | ||||
|                     ? const Icon(Symbols.local_fire_department) | ||||
|                     : const Icon(Symbols.access_time), | ||||
|             tooltip: | ||||
|                 byUsage.value | ||||
|                     ? 'orderByPopularity'.tr() | ||||
|                     : 'orderByReleaseDate'.tr(), | ||||
|           ), | ||||
|           const Gap(8), | ||||
|         ], | ||||
|       ), | ||||
|       body: PagingHelperView( | ||||
|         provider: marketplaceStickerPacksNotifierProvider( | ||||
|           byUsage: byUsage.value, | ||||
|           query: query.value, | ||||
|         ), | ||||
|         futureRefreshable: | ||||
|             marketplaceStickerPacksNotifierProvider( | ||||
|               byUsage: byUsage.value, | ||||
|               query: query.value, | ||||
|             ).future, | ||||
|         notifierRefreshable: | ||||
|             marketplaceStickerPacksNotifierProvider( | ||||
|               byUsage: byUsage.value, | ||||
|               query: query.value, | ||||
|             ).notifier, | ||||
|         contentBuilder: | ||||
|             (data, widgetCount, endItemView) => Column( | ||||
|               children: [ | ||||
|                 // Search bar above the list | ||||
|                 Padding( | ||||
|                   padding: const EdgeInsets.all(16), | ||||
|                   child: SearchBar( | ||||
|                     elevation: WidgetStateProperty.all(4), | ||||
|                     controller: searchController, | ||||
|                     focusNode: focusNode, | ||||
|                     hintText: 'search'.tr(), | ||||
|                     leading: const Icon(Symbols.search), | ||||
|                     padding: WidgetStateProperty.all( | ||||
|                       const EdgeInsets.symmetric(horizontal: 24), | ||||
|                     ), | ||||
|                     onTapOutside: | ||||
|                         (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                     trailing: [ | ||||
|                       if (query.value != null && query.value!.isNotEmpty) | ||||
|                         IconButton( | ||||
|                           icon: const Icon(Symbols.close), | ||||
|                           onPressed: () { | ||||
|                             query.value = null; | ||||
|                             searchController.clear(); | ||||
|                             focusNode.unfocus(); | ||||
|                           }, | ||||
|                         ), | ||||
|                     ], | ||||
|                     onChanged: (value) { | ||||
|                       // Debounce search to avoid excessive API calls | ||||
|                       debounceTimer.value?.cancel(); | ||||
|                       debounceTimer.value = Timer( | ||||
|                         const Duration(milliseconds: 500), | ||||
|                         () { | ||||
|                           query.value = value.isEmpty ? null : value; | ||||
|                         }, | ||||
|                       ); | ||||
|                     }, | ||||
|                     onSubmitted: (value) { | ||||
|                       query.value = value.isEmpty ? null : value; | ||||
|                       focusNode.unfocus(); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ), | ||||
|                 Expanded( | ||||
|                   child: ListView.builder( | ||||
|                     padding: EdgeInsets.zero, | ||||
|                     itemCount: widgetCount, | ||||
|                     itemBuilder: (context, index) { | ||||
|                       if (index == widgetCount - 1) { | ||||
|                         return endItemView; | ||||
|                       } | ||||
|  | ||||
|                       final pack = data.items[index]; | ||||
|                       return ListTile( | ||||
|                         title: Text(pack.name), | ||||
|                         subtitle: Text(pack.description), | ||||
|                         trailing: const Icon(Symbols.chevron_right), | ||||
|                         onTap: () { | ||||
|                           // Navigate to user-facing sticker pack detail page. | ||||
|                           // Adjust the route name/parameters if your app uses different ones. | ||||
|                           context.pushNamed( | ||||
|                             'stickerPackDetail', | ||||
|                             pathParameters: {'packId': pack.id}, | ||||
|                           ); | ||||
|                         }, | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										213
									
								
								lib/screens/stickers/sticker_marketplace.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										213
									
								
								lib/screens/stickers/sticker_marketplace.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,213 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'sticker_marketplace.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$marketplaceStickerPacksNotifierHash() => | ||||
|     r'3bde76e18bb024f45ff6261fe735cdba97b02808'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
|   _SystemHash._(); | ||||
|  | ||||
|   static int combine(int hash, int value) { | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + value); | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); | ||||
|     return hash ^ (hash >> 6); | ||||
|   } | ||||
|  | ||||
|   static int finish(int hash) { | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); | ||||
|     // ignore: parameter_assignments | ||||
|     hash = hash ^ (hash >> 11); | ||||
|     return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); | ||||
|   } | ||||
| } | ||||
|  | ||||
| abstract class _$MarketplaceStickerPacksNotifier | ||||
|     extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnStickerPack>> { | ||||
|   late final String? query; | ||||
|   late final bool byUsage; | ||||
|  | ||||
|   FutureOr<CursorPagingData<SnStickerPack>> build({ | ||||
|     required String? query, | ||||
|     required bool byUsage, | ||||
|   }); | ||||
| } | ||||
|  | ||||
| /// See also [MarketplaceStickerPacksNotifier]. | ||||
| @ProviderFor(MarketplaceStickerPacksNotifier) | ||||
| const marketplaceStickerPacksNotifierProvider = | ||||
|     MarketplaceStickerPacksNotifierFamily(); | ||||
|  | ||||
| /// See also [MarketplaceStickerPacksNotifier]. | ||||
| class MarketplaceStickerPacksNotifierFamily | ||||
|     extends Family<AsyncValue<CursorPagingData<SnStickerPack>>> { | ||||
|   /// See also [MarketplaceStickerPacksNotifier]. | ||||
|   const MarketplaceStickerPacksNotifierFamily(); | ||||
|  | ||||
|   /// See also [MarketplaceStickerPacksNotifier]. | ||||
|   MarketplaceStickerPacksNotifierProvider call({ | ||||
|     required String? query, | ||||
|     required bool byUsage, | ||||
|   }) { | ||||
|     return MarketplaceStickerPacksNotifierProvider( | ||||
|       query: query, | ||||
|       byUsage: byUsage, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   MarketplaceStickerPacksNotifierProvider getProviderOverride( | ||||
|     covariant MarketplaceStickerPacksNotifierProvider provider, | ||||
|   ) { | ||||
|     return call(query: provider.query, byUsage: provider.byUsage); | ||||
|   } | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _dependencies = null; | ||||
|  | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get dependencies => _dependencies; | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null; | ||||
|  | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get allTransitiveDependencies => | ||||
|       _allTransitiveDependencies; | ||||
|  | ||||
|   @override | ||||
|   String? get name => r'marketplaceStickerPacksNotifierProvider'; | ||||
| } | ||||
|  | ||||
| /// See also [MarketplaceStickerPacksNotifier]. | ||||
| class MarketplaceStickerPacksNotifierProvider | ||||
|     extends | ||||
|         AutoDisposeAsyncNotifierProviderImpl< | ||||
|           MarketplaceStickerPacksNotifier, | ||||
|           CursorPagingData<SnStickerPack> | ||||
|         > { | ||||
|   /// See also [MarketplaceStickerPacksNotifier]. | ||||
|   MarketplaceStickerPacksNotifierProvider({ | ||||
|     required String? query, | ||||
|     required bool byUsage, | ||||
|   }) : this._internal( | ||||
|          () => | ||||
|              MarketplaceStickerPacksNotifier() | ||||
|                ..query = query | ||||
|                ..byUsage = byUsage, | ||||
|          from: marketplaceStickerPacksNotifierProvider, | ||||
|          name: r'marketplaceStickerPacksNotifierProvider', | ||||
|          debugGetCreateSourceHash: | ||||
|              const bool.fromEnvironment('dart.vm.product') | ||||
|                  ? null | ||||
|                  : _$marketplaceStickerPacksNotifierHash, | ||||
|          dependencies: MarketplaceStickerPacksNotifierFamily._dependencies, | ||||
|          allTransitiveDependencies: | ||||
|              MarketplaceStickerPacksNotifierFamily._allTransitiveDependencies, | ||||
|          query: query, | ||||
|          byUsage: byUsage, | ||||
|        ); | ||||
|  | ||||
|   MarketplaceStickerPacksNotifierProvider._internal( | ||||
|     super._createNotifier, { | ||||
|     required super.name, | ||||
|     required super.dependencies, | ||||
|     required super.allTransitiveDependencies, | ||||
|     required super.debugGetCreateSourceHash, | ||||
|     required super.from, | ||||
|     required this.query, | ||||
|     required this.byUsage, | ||||
|   }) : super.internal(); | ||||
|  | ||||
|   final String? query; | ||||
|   final bool byUsage; | ||||
|  | ||||
|   @override | ||||
|   FutureOr<CursorPagingData<SnStickerPack>> runNotifierBuild( | ||||
|     covariant MarketplaceStickerPacksNotifier notifier, | ||||
|   ) { | ||||
|     return notifier.build(query: query, byUsage: byUsage); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Override overrideWith(MarketplaceStickerPacksNotifier Function() create) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: MarketplaceStickerPacksNotifierProvider._internal( | ||||
|         () => | ||||
|             create() | ||||
|               ..query = query | ||||
|               ..byUsage = byUsage, | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         query: query, | ||||
|         byUsage: byUsage, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeAsyncNotifierProviderElement< | ||||
|     MarketplaceStickerPacksNotifier, | ||||
|     CursorPagingData<SnStickerPack> | ||||
|   > | ||||
|   createElement() { | ||||
|     return _MarketplaceStickerPacksNotifierProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is MarketplaceStickerPacksNotifierProvider && | ||||
|         other.query == query && | ||||
|         other.byUsage == byUsage; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||
|     hash = _SystemHash.combine(hash, query.hashCode); | ||||
|     hash = _SystemHash.combine(hash, byUsage.hashCode); | ||||
|  | ||||
|     return _SystemHash.finish(hash); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| mixin MarketplaceStickerPacksNotifierRef | ||||
|     on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnStickerPack>> { | ||||
|   /// The parameter `query` of this provider. | ||||
|   String? get query; | ||||
|  | ||||
|   /// The parameter `byUsage` of this provider. | ||||
|   bool get byUsage; | ||||
| } | ||||
|  | ||||
| class _MarketplaceStickerPacksNotifierProviderElement | ||||
|     extends | ||||
|         AutoDisposeAsyncNotifierProviderElement< | ||||
|           MarketplaceStickerPacksNotifier, | ||||
|           CursorPagingData<SnStickerPack> | ||||
|         > | ||||
|     with MarketplaceStickerPacksNotifierRef { | ||||
|   _MarketplaceStickerPacksNotifierProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   String? get query => | ||||
|       (origin as MarketplaceStickerPacksNotifierProvider).query; | ||||
|   @override | ||||
|   bool get byUsage => | ||||
|       (origin as MarketplaceStickerPacksNotifierProvider).byUsage; | ||||
| } | ||||
|  | ||||
| // 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 | ||||
| @@ -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, | ||||
|   | ||||
| @@ -50,6 +50,6 @@ class AppWrapper extends HookConsumerWidget { | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return TourTriggerWidget(child: child); | ||||
|     return TourTriggerWidget(key: UniqueKey(), child: child); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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), | ||||
|                   ], | ||||
|   | ||||
| @@ -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), | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										108
									
								
								lib/widgets/post/post_shuffle.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								lib/widgets/post/post_shuffle.dart
									
									
									
									
									
										Normal 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(), | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										52
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										52
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -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: | ||||
|   | ||||
							
								
								
									
										11
									
								
								pubspec.yaml
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								pubspec.yaml
									
									
									
									
									
								
							| @@ -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: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user