Compare commits

...

2 Commits

Author SHA1 Message Date
0b1a23e81a 💄 Optimize publisher first time UX
♻️ Split up the forms and list screens
💄 Use dropdown forms fields instead of selection
2025-10-03 15:42:56 +08:00
c87e6cfe07 ♻️ Refactored the stellar program tab 2025-10-02 13:03:18 +08:00
24 changed files with 2888 additions and 1074 deletions

View File

@@ -124,3 +124,30 @@ sealed class SnWalletOrder with _$SnWalletOrder {
factory SnWalletOrder.fromJson(Map<String, dynamic> json) =>
_$SnWalletOrderFromJson(json);
}
@freezed
sealed class SnWalletGift with _$SnWalletGift {
const factory SnWalletGift({
required String id,
required String giftCode,
required String subscriptionIdentifier,
required String? recipientId,
required SnAccount? recipient,
required String gifterId,
required SnAccount? gifter,
required String? redeemerId,
required SnAccount? redeemer,
required String? message,
required int status,
required DateTime? redeemedAt,
required DateTime? expiredAt,
required String? subscriptionId,
required SnWalletSubscription? subscription,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
}) = _SnWalletGift;
factory SnWalletGift.fromJson(Map<String, dynamic> json) =>
_$SnWalletGiftFromJson(json);
}

View File

