Compare commits

...

22 Commits

Author SHA1 Message Date
LittleSheep
4beda9200e Add developer projects 2025-08-23 02:56:28 +08:00
LittleSheep
7dfe411053 Add more developer pages (wip) 2025-08-23 02:19:07 +08:00
LittleSheep
1232318a5d Add feed subscription (wip) 2025-08-23 01:32:58 +08:00
LittleSheep
56f41b6c0e 🔀 Merge pull request #173 from Texas0295/v3
🐛 linux/userinfo.dart: guard Firebase calls if Firebase is uninitialized
2025-08-23 01:28:54 +08:00
Texas0295
3ea717d25a 🐛 linux/userinfo.dart: guard Firebase calls if Firebase is uninitialized 2025-08-23 00:34:56 +08:00
LittleSheep
1fe4889460 Search for stickers 2025-08-22 23:31:31 +08:00
LittleSheep
cdf2722268 Stickers order by usage 2025-08-22 22:59:41 +08:00
LittleSheep
a127b5bace Social credits 2025-08-22 19:55:26 +08:00
LittleSheep
b2097cf044 🐛 Fix chat summary failed when lastMessage is null 2025-08-22 19:00:06 +08:00
LittleSheep
701f29748d Debug set access token 2025-08-22 18:59:40 +08:00
LittleSheep
9e40ed4600 Show leveling BonusMultiplier 2025-08-22 17:58:28 +08:00
LittleSheep
c90e6fe661 Experience records & refine leveling page 2025-08-22 17:53:07 +08:00
LittleSheep
569483300d Card shuffle 2025-08-22 16:20:13 +08:00
LittleSheep
bab602d98b Shuffle post 2025-08-22 01:41:25 +08:00
LittleSheep
b4f2bb803a ⬆️ Upgrade deps 2025-08-22 00:22:06 +08:00
LittleSheep
03bfed6f46 💄 Optimize cloud file info 2025-08-22 00:17:13 +08:00
LittleSheep
f98e5a0aec Post browse by categories, tags 2025-08-21 23:21:30 +08:00
LittleSheep
3d473e2fec 🐛 Replace the push with go to view posts in creator centre in order to fix #172 2025-08-21 20:09:49 +08:00
LittleSheep
0b6efa373a 💄 Optimize realm list 2025-08-21 18:49:42 +08:00
LittleSheep
9b60e96cde 💄 Optimize post detail error page 2025-08-21 02:25:13 +08:00
LittleSheep
81cd9b2082 🐛 Fix issues when saving image to gallery without ext name 2025-08-21 02:22:21 +08:00
LittleSheep
923d5d7514 👽 Update stickers api call 2025-08-20 14:26:37 +08:00
69 changed files with 7727 additions and 574 deletions

View File

@@ -643,6 +643,14 @@
"enrollDeveloperHint": "Enroll one of your publishers to become a developer.",
"noPublishersToEnroll": "You don't have any publishers that can be enrolled as a developer.",
"totalCustomApps": "Total Custom Apps",
"projects": "Projects",
"noProjects": "No projects found.",
"deleteProject": "Delete Project",
"deleteProjectHint": "Are you sure you want to delete this project? This action cannot be undone.",
"createProject": "Create Project",
"editProject": "Edit Project",
"projectDetails": "Project Details",
"createBot": "Create Bot",
"customApps": "Custom Apps",
"noCustomApps": "No custom apps yet.",
"createCustomApp": "Create Custom App",
@@ -854,5 +862,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"
}

View File

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

View File

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

View File

