💄 Optimize publisher first time UX

♻️ Split up the forms and list screens
💄 Use dropdown forms fields instead of selection
This commit is contained in:
2025-10-03 15:42:56 +08:00
parent c87e6cfe07
commit 0b1a23e81a
21 changed files with 2385 additions and 585 deletions

View File

@@ -124,3 +124,30 @@ sealed class SnWalletOrder with _$SnWalletOrder {
factory SnWalletOrder.fromJson(Map<String, dynamic> json) => factory SnWalletOrder.fromJson(Map<String, dynamic> json) =>
_$SnWalletOrderFromJson(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 // dart format on

View File

@@ -228,3 +228,70 @@ Map<String, dynamic> _$SnWalletOrderToJson(_SnWalletOrder instance) =>
'updated_at': instance.updatedAt.toIso8601String(), 'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.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

@@ -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/leveling.dart';
import 'package:island/screens/account/me/account_settings.dart'; import 'package:island/screens/account/me/account_settings.dart';
import 'package:island/screens/chat/chat.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.dart';
import 'package:island/screens/chat/room_detail.dart'; import 'package:island/screens/chat/room_detail.dart';
import 'package:island/screens/chat/call.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_marketplace.dart';
import 'package:island/screens/discovery/feeds/feed_detail.dart'; import 'package:island/screens/discovery/feeds/feed_detail.dart';
import 'package:island/screens/creators/poll/poll_list.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_list.dart';
import 'package:island/screens/creators/webfeed/webfeed_edit.dart'; import 'package:island/screens/creators/webfeed/webfeed_edit.dart';
import 'package:island/screens/poll/poll_editor.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/auth/create_account.dart';
import 'package:island/screens/settings.dart'; import 'package:island/screens/settings.dart';
import 'package:island/screens/realm/realms.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/realm/realm_detail.dart';
import 'package:island/screens/account/event_calendar.dart'; import 'package:island/screens/account/event_calendar.dart';
import 'package:island/screens/discovery/realms.dart'; import 'package:island/screens/discovery/realms.dart';

View File

@@ -1,10 +1,12 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/network.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'captcha.config.g.dart'; part 'captcha.config.g.dart';
@riverpod @riverpod
Future<String> captchaUrl(Ref ref) async { 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'; 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:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/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/call.dart';
import 'package:island/pods/chat/chat_summary.dart'; import 'package:island/pods/chat/chat_summary.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/screens/realm/realms.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/services/responsive.dart';
import 'package:island/widgets/account/account_picker.dart'; import 'package:island/widgets/account/account_picker.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sheet.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:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:relative_time/relative_time.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 @riverpod
Future<List<SnChatMember>> chatroomInvites(Ref ref) async { Future<List<SnChatMember>> chatroomInvites(Ref ref) async {
final client = ref.watch(apiClientProvider); 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/post.dart';
import 'package:island/models/publisher.dart'; import 'package:island/models/publisher.dart';
import 'package:island/pods/network.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/services/responsive.dart';
import 'package:island/utils/text.dart'; import 'package:island/utils/text.dart';
import 'package:island/widgets/account/account_picker.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/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.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:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
part 'publishers.g.dart'; part 'publishers_form.g.dart';
@riverpod @riverpod
Future<List<SnPublisher>> publishersManaged(Ref ref) async { Future<List<SnPublisher>> publishersManaged(Ref ref) async {
@@ -187,19 +186,6 @@ class EditPublisherScreen extends HookConsumerWidget {
padding: EdgeInsets.only(bottom: 16), padding: EdgeInsets.only(bottom: 16),
child: Column( child: Column(
children: [ 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(
aspectRatio: 16 / 7, aspectRatio: 16 / 7,
child: Stack( child: Stack(
@@ -273,6 +259,32 @@ class EditPublisherScreen extends HookConsumerWidget {
onTapOutside: onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(), (_) => 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( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [

View File

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

View File

@@ -8,7 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/developer.dart'; import 'package:island/models/developer.dart';
import 'package:island/models/publisher.dart'; import 'package:island/models/publisher.dart';
import 'package:island/pods/network.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/services/responsive.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.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:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/models/post.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/screens/posts/compose_article.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:island/widgets/app_scaffold.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/file.dart';
import 'package:island/models/post.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/services/responsive.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/attachment_uploader.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:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/models/realm.dart';
import 'package:island/pods/network.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/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.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(); 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 { class RealmListScreen extends HookConsumerWidget {
const RealmListScreen({super.key}); 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 @riverpod
Future<List<SnRealmMember>> realmInvites(Ref ref) async { Future<List<SnRealmMember>> realmInvites(Ref ref) async {
final client = ref.watch(apiClientProvider); final client = ref.watch(apiClientProvider);

View File

@@ -5,12 +5,15 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/account.dart';
import 'package:island/models/wallet.dart'; import 'package:island/models/wallet.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
import 'package:island/services/time.dart'; import 'package:island/services/time.dart';
import 'package:island/widgets/account/account_picker.dart';
import 'package:island/widgets/account/restore_purchase_sheet.dart'; import 'package:island/widgets/account/restore_purchase_sheet.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/payment/payment_overlay.dart'; import 'package:island/widgets/payment/payment_overlay.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
@@ -31,6 +34,39 @@ Future<SnWalletSubscription?> accountStellarSubscription(Ref ref) async {
} }
} }
@riverpod
Future<List<SnWalletGift>> accountSentGifts(
Ref ref, {
int offset = 0,
int take = 20,
}) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get(
'/id/subscriptions/gifts/sent?offset=$offset&take=$take',
);
return (resp.data as List).map((e) => SnWalletGift.fromJson(e)).toList();
}
@riverpod
Future<List<SnWalletGift>> accountReceivedGifts(
Ref ref, {
int offset = 0,
int take = 20,
}) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get(
'/id/subscriptions/gifts/received?offset=$offset&take=$take',
);
return (resp.data as List).map((e) => SnWalletGift.fromJson(e)).toList();
}
@riverpod
Future<SnWalletGift> accountGift(Ref ref, String giftId) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/id/subscriptions/gifts/$giftId');
return SnWalletGift.fromJson(resp.data);
}
class StellarProgramTab extends HookConsumerWidget { class StellarProgramTab extends HookConsumerWidget {
const StellarProgramTab({super.key}); const StellarProgramTab({super.key});
@@ -45,6 +81,8 @@ class StellarProgramTab extends HookConsumerWidget {
children: [ children: [
_buildMembershipSection(context, ref, stellarSubscription), _buildMembershipSection(context, ref, stellarSubscription),
const Gap(16), const Gap(16),
_buildGiftingSection(context, ref),
const Gap(16),
], ],
), ),
); );
@@ -466,4 +504,728 @@ class StellarProgramTab extends HookConsumerWidget {
if (context.mounted) hideLoadingModal(context); if (context.mounted) hideLoadingModal(context);
} }
} }
Widget _buildGiftingSection(BuildContext context, WidgetRef ref) {
final sentGifts = ref.watch(accountSentGiftsProvider());
final receivedGifts = ref.watch(accountReceivedGiftsProvider());
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Icon(
Icons.card_giftcard,
color: Theme.of(context).colorScheme.primary,
size: 24,
),
const Gap(8),
Text(
'Gift Subscriptions',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
],
),
const Gap(12),
// Purchase Gift Section
Text(
'Purchase a Gift',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const Gap(8),
_buildGiftPurchaseOptions(context, ref),
const Gap(16),
// Redeem Gift Section
Text(
'Redeem a Gift',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const Gap(8),
_buildGiftRedeemSection(context, ref),
const Gap(16),
// Gift History
Text(
'Gift History',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const Gap(8),
_buildGiftHistory(context, ref, sentGifts, receivedGifts),
],
).padding(all: 16),
);
}
Widget _buildGiftPurchaseOptions(BuildContext context, WidgetRef ref) {
final tiers = [
{
'id': 'solian.stellar.primary',
'name': 'Stellar Gift',
'price': 'Same as membership',
'color': Colors.blue,
},
{
'id': 'solian.stellar.nova',
'name': 'Nova Gift',
'price': 'Same as membership',
'color': Color.fromRGBO(57, 197, 187, 1),
},
{
'id': 'solian.stellar.supernova',
'name': 'Supernova Gift',
'price': 'Same as membership',
'color': Colors.orange,
},
];
return Column(
children:
tiers.map((tier) {
final tierColor = tier['color'] as Color;
return Container(
margin: const EdgeInsets.only(bottom: 8),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap:
() => _showPurchaseGiftDialog(
context,
ref,
tier['id'] as String,
),
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Theme.of(
context,
).colorScheme.outline.withOpacity(0.2),
width: 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: [
Text(
tier['name'] as String,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
Text(
tier['price'] as String,
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(
color:
Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
],
),
),
Icon(
Icons.arrow_forward_ios,
size: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
),
),
),
),
);
}).toList(),
);
}
Widget _buildGiftRedeemSection(BuildContext context, WidgetRef ref) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Enter gift code to redeem',
style: Theme.of(context).textTheme.bodyMedium,
),
const Gap(8),
TextField(
decoration: InputDecoration(
hintText: 'Enter gift code',
border: OutlineInputBorder(),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
suffixIcon: IconButton(
icon: Icon(Icons.redeem),
onPressed: () => _showRedeemGiftDialog(context, ref),
),
),
onSubmitted: (code) => _redeemGift(context, ref, code),
),
],
),
);
}
Widget _buildGiftHistory(
BuildContext context,
WidgetRef ref,
AsyncValue<List<SnWalletGift>> sentGifts,
AsyncValue<List<SnWalletGift>> receivedGifts,
) {
return Row(
children: [
Expanded(
child: OutlinedButton(
onPressed:
() => _showGiftHistorySheet(context, ref, sentGifts, true),
child: Text('Sent Gifts'),
),
),
const Gap(8),
Expanded(
child: OutlinedButton(
onPressed:
() => _showGiftHistorySheet(context, ref, receivedGifts, false),
child: Text('Received Gifts'),
),
),
],
);
}
Future<void> _showGiftHistorySheet(
BuildContext context,
WidgetRef ref,
AsyncValue<List<SnWalletGift>> giftsAsync,
bool isSent,
) async {
await showModalBottomSheet(
isScrollControlled: true,
useRootNavigator: true,
context: context,
builder:
(context) => SheetScaffold(
titleText: isSent ? 'Sent Gifts' : 'Received Gifts',
child: giftsAsync.when(
data:
(gifts) =>
gifts.isEmpty
? Padding(
padding: const EdgeInsets.all(16),
child: Text(
isSent ? 'No sent gifts' : 'No received gifts',
),
)
: ListView.builder(
itemCount: gifts.length,
itemBuilder:
(context, index) => _buildGiftItem(
context,
ref,
gifts[index],
isSent,
),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
),
),
);
}
Widget _buildGiftItem(
BuildContext context,
WidgetRef ref,
SnWalletGift gift,
bool isSent,
) {
final statusText = _getGiftStatusText(gift.status);
final statusColor = _getGiftStatusColor(context, gift.status);
final canCancel = isSent && (gift.status == 0 || gift.status == 1);
return Container(
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'Code: ${gift.giftCode}',
style: TextStyle(fontWeight: FontWeight.w600),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
statusText,
style: TextStyle(
color: statusColor,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
],
),
const Gap(4),
Text(
'Subscription: ${gift.subscriptionIdentifier}',
style: Theme.of(context).textTheme.bodySmall,
),
if (gift.recipient != null && isSent) ...[
const Gap(4),
Text(
'To: ${gift.recipient!.name}',
style: Theme.of(context).textTheme.bodySmall,
),
],
if (gift.gifter != null && !isSent) ...[
const Gap(4),
Text(
'From: ${gift.gifter!.name}',
style: Theme.of(context).textTheme.bodySmall,
),
],
if (gift.message != null && gift.message!.isNotEmpty) ...[
const Gap(4),
Text(
'Message: ${gift.message}',
style: Theme.of(context).textTheme.bodySmall,
),
],
if (canCancel) ...[
const Gap(8),
Align(
alignment: Alignment.centerRight,
child: OutlinedButton.icon(
onPressed: () => _cancelGift(context, ref, gift),
icon: const Icon(Icons.cancel, size: 16),
label: const Text('Cancel'),
style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
side: BorderSide(color: Theme.of(context).colorScheme.error),
),
),
),
],
],
),
);
}
String _getGiftStatusText(int status) {
switch (status) {
case 0:
return 'Created';
case 1:
return 'Sent';
case 2:
return 'Redeemed';
case 3:
return 'Cancelled';
case 4:
return 'Expired';
default:
return 'Unknown';
}
}
Color _getGiftStatusColor(BuildContext context, int status) {
switch (status) {
case 0:
return Colors.grey;
case 1:
return Colors.blue;
case 2:
return Colors.green;
case 3:
return Colors.red;
case 4:
return Colors.orange;
default:
return Theme.of(context).colorScheme.primary;
}
}
Future<void> _showPurchaseGiftDialog(
BuildContext context,
WidgetRef ref,
String subscriptionId,
) async {
final messageController = TextEditingController();
final recipient = await showModalBottomSheet<SnAccount>(
isScrollControlled: true,
useRootNavigator: true,
context: context,
builder:
(context) => SheetScaffold(
titleText: 'Select Recipient (Optional)',
child: Column(
children: [
Expanded(child: AccountPickerSheet()),
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Skip (Open Gift)'),
),
),
],
),
),
],
),
),
);
if (!context.mounted) return;
final message = await showModalBottomSheet<String>(
isScrollControlled: true,
useRootNavigator: true,
context: context,
builder:
(context) => SheetScaffold(
titleText: 'Add Message (Optional)',
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: messageController,
decoration: InputDecoration(
labelText: 'Message',
hintText: 'Add a personal message',
border: OutlineInputBorder(),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(
context,
).colorScheme.outline.withOpacity(0.2),
),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
),
maxLines: 3,
autofocus: true,
),
const Gap(16),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Skip'),
),
),
const Gap(8),
Expanded(
child: FilledButton(
onPressed:
() => Navigator.of(context).pop(
messageController.text.trim().isEmpty
? null
: messageController.text.trim(),
),
child: Text('Add Message'),
),
),
],
),
],
),
),
),
);
if (context.mounted) {
await _purchaseGift(context, ref, subscriptionId, recipient?.id, message);
}
}
Future<void> _purchaseGift(
BuildContext context,
WidgetRef ref,
String subscriptionId,
String? recipientId,
String? message,
) async {
final client = ref.watch(apiClientProvider);
try {
showLoadingModal(context);
final resp = await client.post(
'/id/subscriptions/gifts/purchase',
data: {
'subscription_identifier': subscriptionId,
if (recipientId != null) 'recipient_id': recipientId,
'payment_method': 'solian.wallet',
'payment_details': {'currency': 'golds'},
if (message != null) 'message': message,
'gift_duration_days': 30,
'subscription_duration_days': 30,
},
options: Options(headers: {'X-Noop': true}),
);
final gift = SnWalletGift.fromJson(resp.data);
if (gift.status == 1) return; // Already paid
final orderResp = await client.post(
'/id/subscriptions/gifts/${gift.id}/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));
// Get the updated gift
final giftResp = await client.get('/id/subscriptions/gifts/${gift.id}');
final updatedGift = SnWalletGift.fromJson(giftResp.data);
if (context.mounted) hideLoadingModal(context);
// Show gift code dialog
if (context.mounted) {
await showDialog(
context: context,
builder:
(context) => AlertDialog(
title: Text('Gift Purchased!'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Gift Code: ${updatedGift.giftCode}'),
const Gap(8),
Text(
'Share this code with the recipient to redeem the gift.',
),
if (updatedGift.recipientId == null) ...[
const Gap(8),
Text('This is an open gift that anyone can redeem.'),
],
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('OK'),
),
],
),
);
}
}
ref.invalidate(accountSentGiftsProvider);
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
Future<void> _showRedeemGiftDialog(
BuildContext context,
WidgetRef ref,
) async {
final codeController = TextEditingController();
final result = await showDialog<String>(
context: context,
builder:
(context) => AlertDialog(
title: Text('Redeem Gift'),
content: TextField(
controller: codeController,
decoration: InputDecoration(
labelText: 'Gift Code',
hintText: 'Enter the gift code',
border: OutlineInputBorder(),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(
context,
).colorScheme.outline.withOpacity(0.2),
),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
),
autofocus: true,
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Cancel'),
),
FilledButton(
onPressed:
() => Navigator.of(context).pop(codeController.text.trim()),
child: Text('Redeem'),
),
],
),
);
if (result != null && result.isNotEmpty && context.mounted) {
await _redeemGift(context, ref, result);
}
}
Future<void> _redeemGift(
BuildContext context,
WidgetRef ref,
String giftCode,
) async {
final client = ref.watch(apiClientProvider);
try {
showLoadingModal(context);
// First check if gift can be redeemed
final checkResp = await client.get(
'/id/subscriptions/gifts/check/$giftCode',
);
final checkData = checkResp.data as Map<String, dynamic>;
if (!checkData['can_redeem']) {
if (context.mounted) hideLoadingModal(context);
showErrorAlert(checkData['error'] ?? 'Gift cannot be redeemed');
return;
}
// Redeem the gift
await client.post(
'/id/subscriptions/gifts/redeem',
data: {'gift_code': giftCode},
);
if (context.mounted) {
hideLoadingModal(context);
await showDialog(
context: context,
builder:
(context) => AlertDialog(
title: Text('Gift Redeemed!'),
content: Text(
'You have successfully redeemed the gift. Your new subscription is now active.',
),
actions: [
FilledButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('OK'),
),
],
),
);
}
ref.invalidate(accountReceivedGiftsProvider);
ref.invalidate(accountStellarSubscriptionProvider);
ref.read(userInfoProvider.notifier).fetchUser();
} catch (err) {
if (context.mounted) hideLoadingModal(context);
showErrorAlert(err);
}
}
Future<void> _cancelGift(
BuildContext context,
WidgetRef ref,
SnWalletGift gift,
) async {
final confirm = await showConfirmAlert(
'Cancel Gift',
'Are you sure you want to cancel this gift? This action cannot be undone.',
);
if (!confirm || !context.mounted) return;
final client = ref.watch(apiClientProvider);
try {
showLoadingModal(context);
await client.post('/id/subscriptions/gifts/${gift.id}/cancel');
ref.invalidate(accountSentGiftsProvider);
if (context.mounted) {
hideLoadingModal(context);
showSnackBar('Gift cancelled successfully');
}
} catch (err) {
if (context.mounted) hideLoadingModal(context);
showErrorAlert(err);
}
}
} }