@@ -1852,4 +1852,408 @@ as DateTime?,
}
/// @nodoc
mixin _$SnWalletGift {
String get id; String get giftCode; String get subscriptionIdentifier; String? get recipientId; SnAccount? get recipient; String get gifterId; SnAccount? get gifter; String? get redeemerId; SnAccount? get redeemer; String? get message; int get status; DateTime? get redeemedAt; DateTime? get expiredAt; String? get subscriptionId; SnWalletSubscription? get subscription; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnWalletGift
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnWalletGiftCopyWith<SnWalletGift> get copyWith => _$SnWalletGiftCopyWithImpl<SnWalletGift>(this as SnWalletGift, _$identity);
/// Serializes this SnWalletGift to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnWalletGift&&(identical(other.id, id) || other.id == id)&&(identical(other.giftCode, giftCode) || other.giftCode == giftCode)&&(identical(other.subscriptionIdentifier, subscriptionIdentifier) || other.subscriptionIdentifier == subscriptionIdentifier)&&(identical(other.recipientId, recipientId) || other.recipientId == recipientId)&&(identical(other.recipient, recipient) || other.recipient == recipient)&&(identical(other.gifterId, gifterId) || other.gifterId == gifterId)&&(identical(other.gifter, gifter) || other.gifter == gifter)&&(identical(other.redeemerId, redeemerId) || other.redeemerId == redeemerId)&&(identical(other.redeemer, redeemer) || other.redeemer == redeemer)&&(identical(other.message, message) || other.message == message)&&(identical(other.status, status) || other.status == status)&&(identical(other.redeemedAt, redeemedAt) || other.redeemedAt == redeemedAt)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.subscriptionId, subscriptionId) || other.subscriptionId == subscriptionId)&&(identical(other.subscription, subscription) || other.subscription == subscription)&&(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,giftCode,subscriptionIdentifier,recipientId,recipient,gifterId,gifter,redeemerId,redeemer,message,status,redeemedAt,expiredAt,subscriptionId,subscription,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnWalletGift(id: $id, giftCode: $giftCode, subscriptionIdentifier: $subscriptionIdentifier, recipientId: $recipientId, recipient: $recipient, gifterId: $gifterId, gifter: $gifter, redeemerId: $redeemerId, redeemer: $redeemer, message: $message, status: $status, redeemedAt: $redeemedAt, expiredAt: $expiredAt, subscriptionId: $subscriptionId, subscription: $subscription, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnWalletGiftCopyWith<$Res> {
factory $SnWalletGiftCopyWith(SnWalletGift value, $Res Function(SnWalletGift) _then) = _$SnWalletGiftCopyWithImpl;
@useResult
$Res call({
String id, String giftCode, String subscriptionIdentifier, String? recipientId, SnAccount? recipient, String gifterId, SnAccount? gifter, String? redeemerId, SnAccount? redeemer, String? message, int status, DateTime? redeemedAt, DateTime? expiredAt, String? subscriptionId, SnWalletSubscription? subscription, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
$SnAccountCopyWith<$Res>? get recipient;$SnAccountCopyWith<$Res>? get gifter;$SnAccountCopyWith<$Res>? get redeemer;$SnWalletSubscriptionCopyWith<$Res>? get subscription;
}
/// @nodoc
class _$SnWalletGiftCopyWithImpl<$Res>
implements $SnWalletGiftCopyWith<$Res> {
_$SnWalletGiftCopyWithImpl(this._self, this._then);
final SnWalletGift _self;
final $Res Function(SnWalletGift) _then;
/// Create a copy of SnWalletGift
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? giftCode = null,Object? subscriptionIdentifier = null,Object? recipientId = freezed,Object? recipient = freezed,Object? gifterId = null,Object? gifter = freezed,Object? redeemerId = freezed,Object? redeemer = freezed,Object? message = freezed,Object? status = null,Object? redeemedAt = freezed,Object? expiredAt = freezed,Object? subscriptionId = freezed,Object? subscription = 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,giftCode: null == giftCode ? _self.giftCode : giftCode // ignore: cast_nullable_to_non_nullable
as String,subscriptionIdentifier: null == subscriptionIdentifier ? _self.subscriptionIdentifier : subscriptionIdentifier // ignore: cast_nullable_to_non_nullable
as String,recipientId: freezed == recipientId ? _self.recipientId : recipientId // ignore: cast_nullable_to_non_nullable
as String?,recipient: freezed == recipient ? _self.recipient : recipient // ignore: cast_nullable_to_non_nullable
as SnAccount?,gifterId: null == gifterId ? _self.gifterId : gifterId // ignore: cast_nullable_to_non_nullable
as String,gifter: freezed == gifter ? _self.gifter : gifter // ignore: cast_nullable_to_non_nullable
as SnAccount?,redeemerId: freezed == redeemerId ? _self.redeemerId : redeemerId // ignore: cast_nullable_to_non_nullable
as String?,redeemer: freezed == redeemer ? _self.redeemer : redeemer // ignore: cast_nullable_to_non_nullable
as SnAccount?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String?,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as int,redeemedAt: freezed == redeemedAt ? _self.redeemedAt : redeemedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime?,subscriptionId: freezed == subscriptionId ? _self.subscriptionId : subscriptionId // ignore: cast_nullable_to_non_nullable
as String?,subscription: freezed == subscription ? _self.subscription : subscription // ignore: cast_nullable_to_non_nullable
as SnWalletSubscription?,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?,
));
}
/// Create a copy of SnWalletGift
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get recipient {
if (_self.recipient == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.recipient!, (value) {
return _then(_self.copyWith(recipient: value));
});
}/// Create a copy of SnWalletGift
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get gifter {
if (_self.gifter == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.gifter!, (value) {
return _then(_self.copyWith(gifter: value));
});
}/// Create a copy of SnWalletGift
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get redeemer {
if (_self.redeemer == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.redeemer!, (value) {
return _then(_self.copyWith(redeemer: value));
});
}/// Create a copy of SnWalletGift
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnWalletSubscriptionCopyWith<$Res>? get subscription {
if (_self.subscription == null) {
return null;
}
return $SnWalletSubscriptionCopyWith<$Res>(_self.subscription!, (value) {
return _then(_self.copyWith(subscription: value));
});
}
}
/// Adds pattern-matching-related methods to [SnWalletGift].
extension SnWalletGiftPatterns on SnWalletGift {
/// 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( _SnWalletGift value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnWalletGift() 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( _SnWalletGift value) $default,){
final _that = this;
switch (_that) {
case _SnWalletGift():
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( _SnWalletGift value)? $default,){
final _that = this;
switch (_that) {
case _SnWalletGift() 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, String giftCode, String subscriptionIdentifier, String? recipientId, SnAccount? recipient, String gifterId, SnAccount? gifter, String? redeemerId, SnAccount? redeemer, String? message, int status, DateTime? redeemedAt, DateTime? expiredAt, String? subscriptionId, SnWalletSubscription? subscription, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnWalletGift() when $default != null:
return $default(_that.id,_that.giftCode,_that.subscriptionIdentifier,_that.recipientId,_that.recipient,_that.gifterId,_that.gifter,_that.redeemerId,_that.redeemer,_that.message,_that.status,_that.redeemedAt,_that.expiredAt,_that.subscriptionId,_that.subscription,_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, String giftCode, String subscriptionIdentifier, String? recipientId, SnAccount? recipient, String gifterId, SnAccount? gifter, String? redeemerId, SnAccount? redeemer, String? message, int status, DateTime? redeemedAt, DateTime? expiredAt, String? subscriptionId, SnWalletSubscription? subscription, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) {
case _SnWalletGift():
return $default(_that.id,_that.giftCode,_that.subscriptionIdentifier,_that.recipientId,_that.recipient,_that.gifterId,_that.gifter,_that.redeemerId,_that.redeemer,_that.message,_that.status,_that.redeemedAt,_that.expiredAt,_that.subscriptionId,_that.subscription,_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, String giftCode, String subscriptionIdentifier, String? recipientId, SnAccount? recipient, String gifterId, SnAccount? gifter, String? redeemerId, SnAccount? redeemer, String? message, int status, DateTime? redeemedAt, DateTime? expiredAt, String? subscriptionId, SnWalletSubscription? subscription, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) {
case _SnWalletGift() when $default != null:
return $default(_that.id,_that.giftCode,_that.subscriptionIdentifier,_that.recipientId,_that.recipient,_that.gifterId,_that.gifter,_that.redeemerId,_that.redeemer,_that.message,_that.status,_that.redeemedAt,_that.expiredAt,_that.subscriptionId,_that.subscription,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnWalletGift implements SnWalletGift {
const _SnWalletGift({required this.id, required this.giftCode, required this.subscriptionIdentifier, required this.recipientId, required this.recipient, required this.gifterId, required this.gifter, required this.redeemerId, required this.redeemer, required this.message, required this.status, required this.redeemedAt, required this.expiredAt, required this.subscriptionId, required this.subscription, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnWalletGift.fromJson(Map<String, dynamic> json) => _$SnWalletGiftFromJson(json);
@override final String id;
@override final String giftCode;
@override final String subscriptionIdentifier;
@override final String? recipientId;
@override final SnAccount? recipient;
@override final String gifterId;
@override final SnAccount? gifter;
@override final String? redeemerId;
@override final SnAccount? redeemer;
@override final String? message;
@override final int status;
@override final DateTime? redeemedAt;
@override final DateTime? expiredAt;
@override final String? subscriptionId;
@override final SnWalletSubscription? subscription;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnWalletGift
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnWalletGiftCopyWith<_SnWalletGift> get copyWith => __$SnWalletGiftCopyWithImpl<_SnWalletGift>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnWalletGiftToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnWalletGift&&(identical(other.id, id) || other.id == id)&&(identical(other.giftCode, giftCode) || other.giftCode == giftCode)&&(identical(other.subscriptionIdentifier, subscriptionIdentifier) || other.subscriptionIdentifier == subscriptionIdentifier)&&(identical(other.recipientId, recipientId) || other.recipientId == recipientId)&&(identical(other.recipient, recipient) || other.recipient == recipient)&&(identical(other.gifterId, gifterId) || other.gifterId == gifterId)&&(identical(other.gifter, gifter) || other.gifter == gifter)&&(identical(other.redeemerId, redeemerId) || other.redeemerId == redeemerId)&&(identical(other.redeemer, redeemer) || other.redeemer == redeemer)&&(identical(other.message, message) || other.message == message)&&(identical(other.status, status) || other.status == status)&&(identical(other.redeemedAt, redeemedAt) || other.redeemedAt == redeemedAt)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.subscriptionId, subscriptionId) || other.subscriptionId == subscriptionId)&&(identical(other.subscription, subscription) || other.subscription == subscription)&&(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,giftCode,subscriptionIdentifier,recipientId,recipient,gifterId,gifter,redeemerId,redeemer,message,status,redeemedAt,expiredAt,subscriptionId,subscription,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnWalletGift(id: $id, giftCode: $giftCode, subscriptionIdentifier: $subscriptionIdentifier, recipientId: $recipientId, recipient: $recipient, gifterId: $gifterId, gifter: $gifter, redeemerId: $redeemerId, redeemer: $redeemer, message: $message, status: $status, redeemedAt: $redeemedAt, expiredAt: $expiredAt, subscriptionId: $subscriptionId, subscription: $subscription, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnWalletGiftCopyWith<$Res> implements $SnWalletGiftCopyWith<$Res> {
factory _$SnWalletGiftCopyWith(_SnWalletGift value, $Res Function(_SnWalletGift) _then) = __$SnWalletGiftCopyWithImpl;
@override @useResult
$Res call({
String id, String giftCode, String subscriptionIdentifier, String? recipientId, SnAccount? recipient, String gifterId, SnAccount? gifter, String? redeemerId, SnAccount? redeemer, String? message, int status, DateTime? redeemedAt, DateTime? expiredAt, String? subscriptionId, SnWalletSubscription? subscription, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@override $SnAccountCopyWith<$Res>? get recipient;@override $SnAccountCopyWith<$Res>? get gifter;@override $SnAccountCopyWith<$Res>? get redeemer;@override $SnWalletSubscriptionCopyWith<$Res>? get subscription;
}
/// @nodoc
class __$SnWalletGiftCopyWithImpl<$Res>
implements _$SnWalletGiftCopyWith<$Res> {
__$SnWalletGiftCopyWithImpl(this._self, this._then);
final _SnWalletGift _self;
final $Res Function(_SnWalletGift) _then;
/// Create a copy of SnWalletGift
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? giftCode = null,Object? subscriptionIdentifier = null,Object? recipientId = freezed,Object? recipient = freezed,Object? gifterId = null,Object? gifter = freezed,Object? redeemerId = freezed,Object? redeemer = freezed,Object? message = freezed,Object? status = null,Object? redeemedAt = freezed,Object? expiredAt = freezed,Object? subscriptionId = freezed,Object? subscription = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnWalletGift(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,giftCode: null == giftCode ? _self.giftCode : giftCode // ignore: cast_nullable_to_non_nullable
as String,subscriptionIdentifier: null == subscriptionIdentifier ? _self.subscriptionIdentifier : subscriptionIdentifier // ignore: cast_nullable_to_non_nullable
as String,recipientId: freezed == recipientId ? _self.recipientId : recipientId // ignore: cast_nullable_to_non_nullable
as String?,recipient: freezed == recipient ? _self.recipient : recipient // ignore: cast_nullable_to_non_nullable
as SnAccount?,gifterId: null == gifterId ? _self.gifterId : gifterId // ignore: cast_nullable_to_non_nullable
as String,gifter: freezed == gifter ? _self.gifter : gifter // ignore: cast_nullable_to_non_nullable
as SnAccount?,redeemerId: freezed == redeemerId ? _self.redeemerId : redeemerId // ignore: cast_nullable_to_non_nullable
as String?,redeemer: freezed == redeemer ? _self.redeemer : redeemer // ignore: cast_nullable_to_non_nullable
as SnAccount?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String?,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as int,redeemedAt: freezed == redeemedAt ? _self.redeemedAt : redeemedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime?,subscriptionId: freezed == subscriptionId ? _self.subscriptionId : subscriptionId // ignore: cast_nullable_to_non_nullable
as String?,subscription: freezed == subscription ? _self.subscription : subscription // ignore: cast_nullable_to_non_nullable
as SnWalletSubscription?,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?,
));
}
/// Create a copy of SnWalletGift
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get recipient {
if (_self.recipient == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.recipient!, (value) {
return _then(_self.copyWith(recipient: value));
});
}/// Create a copy of SnWalletGift
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get gifter {
if (_self.gifter == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.gifter!, (value) {
return _then(_self.copyWith(gifter: value));
});
}/// Create a copy of SnWalletGift
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get redeemer {
if (_self.redeemer == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.redeemer!, (value) {
return _then(_self.copyWith(redeemer: value));
});
}/// Create a copy of SnWalletGift
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnWalletSubscriptionCopyWith<$Res>? get subscription {
if (_self.subscription == null) {
return null;
}
return $SnWalletSubscriptionCopyWith<$Res>(_self.subscription!, (value) {
return _then(_self.copyWith(subscription: value));
});
}
}
// dart format on

View File

@@ -228,3 +228,70 @@ Map<String, dynamic> _$SnWalletOrderToJson(_SnWalletOrder instance) =>
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};
_SnWalletGift _$SnWalletGiftFromJson(Map<String, dynamic> json) =>
_SnWalletGift(
id: json['id'] as String,
giftCode: json['gift_code'] as String,
subscriptionIdentifier: json['subscription_identifier'] as String,
recipientId: json['recipient_id'] as String?,
recipient:
json['recipient'] == null
? null
: SnAccount.fromJson(json['recipient'] as Map<String, dynamic>),
gifterId: json['gifter_id'] as String,
gifter:
json['gifter'] == null
? null
: SnAccount.fromJson(json['gifter'] as Map<String, dynamic>),
redeemerId: json['redeemer_id'] as String?,
redeemer:
json['redeemer'] == null
? null
: SnAccount.fromJson(json['redeemer'] as Map<String, dynamic>),
message: json['message'] as String?,
status: (json['status'] as num).toInt(),
redeemedAt:
json['redeemed_at'] == null
? null
: DateTime.parse(json['redeemed_at'] as String),
expiredAt:
json['expired_at'] == null
? null
: DateTime.parse(json['expired_at'] as String),
subscriptionId: json['subscription_id'] as String?,
subscription:
json['subscription'] == null
? null
: SnWalletSubscription.fromJson(
json['subscription'] as Map<String, dynamic>,
),
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> _$SnWalletGiftToJson(_SnWalletGift instance) =>
<String, dynamic>{
'id': instance.id,
'gift_code': instance.giftCode,
'subscription_identifier': instance.subscriptionIdentifier,
'recipient_id': instance.recipientId,
'recipient': instance.recipient?.toJson(),
'gifter_id': instance.gifterId,
'gifter': instance.gifter?.toJson(),
'redeemer_id': instance.redeemerId,
'redeemer': instance.redeemer?.toJson(),
'message': instance.message,
'status': instance.status,
'redeemed_at': instance.redeemedAt?.toIso8601String(),
'expired_at': instance.expiredAt?.toIso8601String(),
'subscription_id': instance.subscriptionId,
'subscription': instance.subscription?.toJson(),
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};

View File

@@ -6,7 +6,7 @@ part of 'messages_notifier.dart';
// RiverpodGenerator
// **************************************************************************
String _$messagesNotifierHash() => r'b0cff44cea9f15a2684b602c48b32cd3d78875ab';
String _$messagesNotifierHash() => r'70acac63c720987d8b1688500e3735f1c2d16fdc';
/// Copied from Dart SDK
class _SystemHash {

View File

@@ -35,6 +35,7 @@ import 'package:island/screens/account/me/profile_update.dart';
import 'package:island/screens/account/leveling.dart';
import 'package:island/screens/account/me/account_settings.dart';
import 'package:island/screens/chat/chat.dart';
import 'package:island/screens/chat/chat_form.dart';
import 'package:island/screens/chat/room.dart';
import 'package:island/screens/chat/room_detail.dart';
import 'package:island/screens/chat/call.dart';
@@ -48,7 +49,7 @@ 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/publishers_form.dart';
import 'package:island/screens/creators/webfeed/webfeed_list.dart';
import 'package:island/screens/creators/webfeed/webfeed_edit.dart';
import 'package:island/screens/poll/poll_editor.dart';
@@ -59,6 +60,7 @@ import 'package:island/screens/auth/login.dart';
import 'package:island/screens/auth/create_account.dart';
import 'package:island/screens/settings.dart';
import 'package:island/screens/realm/realms.dart';
import 'package:island/screens/realm/realm_form.dart';
import 'package:island/screens/realm/realm_detail.dart';
import 'package:island/screens/account/event_calendar.dart';
import 'package:island/screens/discovery/realms.dart';

View File

@@ -1,41 +1,21 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.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/models/wallet.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/screens/account/credits.dart';
import 'package:island/services/time.dart';
import 'package:island/widgets/account/leveling_progress.dart';
import 'package:island/widgets/account/restore_purchase_sheet.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/account/stellar_program_tab.dart';
import 'package:island/widgets/app_scaffold.dart';
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';
@riverpod
Future<SnWalletSubscription?> accountStellarSubscription(Ref ref) async {
try {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/id/subscriptions/fuzzy/solian.stellar');
return SnWalletSubscription.fromJson(resp.data);
} catch (err) {
if (err is DioException && err.response?.statusCode == 404) return null;
rethrow;
}
}
@riverpod
class LevelingHistoryNotifier extends _$LevelingHistoryNotifier
with CursorPagingNotifierMixin<SnExperienceRecord> {
@@ -128,7 +108,7 @@ class LevelingScreen extends HookConsumerWidget {
children: [
_buildLevelingTab(context, ref, user.value!),
const SocialCreditsTab(),
_buildStellarProgramTab(context, ref),
const StellarProgramTab(),
],
),
),
@@ -264,451 +244,6 @@ class LevelingScreen extends HookConsumerWidget {
),
);
}
Widget _buildStellarProgramTab(BuildContext context, WidgetRef ref) {
final stellarSubscription = ref.watch(accountStellarSubscriptionProvider);
return SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildMembershipSection(context, ref, stellarSubscription),
const Gap(16),
],
),
);
}
Widget _buildMembershipSection(
BuildContext context,
WidgetRef ref,
AsyncValue<SnWalletSubscription?> stellarSubscriptionAsync,
) {
return stellarSubscriptionAsync.when(
data: (membership) => _buildMembershipContent(context, ref, membership),
loading:
() => Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.primaryContainer,
Theme.of(context).colorScheme.secondaryContainer,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12),
),
child: const Center(child: CircularProgressIndicator()),
),
error:
(error, stack) => Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.primaryContainer,
Theme.of(context).colorScheme.secondaryContainer,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12),
),
child: Text('Error loading membership: $error'),
),
);
}
Widget _buildMembershipContent(
BuildContext context,
WidgetRef ref,
SnWalletSubscription? membership,
) {
final isActive = membership?.isActive ?? false;
Future<void> membershipCancel() async {
if (!isActive || membership == null) return;
final confirm = await showConfirmAlert(
'membershipCancelHint'.tr(),
'membershipCancelConfirm'.tr(),
);
if (!confirm || !context.mounted) return;
try {
showLoadingModal(context);
final client = ref.watch(apiClientProvider);
await client.post('/id/subscriptions/${membership.identifier}/cancel');
ref.invalidate(accountStellarSubscriptionProvider);
ref.read(userInfoProvider.notifier).fetchUser();
if (context.mounted) {
hideLoadingModal(context);
showSnackBar('membershipCancelSuccess'.tr());
}
} catch (err) {
if (context.mounted) hideLoadingModal(context);
showErrorAlert(err);
}
}
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.primaryContainer,
Theme.of(context).colorScheme.secondaryContainer,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Icon(
isActive ? Icons.star : Icons.star_border,
color: Theme.of(context).colorScheme.primary,
size: 24,
),
const Gap(8),
Text(
'stellarMembership'.tr(),
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
],
),
const Gap(12),
if (isActive) ...[
_buildCurrentMembershipCard(context, membership!),
const Gap(12),
FilledButton.icon(
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
Theme.of(context).colorScheme.error,
),
foregroundColor: WidgetStateProperty.all(
Theme.of(context).colorScheme.onError,
),
),
onPressed: membershipCancel,
icon: const Icon(Symbols.cancel),
label: Text('membershipCancel'.tr()),
),
],
if (!isActive) ...[
Text(
'chooseYourPlan'.tr(),
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const Gap(12),
_buildMembershipTiers(context, ref, membership),
],
// Restore Purchase Button
// As you know Apple platform need IAP
if (kIsWeb || !(Platform.isIOS || Platform.isMacOS))
OutlinedButton.icon(
onPressed: () => _showRestorePurchaseSheet(context, ref),
icon: const Icon(Icons.restore),
label: Text('restorePurchase'.tr()),
style: OutlinedButton.styleFrom(
minimumSize: const Size(double.infinity, 48),
),
).padding(top: 12),
],
),
);
}
Widget _buildCurrentMembershipCard(
BuildContext context,
SnWalletSubscription membership,
) {
final tierName = _getMembershipTierName(membership.identifier);
final tierColor = _getMembershipTierColor(context, membership.identifier);
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: tierColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: tierColor, width: 1),
),
child: Row(
children: [
Icon(Icons.verified, color: tierColor, size: 20),
const Gap(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'currentMembership'.tr(args: [tierName]),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: tierColor,
),
),
if (membership.endedAt != null)
Text(
'membershipExpires'.tr(
args: [membership.endedAt!.formatSystem()],
),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
],
),
);
}
Widget _buildMembershipTiers(
BuildContext context,
WidgetRef ref,
SnWalletSubscription? currentMembership,
) {
final tiers = [
{
'id': 'solian.stellar.primary',
'name': 'membershipTierStellar'.tr(),
'price': 'membershipPriceStellar'.tr(),
'color': Colors.blue,
},
{
'id': 'solian.stellar.nova',
'name': 'membershipTierNova'.tr(),
'price': 'membershipPriceNova'.tr(),
'color': Color.fromRGBO(57, 197, 187, 1),
},
{
'id': 'solian.stellar.supernova',
'name': 'membershipTierSupernova'.tr(),
'price': 'membershipPriceSupernova'.tr(),
'color': Colors.orange,
},
];
return Column(
children:
tiers.map((tier) {
final isCurrentTier = currentMembership?.identifier == tier['id'];
final tierColor = tier['color'] as Color;
return Container(
margin: const EdgeInsets.only(bottom: 8),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap:
isCurrentTier
? null
: () => _purchaseMembership(
context,
ref,
tier['id'] as String,
),
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color:
isCurrentTier
? tierColor.withOpacity(0.1)
: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color:
isCurrentTier
? tierColor
: Theme.of(
context,
).colorScheme.outline.withOpacity(0.2),
width: isCurrentTier ? 2 : 1,
),
),
child: Row(
children: [
Container(
width: 4,
height: 40,
decoration: BoxDecoration(
color: tierColor,
borderRadius: BorderRadius.circular(2),
),
),
const Gap(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
tier['name'] as String,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: isCurrentTier ? tierColor : null,
),
),
const Gap(8),
if (isCurrentTier)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: tierColor,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'membershipCurrentBadge'.tr(),
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
],
),
Text(
tier['price'] as String,
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(
color:
Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
],
),
),
if (!isCurrentTier)
Icon(
Icons.arrow_forward_ios,
size: 16,
color:
Theme.of(context).colorScheme.onSurfaceVariant,
),
],
),
),
),
),
);
}).toList(),
);
}
String _getMembershipTierName(String identifier) {
switch (identifier) {
case 'solian.stellar.primary':
return 'membershipTierStellar'.tr();
case 'solian.stellar.nova':
return 'membershipTierNova'.tr();
case 'solian.stellar.supernova':
return 'membershipTierSupernova'.tr();
default:
return 'membershipTierUnknown'.tr();
}
}
Color _getMembershipTierColor(BuildContext context, String identifier) {
switch (identifier) {
case 'solian.stellar.primary':
return Colors.blue;
case 'solian.stellar.nova':
return Colors.purple;
case 'solian.stellar.supernova':
return Colors.orange;
default:
return Theme.of(context).colorScheme.primary;
}
}
Future<void> _showRestorePurchaseSheet(
BuildContext context,
WidgetRef ref,
) async {
await showModalBottomSheet(
context: context,
builder: (context) => const RestorePurchaseSheet(),
);
}
Future<void> _purchaseMembership(
BuildContext context,
WidgetRef ref,
String tierId,
) async {
final client = ref.watch(apiClientProvider);
try {
showLoadingModal(context);
final resp = await client.post(
'/id/subscriptions',
data: {
'identifier': tierId,
'payment_method': 'solian.wallet',
'payment_details': {'currency': 'golds'},
'cycle_duration_days': 30,
},
options: Options(headers: {'X-Noop': true}),
);
final subscription = SnWalletSubscription.fromJson(resp.data);
if (subscription.status == 1) return;
final orderResp = await client.post(
'/id/subscriptions/${subscription.identifier}/order',
);
final order = SnWalletOrder.fromJson(orderResp.data);
if (context.mounted) hideLoadingModal(context);
// Show payment overlay to complete the payment
if (!context.mounted) return;
final paidOrder = await PaymentOverlay.show(
context: context,
order: order,
enableBiometric: true,
);
if (context.mounted) showLoadingModal(context);
if (paidOrder != null) {
// Wait for server to handle order
await Future.delayed(const Duration(seconds: 1));
ref.invalidate(accountStellarSubscriptionProvider);
ref.read(userInfoProvider.notifier).fetchUser();
if (context.mounted) {
showSnackBar('membershipPurchaseSuccess'.tr());
}
}
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
}
class LevelStairsPainter extends CustomPainter {

View File

@@ -6,27 +6,6 @@ part of 'leveling.dart';
// RiverpodGenerator
// **************************************************************************
String _$accountStellarSubscriptionHash() =>
r'80abcdefb3868775fd8fe3c980215713efff5948';
/// See also [accountStellarSubscription].
@ProviderFor(accountStellarSubscription)
final accountStellarSubscriptionProvider =
AutoDisposeFutureProvider<SnWalletSubscription?>.internal(
accountStellarSubscription,
name: r'accountStellarSubscriptionProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$accountStellarSubscriptionHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef AccountStellarSubscriptionRef =
AutoDisposeFutureProviderRef<SnWalletSubscription?>;
String _$levelingHistoryNotifierHash() =>
r'e795f9b7911c9e50f15c095ea237cb0e87bf1e89';

View File

@@ -1,10 +1,12 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/network.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'captcha.config.g.dart';
@riverpod
Future<String> captchaUrl(Ref ref) async {
const baseUrl = "https://solian.app";
final apiClient = ref.watch(apiClientProvider);
final baseUrl = await apiClient.get('/config/site');
return '$baseUrl/auth/captcha';
}

View File

@@ -1,5 +1,3 @@
import 'package:collection/collection.dart';
import 'package:croppy/croppy.dart' hide cropImage;
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@@ -7,23 +5,17 @@ import 'package:go_router/go_router.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:island/models/chat.dart';
import 'package:island/models/file.dart';
import 'package:island/models/realm.dart';
import 'package:island/pods/chat/call.dart';
import 'package:island/pods/chat/chat_summary.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/realm/realms.dart';
import 'package:island/services/file.dart';
import 'package:island/services/file_uploader.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/account/account_picker.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/content/sheet.dart';
import 'package:island/widgets/realm/realm_selection_dropdown.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:relative_time/relative_time.dart';
@@ -568,269 +560,6 @@ Future<SnChatMember?> chatroomIdentity(Ref ref, String? identifier) async {
}
}
class NewChatScreen extends StatelessWidget {
const NewChatScreen({super.key});
@override
Widget build(BuildContext context) {
return const EditChatScreen();
}
}
class EditChatScreen extends HookConsumerWidget {
final String? id;
const EditChatScreen({super.key, this.id});
@override
Widget build(BuildContext context, WidgetRef ref) {
final formKey = useMemoized(() => GlobalKey<FormState>(), []);
final submitting = useState(false);
final nameController = useTextEditingController();
final descriptionController = useTextEditingController();
final picture = useState<SnCloudFile?>(null);
final background = useState<SnCloudFile?>(null);
final isPublic = useState(true);
final isCommunity = useState(false);
final chat = ref.watch(chatroomProvider(id));
final joinedRealms = ref.watch(realmsJoinedProvider);
final currentRealm = useState<SnRealm?>(null);
useEffect(() {
if (chat.value != null) {
nameController.text = chat.value!.name ?? '';
descriptionController.text = chat.value!.description ?? '';
picture.value = chat.value!.picture;
background.value = chat.value!.background;
isPublic.value = chat.value!.isPublic;
isCommunity.value = chat.value!.isCommunity;
currentRealm.value = joinedRealms.value?.firstWhereOrNull(
(realm) => realm.id == chat.value!.realmId,
);
}
return;
}, [chat, joinedRealms]);
void setPicture(String position) 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: [
if (position == 'background')
const CropAspectRatio(height: 7, width: 16)
else
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 cloudFile =
await FileUploader.createCloudFile(
client: ref.read(apiClientProvider),
fileData: UniversalFile(
data: result,
type: UniversalFileType.image,
),
).future;
if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...');
}
switch (position) {
case 'picture':
picture.value = cloudFile;
case 'background':
background.value = cloudFile;
}
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
submitting.value = false;
}
}
Future<void> performAction() async {
if (!formKey.currentState!.validate()) return;
submitting.value = true;
try {
final client = ref.watch(apiClientProvider);
final resp = await client.request(
id == null ? '/sphere/chat' : '/sphere/chat/$id',
data: {
'name': nameController.text,
'description': descriptionController.text,
'background_id': background.value?.id,
'picture_id': picture.value?.id,
'realm_id': currentRealm.value?.id,
'is_public': isPublic.value,
'is_community': isCommunity.value,
},
options: Options(method: id == null ? 'POST' : 'PATCH'),
);
if (context.mounted) {
context.pop(SnChatRoom.fromJson(resp.data));
}
} catch (err) {
showErrorAlert(err);
} finally {
submitting.value = false;
}
}
return AppScaffold(
appBar: AppBar(
title: Text(id == null ? 'createChatRoom' : 'editChatRoom').tr(),
leading: const PageBackButton(),
),
body: SingleChildScrollView(
child: Column(
children: [
RealmSelectionDropdown(
value: currentRealm.value,
realms: joinedRealms.when(
data: (realms) => realms,
loading: () => [],
error: (_, _) => [],
),
onChanged: (SnRealm? value) {
currentRealm.value = value;
},
isLoading: joinedRealms.isLoading,
error: joinedRealms.error?.toString(),
),
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
GestureDetector(
child: Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child:
background.value != null
? CloudFileWidget(
item: background.value!,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
),
onTap: () {
setPicture('background');
},
),
Positioned(
left: 20,
bottom: -32,
child: GestureDetector(
child: ProfilePictureWidget(
fileId: picture.value?.id,
radius: 40,
fallbackIcon: Symbols.group,
),
onTap: () {
setPicture('picture');
},
),
),
],
),
).padding(bottom: 32),
Form(
key: formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: nameController,
decoration: const InputDecoration(labelText: 'Name'),
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
TextFormField(
controller: descriptionController,
decoration: const InputDecoration(
labelText: 'Description',
alignLabelWithHint: true,
),
minLines: 3,
maxLines: null,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
Card(
margin: EdgeInsets.zero,
child: Column(
children: [
CheckboxListTile(
secondary: const Icon(Symbols.public),
title: Text('publicChat').tr(),
subtitle: Text('publicChatDescription').tr(),
value: isPublic.value,
onChanged: (value) {
isPublic.value = value ?? true;
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
CheckboxListTile(
secondary: const Icon(Symbols.travel_explore),
title: Text('communityChat').tr(),
subtitle: Text('communityChatDescription').tr(),
value: isCommunity.value,
onChanged: (value) {
isCommunity.value = value ?? false;
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
],
),
),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: submitting.value ? null : performAction,
label: const Text('Save'),
icon: const Icon(Symbols.save),
),
),
],
).padding(all: 24),
),
],
),
),
);
}
}
@riverpod
Future<List<SnChatMember>> chatroomInvites(Ref ref) async {
final client = ref.watch(apiClientProvider);

View File

@@ -0,0 +1,299 @@
import 'package:collection/collection.dart';
import 'package:croppy/croppy.dart' hide cropImage;
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:island/models/chat.dart';
import 'package:island/models/file.dart';
import 'package:island/models/realm.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/chat/chat.dart';
import 'package:island/screens/realm/realms.dart';
import 'package:island/services/file.dart';
import 'package:island/services/file_uploader.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
class NewChatScreen extends StatelessWidget {
const NewChatScreen({super.key});
@override
Widget build(BuildContext context) {
return const EditChatScreen();
}
}
class EditChatScreen extends HookConsumerWidget {
final String? id;
const EditChatScreen({super.key, this.id});
@override
Widget build(BuildContext context, WidgetRef ref) {
final formKey = useMemoized(() => GlobalKey<FormState>(), []);
final submitting = useState(false);
final nameController = useTextEditingController();
final descriptionController = useTextEditingController();
final picture = useState<SnCloudFile?>(null);
final background = useState<SnCloudFile?>(null);
final isPublic = useState(true);
final isCommunity = useState(false);
final chat = ref.watch(chatroomProvider(id));
final joinedRealms = ref.watch(realmsJoinedProvider);
final currentRealm = useState<SnRealm?>(null);
useEffect(() {
if (chat.value != null) {
nameController.text = chat.value!.name ?? '';
descriptionController.text = chat.value!.description ?? '';
picture.value = chat.value!.picture;
background.value = chat.value!.background;
isPublic.value = chat.value!.isPublic;
isCommunity.value = chat.value!.isCommunity;
currentRealm.value = joinedRealms.value?.firstWhereOrNull(
(realm) => realm.id == chat.value!.realmId,
);
}
return;
}, [chat, joinedRealms]);
void setPicture(String position) 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: [
if (position == 'background')
const CropAspectRatio(height: 7, width: 16)
else
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 cloudFile =
await FileUploader.createCloudFile(
client: ref.read(apiClientProvider),
fileData: UniversalFile(
data: result,
type: UniversalFileType.image,
),
).future;
if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...');
}
switch (position) {
case 'picture':
picture.value = cloudFile;
case 'background':
background.value = cloudFile;
}
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
submitting.value = false;
}
}
Future<void> performAction() async {
if (!formKey.currentState!.validate()) return;
submitting.value = true;
try {
final client = ref.watch(apiClientProvider);
final resp = await client.request(
id == null ? '/sphere/chat' : '/sphere/chat/$id',
data: {
'name': nameController.text,
'description': descriptionController.text,
'background_id': background.value?.id,
'picture_id': picture.value?.id,
'realm_id': currentRealm.value?.id,
'is_public': isPublic.value,
'is_community': isCommunity.value,
},
options: Options(method: id == null ? 'POST' : 'PATCH'),
);
if (context.mounted) {
context.pop(SnChatRoom.fromJson(resp.data));
}
} catch (err) {
showErrorAlert(err);
} finally {
submitting.value = false;
}
}
return AppScaffold(
appBar: AppBar(
title: Text(id == null ? 'createChatRoom' : 'editChatRoom').tr(),
leading: const PageBackButton(),
),
body: SingleChildScrollView(
child: Column(
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
GestureDetector(
child: Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child:
background.value != null
? CloudFileWidget(
item: background.value!,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
),
onTap: () {
setPicture('background');
},
),
Positioned(
left: 20,
bottom: -32,
child: GestureDetector(
child: ProfilePictureWidget(
fileId: picture.value?.id,
radius: 40,
fallbackIcon: Symbols.group,
),
onTap: () {
setPicture('picture');
},
),
),
],
),
).padding(bottom: 32),
Form(
key: formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: nameController,
decoration: const InputDecoration(labelText: 'Name'),
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
TextFormField(
controller: descriptionController,
decoration: const InputDecoration(
labelText: 'Description',
alignLabelWithHint: true,
),
minLines: 3,
maxLines: null,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
DropdownButtonFormField<SnRealm>(
value: currentRealm.value,
decoration: InputDecoration(labelText: 'realm'.tr()),
items: [
DropdownMenuItem<SnRealm>(
value: null,
child: Text('none'.tr()),
),
...joinedRealms.maybeWhen(
data:
(realms) => realms.map(
(realm) => DropdownMenuItem(
value: realm,
child: Text(realm.name),
),
),
orElse: () => [],
),
],
onChanged:
joinedRealms.isLoading
? null
: (SnRealm? value) {
currentRealm.value = value;
},
),
const SizedBox(height: 16),
Card(
margin: EdgeInsets.zero,
child: Column(
children: [
CheckboxListTile(
secondary: const Icon(Symbols.public),
title: Text('publicChat').tr(),
subtitle: Text('publicChatDescription').tr(),
value: isPublic.value,
onChanged: (value) {
isPublic.value = value ?? true;
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
CheckboxListTile(
secondary: const Icon(Symbols.travel_explore),
title: Text('communityChat').tr(),
subtitle: Text('communityChatDescription').tr(),
value: isCommunity.value,
onChanged: (value) {
isCommunity.value = value ?? false;
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
],
),
),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: submitting.value ? null : performAction,
label: const Text('Save'),
icon: const Icon(Symbols.save),
),
),
],
).padding(all: 24),
),
],
),
),
);
}
}