@@ -3018,6 +3018,562 @@ as bool,
}
}
/// @nodoc
mixin _$SnExperienceRecord {
String get id; int get delta; String get reasonType; String get reason; double? get bonusMultiplier; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnExperienceRecord
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnExperienceRecordCopyWith<SnExperienceRecord> get copyWith => _$SnExperienceRecordCopyWithImpl<SnExperienceRecord>(this as SnExperienceRecord, _$identity);
/// Serializes this SnExperienceRecord to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnExperienceRecord&&(identical(other.id, id) || other.id == id)&&(identical(other.delta, delta) || other.delta == delta)&&(identical(other.reasonType, reasonType) || other.reasonType == reasonType)&&(identical(other.reason, reason) || other.reason == reason)&&(identical(other.bonusMultiplier, bonusMultiplier) || other.bonusMultiplier == bonusMultiplier)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,delta,reasonType,reason,bonusMultiplier,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnExperienceRecord(id: $id, delta: $delta, reasonType: $reasonType, reason: $reason, bonusMultiplier: $bonusMultiplier, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnExperienceRecordCopyWith<$Res> {
factory $SnExperienceRecordCopyWith(SnExperienceRecord value, $Res Function(SnExperienceRecord) _then) = _$SnExperienceRecordCopyWithImpl;
@useResult
$Res call({
String id, int delta, String reasonType, String reason, double? bonusMultiplier, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class _$SnExperienceRecordCopyWithImpl<$Res>
implements $SnExperienceRecordCopyWith<$Res> {
_$SnExperienceRecordCopyWithImpl(this._self, this._then);
final SnExperienceRecord _self;
final $Res Function(SnExperienceRecord) _then;
/// Create a copy of SnExperienceRecord
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? delta = null,Object? reasonType = null,Object? reason = null,Object? bonusMultiplier = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,delta: null == delta ? _self.delta : delta // ignore: cast_nullable_to_non_nullable
as int,reasonType: null == reasonType ? _self.reasonType : reasonType // ignore: cast_nullable_to_non_nullable
as String,reason: null == reason ? _self.reason : reason // ignore: cast_nullable_to_non_nullable
as String,bonusMultiplier: freezed == bonusMultiplier ? _self.bonusMultiplier : bonusMultiplier // ignore: cast_nullable_to_non_nullable
as double?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
/// Adds pattern-matching-related methods to [SnExperienceRecord].
extension SnExperienceRecordPatterns on SnExperienceRecord {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnExperienceRecord value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnExperienceRecord() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnExperienceRecord value) $default,){
final _that = this;
switch (_that) {
case _SnExperienceRecord():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnExperienceRecord value)? $default,){
final _that = this;
switch (_that) {
case _SnExperienceRecord() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, int delta, String reasonType, String reason, double? bonusMultiplier, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnExperienceRecord() when $default != null:
return $default(_that.id,_that.delta,_that.reasonType,_that.reason,_that.bonusMultiplier,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, int delta, String reasonType, String reason, double? bonusMultiplier, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) {
case _SnExperienceRecord():
return $default(_that.id,_that.delta,_that.reasonType,_that.reason,_that.bonusMultiplier,_that.createdAt,_that.updatedAt,_that.deletedAt);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, int delta, String reasonType, String reason, double? bonusMultiplier, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) {
case _SnExperienceRecord() when $default != null:
return $default(_that.id,_that.delta,_that.reasonType,_that.reason,_that.bonusMultiplier,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnExperienceRecord implements SnExperienceRecord {
const _SnExperienceRecord({required this.id, required this.delta, required this.reasonType, required this.reason, this.bonusMultiplier = 1.0, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnExperienceRecord.fromJson(Map<String, dynamic> json) => _$SnExperienceRecordFromJson(json);
@override final String id;
@override final int delta;
@override final String reasonType;
@override final String reason;
@override@JsonKey() final double? bonusMultiplier;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnExperienceRecord
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnExperienceRecordCopyWith<_SnExperienceRecord> get copyWith => __$SnExperienceRecordCopyWithImpl<_SnExperienceRecord>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnExperienceRecordToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnExperienceRecord&&(identical(other.id, id) || other.id == id)&&(identical(other.delta, delta) || other.delta == delta)&&(identical(other.reasonType, reasonType) || other.reasonType == reasonType)&&(identical(other.reason, reason) || other.reason == reason)&&(identical(other.bonusMultiplier, bonusMultiplier) || other.bonusMultiplier == bonusMultiplier)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,delta,reasonType,reason,bonusMultiplier,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnExperienceRecord(id: $id, delta: $delta, reasonType: $reasonType, reason: $reason, bonusMultiplier: $bonusMultiplier, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnExperienceRecordCopyWith<$Res> implements $SnExperienceRecordCopyWith<$Res> {
factory _$SnExperienceRecordCopyWith(_SnExperienceRecord value, $Res Function(_SnExperienceRecord) _then) = __$SnExperienceRecordCopyWithImpl;
@override @useResult
$Res call({
String id, int delta, String reasonType, String reason, double? bonusMultiplier, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class __$SnExperienceRecordCopyWithImpl<$Res>
implements _$SnExperienceRecordCopyWith<$Res> {
__$SnExperienceRecordCopyWithImpl(this._self, this._then);
final _SnExperienceRecord _self;
final $Res Function(_SnExperienceRecord) _then;
/// Create a copy of SnExperienceRecord
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? delta = null,Object? reasonType = null,Object? reason = null,Object? bonusMultiplier = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnExperienceRecord(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,delta: null == delta ? _self.delta : delta // ignore: cast_nullable_to_non_nullable
as int,reasonType: null == reasonType ? _self.reasonType : reasonType // ignore: cast_nullable_to_non_nullable
as String,reason: null == reason ? _self.reason : reason // ignore: cast_nullable_to_non_nullable
as String,bonusMultiplier: freezed == bonusMultiplier ? _self.bonusMultiplier : bonusMultiplier // ignore: cast_nullable_to_non_nullable
as double?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
/// @nodoc
mixin _$SnSocialCreditRecord {
String get id; double get delta; String get reasonType; String get reason; DateTime? get expiredAt; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnSocialCreditRecord
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnSocialCreditRecordCopyWith<SnSocialCreditRecord> get copyWith => _$SnSocialCreditRecordCopyWithImpl<SnSocialCreditRecord>(this as SnSocialCreditRecord, _$identity);
/// Serializes this SnSocialCreditRecord to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnSocialCreditRecord&&(identical(other.id, id) || other.id == id)&&(identical(other.delta, delta) || other.delta == delta)&&(identical(other.reasonType, reasonType) || other.reasonType == reasonType)&&(identical(other.reason, reason) || other.reason == reason)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,delta,reasonType,reason,expiredAt,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnSocialCreditRecord(id: $id, delta: $delta, reasonType: $reasonType, reason: $reason, expiredAt: $expiredAt, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnSocialCreditRecordCopyWith<$Res> {
factory $SnSocialCreditRecordCopyWith(SnSocialCreditRecord value, $Res Function(SnSocialCreditRecord) _then) = _$SnSocialCreditRecordCopyWithImpl;
@useResult
$Res call({
String id, double delta, String reasonType, String reason, DateTime? expiredAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class _$SnSocialCreditRecordCopyWithImpl<$Res>
implements $SnSocialCreditRecordCopyWith<$Res> {
_$SnSocialCreditRecordCopyWithImpl(this._self, this._then);
final SnSocialCreditRecord _self;
final $Res Function(SnSocialCreditRecord) _then;
/// Create a copy of SnSocialCreditRecord
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? delta = null,Object? reasonType = null,Object? reason = null,Object? expiredAt = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,delta: null == delta ? _self.delta : delta // ignore: cast_nullable_to_non_nullable
as double,reasonType: null == reasonType ? _self.reasonType : reasonType // ignore: cast_nullable_to_non_nullable
as String,reason: null == reason ? _self.reason : reason // ignore: cast_nullable_to_non_nullable
as String,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
/// Adds pattern-matching-related methods to [SnSocialCreditRecord].
extension SnSocialCreditRecordPatterns on SnSocialCreditRecord {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnSocialCreditRecord value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnSocialCreditRecord() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnSocialCreditRecord value) $default,){
final _that = this;
switch (_that) {
case _SnSocialCreditRecord():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnSocialCreditRecord value)? $default,){
final _that = this;
switch (_that) {
case _SnSocialCreditRecord() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, double delta, String reasonType, String reason, DateTime? expiredAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnSocialCreditRecord() when $default != null:
return $default(_that.id,_that.delta,_that.reasonType,_that.reason,_that.expiredAt,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, double delta, String reasonType, String reason, DateTime? expiredAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) {
case _SnSocialCreditRecord():
return $default(_that.id,_that.delta,_that.reasonType,_that.reason,_that.expiredAt,_that.createdAt,_that.updatedAt,_that.deletedAt);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, double delta, String reasonType, String reason, DateTime? expiredAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) {
case _SnSocialCreditRecord() when $default != null:
return $default(_that.id,_that.delta,_that.reasonType,_that.reason,_that.expiredAt,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnSocialCreditRecord implements SnSocialCreditRecord {
const _SnSocialCreditRecord({required this.id, required this.delta, required this.reasonType, required this.reason, required this.expiredAt, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnSocialCreditRecord.fromJson(Map<String, dynamic> json) => _$SnSocialCreditRecordFromJson(json);
@override final String id;
@override final double delta;
@override final String reasonType;
@override final String reason;
@override final DateTime? expiredAt;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnSocialCreditRecord
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnSocialCreditRecordCopyWith<_SnSocialCreditRecord> get copyWith => __$SnSocialCreditRecordCopyWithImpl<_SnSocialCreditRecord>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnSocialCreditRecordToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnSocialCreditRecord&&(identical(other.id, id) || other.id == id)&&(identical(other.delta, delta) || other.delta == delta)&&(identical(other.reasonType, reasonType) || other.reasonType == reasonType)&&(identical(other.reason, reason) || other.reason == reason)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,delta,reasonType,reason,expiredAt,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnSocialCreditRecord(id: $id, delta: $delta, reasonType: $reasonType, reason: $reason, expiredAt: $expiredAt, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnSocialCreditRecordCopyWith<$Res> implements $SnSocialCreditRecordCopyWith<$Res> {
factory _$SnSocialCreditRecordCopyWith(_SnSocialCreditRecord value, $Res Function(_SnSocialCreditRecord) _then) = __$SnSocialCreditRecordCopyWithImpl;
@override @useResult
$Res call({
String id, double delta, String reasonType, String reason, DateTime? expiredAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class __$SnSocialCreditRecordCopyWithImpl<$Res>
implements _$SnSocialCreditRecordCopyWith<$Res> {
__$SnSocialCreditRecordCopyWithImpl(this._self, this._then);
final _SnSocialCreditRecord _self;
final $Res Function(_SnSocialCreditRecord) _then;
/// Create a copy of SnSocialCreditRecord
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? delta = null,Object? reasonType = null,Object? reason = null,Object? expiredAt = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnSocialCreditRecord(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,delta: null == delta ? _self.delta : delta // ignore: cast_nullable_to_non_nullable
as double,reasonType: null == reasonType ? _self.reasonType : reasonType // ignore: cast_nullable_to_non_nullable
as String,reason: null == reason ? _self.reason : reason // ignore: cast_nullable_to_non_nullable
as String,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
// dart format on

View File

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

69
lib/models/bot.dart Normal file
View File

@@ -0,0 +1,69 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/file.dart';
import 'package:island/models/account.dart';
part 'bot.freezed.dart';
part 'bot.g.dart';
@freezed
sealed class Bot with _$Bot {
const factory Bot({
@Default('') String id,
@Default('') String name,
@Default('') String slug,
String? description,
@Default(0) int status,
SnCloudFile? picture,
SnCloudFile? background,
SnVerificationMark? verification,
BotConfig? config,
BotLinks? links,
@Default('') String publisherId,
@Default('') String appId,
DateTime? createdAt,
DateTime? updatedAt,
}) = _Bot;
factory Bot.fromJson(Map<String, dynamic> json) => _$BotFromJson(json);
}
@freezed
sealed class BotConfig with _$BotConfig {
const factory BotConfig({
@Default(false) bool isPublic,
@Default(false) bool isInteractive,
@Default([]) List<String> allowedRealms,
@Default([]) List<String> allowedChatTypes,
@Default({}) Map<String, dynamic> metadata,
}) = _BotConfig;
factory BotConfig.fromJson(Map<String, dynamic> json) =>
_$BotConfigFromJson(json);
}
@freezed
sealed class BotLinks with _$BotLinks {
const factory BotLinks({
String? website,
String? documentation,
String? privacyPolicy,
String? termsOfService,
}) = _BotLinks;
factory BotLinks.fromJson(Map<String, dynamic> json) =>
_$BotLinksFromJson(json);
}
@freezed
sealed class BotSecret with _$BotSecret {
const factory BotSecret({
@Default('') String id,
@Default('') String secret,
String? description,
DateTime? expiredAt,
@Default('') String botId,
}) = _BotSecret;
factory BotSecret.fromJson(Map<String, dynamic> json) =>
_$BotSecretFromJson(json);
}

1252
lib/models/bot.freezed.dart Normal file

File diff suppressed because it is too large Load Diff

123
lib/models/bot.g.dart Normal file
View File

@@ -0,0 +1,123 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'bot.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_Bot _$BotFromJson(Map<String, dynamic> json) => _Bot(
id: json['id'] as String? ?? '',
name: json['name'] as String? ?? '',
slug: json['slug'] as String? ?? '',
description: json['description'] as String?,
status: (json['status'] as num?)?.toInt() ?? 0,
picture:
json['picture'] == null
? null
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
background:
json['background'] == null
? null
: SnCloudFile.fromJson(json['background'] as Map<String, dynamic>),
verification:
json['verification'] == null
? null
: SnVerificationMark.fromJson(
json['verification'] as Map<String, dynamic>,
),
config:
json['config'] == null
? null
: BotConfig.fromJson(json['config'] as Map<String, dynamic>),
links:
json['links'] == null
? null
: BotLinks.fromJson(json['links'] as Map<String, dynamic>),
publisherId: json['publisher_id'] as String? ?? '',
appId: json['app_id'] as String? ?? '',
createdAt:
json['created_at'] == null
? null
: DateTime.parse(json['created_at'] as String),
updatedAt:
json['updated_at'] == null
? null
: DateTime.parse(json['updated_at'] as String),
);
Map<String, dynamic> _$BotToJson(_Bot instance) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'slug': instance.slug,
'description': instance.description,
'status': instance.status,
'picture': instance.picture?.toJson(),
'background': instance.background?.toJson(),
'verification': instance.verification?.toJson(),
'config': instance.config?.toJson(),
'links': instance.links?.toJson(),
'publisher_id': instance.publisherId,
'app_id': instance.appId,
'created_at': instance.createdAt?.toIso8601String(),
'updated_at': instance.updatedAt?.toIso8601String(),
};
_BotConfig _$BotConfigFromJson(Map<String, dynamic> json) => _BotConfig(
isPublic: json['is_public'] as bool? ?? false,
isInteractive: json['is_interactive'] as bool? ?? false,
allowedRealms:
(json['allowed_realms'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
allowedChatTypes:
(json['allowed_chat_types'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
metadata: json['metadata'] as Map<String, dynamic>? ?? const {},
);
Map<String, dynamic> _$BotConfigToJson(_BotConfig instance) =>
<String, dynamic>{
'is_public': instance.isPublic,
'is_interactive': instance.isInteractive,
'allowed_realms': instance.allowedRealms,
'allowed_chat_types': instance.allowedChatTypes,
'metadata': instance.metadata,
};
_BotLinks _$BotLinksFromJson(Map<String, dynamic> json) => _BotLinks(
website: json['website'] as String?,
documentation: json['documentation'] as String?,
privacyPolicy: json['privacy_policy'] as String?,
termsOfService: json['terms_of_service'] as String?,
);
Map<String, dynamic> _$BotLinksToJson(_BotLinks instance) => <String, dynamic>{
'website': instance.website,
'documentation': instance.documentation,
'privacy_policy': instance.privacyPolicy,
'terms_of_service': instance.termsOfService,
};
_BotSecret _$BotSecretFromJson(Map<String, dynamic> json) => _BotSecret(
id: json['id'] as String? ?? '',
secret: json['secret'] as String? ?? '',
description: json['description'] as String?,
expiredAt:
json['expired_at'] == null
? null
: DateTime.parse(json['expired_at'] as String),
botId: json['bot_id'] as String? ?? '',
);
Map<String, dynamic> _$BotSecretToJson(_BotSecret instance) =>
<String, dynamic>{
'id': instance.id,
'secret': instance.secret,
'description': instance.description,
'expired_at': instance.expiredAt?.toIso8601String(),
'bot_id': instance.botId,
};

View File

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

View File

@@ -1410,7 +1410,7 @@ $SnAccountStatusCopyWith<$Res>? get status {
/// @nodoc
mixin _$SnChatSummary {
int get unreadCount; SnChatMessage get lastMessage;
int get unreadCount; SnChatMessage? get lastMessage;
/// Create a copy of SnChatSummary
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -1443,11 +1443,11 @@ abstract mixin class $SnChatSummaryCopyWith<$Res> {
factory $SnChatSummaryCopyWith(SnChatSummary value, $Res Function(SnChatSummary) _then) = _$SnChatSummaryCopyWithImpl;
@useResult
$Res call({
int unreadCount, SnChatMessage lastMessage
int unreadCount, SnChatMessage? lastMessage
});
$SnChatMessageCopyWith<$Res> get lastMessage;
$SnChatMessageCopyWith<$Res>? get lastMessage;
}
/// @nodoc
@@ -1460,20 +1460,23 @@ class _$SnChatSummaryCopyWithImpl<$Res>
/// Create a copy of SnChatSummary
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? unreadCount = null,Object? lastMessage = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? unreadCount = null,Object? lastMessage = freezed,}) {
return _then(_self.copyWith(
unreadCount: null == unreadCount ? _self.unreadCount : unreadCount // ignore: cast_nullable_to_non_nullable
as int,lastMessage: null == lastMessage ? _self.lastMessage : lastMessage // ignore: cast_nullable_to_non_nullable
as SnChatMessage,
as int,lastMessage: freezed == lastMessage ? _self.lastMessage : lastMessage // ignore: cast_nullable_to_non_nullable
as SnChatMessage?,
));
}
/// Create a copy of SnChatSummary
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnChatMessageCopyWith<$Res> get lastMessage {
return $SnChatMessageCopyWith<$Res>(_self.lastMessage, (value) {
$SnChatMessageCopyWith<$Res>? get lastMessage {
if (_self.lastMessage == null) {
return null;
}
return $SnChatMessageCopyWith<$Res>(_self.lastMessage!, (value) {
return _then(_self.copyWith(lastMessage: value));
});
}
@@ -1555,7 +1558,7 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int unreadCount, SnChatMessage lastMessage)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int unreadCount, SnChatMessage? lastMessage)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnChatSummary() when $default != null:
return $default(_that.unreadCount,_that.lastMessage);case _:
@@ -1576,7 +1579,7 @@ return $default(_that.unreadCount,_that.lastMessage);case _:
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int unreadCount, SnChatMessage lastMessage) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int unreadCount, SnChatMessage? lastMessage) $default,) {final _that = this;
switch (_that) {
case _SnChatSummary():
return $default(_that.unreadCount,_that.lastMessage);}
@@ -1593,7 +1596,7 @@ return $default(_that.unreadCount,_that.lastMessage);}
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int unreadCount, SnChatMessage lastMessage)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int unreadCount, SnChatMessage? lastMessage)? $default,) {final _that = this;
switch (_that) {
case _SnChatSummary() when $default != null:
return $default(_that.unreadCount,_that.lastMessage);case _:
@@ -1612,7 +1615,7 @@ class _SnChatSummary implements SnChatSummary {
factory _SnChatSummary.fromJson(Map<String, dynamic> json) => _$SnChatSummaryFromJson(json);
@override final int unreadCount;
@override final SnChatMessage lastMessage;
@override final SnChatMessage? lastMessage;
/// Create a copy of SnChatSummary
/// with the given fields replaced by the non-null parameter values.
@@ -1647,11 +1650,11 @@ abstract mixin class _$SnChatSummaryCopyWith<$Res> implements $SnChatSummaryCopy
factory _$SnChatSummaryCopyWith(_SnChatSummary value, $Res Function(_SnChatSummary) _then) = __$SnChatSummaryCopyWithImpl;
@override @useResult
$Res call({
int unreadCount, SnChatMessage lastMessage
int unreadCount, SnChatMessage? lastMessage
});
@override $SnChatMessageCopyWith<$Res> get lastMessage;
@override $SnChatMessageCopyWith<$Res>? get lastMessage;
}
/// @nodoc
@@ -1664,11 +1667,11 @@ class __$SnChatSummaryCopyWithImpl<$Res>
/// Create a copy of SnChatSummary
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? unreadCount = null,Object? lastMessage = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? unreadCount = null,Object? lastMessage = freezed,}) {
return _then(_SnChatSummary(
unreadCount: null == unreadCount ? _self.unreadCount : unreadCount // ignore: cast_nullable_to_non_nullable
as int,lastMessage: null == lastMessage ? _self.lastMessage : lastMessage // ignore: cast_nullable_to_non_nullable
as SnChatMessage,
as int,lastMessage: freezed == lastMessage ? _self.lastMessage : lastMessage // ignore: cast_nullable_to_non_nullable
as SnChatMessage?,
));
}
@@ -1676,9 +1679,12 @@ as SnChatMessage,
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnChatMessageCopyWith<$Res> get lastMessage {
return $SnChatMessageCopyWith<$Res>(_self.lastMessage, (value) {
$SnChatMessageCopyWith<$Res>? get lastMessage {
if (_self.lastMessage == null) {
return null;
}
return $SnChatMessageCopyWith<$Res>(_self.lastMessage!, (value) {
return _then(_self.copyWith(lastMessage: value));
});
}

View File

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

View File

@@ -0,0 +1,23 @@
class DevProject {
final String id;
final String slug;
final String name;
final String? description;
DevProject({
required this.id,
required this.slug,
required this.name,
this.description,
});
factory DevProject.fromJson(Map<String, dynamic> json) {
return DevProject(
id: json['id'],
slug: json['slug'],
name: json['name'],
description: json['description'],
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,11 +6,17 @@ 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/developers/apps.dart';
import 'package:island/screens/account/credits.dart';
import 'package:island/screens/developers/edit_app.dart';
import 'package:island/screens/developers/edit_bot.dart';
import 'package:island/screens/developers/new_app.dart';
import 'package:island/screens/developers/hub.dart';
import 'package:island/screens/developers/projects.dart';
import 'package:island/screens/developers/edit_project.dart';
import 'package:island/screens/developers/new_project.dart';
import 'package:island/screens/developers/project_detail.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 +39,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 +60,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>();
@@ -286,30 +295,90 @@ final routerProvider = Provider<GoRouter>((ref) {
builder: (context, state) => const DeveloperHubScreen(),
),
GoRoute(
name: 'developerApps',
path: '/developers/:name/apps',
name: 'developerProjects',
path: '/developers/:name/projects',
builder:
(context, state) => CustomAppsScreen(
(context, state) => DevProjectsScreen(
publisherName: state.pathParameters['name']!,
),
),
GoRoute(
name: 'developerAppNew',
path: '/developers/:name/apps/new',
name: 'developerProjectNew',
path: '/developers/:name/projects/new',
builder:
(context, state) => NewCustomAppScreen(
(context, state) => NewProjectScreen(
publisherName: state.pathParameters['name']!,
),
),
GoRoute(
name: 'developerAppEdit',
path: '/developers/:name/apps/:id',
name: 'developerProjectEdit',
path: '/developers/:name/projects/:id/edit',
builder:
(context, state) => EditAppScreen(
(context, state) => EditProjectScreen(
publisherName: state.pathParameters['name']!,
id: state.pathParameters['id']!,
),
),
GoRoute(
name: 'developerProjectDetail',
path: '/developers/:name/projects/:projectId',
builder:
(context, state) => ProjectDetailScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
),
routes: [
GoRoute(
name: 'developerAppNew',
path: 'apps/new',
builder:
(context, state) => NewCustomAppScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
),
),
GoRoute(
name: 'developerAppEdit',
path: 'apps/:id/edit',
builder:
(context, state) => EditAppScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
id: state.pathParameters['id']!,
),
),
GoRoute(
name: 'developerBotNew',
path: 'bots/new',
builder:
(context, state) => EditBotScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
),
),
GoRoute(
name: 'developerBotEdit',
path: 'bots/:id/edit',
builder:
(context, state) => EditBotScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
id: state.pathParameters['id']!,
),
),
GoRoute(
name: 'developerBotDetail',
path: 'bots/:id/detail',
builder:
(context, state) => EditBotScreen(
// Assuming EditBotScreen can also serve as a detail view
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
id: state.pathParameters['id']!,
),
),
],
),
],
),
@@ -376,12 +445,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 +462,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 +478,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 +612,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 +638,11 @@ final routerProvider = Provider<GoRouter>((ref) {
path: '/account/wallet',
builder: (context, state) => const WalletScreen(),
),
GoRoute(
name: 'socialCredits',
path: '/account/credits',
builder: (context, state) => const SocialCreditsScreen(),
),
GoRoute(
name: 'relationships',
path: '/account/relationships',

View File

@@ -236,6 +236,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');

View File

@@ -0,0 +1,152 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/account.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart';
part 'credits.g.dart';
@riverpod
Future<double> socialCredits(Ref ref) async {
final client = ref.watch(apiClientProvider);
final response = await client.get('/id/accounts/me/credits');
if (response.statusCode != 200) {
throw Exception('Failed to load social credits');
}
return response.data?.toDouble() ?? 0.0;
}
@riverpod
class SocialCreditHistoryNotifier extends _$SocialCreditHistoryNotifier
with CursorPagingNotifierMixin<SnSocialCreditRecord> {
static const int _pageSize = 20;
@override
Future<CursorPagingData<SnSocialCreditRecord>> build() => fetch(cursor: null);
@override
Future<CursorPagingData<SnSocialCreditRecord>> fetch({
required String? cursor,
}) async {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final queryParams = {'offset': offset, 'take': _pageSize};
final response = await client.get(
'/id/accounts/me/credits/history',
queryParameters: queryParams,
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final records =
data.map((json) => SnSocialCreditRecord.fromJson(json)).toList();
final hasMore = offset + records.length < total;
final nextCursor = hasMore ? (offset + records.length).toString() : null;
return CursorPagingData(
items: records,
hasMore: hasMore,
nextCursor: nextCursor,
);
}
}
class SocialCreditsScreen extends HookConsumerWidget {
const SocialCreditsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final socialCredits = ref.watch(socialCreditsProvider);
return AppScaffold(
appBar: AppBar(title: Text('socialCredits').tr()),
body: Column(
children: [
Card(
margin: EdgeInsets.only(left: 16, right: 16, top: 8),
child: socialCredits
.when(
data:
(credits) => Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
credits < 100
? 'socialCreditsLevelPoor'.tr()
: credits < 150
? 'socialCreditsLevelNormal'.tr()
: credits < 200
? 'socialCreditsLevelGood'.tr()
: 'socialCreditsLevelExcellent'.tr(),
).tr().bold().fontSize(20),
Text(
'${credits.toStringAsFixed(2)} pts',
).fontSize(14),
const Gap(8),
LinearProgressIndicator(value: credits / 200),
],
),
Positioned(
right: 0,
top: 0,
child: IconButton(
onPressed: () {},
icon: const Icon(Symbols.info),
tooltip: 'socialCreditsDescription'.tr(),
),
),
],
),
error: (_, _) => Text('Error loading credits'),
loading: () => const LinearProgressIndicator(),
)
.padding(horizontal: 20, vertical: 16),
),
Expanded(
child: PagingHelperView(
provider: socialCreditHistoryNotifierProvider,
futureRefreshable: socialCreditHistoryNotifierProvider.future,
notifierRefreshable: socialCreditHistoryNotifierProvider.notifier,
contentBuilder:
(data, widgetCount, endItemView) => ListView.builder(
padding: EdgeInsets.zero,
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
final record = data.items[index];
return ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text(record.reason),
subtitle: Text(
DateFormat.yMMMd().format(record.createdAt),
),
trailing: Text(
record.delta > 0
? '+${record.delta}'
: '${record.delta}',
style: TextStyle(
color: record.delta > 0 ? Colors.green : Colors.red,
),
),
);
},
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,49 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'credits.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$socialCreditsHash() => r'2599844e892127ee4d315caced5c10e4dbaea142';
/// See also [socialCredits].
@ProviderFor(socialCredits)
final socialCreditsProvider = AutoDisposeFutureProvider<double>.internal(
socialCredits,
name: r'socialCreditsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$socialCreditsHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef SocialCreditsRef = AutoDisposeFutureProviderRef<double>;
String _$socialCreditHistoryNotifierHash() =>
r'950db020754160f835c64cedf3fa2175e61e4d64';
/// See also [SocialCreditHistoryNotifier].
@ProviderFor(SocialCreditHistoryNotifier)
final socialCreditHistoryNotifierProvider = AutoDisposeAsyncNotifierProvider<
SocialCreditHistoryNotifier,
CursorPagingData<SnSocialCreditRecord>
>.internal(
SocialCreditHistoryNotifier.new,
name: r'socialCreditHistoryNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$socialCreditHistoryNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SocialCreditHistoryNotifier =
AutoDisposeAsyncNotifier<CursorPagingData<SnSocialCreditRecord>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/custom_app.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
@@ -16,154 +15,179 @@ import 'package:styled_widget/styled_widget.dart';
part 'apps.g.dart';
@riverpod
Future<List<CustomApp>> customApps(Ref ref, String publisherName) async {
Future<List<CustomApp>> customApps(
Ref ref,
String publisherName,
String projectId,
) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/develop/developers/$publisherName/apps');
return resp.data.map((e) => CustomApp.fromJson(e)).cast<CustomApp>().toList();
final resp = await client.get(
'/develop/developers/$publisherName/projects/$projectId/apps',
);
return (resp.data as List)
.map((e) => CustomApp.fromJson(e))
.cast<CustomApp>()
.toList();
}
class CustomAppsScreen extends HookConsumerWidget {
final String publisherName;
const CustomAppsScreen({super.key, required this.publisherName});
final String projectId;
const CustomAppsScreen({
super.key,
required this.publisherName,
required this.projectId,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final apps = ref.watch(customAppsProvider(publisherName));
final apps = ref.watch(customAppsProvider(publisherName, projectId));
return AppScaffold(
appBar: AppBar(
title: Text('customApps').tr(),
actions: [
IconButton(
icon: const Icon(Symbols.add),
onPressed: () {
context.pushNamed(
'developerAppNew',
pathParameters: {'name': publisherName},
return apps.when(
data: (data) {
if (data.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('noCustomApps').tr(),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () {
context.pushNamed(
'developerAppNew',
pathParameters: {
'name': publisherName,
'projectId': projectId,
},
);
},
icon: const Icon(Symbols.add),
label: Text('createCustomApp').tr(),
),
],
),
);
}
return RefreshIndicator(
onRefresh:
() => ref.refresh(
customAppsProvider(publisherName, projectId).future,
),
child: ListView.builder(
padding: EdgeInsets.only(top: 4),
itemCount: data.length,
itemBuilder: (context, index) {
final app = data[index];
return Card(
margin: const EdgeInsets.all(8.0),
child: Column(
children: [
SizedBox(
height: 150,
child: Stack(
fit: StackFit.expand,
children: [
if (app.background != null)
CloudFileWidget(
item: app.background!,
fit: BoxFit.cover,
).clipRRect(topLeft: 8, topRight: 8),
if (app.picture != null)
Positioned(
left: 16,
bottom: 16,
child: ProfilePictureWidget(
fileId: app.picture!.id,
radius: 40,
fallbackIcon: Symbols.apps,
),
),
],
),
),
ListTile(
title: Text(app.name),
subtitle: Text(
app.slug,
style: GoogleFonts.robotoMono(fontSize: 12),
),
contentPadding: EdgeInsets.only(left: 20, right: 12),
trailing: PopupMenuButton(
itemBuilder:
(context) => [
PopupMenuItem(
value: 'edit',
child: Row(
children: [
const Icon(Symbols.edit),
const SizedBox(width: 12),
Text('edit').tr(),
],
),
),
PopupMenuItem(
value: 'delete',
child: Row(
children: [
const Icon(
Symbols.delete,
color: Colors.red,
),
const SizedBox(width: 12),
Text(
'delete',
style: TextStyle(color: Colors.red),
).tr(),
],
),
),
],
onSelected: (value) {
if (value == 'edit') {
context.pushNamed(
'developerAppEdit',
pathParameters: {
'name': publisherName,
'projectId': projectId,
'id': app.id,
},
);
} else if (value == 'delete') {
showConfirmAlert(
'deleteCustomAppHint'.tr(),
'deleteCustomApp'.tr(),
).then((confirm) {
if (confirm) {
final client = ref.read(apiClientProvider);
client.delete(
'/develop/developers/$publisherName/projects/$projectId/apps/${app.id}',
);
ref.invalidate(
customAppsProvider(publisherName, projectId),
);
}
});
}
},
),
),
],
),
);
},
),
],
),
body: apps.when(
data: (data) {
if (data.isEmpty) {
return Center(child: Text('noCustomApps').tr());
}
return RefreshIndicator(
onRefresh:
() => ref.refresh(customAppsProvider(publisherName).future),
child: ListView.builder(
padding: EdgeInsets.only(top: 4),
itemCount: data.length,
itemBuilder: (context, index) {
final app = data[index];
return Card(
margin: const EdgeInsets.all(8.0),
child: Column(
children: [
SizedBox(
height: 150,
child: Stack(
fit: StackFit.expand,
children: [
if (app.background != null)
CloudFileWidget(
item: app.background!,
fit: BoxFit.cover,
).clipRRect(topLeft: 8, topRight: 8),
if (app.picture != null)
Positioned(
left: 16,
bottom: 16,
child: ProfilePictureWidget(
fileId: app.picture!.id,
radius: 40,
fallbackIcon: Symbols.apps,
),
),
],
),
),
ListTile(
title: Text(app.name),
subtitle: Text(
app.slug,
style: GoogleFonts.robotoMono(fontSize: 12),
),
contentPadding: EdgeInsets.only(left: 20, right: 12),
trailing: PopupMenuButton(
itemBuilder:
(context) => [
PopupMenuItem(
value: 'edit',
child: Row(
children: [
const Icon(Symbols.edit),
const SizedBox(width: 12),
Text('edit').tr(),
],
),
),
PopupMenuItem(
value: 'delete',
child: Row(
children: [
const Icon(
Symbols.delete,
color: Colors.red,
),
const SizedBox(width: 12),
Text(
'delete',
style: TextStyle(color: Colors.red),
).tr(),
],
),
),
],
onSelected: (value) {
if (value == 'edit') {
context.pushNamed(
'developerAppEdit',
pathParameters: {
'name': publisherName,
'id': app.id,
},
);
} else if (value == 'delete') {
showConfirmAlert(
'deleteCustomAppHint'.tr(),
'deleteCustomApp'.tr(),
).then((confirm) {
if (confirm) {
final client = ref.read(apiClientProvider);
client.delete(
'/develop/developers/$publisherName/apps/${app.id}',
);
ref.invalidate(
customAppsProvider(publisherName),
);
}
});
}
},
),
),
],
),
);
},
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(err, stack) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(customAppsProvider(publisherName)),
),
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(err, stack) => ResponseErrorWidget(
error: err,
onRetry:
() => ref.invalidate(
customAppsProvider(publisherName, projectId),
),
),
);
}
}

View File

@@ -6,7 +6,7 @@ part of 'apps.dart';
// RiverpodGenerator
// **************************************************************************
String _$customAppsHash() => r'c6ac78060eb51a2b208a749a81ecbe0a9c608ce1';
String _$customAppsHash() => r'c36e5ee59f16a29220dc0e9fba65e579d341a28f';
/// Copied from Dart SDK
class _SystemHash {
@@ -39,15 +39,15 @@ class CustomAppsFamily extends Family<AsyncValue<List<CustomApp>>> {
const CustomAppsFamily();
/// See also [customApps].
CustomAppsProvider call(String publisherName) {
return CustomAppsProvider(publisherName);
CustomAppsProvider call(String publisherName, String projectId) {
return CustomAppsProvider(publisherName, projectId);
}
@override
CustomAppsProvider getProviderOverride(
covariant CustomAppsProvider provider,
) {
return call(provider.publisherName);
return call(provider.publisherName, provider.projectId);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@@ -68,9 +68,9 @@ class CustomAppsFamily extends Family<AsyncValue<List<CustomApp>>> {
/// See also [customApps].
class CustomAppsProvider extends AutoDisposeFutureProvider<List<CustomApp>> {
/// See also [customApps].
CustomAppsProvider(String publisherName)
CustomAppsProvider(String publisherName, String projectId)
: this._internal(
(ref) => customApps(ref as CustomAppsRef, publisherName),
(ref) => customApps(ref as CustomAppsRef, publisherName, projectId),
from: customAppsProvider,
name: r'customAppsProvider',
debugGetCreateSourceHash:
@@ -80,6 +80,7 @@ class CustomAppsProvider extends AutoDisposeFutureProvider<List<CustomApp>> {
dependencies: CustomAppsFamily._dependencies,
allTransitiveDependencies: CustomAppsFamily._allTransitiveDependencies,
publisherName: publisherName,
projectId: projectId,
);
CustomAppsProvider._internal(
@@ -90,9 +91,11 @@ class CustomAppsProvider extends AutoDisposeFutureProvider<List<CustomApp>> {
required super.debugGetCreateSourceHash,
required super.from,
required this.publisherName,
required this.projectId,
}) : super.internal();
final String publisherName;
final String projectId;
@override
Override overrideWith(
@@ -108,6 +111,7 @@ class CustomAppsProvider extends AutoDisposeFutureProvider<List<CustomApp>> {
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
publisherName: publisherName,
projectId: projectId,
),
);
}
@@ -119,13 +123,16 @@ class CustomAppsProvider extends AutoDisposeFutureProvider<List<CustomApp>> {
@override
bool operator ==(Object other) {
return other is CustomAppsProvider && other.publisherName == publisherName;
return other is CustomAppsProvider &&
other.publisherName == publisherName &&
other.projectId == projectId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, publisherName.hashCode);
hash = _SystemHash.combine(hash, projectId.hashCode);
return _SystemHash.finish(hash);
}
@@ -136,6 +143,9 @@ class CustomAppsProvider extends AutoDisposeFutureProvider<List<CustomApp>> {
mixin CustomAppsRef on AutoDisposeFutureProviderRef<List<CustomApp>> {
/// The parameter `publisherName` of this provider.
String get publisherName;
/// The parameter `projectId` of this provider.
String get projectId;
}
class _CustomAppsProviderElement
@@ -145,6 +155,8 @@ class _CustomAppsProviderElement
@override
String get publisherName => (origin as CustomAppsProvider).publisherName;
@override
String get projectId => (origin as CustomAppsProvider).projectId;
}
// ignore_for_file: type=lint

View File

@@ -0,0 +1,162 @@
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/bot.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'bots.g.dart';
@riverpod
Future<List<Bot>> bots(Ref ref, String publisherName, String projectId) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get(
'/develop/developers/$publisherName/projects/$projectId/bots',
);
return (resp.data as List).map((e) => Bot.fromJson(e)).cast<Bot>().toList();
}
class BotsScreen extends HookConsumerWidget {
final String publisherName;
final String projectId;
const BotsScreen({
super.key,
required this.publisherName,
required this.projectId,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final botsList = ref.watch(botsProvider(publisherName, projectId));
return botsList.when(
data: (data) {
if (data.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('noBots').tr(),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () {
context.pushNamed(
'developerBotNew',
pathParameters: {
'name': publisherName,
'projectId': projectId,
},
);
},
icon: const Icon(Symbols.add),
label: Text('createBot').tr(),
),
],
),
);
}
return RefreshIndicator(
onRefresh:
() => ref.refresh(botsProvider(publisherName, projectId).future),
child: ListView.builder(
padding: const EdgeInsets.only(top: 4),
itemCount: data.length,
itemBuilder: (context, index) {
final bot = data[index];
return Card(
margin: const EdgeInsets.all(8.0),
child: ListTile(
leading: CircleAvatar(
child:
bot.picture != null
? CloudFileWidget(item: bot.picture!)
: const Icon(Symbols.smart_toy),
),
title: Text(bot.name),
subtitle: Text(bot.description ?? ''),
trailing: PopupMenuButton(
itemBuilder:
(context) => [
PopupMenuItem(
value: 'edit',
child: Row(
children: [
const Icon(Symbols.edit),
const SizedBox(width: 12),
Text('edit').tr(),
],
),
),
PopupMenuItem(
value: 'delete',
child: Row(
children: [
const Icon(Symbols.delete, color: Colors.red),
const SizedBox(width: 12),
Text(
'delete',
style: TextStyle(color: Colors.red),
).tr(),
],
),
),
],
onSelected: (value) {
if (value == 'edit') {
context.pushNamed(
'developerBotEdit',
pathParameters: {
'name': publisherName,
'projectId': projectId,
'id': bot.id,
},
);
} else if (value == 'delete') {
showConfirmAlert(
'deleteBotHint'.tr(),
'deleteBot'.tr(),
).then((confirm) {
if (confirm) {
final client = ref.read(apiClientProvider);
client.delete(
'/develop/developers/$publisherName/projects/$projectId/bots/${bot.id}',
);
ref.invalidate(
botsProvider(publisherName, projectId),
);
}
});
}
},
),
onTap: () {
context.pushNamed(
'developerBotDetail',
pathParameters: {
'name': publisherName,
'projectId': projectId,
'id': bot.id,
},
);
},
),
);
},
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(err, stack) => ResponseErrorWidget(
error: err,
onRetry:
() => ref.invalidate(botsProvider(publisherName, projectId)),
),
);
}
}

View File

@@ -0,0 +1,156 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'bots.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$botsHash() => r'a54c8b4df23f94754398706779044903fcca6eea';
/// 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));
}
}
/// See also [bots].
@ProviderFor(bots)
const botsProvider = BotsFamily();
/// See also [bots].
class BotsFamily extends Family<AsyncValue<List<Bot>>> {
/// See also [bots].
const BotsFamily();
/// See also [bots].
BotsProvider call(String publisherName, String projectId) {
return BotsProvider(publisherName, projectId);
}
@override
BotsProvider getProviderOverride(covariant BotsProvider provider) {
return call(provider.publisherName, provider.projectId);
}
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'botsProvider';
}
/// See also [bots].
class BotsProvider extends AutoDisposeFutureProvider<List<Bot>> {
/// See also [bots].
BotsProvider(String publisherName, String projectId)
: this._internal(
(ref) => bots(ref as BotsRef, publisherName, projectId),
from: botsProvider,
name: r'botsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$botsHash,
dependencies: BotsFamily._dependencies,
allTransitiveDependencies: BotsFamily._allTransitiveDependencies,
publisherName: publisherName,
projectId: projectId,
);
BotsProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.publisherName,
required this.projectId,
}) : super.internal();
final String publisherName;
final String projectId;
@override
Override overrideWith(FutureOr<List<Bot>> Function(BotsRef provider) create) {
return ProviderOverride(
origin: this,
override: BotsProvider._internal(
(ref) => create(ref as BotsRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
publisherName: publisherName,
projectId: projectId,
),
);
}
@override
AutoDisposeFutureProviderElement<List<Bot>> createElement() {
return _BotsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is BotsProvider &&
other.publisherName == publisherName &&
other.projectId == projectId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, publisherName.hashCode);
hash = _SystemHash.combine(hash, projectId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin BotsRef on AutoDisposeFutureProviderRef<List<Bot>> {
/// The parameter `publisherName` of this provider.
String get publisherName;
/// The parameter `projectId` of this provider.
String get projectId;
}
class _BotsProviderElement extends AutoDisposeFutureProviderElement<List<Bot>>
with BotsRef {
_BotsProviderElement(super.provider);
@override
String get publisherName => (origin as BotsProvider).publisherName;
@override
String get projectId => (origin as BotsProvider).projectId;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -22,21 +22,37 @@ import 'package:island/widgets/content/sheet.dart';
part 'edit_app.g.dart';
@riverpod
Future<CustomApp?> customApp(Ref ref, String publisherName, String id) async {
Future<CustomApp?> customApp(
Ref ref,
String publisherName,
String projectId,
String id,
) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/develop/developers/$publisherName/apps/$id');
final resp = await client.get(
'/develop/developers/$publisherName/projects/$projectId/apps/$id',
);
return CustomApp.fromJson(resp.data);
}
class EditAppScreen extends HookConsumerWidget {
final String publisherName;
final String projectId;
final String? id;
const EditAppScreen({super.key, required this.publisherName, this.id});
const EditAppScreen({
super.key,
required this.publisherName,
required this.projectId,
this.id,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isNew = id == null;
final app = isNew ? null : ref.watch(customAppProvider(publisherName, id!));
final app =
isNew
? null
: ref.watch(customAppProvider(publisherName, projectId, id!));
final formKey = useMemoized(() => GlobalKey<FormState>());
@@ -283,16 +299,16 @@ class EditAppScreen extends HookConsumerWidget {
};
if (isNew) {
await client.post(
'/develop/developers/$publisherName/apps',
'/develop/developers/$publisherName/projects/$projectId/apps',
data: data,
);
} else {
await client.patch(
'/develop/developers/$publisherName/apps/$id',
'/develop/developers/$publisherName/projects/$projectId/apps/$id',
data: data,
);
}
ref.invalidate(customAppsProvider(publisherName));
ref.invalidate(customAppsProvider(publisherName, projectId));
if (context.mounted) {
Navigator.pop(context);
}
@@ -309,7 +325,9 @@ class EditAppScreen extends HookConsumerWidget {
? ResponseErrorWidget(
error: app!.error,
onRetry:
() => ref.invalidate(customAppProvider(publisherName, id!)),
() => ref.invalidate(
customAppProvider(publisherName, projectId, id!),
),
)
: SingleChildScrollView(
child: Column(

View File

@@ -6,7 +6,7 @@ part of 'edit_app.dart';
// RiverpodGenerator
// **************************************************************************
String _$customAppHash() => r'42ad937b8439c793e3c5c35568bb5fa4da017df3';
String _$customAppHash() => r'17b3d1385e59bc5ee7f13fb0f11c56cf8a9ba41f';
/// Copied from Dart SDK
class _SystemHash {
@@ -39,13 +39,13 @@ class CustomAppFamily extends Family<AsyncValue<CustomApp?>> {
const CustomAppFamily();
/// See also [customApp].
CustomAppProvider call(String publisherName, String id) {
return CustomAppProvider(publisherName, id);
CustomAppProvider call(String publisherName, String projectId, String id) {
return CustomAppProvider(publisherName, projectId, id);
}
@override
CustomAppProvider getProviderOverride(covariant CustomAppProvider provider) {
return call(provider.publisherName, provider.id);
return call(provider.publisherName, provider.projectId, provider.id);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@@ -66,9 +66,9 @@ class CustomAppFamily extends Family<AsyncValue<CustomApp?>> {
/// See also [customApp].
class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp?> {
/// See also [customApp].
CustomAppProvider(String publisherName, String id)
CustomAppProvider(String publisherName, String projectId, String id)
: this._internal(
(ref) => customApp(ref as CustomAppRef, publisherName, id),
(ref) => customApp(ref as CustomAppRef, publisherName, projectId, id),
from: customAppProvider,
name: r'customAppProvider',
debugGetCreateSourceHash:
@@ -78,6 +78,7 @@ class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp?> {
dependencies: CustomAppFamily._dependencies,
allTransitiveDependencies: CustomAppFamily._allTransitiveDependencies,
publisherName: publisherName,
projectId: projectId,
id: id,
);
@@ -89,10 +90,12 @@ class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp?> {
required super.debugGetCreateSourceHash,
required super.from,
required this.publisherName,
required this.projectId,
required this.id,
}) : super.internal();
final String publisherName;
final String projectId;
final String id;
@override
@@ -109,6 +112,7 @@ class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp?> {
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
publisherName: publisherName,
projectId: projectId,
id: id,
),
);
@@ -123,6 +127,7 @@ class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp?> {
bool operator ==(Object other) {
return other is CustomAppProvider &&
other.publisherName == publisherName &&
other.projectId == projectId &&
other.id == id;
}
@@ -130,6 +135,7 @@ class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp?> {
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, publisherName.hashCode);
hash = _SystemHash.combine(hash, projectId.hashCode);
hash = _SystemHash.combine(hash, id.hashCode);
return _SystemHash.finish(hash);
@@ -142,6 +148,9 @@ mixin CustomAppRef on AutoDisposeFutureProviderRef<CustomApp?> {
/// The parameter `publisherName` of this provider.
String get publisherName;
/// The parameter `projectId` of this provider.
String get projectId;
/// The parameter `id` of this provider.
String get id;
}
@@ -154,6 +163,8 @@ class _CustomAppProviderElement
@override
String get publisherName => (origin as CustomAppProvider).publisherName;
@override
String get projectId => (origin as CustomAppProvider).projectId;
@override
String get id => (origin as CustomAppProvider).id;
}

View File

@@ -0,0 +1,279 @@
import 'package:croppy/croppy.dart' hide cropImage;
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:island/models/bot.dart';
import 'package:island/models/file.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/file.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
part 'edit_bot.g.dart';
@riverpod
Future<Bot?> bot(
Ref ref,
String publisherName,
String projectId,
String id,
) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get(
'/develop/developers/$publisherName/projects/$projectId/bots/$id',
);
return Bot.fromJson(resp.data);
}
class EditBotScreen extends HookConsumerWidget {
final String publisherName;
final String projectId;
final String? id;
const EditBotScreen({
super.key,
required this.publisherName,
required this.projectId,
this.id,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isNew = id == null;
final botData =
isNew ? null : ref.watch(botProvider(publisherName, projectId, id!));
final formKey = useMemoized(() => GlobalKey<FormState>());
final submitting = useState(false);
final nameController = useTextEditingController();
final slugController = useTextEditingController();
final descriptionController = useTextEditingController();
final picture = useState<SnCloudFile?>(null);
final websiteController = useTextEditingController();
final documentationController = useTextEditingController();
final isPublic = useState(false);
final isInteractive = useState(false);
useEffect(() {
if (botData?.value != null) {
nameController.text = botData!.value!.name;
slugController.text = botData.value!.slug;
descriptionController.text = botData.value!.description ?? '';
picture.value = botData.value!.picture;
websiteController.text = botData.value!.links?.website ?? '';
documentationController.text =
botData.value!.links?.documentation ?? '';
isPublic.value = botData.value!.config?.isPublic ?? false;
isInteractive.value = botData.value!.config?.isInteractive ?? false;
}
return null;
}, [botData]);
void setPicture() async {
showLoadingModal(context);
var result = await ref
.read(imagePickerProvider)
.pickImage(source: ImageSource.gallery);
if (result == null) {
if (context.mounted) hideLoadingModal(context);
return;
}
if (!context.mounted) return;
hideLoadingModal(context);
result = await cropImage(
context,
image: result,
allowedAspectRatios: [const CropAspectRatio(height: 1, width: 1)],
);
if (result == null) {
if (context.mounted) hideLoadingModal(context);
return;
}
if (!context.mounted) return;
showLoadingModal(context);
submitting.value = true;
try {
final baseUrl = ref.watch(serverUrlProvider);
final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Token is null');
final cloudFile =
await putMediaToCloud(
fileData: UniversalFile(
data: result,
type: UniversalFileType.image,
),
atk: token,
baseUrl: baseUrl,
filename: result.name,
mimetype: result.mimeType ?? 'image/jpeg',
).future;
if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...');
}
picture.value = cloudFile;
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
submitting.value = false;
}
}
void performAction() async {
final client = ref.read(apiClientProvider);
final data = {
'name': nameController.text,
'slug': slugController.text,
'description': descriptionController.text,
'picture_id': picture.value?.id,
'config': {
'is_public': isPublic.value,
'is_interactive': isInteractive.value,
},
'links': {
'website':
websiteController.text.isNotEmpty ? websiteController.text : null,
'documentation':
documentationController.text.isNotEmpty
? documentationController.text
: null,
},
};
if (isNew) {
await client.post(
'/develop/developers/$publisherName/projects/$projectId/bots',
data: data,
);
} else {
await client.patch(
'/develop/developers/$publisherName/projects/$projectId/bots/$id',
data: data,
);
}
if (context.mounted) {
context.pop();
}
}
return AppScaffold(
appBar: AppBar(title: Text(isNew ? 'createBot'.tr() : 'editBot'.tr())),
body:
botData == null && !isNew
? const Center(child: CircularProgressIndicator())
: botData?.hasError == true && !isNew
? ResponseErrorWidget(
error: botData!.error,
onRetry:
() => ref.invalidate(
botProvider(publisherName, projectId, id!),
),
)
: SingleChildScrollView(
child: Column(
children: [
AspectRatio(
aspectRatio: 1,
child: GestureDetector(
onTap: setPicture,
child: Container(
color:
Theme.of(
context,
).colorScheme.surfaceContainerHigh,
child:
picture.value != null
? CloudFileWidget(
item: picture.value!,
fit: BoxFit.cover,
)
: const Icon(Symbols.smart_toy, size: 48),
),
),
).padding(bottom: 32),
Form(
key: formKey,
child: Column(
children: [
TextFormField(
controller: nameController,
decoration: InputDecoration(labelText: 'name'.tr()),
),
const SizedBox(height: 16),
TextFormField(
controller: slugController,
decoration: InputDecoration(
labelText: 'slug'.tr(),
helperText: 'slugHint'.tr(),
),
),
const SizedBox(height: 16),
TextFormField(
controller: descriptionController,
decoration: InputDecoration(
labelText: 'description'.tr(),
alignLabelWithHint: true,
),
maxLines: 3,
),
const SizedBox(height: 16),
TextFormField(
controller: websiteController,
decoration: InputDecoration(
labelText: 'websiteUrl'.tr(),
hintText: 'https://example.com',
),
keyboardType: TextInputType.url,
),
const SizedBox(height: 16),
TextFormField(
controller: documentationController,
decoration: InputDecoration(
labelText: 'documentationUrl'.tr(),
hintText: 'https://example.com/docs',
),
keyboardType: TextInputType.url,
),
const SizedBox(height: 16),
SwitchListTile(
title: Text('isPublic').tr(),
value: isPublic.value,
onChanged: (value) => isPublic.value = value,
),
SwitchListTile(
title: Text('isInteractive').tr(),
value: isInteractive.value,
onChanged: (value) => isInteractive.value = value,
),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed:
submitting.value ? null : performAction,
label: Text('saveChanges'.tr()),
icon: const Icon(Symbols.save),
),
),
],
).padding(all: 24),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,167 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'edit_bot.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$botHash() => r'a3e412ed575c513434bc718b7920db1d017111f4';
/// 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));
}
}
/// See also [bot].
@ProviderFor(bot)
const botProvider = BotFamily();
/// See also [bot].
class BotFamily extends Family<AsyncValue<Bot?>> {
/// See also [bot].
const BotFamily();
/// See also [bot].
BotProvider call(String publisherName, String projectId, String id) {
return BotProvider(publisherName, projectId, id);
}
@override
BotProvider getProviderOverride(covariant BotProvider provider) {
return call(provider.publisherName, provider.projectId, provider.id);
}
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'botProvider';
}
/// See also [bot].
class BotProvider extends AutoDisposeFutureProvider<Bot?> {
/// See also [bot].
BotProvider(String publisherName, String projectId, String id)
: this._internal(
(ref) => bot(ref as BotRef, publisherName, projectId, id),
from: botProvider,
name: r'botProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$botHash,
dependencies: BotFamily._dependencies,
allTransitiveDependencies: BotFamily._allTransitiveDependencies,
publisherName: publisherName,
projectId: projectId,
id: id,
);
BotProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.publisherName,
required this.projectId,
required this.id,
}) : super.internal();
final String publisherName;
final String projectId;
final String id;
@override
Override overrideWith(FutureOr<Bot?> Function(BotRef provider) create) {
return ProviderOverride(
origin: this,
override: BotProvider._internal(
(ref) => create(ref as BotRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
publisherName: publisherName,
projectId: projectId,
id: id,
),
);
}
@override
AutoDisposeFutureProviderElement<Bot?> createElement() {
return _BotProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is BotProvider &&
other.publisherName == publisherName &&
other.projectId == projectId &&
other.id == id;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, publisherName.hashCode);
hash = _SystemHash.combine(hash, projectId.hashCode);
hash = _SystemHash.combine(hash, id.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin BotRef on AutoDisposeFutureProviderRef<Bot?> {
/// The parameter `publisherName` of this provider.
String get publisherName;
/// The parameter `projectId` of this provider.
String get projectId;
/// The parameter `id` of this provider.
String get id;
}
class _BotProviderElement extends AutoDisposeFutureProviderElement<Bot?>
with BotRef {
_BotProviderElement(super.provider);
@override
String get publisherName => (origin as BotProvider).publisherName;
@override
String get projectId => (origin as BotProvider).projectId;
@override
String get id => (origin as BotProvider).id;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -0,0 +1,130 @@
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/dev_project.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/developers/projects.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
part 'edit_project.g.dart';
@riverpod
Future<DevProject?> devProject(Ref ref, String pubName, String id) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/develop/developers/$pubName/projects/$id');
return DevProject.fromJson(resp.data);
}
class EditProjectScreen extends HookConsumerWidget {
final String publisherName;
final String? id;
const EditProjectScreen({super.key, required this.publisherName, this.id});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isNew = id == null;
final projectData =
isNew ? null : ref.watch(devProjectProvider(publisherName, id!));
final formKey = useMemoized(() => GlobalKey<FormState>());
final submitting = useState(false);
final nameController = useTextEditingController();
final slugController = useTextEditingController();
final descriptionController = useTextEditingController();
useEffect(() {
if (projectData?.value != null) {
nameController.text = projectData!.value!.name;
slugController.text = projectData.value!.slug;
descriptionController.text = projectData.value!.description ?? '';
}
return null;
}, [projectData]);
void performAction() async {
final client = ref.read(apiClientProvider);
final data = {
'name': nameController.text,
'slug': slugController.text,
'description': descriptionController.text,
};
if (isNew) {
await client.post(
'/develop/developers/$publisherName/projects',
data: data,
);
} else {
await client.put(
'/develop/developers/$publisherName/projects/$id',
data: data,
);
}
ref.invalidate(devProjectsProvider(publisherName));
if (context.mounted) {
context.pop();
}
}
return AppScaffold(
appBar: AppBar(
title: Text(isNew ? 'createProject'.tr() : 'editProject'.tr()),
),
body:
projectData == null && !isNew
? const Center(child: CircularProgressIndicator())
: projectData?.hasError == true && !isNew
? ResponseErrorWidget(
error: projectData!.error,
onRetry:
() =>
ref.invalidate(devProjectProvider(publisherName, id!)),
)
: SingleChildScrollView(
child: Form(
key: formKey,
child: Column(
children: [
TextFormField(
controller: nameController,
decoration: InputDecoration(labelText: 'name'.tr()),
),
const SizedBox(height: 16),
TextFormField(
controller: slugController,
decoration: InputDecoration(
labelText: 'slug'.tr(),
helperText: 'slugHint'.tr(),
),
),
const SizedBox(height: 16),
TextFormField(
controller: descriptionController,
decoration: InputDecoration(
labelText: 'description'.tr(),
alignLabelWithHint: true,
),
maxLines: 3,
),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: submitting.value ? null : performAction,
label: Text('saveChanges'.tr()),
icon: const Icon(Symbols.save),
),
),
],
).padding(all: 24),
),
),
);
}
}

View File

@@ -0,0 +1,163 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'edit_project.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$devProjectHash() => r'fc68254c6e598e3fa05c86c36f1469c0b689bc43';
/// 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));
}
}
/// See also [devProject].
@ProviderFor(devProject)
const devProjectProvider = DevProjectFamily();
/// See also [devProject].
class DevProjectFamily extends Family<AsyncValue<DevProject?>> {
/// See also [devProject].
const DevProjectFamily();
/// See also [devProject].
DevProjectProvider call(String pubName, String id) {
return DevProjectProvider(pubName, id);
}
@override
DevProjectProvider getProviderOverride(
covariant DevProjectProvider provider,
) {
return call(provider.pubName, provider.id);
}
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'devProjectProvider';
}
/// See also [devProject].
class DevProjectProvider extends AutoDisposeFutureProvider<DevProject?> {
/// See also [devProject].
DevProjectProvider(String pubName, String id)
: this._internal(
(ref) => devProject(ref as DevProjectRef, pubName, id),
from: devProjectProvider,
name: r'devProjectProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$devProjectHash,
dependencies: DevProjectFamily._dependencies,
allTransitiveDependencies: DevProjectFamily._allTransitiveDependencies,
pubName: pubName,
id: id,
);
DevProjectProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.pubName,
required this.id,
}) : super.internal();
final String pubName;
final String id;
@override
Override overrideWith(
FutureOr<DevProject?> Function(DevProjectRef provider) create,
) {
return ProviderOverride(
origin: this,
override: DevProjectProvider._internal(
(ref) => create(ref as DevProjectRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
pubName: pubName,
id: id,
),
);
}
@override
AutoDisposeFutureProviderElement<DevProject?> createElement() {
return _DevProjectProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is DevProjectProvider &&
other.pubName == pubName &&
other.id == id;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, pubName.hashCode);
hash = _SystemHash.combine(hash, id.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin DevProjectRef on AutoDisposeFutureProviderRef<DevProject?> {
/// The parameter `pubName` of this provider.
String get pubName;
/// The parameter `id` of this provider.
String get id;
}
class _DevProjectProviderElement
extends AutoDisposeFutureProviderElement<DevProject?>
with DevProjectRef {
_DevProjectProviderElement(super.provider);
@override
String get pubName => (origin as DevProjectProvider).pubName;
@override
String get id => (origin as DevProjectProvider).id;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -235,15 +235,15 @@ class DeveloperHubScreen extends HookConsumerWidget {
).padding(vertical: 12, horizontal: 12),
ListTile(
minTileHeight: 48,
title: Text('customApps').tr(),
trailing: Icon(Symbols.chevron_right),
leading: const Icon(Symbols.apps),
contentPadding: EdgeInsets.symmetric(
title: Text('projects').tr(),
trailing: const Icon(Symbols.chevron_right),
leading: const Icon(Symbols.folder_managed),
contentPadding: const EdgeInsets.symmetric(
horizontal: 24,
),
onTap: () {
context.pushNamed(
'developerApps',
'developerProjects',
pathParameters: {
'name':
currentDeveloper.value!.publisher!.name,

View File

@@ -3,10 +3,11 @@ import 'package:island/screens/developers/edit_app.dart';
class NewCustomAppScreen extends StatelessWidget {
final String publisherName;
const NewCustomAppScreen({super.key, required this.publisherName});
final String projectId;
const NewCustomAppScreen({super.key, required this.publisherName, required this.projectId});
@override
Widget build(BuildContext context) {
return EditAppScreen(publisherName: publisherName);
return EditAppScreen(publisherName: publisherName, projectId: projectId);
}
}

View File

@@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
import 'package:island/screens/developers/edit_project.dart';
class NewProjectScreen extends StatelessWidget {
final String publisherName;
const NewProjectScreen({super.key, required this.publisherName});
@override
Widget build(BuildContext context) {
return EditProjectScreen(publisherName: publisherName);
}
}

View File

@@ -0,0 +1,71 @@
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/screens/developers/apps.dart';
import 'package:island/screens/developers/bots.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart';
class ProjectDetailScreen extends HookConsumerWidget {
final String publisherName;
final String projectId;
const ProjectDetailScreen({
super.key,
required this.publisherName,
required this.projectId,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return DefaultTabController(
length: 2,
child: AppScaffold(
appBar: AppBar(
title: Text('projectDetails').tr(),
actions: [
IconButton(
icon: const Icon(Symbols.add),
onPressed: () {
// Get current tab index
final tabController = DefaultTabController.of(context);
final index = tabController.index;
if (index == 0) {
context.pushNamed(
'developerAppNew',
pathParameters: {
'name': publisherName,
'projectId': projectId
},
);
} else {
context.pushNamed(
'developerBotNew',
pathParameters: {
'name': publisherName,
'projectId': projectId
},
);
}
},
),
],
bottom: TabBar(
tabs: [
Tab(text: 'customApps'.tr()),
Tab(text: 'bots'.tr()),
],
),
),
body: TabBarView(
children: [
CustomAppsScreen(publisherName: publisherName, projectId: projectId),
BotsScreen(publisherName: publisherName, projectId: projectId),
],
),
),
);
}
}

View File

@@ -0,0 +1,144 @@
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/dev_project.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'projects.g.dart';
@riverpod
Future<List<DevProject>> devProjects(Ref ref, String pubName) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/develop/developers/$pubName/projects');
return (resp.data as List)
.map((e) => DevProject.fromJson(e))
.cast<DevProject>()
.toList();
}
class DevProjectsScreen extends HookConsumerWidget {
final String publisherName;
const DevProjectsScreen({super.key, required this.publisherName});
@override
Widget build(BuildContext context, WidgetRef ref) {
final projects = ref.watch(devProjectsProvider(publisherName));
return AppScaffold(
appBar: AppBar(
title: Text('projects').tr(),
actions: [
IconButton(
icon: const Icon(Symbols.add),
onPressed: () {
context.pushNamed(
'developerProjectNew',
pathParameters: {'name': publisherName},
);
},
),
],
),
body: projects.when(
data: (data) {
if (data.isEmpty) {
return Center(child: Text('noProjects').tr());
}
return RefreshIndicator(
onRefresh:
() => ref.refresh(devProjectsProvider(publisherName).future),
child: ListView.builder(
padding: EdgeInsets.only(top: 4),
itemCount: data.length,
itemBuilder: (context, index) {
final project = data[index];
return Card(
margin: const EdgeInsets.all(8.0),
child: ListTile(
title: Text(project.name),
subtitle: Text(project.description ?? ''),
trailing: PopupMenuButton(
itemBuilder:
(context) => [
PopupMenuItem(
value: 'edit',
child: Row(
children: [
const Icon(Symbols.edit),
const SizedBox(width: 12),
Text('edit').tr(),
],
),
),
PopupMenuItem(
value: 'delete',
child: Row(
children: [
const Icon(Symbols.delete, color: Colors.red),
const SizedBox(width: 12),
Text(
'delete',
style: TextStyle(color: Colors.red),
).tr(),
],
),
),
],
onSelected: (value) {
if (value == 'edit') {
context.pushNamed(
'developerProjectEdit',
pathParameters: {
'name': publisherName,
'id': project.id,
},
);
} else if (value == 'delete') {
showConfirmAlert(
'deleteProjectHint'.tr(),
'deleteProject'.tr(),
).then((confirm) {
if (confirm) {
final client = ref.read(apiClientProvider);
client.delete(
'/develop/developers/$publisherName/projects/${project.id}',
);
ref.invalidate(
devProjectsProvider(publisherName),
);
}
});
}
},
),
onTap: () {
context.pushNamed(
'developerProjectDetail',
pathParameters: {
'name': publisherName,
'projectId': project.id,
},
);
},
),
);
},
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(err, stack) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(devProjectsProvider(publisherName)),
),
),
);
}
}

View File

@@ -0,0 +1,151 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'projects.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$devProjectsHash() => r'4c86ea5c3c02185514dbfa32804f1529f68d56c7';
/// 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));
}
}
/// See also [devProjects].
@ProviderFor(devProjects)
const devProjectsProvider = DevProjectsFamily();
/// See also [devProjects].
class DevProjectsFamily extends Family<AsyncValue<List<DevProject>>> {
/// See also [devProjects].
const DevProjectsFamily();
/// See also [devProjects].
DevProjectsProvider call(String pubName) {
return DevProjectsProvider(pubName);
}
@override
DevProjectsProvider getProviderOverride(
covariant DevProjectsProvider provider,
) {
return call(provider.pubName);
}
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'devProjectsProvider';
}
/// See also [devProjects].
class DevProjectsProvider extends AutoDisposeFutureProvider<List<DevProject>> {
/// See also [devProjects].
DevProjectsProvider(String pubName)
: this._internal(
(ref) => devProjects(ref as DevProjectsRef, pubName),
from: devProjectsProvider,
name: r'devProjectsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$devProjectsHash,
dependencies: DevProjectsFamily._dependencies,
allTransitiveDependencies: DevProjectsFamily._allTransitiveDependencies,
pubName: pubName,
);
DevProjectsProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.pubName,
}) : super.internal();
final String pubName;
@override
Override overrideWith(
FutureOr<List<DevProject>> Function(DevProjectsRef provider) create,
) {
return ProviderOverride(
origin: this,
override: DevProjectsProvider._internal(
(ref) => create(ref as DevProjectsRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
pubName: pubName,
),
);
}
@override
AutoDisposeFutureProviderElement<List<DevProject>> createElement() {
return _DevProjectsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is DevProjectsProvider && other.pubName == pubName;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, pubName.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin DevProjectsRef on AutoDisposeFutureProviderRef<List<DevProject>> {
/// The parameter `pubName` of this provider.
String get pubName;
}
class _DevProjectsProviderElement
extends AutoDisposeFutureProviderElement<List<DevProject>>
with DevProjectsRef {
_DevProjectsProviderElement(super.provider);
@override
String get pubName => (origin as DevProjectsProvider).pubName;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -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),
],
),
);
}
}

View 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

View 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},
);
},
);
},
),
),
],
),
),
);
}
}

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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},
);
},
);
},
),
);
}
}

View File

@@ -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

View 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},
);
},
);
},
),
),
],
),
),
);
}
}

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1169
swagger-develop.json Normal file

File diff suppressed because it is too large Load Diff