View File

@@ -27,5 +27,426 @@ final accountStellarSubscriptionProvider =
// ignore: unused_element // ignore: unused_element
typedef AccountStellarSubscriptionRef = typedef AccountStellarSubscriptionRef =
AutoDisposeFutureProviderRef<SnWalletSubscription?>; 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: 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 // 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/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
import 'package:island/pods/network.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/screens/posts/compose.dart';
import 'package:island/services/compose_storage_db.dart'; import 'package:island/services/compose_storage_db.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
@@ -31,6 +33,7 @@ class PostComposeCard extends HookConsumerWidget {
final VoidCallback? onCancel; final VoidCallback? onCancel;
final Function(SnPost)? onSubmit; final Function(SnPost)? onSubmit;
final Function(ComposeState)? onStateChanged; final Function(ComposeState)? onStateChanged;
final bool isInDialog;
const PostComposeCard({ const PostComposeCard({
super.key, super.key,
@@ -39,6 +42,7 @@ class PostComposeCard extends HookConsumerWidget {
this.onCancel, this.onCancel,
this.onSubmit, this.onSubmit,
this.onStateChanged, this.onStateChanged,
this.isInDialog = false,
}); });
@override @override
@@ -441,7 +445,11 @@ class PostComposeCard extends HookConsumerWidget {
), ),
), ),
IconButton( IconButton(
onPressed: state.submitting.value ? null : performSubmit, onPressed:
(state.submitting.value ||
state.currentPublisher.value == null)
? null
: performSubmit,
icon: icon:
state.submitting.value state.submitting.value
? SizedBox( ? SizedBox(
@@ -515,6 +523,20 @@ class PostComposeCard extends HookConsumerWidget {
: null, : null,
), ),
onTap: () { onTap: () {
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( showModalBottomSheet(
isScrollControlled: true, isScrollControlled: true,
useRootNavigator: true, useRootNavigator: true,
@@ -525,6 +547,7 @@ class PostComposeCard extends HookConsumerWidget {
state.currentPublisher.value = value; state.currentPublisher.value = value;
} }
}); });
}
}, },
).padding(top: 8), ).padding(top: 8),
@@ -533,8 +556,43 @@ class PostComposeCard extends HookConsumerWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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( TextField(
controller: state.titleController, controller: state.titleController,
enabled: state.currentPublisher.value != null,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'postTitle'.tr(), hintText: 'postTitle'.tr(),
border: InputBorder.none, border: InputBorder.none,
@@ -552,6 +610,7 @@ class PostComposeCard extends HookConsumerWidget {
), ),
TextField( TextField(
controller: state.descriptionController, controller: state.descriptionController,
enabled: state.currentPublisher.value != null,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'postDescription'.tr(), hintText: 'postDescription'.tr(),
border: InputBorder.none, border: InputBorder.none,
@@ -573,6 +632,7 @@ class PostComposeCard extends HookConsumerWidget {
), ),
TextField( TextField(
controller: state.contentController, controller: state.contentController,
enabled: state.currentPublisher.value != null,
style: theme.textTheme.bodyMedium, style: theme.textTheme.bodyMedium,
decoration: InputDecoration( decoration: InputDecoration(
border: InputBorder.none, border: InputBorder.none,

View File

@@ -70,6 +70,7 @@ class PostComposeDialog extends HookConsumerWidget {
initialState: restoredInitialState.value ?? initialState, initialState: restoredInitialState.value ?? initialState,
onCancel: () => Navigator.of(context).pop(), onCancel: () => Navigator.of(context).pop(),
onSubmit: (post) => Navigator.of(context).pop(post), 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/post.dart';
import 'package:island/models/publisher.dart'; import 'package:island/models/publisher.dart';
import 'package:island/pods/network.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/screens/posts/compose.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_files.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:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.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:island/widgets/content/cloud_files.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@@ -43,7 +43,9 @@ class PublisherModal extends HookConsumerWidget {
const Gap(12), const Gap(12),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
context.pushNamed('creatorNew').then((value) { context.pushNamed('creatorNew').then((
value,
) {
if (value != null) { if (value != null) {
ref.invalidate( ref.invalidate(
publishersManagedProvider, publishersManagedProvider,