View File

@@ -9,7 +9,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/screens/creators/publishers_form.dart';
import 'package:island/services/responsive.dart';
import 'package:island/utils/text.dart';
import 'package:island/widgets/account/account_picker.dart';

View File

@@ -18,12 +18,11 @@ import 'package:island/services/file_uploader.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/realm/realm_selection_dropdown.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
part 'publishers.g.dart';
part 'publishers_form.g.dart';
@riverpod
Future<List<SnPublisher>> publishersManaged(Ref ref) async {
@@ -187,19 +186,6 @@ class EditPublisherScreen extends HookConsumerWidget {
padding: EdgeInsets.only(bottom: 16),
child: Column(
children: [
RealmSelectionDropdown(
value: currentRealm.value,
realms: joinedRealms.when(
data: (realms) => realms,
loading: () => [],
error: (_, _) => [],
),
onChanged: (SnRealm? value) {
currentRealm.value = value;
},
isLoading: joinedRealms.isLoading,
error: joinedRealms.error?.toString(),
),
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
@@ -273,6 +259,32 @@ class EditPublisherScreen extends HookConsumerWidget {
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
DropdownButtonFormField<SnRealm>(
value: currentRealm.value,
decoration: InputDecoration(labelText: 'realm'.tr()),
items: [
DropdownMenuItem<SnRealm>(
value: null,
child: Text('individual'.tr()),
),
...joinedRealms.maybeWhen(
data:
(realms) => realms.map(
(realm) => DropdownMenuItem(
value: realm,
child: Text(realm.name),
),
),
orElse: () => [],
),
],
onChanged:
joinedRealms.isLoading
? null
: (SnRealm? value) {
currentRealm.value = value;
},
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [

View File

@@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'publishers.dart';
part of 'publishers_form.dart';
// **************************************************************************
// RiverpodGenerator

View File

@@ -8,7 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/developer.dart';
import 'package:island/models/publisher.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/screens/creators/publishers_form.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';

View File

@@ -6,7 +6,7 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart';
import 'package:island/models/post.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/screens/creators/publishers_form.dart';
import 'package:island/screens/posts/compose_article.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/app_scaffold.dart';

View File

@@ -8,7 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart';
import 'package:island/models/post.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/screens/creators/publishers_form.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/attachment_uploader.dart';

View File

@@ -0,0 +1,276 @@
import 'package:croppy/croppy.dart' show CropAspectRatio;
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.dart';
import 'package:island/models/realm.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/realm/realms.dart';
import 'package:island/services/file.dart';
import 'package:island/services/file_uploader.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
class NewRealmScreen extends StatelessWidget {
const NewRealmScreen({super.key});
@override
Widget build(BuildContext context) {
return const EditRealmScreen();
}
}
class EditRealmScreen extends HookConsumerWidget {
final String? slug;
const EditRealmScreen({super.key, this.slug});
@override
Widget build(BuildContext context, WidgetRef ref) {
final submitting = useState(false);
final picture = useState<SnCloudFile?>(null);
final background = useState<SnCloudFile?>(null);
final isPublic = useState(true);
final isCommunity = useState(false);
final slugController = useTextEditingController();
final nameController = useTextEditingController();
final descriptionController = useTextEditingController();
final formKey = useMemoized(GlobalKey<FormState>.new, const []);
final realm = ref.watch(realmProvider(slug));
useEffect(() {
if (realm.value != null) {
picture.value = realm.value!.picture;
background.value = realm.value!.background;
slugController.text = realm.value!.slug;
nameController.text = realm.value!.name;
descriptionController.text = realm.value!.description;
isPublic.value = realm.value!.isPublic;
isCommunity.value = realm.value!.isCommunity;
}
return null;
}, [realm]);
void setPicture(String position) 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: [
if (position == 'background')
const CropAspectRatio(height: 7, width: 16)
else
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 cloudFile =
await FileUploader.createCloudFile(
client: ref.read(apiClientProvider),
fileData: UniversalFile(
data: result,
type: UniversalFileType.image,
),
).future;
if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...');
}
switch (position) {
case 'picture':
picture.value = cloudFile;
case 'background':
background.value = cloudFile;
}
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
submitting.value = false;
}
}
Future<void> performAction() async {
if (!formKey.currentState!.validate()) return;
submitting.value = true;
try {
final client = ref.watch(apiClientProvider);
final resp = await client.request(
'/sphere${slug == null ? '/realms' : '/realms/$slug'}',
data: {
'slug': slugController.text,
'name': nameController.text,
'description': descriptionController.text,
'background_id': background.value?.id,
'picture_id': picture.value?.id,
'is_public': isPublic.value,
'is_community': isCommunity.value,
},
options: Options(method: slug == null ? 'POST' : 'PATCH'),
);
if (context.mounted) {
context.pop(SnRealm.fromJson(resp.data));
}
} catch (err) {
showErrorAlert(err);
} finally {
submitting.value = false;
}
}
return AppScaffold(
isNoBackground: false,
appBar: AppBar(
title: Text(slug == null ? 'createRealm'.tr() : 'editRealm'.tr()),
leading: const PageBackButton(),
),
body: SingleChildScrollView(
child: Column(
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
GestureDetector(
child: Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child:
background.value != null
? CloudFileWidget(
item: background.value!,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
),
onTap: () {
setPicture('background');
},
),
Positioned(
left: 20,
bottom: -32,
child: GestureDetector(
child: ProfilePictureWidget(
fileId: picture.value?.id,
radius: 40,
fallbackIcon: Symbols.group,
),
onTap: () {
setPicture('picture');
},
),
),
],
),
).padding(bottom: 32),
Form(
key: formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: slugController,
decoration: InputDecoration(
labelText: 'slug'.tr(),
helperText: 'slugHint'.tr(),
),
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
TextFormField(
controller: nameController,
decoration: InputDecoration(labelText: 'name'.tr()),
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
TextFormField(
controller: descriptionController,
decoration: InputDecoration(
labelText: 'description'.tr(),
alignLabelWithHint: true,
),
minLines: 3,
maxLines: null,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
Card(
margin: EdgeInsets.zero,
child: Column(
children: [
CheckboxListTile(
secondary: const Icon(Symbols.public),
title: Text('publicRealm').tr(),
subtitle: Text('publicRealmDescription').tr(),
value: isPublic.value,
onChanged: (value) {
isPublic.value = value ?? true;
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
CheckboxListTile(
secondary: const Icon(Symbols.travel_explore),
title: Text('communityRealm').tr(),
subtitle: Text('communityRealmDescription').tr(),
value: isCommunity.value,
onChanged: (value) {
isCommunity.value = value ?? false;
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
],
),
),
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

@@ -1,17 +1,10 @@
import 'package:croppy/croppy.dart' show CropAspectRatio;
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.dart';
import 'package:island/models/realm.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/file.dart';
import 'package:island/services/file_uploader.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
@@ -32,6 +25,14 @@ Future<List<SnRealm>> realmsJoined(Ref ref) async {
return resp.data.map((e) => SnRealm.fromJson(e)).cast<SnRealm>().toList();
}
@riverpod
Future<SnRealm?> realm(Ref ref, String? identifier) async {
if (identifier == null) return null;
final client = ref.watch(apiClientProvider);
final resp = await client.get('/sphere/realms/$identifier');
return SnRealm.fromJson(resp.data);
}
class RealmListScreen extends HookConsumerWidget {
const RealmListScreen({super.key});
@@ -124,271 +125,6 @@ class RealmListScreen extends HookConsumerWidget {
}
}
@riverpod
Future<SnRealm?> realm(Ref ref, String? identifier) async {
if (identifier == null) return null;
final client = ref.watch(apiClientProvider);
final resp = await client.get('/sphere/realms/$identifier');
return SnRealm.fromJson(resp.data);
}
class NewRealmScreen extends StatelessWidget {
const NewRealmScreen({super.key});
@override
Widget build(BuildContext context) {
return const EditRealmScreen();
}
}
class EditRealmScreen extends HookConsumerWidget {
final String? slug;
const EditRealmScreen({super.key, this.slug});
@override
Widget build(BuildContext context, WidgetRef ref) {
final submitting = useState(false);
final picture = useState<SnCloudFile?>(null);
final background = useState<SnCloudFile?>(null);
final isPublic = useState(true);
final isCommunity = useState(false);
final slugController = useTextEditingController();
final nameController = useTextEditingController();
final descriptionController = useTextEditingController();
final formKey = useMemoized(GlobalKey<FormState>.new, const []);
final realm = ref.watch(realmProvider(slug));
useEffect(() {
if (realm.value != null) {
picture.value = realm.value!.picture;
background.value = realm.value!.background;
slugController.text = realm.value!.slug;
nameController.text = realm.value!.name;
descriptionController.text = realm.value!.description;
isPublic.value = realm.value!.isPublic;
isCommunity.value = realm.value!.isCommunity;
}
return null;
}, [realm]);
void setPicture(String position) 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: [
if (position == 'background')
const CropAspectRatio(height: 7, width: 16)
else
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 cloudFile =
await FileUploader.createCloudFile(
client: ref.read(apiClientProvider),
fileData: UniversalFile(
data: result,
type: UniversalFileType.image,
),
).future;
if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...');
}
switch (position) {
case 'picture':
picture.value = cloudFile;
case 'background':
background.value = cloudFile;
}
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
submitting.value = false;
}
}
Future<void> performAction() async {
if (!formKey.currentState!.validate()) return;
submitting.value = true;
try {
final client = ref.watch(apiClientProvider);
final resp = await client.request(
'/sphere${slug == null ? '/realms' : '/realms/$slug'}',
data: {
'slug': slugController.text,
'name': nameController.text,
'description': descriptionController.text,
'background_id': background.value?.id,
'picture_id': picture.value?.id,
'is_public': isPublic.value,
'is_community': isCommunity.value,
},
options: Options(method: slug == null ? 'POST' : 'PATCH'),
);
if (context.mounted) {
context.pop(SnRealm.fromJson(resp.data));
}
} catch (err) {
showErrorAlert(err);
} finally {
submitting.value = false;
}
}
return AppScaffold(
isNoBackground: false,
appBar: AppBar(
title: Text(slug == null ? 'createRealm'.tr() : 'editRealm'.tr()),
leading: const PageBackButton(),
),
body: SingleChildScrollView(
child: Column(
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
GestureDetector(
child: Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child:
background.value != null
? CloudFileWidget(
item: background.value!,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
),
onTap: () {
setPicture('background');
},
),
Positioned(
left: 20,
bottom: -32,
child: GestureDetector(
child: ProfilePictureWidget(
fileId: picture.value?.id,
radius: 40,
fallbackIcon: Symbols.group,
),
onTap: () {
setPicture('picture');
},
),
),
],
),
).padding(bottom: 32),
Form(
key: formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: slugController,
decoration: InputDecoration(
labelText: 'slug'.tr(),
helperText: 'slugHint'.tr(),
),
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
TextFormField(
controller: nameController,
decoration: InputDecoration(labelText: 'name'.tr()),
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
TextFormField(
controller: descriptionController,
decoration: InputDecoration(
labelText: 'description'.tr(),
alignLabelWithHint: true,
),
minLines: 3,
maxLines: null,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
Card(
margin: EdgeInsets.zero,
child: Column(
children: [
CheckboxListTile(
secondary: const Icon(Symbols.public),
title: Text('publicRealm').tr(),
subtitle: Text('publicRealmDescription').tr(),
value: isPublic.value,
onChanged: (value) {
isPublic.value = value ?? true;
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
CheckboxListTile(
secondary: const Icon(Symbols.travel_explore),
title: Text('communityRealm').tr(),
subtitle: Text('communityRealmDescription').tr(),
value: isCommunity.value,
onChanged: (value) {
isCommunity.value = value ?? false;
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
],
),
),
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),
),
],
),
),
);
}
}
@riverpod
Future<List<SnRealmMember>> realmInvites(Ref ref) async {
final client = ref.watch(apiClientProvider);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,452 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'stellar_program_tab.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$accountStellarSubscriptionHash() =>
r'80abcdefb3868775fd8fe3c980215713efff5948';
/// See also [accountStellarSubscription].
@ProviderFor(accountStellarSubscription)
final accountStellarSubscriptionProvider =
AutoDisposeFutureProvider<SnWalletSubscription?>.internal(
accountStellarSubscription,
name: r'accountStellarSubscriptionProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$accountStellarSubscriptionHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef AccountStellarSubscriptionRef =
AutoDisposeFutureProviderRef<SnWalletSubscription?>;
String _$accountSentGiftsHash() => r'32a282ec863023c749d81423704787943110a188';
/// 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 [accountSentGifts].
@ProviderFor(accountSentGifts)
const accountSentGiftsProvider = AccountSentGiftsFamily();
/// See also [accountSentGifts].
class AccountSentGiftsFamily extends Family<AsyncValue<List<SnWalletGift>>> {
/// See also [accountSentGifts].
const AccountSentGiftsFamily();
/// See also [accountSentGifts].
AccountSentGiftsProvider call({int offset = 0, int take = 20}) {
return AccountSentGiftsProvider(offset: offset, take: take);
}
@override
AccountSentGiftsProvider getProviderOverride(
covariant AccountSentGiftsProvider provider,
) {
return call(offset: provider.offset, take: provider.take);
}
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'accountSentGiftsProvider';
}
/// See also [accountSentGifts].
class AccountSentGiftsProvider
extends AutoDisposeFutureProvider<List<SnWalletGift>> {
/// See also [accountSentGifts].
AccountSentGiftsProvider({int offset = 0, int take = 20})
: this._internal(
(ref) => accountSentGifts(
ref as AccountSentGiftsRef,
offset: offset,
take: take,
),
from: accountSentGiftsProvider,
name: r'accountSentGiftsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$accountSentGiftsHash,
dependencies: AccountSentGiftsFamily._dependencies,
allTransitiveDependencies:
AccountSentGiftsFamily._allTransitiveDependencies,
offset: offset,
take: take,
);
AccountSentGiftsProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.offset,
required this.take,
}) : super.internal();
final int offset;
final int take;
@override
Override overrideWith(
FutureOr<List<SnWalletGift>> Function(AccountSentGiftsRef provider) create,
) {
return ProviderOverride(
origin: this,
override: AccountSentGiftsProvider._internal(
(ref) => create(ref as AccountSentGiftsRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
offset: offset,
take: take,
),
);
}
@override
AutoDisposeFutureProviderElement<List<SnWalletGift>> createElement() {
return _AccountSentGiftsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is AccountSentGiftsProvider &&
other.offset == offset &&
other.take == take;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, offset.hashCode);
hash = _SystemHash.combine(hash, take.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin AccountSentGiftsRef on AutoDisposeFutureProviderRef<List<SnWalletGift>> {
/// The parameter `offset` of this provider.
int get offset;
/// The parameter `take` of this provider.
int get take;
}
class _AccountSentGiftsProviderElement
extends AutoDisposeFutureProviderElement<List<SnWalletGift>>
with AccountSentGiftsRef {
_AccountSentGiftsProviderElement(super.provider);
@override
int get offset => (origin as AccountSentGiftsProvider).offset;
@override
int get take => (origin as AccountSentGiftsProvider).take;
}
String _$accountReceivedGiftsHash() =>
r'7c0dfcc109f6f50ec326dd64c2d944aaccd9f775';
/// See also [accountReceivedGifts].
@ProviderFor(accountReceivedGifts)
const accountReceivedGiftsProvider = AccountReceivedGiftsFamily();
/// See also [accountReceivedGifts].
class AccountReceivedGiftsFamily
extends Family<AsyncValue<List<SnWalletGift>>> {
/// See also [accountReceivedGifts].
const AccountReceivedGiftsFamily();
/// See also [accountReceivedGifts].
AccountReceivedGiftsProvider call({int offset = 0, int take = 20}) {
return AccountReceivedGiftsProvider(offset: offset, take: take);
}
@override
AccountReceivedGiftsProvider getProviderOverride(
covariant AccountReceivedGiftsProvider provider,
) {
return call(offset: provider.offset, take: provider.take);
}
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'accountReceivedGiftsProvider';
}
/// See also [accountReceivedGifts].
class AccountReceivedGiftsProvider
extends AutoDisposeFutureProvider<List<SnWalletGift>> {
/// See also [accountReceivedGifts].
AccountReceivedGiftsProvider({int offset = 0, int take = 20})
: this._internal(
(ref) => accountReceivedGifts(
ref as AccountReceivedGiftsRef,
offset: offset,
take: take,
),
from: accountReceivedGiftsProvider,
name: r'accountReceivedGiftsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$accountReceivedGiftsHash,
dependencies: AccountReceivedGiftsFamily._dependencies,
allTransitiveDependencies:
AccountReceivedGiftsFamily._allTransitiveDependencies,
offset: offset,
take: take,
);
AccountReceivedGiftsProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.offset,
required this.take,
}) : super.internal();
final int offset;
final int take;
@override
Override overrideWith(
FutureOr<List<SnWalletGift>> Function(AccountReceivedGiftsRef provider)
create,
) {
return ProviderOverride(
origin: this,
override: AccountReceivedGiftsProvider._internal(
(ref) => create(ref as AccountReceivedGiftsRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
offset: offset,
take: take,
),
);
}
@override
AutoDisposeFutureProviderElement<List<SnWalletGift>> createElement() {
return _AccountReceivedGiftsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is AccountReceivedGiftsProvider &&
other.offset == offset &&
other.take == take;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, offset.hashCode);
hash = _SystemHash.combine(hash, take.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin AccountReceivedGiftsRef
on AutoDisposeFutureProviderRef<List<SnWalletGift>> {
/// The parameter `offset` of this provider.
int get offset;
/// The parameter `take` of this provider.
int get take;
}
class _AccountReceivedGiftsProviderElement
extends AutoDisposeFutureProviderElement<List<SnWalletGift>>
with AccountReceivedGiftsRef {
_AccountReceivedGiftsProviderElement(super.provider);
@override
int get offset => (origin as AccountReceivedGiftsProvider).offset;
@override
int get take => (origin as AccountReceivedGiftsProvider).take;
}
String _$accountGiftHash() => r'7169d355f78e4fe3bf6b3ff444350faa46a0d216';
/// See also [accountGift].
@ProviderFor(accountGift)
const accountGiftProvider = AccountGiftFamily();
/// See also [accountGift].
class AccountGiftFamily extends Family<AsyncValue<SnWalletGift>> {
/// See also [accountGift].
const AccountGiftFamily();
/// See also [accountGift].
AccountGiftProvider call(String giftId) {
return AccountGiftProvider(giftId);
}
@override
AccountGiftProvider getProviderOverride(
covariant AccountGiftProvider provider,
) {
return call(provider.giftId);
}
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'accountGiftProvider';
}
/// See also [accountGift].
class AccountGiftProvider extends AutoDisposeFutureProvider<SnWalletGift> {
/// See also [accountGift].
AccountGiftProvider(String giftId)
: this._internal(
(ref) => accountGift(ref as AccountGiftRef, giftId),
from: accountGiftProvider,
name: r'accountGiftProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$accountGiftHash,
dependencies: AccountGiftFamily._dependencies,
allTransitiveDependencies: AccountGiftFamily._allTransitiveDependencies,
giftId: giftId,
);
AccountGiftProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.giftId,
}) : super.internal();
final String giftId;
@override
Override overrideWith(
FutureOr<SnWalletGift> Function(AccountGiftRef provider) create,
) {
return ProviderOverride(
origin: this,
override: AccountGiftProvider._internal(
(ref) => create(ref as AccountGiftRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
giftId: giftId,
),
);
}
@override
AutoDisposeFutureProviderElement<SnWalletGift> createElement() {
return _AccountGiftProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is AccountGiftProvider && other.giftId == giftId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, giftId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin AccountGiftRef on AutoDisposeFutureProviderRef<SnWalletGift> {
/// The parameter `giftId` of this provider.
String get giftId;
}
class _AccountGiftProviderElement
extends AutoDisposeFutureProviderElement<SnWalletGift>
with AccountGiftRef {
_AccountGiftProviderElement(super.provider);
@override
String get giftId => (origin as AccountGiftProvider).giftId;
}
// 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

@@ -3,11 +3,13 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart';
import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/screens/creators/publishers_form.dart';
import 'package:island/screens/posts/compose.dart';
import 'package:island/services/compose_storage_db.dart';
import 'package:island/services/responsive.dart';
@@ -31,6 +33,7 @@ class PostComposeCard extends HookConsumerWidget {
final VoidCallback? onCancel;
final Function(SnPost)? onSubmit;
final Function(ComposeState)? onStateChanged;
final bool isInDialog;
const PostComposeCard({
super.key,
@@ -39,6 +42,7 @@ class PostComposeCard extends HookConsumerWidget {
this.onCancel,
this.onSubmit,
this.onStateChanged,
this.isInDialog = false,
});
@override
@@ -441,7 +445,11 @@ class PostComposeCard extends HookConsumerWidget {
),
),
IconButton(
onPressed: state.submitting.value ? null : performSubmit,
onPressed:
(state.submitting.value ||
state.currentPublisher.value == null)
? null
: performSubmit,
icon:
state.submitting.value
? SizedBox(
@@ -515,16 +523,31 @@ class PostComposeCard extends HookConsumerWidget {
: null,
),
onTap: () {
showModalBottomSheet(
isScrollControlled: true,
useRootNavigator: true,
context: context,
builder: (context) => const PublisherModal(),
).then((value) {
if (value != null) {
state.currentPublisher.value = value;
if (state.currentPublisher.value == null) {
// No publisher loaded, guide user to create one
if (isInDialog) {
Navigator.of(context).pop();
}
});
context.pushNamed('creatorNew').then((value) {
if (value != null) {
state.currentPublisher.value =
value as SnPublisher;
ref.invalidate(publishersManagedProvider);
}
});
} else {
// Show modal to select from existing publishers
showModalBottomSheet(
isScrollControlled: true,
useRootNavigator: true,
context: context,
builder: (context) => const PublisherModal(),
).then((value) {
if (value != null) {
state.currentPublisher.value = value;
}
});
}
},
).padding(top: 8),
@@ -533,8 +556,43 @@ class PostComposeCard extends HookConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state.currentPublisher.value == null)
Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color:
theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(8),
),
child: Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Icon(
Symbols.info,
size: 16,
color: theme.colorScheme.primary,
),
const Gap(8),
Expanded(
child: Text(
'Tap the avatar to create a publisher and start composing.',
style: theme.textTheme.bodySmall
?.copyWith(
color:
theme
.colorScheme
.onSurfaceVariant,
),
),
),
],
),
),
TextField(
controller: state.titleController,
enabled: state.currentPublisher.value != null,
decoration: InputDecoration(
hintText: 'postTitle'.tr(),
border: InputBorder.none,
@@ -552,6 +610,7 @@ class PostComposeCard extends HookConsumerWidget {
),
TextField(
controller: state.descriptionController,
enabled: state.currentPublisher.value != null,
decoration: InputDecoration(
hintText: 'postDescription'.tr(),
border: InputBorder.none,
@@ -573,6 +632,7 @@ class PostComposeCard extends HookConsumerWidget {
),
TextField(
controller: state.contentController,
enabled: state.currentPublisher.value != null,
style: theme.textTheme.bodyMedium,
decoration: InputDecoration(
border: InputBorder.none,

View File

@@ -70,6 +70,7 @@ class PostComposeDialog extends HookConsumerWidget {
initialState: restoredInitialState.value ?? initialState,
onCancel: () => Navigator.of(context).pop(),
onSubmit: (post) => Navigator.of(context).pop(post),
isInDialog: true,
),
),
);

View File

@@ -6,7 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/screens/creators/publishers_form.dart';
import 'package:island/screens/posts/compose.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_files.dart';

View File

@@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/screens/creators/publishers_form.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:styled_widget/styled_widget.dart';
@@ -43,13 +43,15 @@ class PublisherModal extends HookConsumerWidget {
const Gap(12),
ElevatedButton(
onPressed: () {
context.pushNamed('creatorNew').then((value) {
if (value != null) {
ref.invalidate(
publishersManagedProvider,
);
}
});
context.pushNamed('creatorNew').then((
value,
) {
if (value != null) {
ref.invalidate(
publishersManagedProvider,
);
}
});
},
child: Text('createPublisher').tr(),
),