diff --git a/lib/models/wallet.dart b/lib/models/wallet.dart index 7ecf3ba8..940429af 100644 --- a/lib/models/wallet.dart +++ b/lib/models/wallet.dart @@ -124,3 +124,30 @@ sealed class SnWalletOrder with _$SnWalletOrder { factory SnWalletOrder.fromJson(Map 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 json) => + _$SnWalletGiftFromJson(json); +} diff --git a/lib/models/wallet.freezed.dart b/lib/models/wallet.freezed.dart index abd1fba2..0185679a 100644 --- a/lib/models/wallet.freezed.dart +++ b/lib/models/wallet.freezed.dart @@ -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 get copyWith => _$SnWalletGiftCopyWithImpl(this as SnWalletGift, _$identity); + + /// Serializes this SnWalletGift to a JSON map. + Map 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 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 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? 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 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 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? 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 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 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 diff --git a/lib/models/wallet.g.dart b/lib/models/wallet.g.dart index 6a5a8663..32ffaa30 100644 --- a/lib/models/wallet.g.dart +++ b/lib/models/wallet.g.dart @@ -228,3 +228,70 @@ Map _$SnWalletOrderToJson(_SnWalletOrder instance) => 'updated_at': instance.updatedAt.toIso8601String(), 'deleted_at': instance.deletedAt?.toIso8601String(), }; + +_SnWalletGift _$SnWalletGiftFromJson(Map 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), + gifterId: json['gifter_id'] as String, + gifter: + json['gifter'] == null + ? null + : SnAccount.fromJson(json['gifter'] as Map), + redeemerId: json['redeemer_id'] as String?, + redeemer: + json['redeemer'] == null + ? null + : SnAccount.fromJson(json['redeemer'] as Map), + 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, + ), + 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 _$SnWalletGiftToJson(_SnWalletGift instance) => + { + '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(), + }; diff --git a/lib/route.dart b/lib/route.dart index 9fc0bc69..c3f0fde7 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -35,6 +35,7 @@ import 'package:island/screens/account/me/profile_update.dart'; import 'package:island/screens/account/leveling.dart'; import 'package:island/screens/account/me/account_settings.dart'; import 'package:island/screens/chat/chat.dart'; +import 'package:island/screens/chat/chat_form.dart'; import 'package:island/screens/chat/room.dart'; import 'package:island/screens/chat/room_detail.dart'; import 'package:island/screens/chat/call.dart'; @@ -48,7 +49,7 @@ import 'package:island/screens/stickers/pack_detail.dart'; import 'package:island/screens/discovery/feeds/feed_marketplace.dart'; import 'package:island/screens/discovery/feeds/feed_detail.dart'; import 'package:island/screens/creators/poll/poll_list.dart'; -import 'package:island/screens/creators/publishers.dart'; +import 'package:island/screens/creators/publishers_form.dart'; import 'package:island/screens/creators/webfeed/webfeed_list.dart'; import 'package:island/screens/creators/webfeed/webfeed_edit.dart'; import 'package:island/screens/poll/poll_editor.dart'; @@ -59,6 +60,7 @@ import 'package:island/screens/auth/login.dart'; import 'package:island/screens/auth/create_account.dart'; import 'package:island/screens/settings.dart'; import 'package:island/screens/realm/realms.dart'; +import 'package:island/screens/realm/realm_form.dart'; import 'package:island/screens/realm/realm_detail.dart'; import 'package:island/screens/account/event_calendar.dart'; import 'package:island/screens/discovery/realms.dart'; diff --git a/lib/screens/auth/captcha.config.dart b/lib/screens/auth/captcha.config.dart index 8f934b62..cb2f79b3 100644 --- a/lib/screens/auth/captcha.config.dart +++ b/lib/screens/auth/captcha.config.dart @@ -1,10 +1,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/pods/network.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'captcha.config.g.dart'; @riverpod Future 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'; } diff --git a/lib/screens/chat/chat.dart b/lib/screens/chat/chat.dart index 0c5f6303..2a0f5676 100644 --- a/lib/screens/chat/chat.dart +++ b/lib/screens/chat/chat.dart @@ -1,5 +1,3 @@ -import 'package:collection/collection.dart'; -import 'package:croppy/croppy.dart' hide cropImage; import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -7,23 +5,17 @@ import 'package:go_router/go_router.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:image_picker/image_picker.dart'; import 'package:island/models/chat.dart'; -import 'package:island/models/file.dart'; -import 'package:island/models/realm.dart'; import 'package:island/pods/chat/call.dart'; import 'package:island/pods/chat/chat_summary.dart'; import 'package:island/pods/network.dart'; import 'package:island/screens/realm/realms.dart'; -import 'package:island/services/file.dart'; -import 'package:island/services/file_uploader.dart'; import 'package:island/services/responsive.dart'; import 'package:island/widgets/account/account_picker.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/sheet.dart'; -import 'package:island/widgets/realm/realm_selection_dropdown.dart'; import 'package:island/widgets/response.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:relative_time/relative_time.dart'; @@ -568,269 +560,6 @@ Future 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(), []); - - final submitting = useState(false); - - final nameController = useTextEditingController(); - final descriptionController = useTextEditingController(); - final picture = useState(null); - final background = useState(null); - final isPublic = useState(true); - final isCommunity = useState(false); - - final chat = ref.watch(chatroomProvider(id)); - - final joinedRealms = ref.watch(realmsJoinedProvider); - final currentRealm = useState(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 performAction() async { - if (!formKey.currentState!.validate()) return; - - submitting.value = true; - try { - final client = ref.watch(apiClientProvider); - final resp = await client.request( - id == null ? '/sphere/chat' : '/sphere/chat/$id', - data: { - 'name': nameController.text, - 'description': descriptionController.text, - 'background_id': background.value?.id, - 'picture_id': picture.value?.id, - 'realm_id': currentRealm.value?.id, - 'is_public': isPublic.value, - 'is_community': isCommunity.value, - }, - options: Options(method: id == null ? 'POST' : 'PATCH'), - ); - if (context.mounted) { - context.pop(SnChatRoom.fromJson(resp.data)); - } - } catch (err) { - showErrorAlert(err); - } finally { - submitting.value = false; - } - } - - return AppScaffold( - appBar: AppBar( - title: Text(id == null ? 'createChatRoom' : 'editChatRoom').tr(), - leading: const PageBackButton(), - ), - body: SingleChildScrollView( - child: Column( - children: [ - RealmSelectionDropdown( - value: currentRealm.value, - realms: joinedRealms.when( - data: (realms) => realms, - loading: () => [], - error: (_, _) => [], - ), - onChanged: (SnRealm? value) { - currentRealm.value = value; - }, - isLoading: joinedRealms.isLoading, - error: joinedRealms.error?.toString(), - ), - AspectRatio( - aspectRatio: 16 / 7, - child: Stack( - clipBehavior: Clip.none, - fit: StackFit.expand, - children: [ - GestureDetector( - child: Container( - color: Theme.of(context).colorScheme.surfaceContainerHigh, - child: - background.value != null - ? CloudFileWidget( - item: background.value!, - fit: BoxFit.cover, - ) - : const SizedBox.shrink(), - ), - onTap: () { - setPicture('background'); - }, - ), - Positioned( - left: 20, - bottom: -32, - child: GestureDetector( - child: ProfilePictureWidget( - fileId: picture.value?.id, - radius: 40, - fallbackIcon: Symbols.group, - ), - onTap: () { - setPicture('picture'); - }, - ), - ), - ], - ), - ).padding(bottom: 32), - Form( - key: formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextFormField( - controller: nameController, - decoration: const InputDecoration(labelText: 'Name'), - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), - ), - const SizedBox(height: 16), - TextFormField( - controller: descriptionController, - decoration: const InputDecoration( - labelText: 'Description', - alignLabelWithHint: true, - ), - minLines: 3, - maxLines: null, - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), - ), - const SizedBox(height: 16), - Card( - margin: EdgeInsets.zero, - child: Column( - children: [ - CheckboxListTile( - secondary: const Icon(Symbols.public), - title: Text('publicChat').tr(), - subtitle: Text('publicChatDescription').tr(), - value: isPublic.value, - onChanged: (value) { - isPublic.value = value ?? true; - }, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - CheckboxListTile( - secondary: const Icon(Symbols.travel_explore), - title: Text('communityChat').tr(), - subtitle: Text('communityChatDescription').tr(), - value: isCommunity.value, - onChanged: (value) { - isCommunity.value = value ?? false; - }, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ], - ), - ), - const SizedBox(height: 16), - Align( - alignment: Alignment.centerRight, - child: TextButton.icon( - onPressed: submitting.value ? null : performAction, - label: const Text('Save'), - icon: const Icon(Symbols.save), - ), - ), - ], - ).padding(all: 24), - ), - ], - ), - ), - ); - } -} - @riverpod Future> chatroomInvites(Ref ref) async { final client = ref.watch(apiClientProvider); diff --git a/lib/screens/chat/chat_form.dart b/lib/screens/chat/chat_form.dart new file mode 100644 index 00000000..2fa92daa --- /dev/null +++ b/lib/screens/chat/chat_form.dart @@ -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(), []); + + final submitting = useState(false); + + final nameController = useTextEditingController(); + final descriptionController = useTextEditingController(); + final picture = useState(null); + final background = useState(null); + final isPublic = useState(true); + final isCommunity = useState(false); + + final chat = ref.watch(chatroomProvider(id)); + + final joinedRealms = ref.watch(realmsJoinedProvider); + final currentRealm = useState(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 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( + value: currentRealm.value, + decoration: InputDecoration(labelText: 'realm'.tr()), + items: [ + DropdownMenuItem( + 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), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/creators/hub.dart b/lib/screens/creators/hub.dart index 40b230f3..1fb10fd2 100644 --- a/lib/screens/creators/hub.dart +++ b/lib/screens/creators/hub.dart @@ -9,7 +9,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/post.dart'; import 'package:island/models/publisher.dart'; import 'package:island/pods/network.dart'; -import 'package:island/screens/creators/publishers.dart'; +import 'package:island/screens/creators/publishers_form.dart'; import 'package:island/services/responsive.dart'; import 'package:island/utils/text.dart'; import 'package:island/widgets/account/account_picker.dart'; diff --git a/lib/screens/creators/publishers.dart b/lib/screens/creators/publishers_form.dart similarity index 90% rename from lib/screens/creators/publishers.dart rename to lib/screens/creators/publishers_form.dart index 0b1eaad9..e201a4e2 100644 --- a/lib/screens/creators/publishers.dart +++ b/lib/screens/creators/publishers_form.dart @@ -18,12 +18,11 @@ import 'package:island/services/file_uploader.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.dart'; -import 'package:island/widgets/realm/realm_selection_dropdown.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:styled_widget/styled_widget.dart'; -part 'publishers.g.dart'; +part 'publishers_form.g.dart'; @riverpod Future> publishersManaged(Ref ref) async { @@ -187,19 +186,6 @@ class EditPublisherScreen extends HookConsumerWidget { padding: EdgeInsets.only(bottom: 16), child: Column( children: [ - RealmSelectionDropdown( - value: currentRealm.value, - realms: joinedRealms.when( - data: (realms) => realms, - loading: () => [], - error: (_, _) => [], - ), - onChanged: (SnRealm? value) { - currentRealm.value = value; - }, - isLoading: joinedRealms.isLoading, - error: joinedRealms.error?.toString(), - ), AspectRatio( aspectRatio: 16 / 7, child: Stack( @@ -273,6 +259,32 @@ class EditPublisherScreen extends HookConsumerWidget { onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), + DropdownButtonFormField( + value: currentRealm.value, + decoration: InputDecoration(labelText: 'realm'.tr()), + items: [ + DropdownMenuItem( + value: null, + child: Text('individual'.tr()), + ), + ...joinedRealms.maybeWhen( + data: + (realms) => realms.map( + (realm) => DropdownMenuItem( + value: realm, + child: Text(realm.name), + ), + ), + orElse: () => [], + ), + ], + onChanged: + joinedRealms.isLoading + ? null + : (SnRealm? value) { + currentRealm.value = value; + }, + ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/lib/screens/creators/publishers.g.dart b/lib/screens/creators/publishers_form.g.dart similarity index 99% rename from lib/screens/creators/publishers.g.dart rename to lib/screens/creators/publishers_form.g.dart index a94b77ef..e93d8b3e 100644 --- a/lib/screens/creators/publishers.g.dart +++ b/lib/screens/creators/publishers_form.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'publishers.dart'; +part of 'publishers_form.dart'; // ************************************************************************** // RiverpodGenerator diff --git a/lib/screens/developers/hub.dart b/lib/screens/developers/hub.dart index ef19cca3..d0cd0d2f 100644 --- a/lib/screens/developers/hub.dart +++ b/lib/screens/developers/hub.dart @@ -8,7 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/developer.dart'; import 'package:island/models/publisher.dart'; import 'package:island/pods/network.dart'; -import 'package:island/screens/creators/publishers.dart'; +import 'package:island/screens/creators/publishers_form.dart'; import 'package:island/services/responsive.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; diff --git a/lib/screens/posts/compose.dart b/lib/screens/posts/compose.dart index 85ac7218..097ad191 100644 --- a/lib/screens/posts/compose.dart +++ b/lib/screens/posts/compose.dart @@ -6,7 +6,7 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/file.dart'; import 'package:island/models/post.dart'; -import 'package:island/screens/creators/publishers.dart'; +import 'package:island/screens/creators/publishers_form.dart'; import 'package:island/screens/posts/compose_article.dart'; import 'package:island/services/responsive.dart'; import 'package:island/widgets/app_scaffold.dart'; diff --git a/lib/screens/posts/compose_article.dart b/lib/screens/posts/compose_article.dart index 6864d86f..ae90287c 100644 --- a/lib/screens/posts/compose_article.dart +++ b/lib/screens/posts/compose_article.dart @@ -8,7 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/file.dart'; import 'package:island/models/post.dart'; -import 'package:island/screens/creators/publishers.dart'; +import 'package:island/screens/creators/publishers_form.dart'; import 'package:island/services/responsive.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/attachment_uploader.dart'; diff --git a/lib/screens/realm/realm_form.dart b/lib/screens/realm/realm_form.dart new file mode 100644 index 00000000..de4d9650 --- /dev/null +++ b/lib/screens/realm/realm_form.dart @@ -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(null); + final background = useState(null); + final isPublic = useState(true); + final isCommunity = useState(false); + + final slugController = useTextEditingController(); + final nameController = useTextEditingController(); + final descriptionController = useTextEditingController(); + + final formKey = useMemoized(GlobalKey.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 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), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/realm/realms.dart b/lib/screens/realm/realms.dart index 62f0f7f1..a47fae63 100644 --- a/lib/screens/realm/realms.dart +++ b/lib/screens/realm/realms.dart @@ -1,17 +1,10 @@ -import 'package:croppy/croppy.dart' show CropAspectRatio; -import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:island/models/file.dart'; import 'package:island/models/realm.dart'; import 'package:island/pods/network.dart'; -import 'package:island/services/file.dart'; -import 'package:island/services/file_uploader.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.dart'; @@ -32,6 +25,14 @@ Future> realmsJoined(Ref ref) async { return resp.data.map((e) => SnRealm.fromJson(e)).cast().toList(); } +@riverpod +Future realm(Ref ref, String? identifier) async { + if (identifier == null) return null; + final client = ref.watch(apiClientProvider); + final resp = await client.get('/sphere/realms/$identifier'); + return SnRealm.fromJson(resp.data); +} + class RealmListScreen extends HookConsumerWidget { const RealmListScreen({super.key}); @@ -124,271 +125,6 @@ class RealmListScreen extends HookConsumerWidget { } } -@riverpod -Future 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(null); - final background = useState(null); - final isPublic = useState(true); - final isCommunity = useState(false); - - final slugController = useTextEditingController(); - final nameController = useTextEditingController(); - final descriptionController = useTextEditingController(); - - final formKey = useMemoized(GlobalKey.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 performAction() async { - if (!formKey.currentState!.validate()) return; - - submitting.value = true; - try { - final client = ref.watch(apiClientProvider); - final resp = await client.request( - '/sphere${slug == null ? '/realms' : '/realms/$slug'}', - data: { - 'slug': slugController.text, - 'name': nameController.text, - 'description': descriptionController.text, - 'background_id': background.value?.id, - 'picture_id': picture.value?.id, - 'is_public': isPublic.value, - 'is_community': isCommunity.value, - }, - options: Options(method: slug == null ? 'POST' : 'PATCH'), - ); - if (context.mounted) { - context.pop(SnRealm.fromJson(resp.data)); - } - } catch (err) { - showErrorAlert(err); - } finally { - submitting.value = false; - } - } - - return AppScaffold( - isNoBackground: false, - appBar: AppBar( - title: Text(slug == null ? 'createRealm'.tr() : 'editRealm'.tr()), - leading: const PageBackButton(), - ), - body: SingleChildScrollView( - child: Column( - children: [ - AspectRatio( - aspectRatio: 16 / 7, - child: Stack( - clipBehavior: Clip.none, - fit: StackFit.expand, - children: [ - GestureDetector( - child: Container( - color: Theme.of(context).colorScheme.surfaceContainerHigh, - child: - background.value != null - ? CloudFileWidget( - item: background.value!, - fit: BoxFit.cover, - ) - : const SizedBox.shrink(), - ), - onTap: () { - setPicture('background'); - }, - ), - Positioned( - left: 20, - bottom: -32, - child: GestureDetector( - child: ProfilePictureWidget( - fileId: picture.value?.id, - radius: 40, - fallbackIcon: Symbols.group, - ), - onTap: () { - setPicture('picture'); - }, - ), - ), - ], - ), - ).padding(bottom: 32), - Form( - key: formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextFormField( - controller: slugController, - decoration: InputDecoration( - labelText: 'slug'.tr(), - helperText: 'slugHint'.tr(), - ), - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), - ), - const SizedBox(height: 16), - TextFormField( - controller: nameController, - decoration: InputDecoration(labelText: 'name'.tr()), - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), - ), - const SizedBox(height: 16), - TextFormField( - controller: descriptionController, - decoration: InputDecoration( - labelText: 'description'.tr(), - alignLabelWithHint: true, - ), - minLines: 3, - maxLines: null, - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), - ), - const SizedBox(height: 16), - Card( - margin: EdgeInsets.zero, - child: Column( - children: [ - CheckboxListTile( - secondary: const Icon(Symbols.public), - title: Text('publicRealm').tr(), - subtitle: Text('publicRealmDescription').tr(), - value: isPublic.value, - onChanged: (value) { - isPublic.value = value ?? true; - }, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - CheckboxListTile( - secondary: const Icon(Symbols.travel_explore), - title: Text('communityRealm').tr(), - subtitle: Text('communityRealmDescription').tr(), - value: isCommunity.value, - onChanged: (value) { - isCommunity.value = value ?? false; - }, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ], - ), - ), - const SizedBox(height: 16), - Align( - alignment: Alignment.centerRight, - child: TextButton.icon( - onPressed: submitting.value ? null : performAction, - label: Text('saveChanges'.tr()), - icon: const Icon(Symbols.save), - ), - ), - ], - ).padding(all: 24), - ), - ], - ), - ), - ); - } -} - @riverpod Future> realmInvites(Ref ref) async { final client = ref.watch(apiClientProvider); diff --git a/lib/widgets/account/stellar_program_tab.dart b/lib/widgets/account/stellar_program_tab.dart index 902cbd5f..a68857a5 100644 --- a/lib/widgets/account/stellar_program_tab.dart +++ b/lib/widgets/account/stellar_program_tab.dart @@ -5,12 +5,15 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/models/account.dart'; import 'package:island/models/wallet.dart'; import 'package:island/pods/network.dart'; import 'package:island/pods/userinfo.dart'; import 'package:island/services/time.dart'; +import 'package:island/widgets/account/account_picker.dart'; import 'package:island/widgets/account/restore_purchase_sheet.dart'; import 'package:island/widgets/alert.dart'; +import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/payment/payment_overlay.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:material_symbols_icons/symbols.dart'; @@ -31,6 +34,39 @@ Future accountStellarSubscription(Ref ref) async { } } +@riverpod +Future> 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> 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 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 { const StellarProgramTab({super.key}); @@ -45,6 +81,8 @@ class StellarProgramTab extends HookConsumerWidget { children: [ _buildMembershipSection(context, ref, stellarSubscription), const Gap(16), + _buildGiftingSection(context, ref), + const Gap(16), ], ), ); @@ -466,4 +504,728 @@ class StellarProgramTab extends HookConsumerWidget { 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> sentGifts, + AsyncValue> 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 _showGiftHistorySheet( + BuildContext context, + WidgetRef ref, + AsyncValue> 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 _showPurchaseGiftDialog( + BuildContext context, + WidgetRef ref, + String subscriptionId, + ) async { + final messageController = TextEditingController(); + + final recipient = await showModalBottomSheet( + 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( + 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 _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 _showRedeemGiftDialog( + BuildContext context, + WidgetRef ref, + ) async { + final codeController = TextEditingController(); + + final result = await showDialog( + 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 _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; + + 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 _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); + } + } } diff --git a/lib/widgets/account/stellar_program_tab.g.dart b/lib/widgets/account/stellar_program_tab.g.dart index e7ed83b9..06e728d5 100644 --- a/lib/widgets/account/stellar_program_tab.g.dart +++ b/lib/widgets/account/stellar_program_tab.g.dart @@ -27,5 +27,426 @@ final accountStellarSubscriptionProvider = // ignore: unused_element typedef AccountStellarSubscriptionRef = AutoDisposeFutureProviderRef; +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>> { + /// 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? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'accountSentGiftsProvider'; +} + +/// See also [accountSentGifts]. +class AccountSentGiftsProvider + extends AutoDisposeFutureProvider> { + /// 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> 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> 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> { + /// The parameter `offset` of this provider. + int get offset; + + /// The parameter `take` of this provider. + int get take; +} + +class _AccountSentGiftsProviderElement + extends AutoDisposeFutureProviderElement> + 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>> { + /// 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? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'accountReceivedGiftsProvider'; +} + +/// See also [accountReceivedGifts]. +class AccountReceivedGiftsProvider + extends AutoDisposeFutureProvider> { + /// 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> 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> 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> { + /// The parameter `offset` of this provider. + int get offset; + + /// The parameter `take` of this provider. + int get take; +} + +class _AccountReceivedGiftsProviderElement + extends AutoDisposeFutureProviderElement> + 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> { + /// 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? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'accountGiftProvider'; +} + +/// See also [accountGift]. +class AccountGiftProvider extends AutoDisposeFutureProvider { + /// 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 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 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 { + /// The parameter `giftId` of this provider. + String get giftId; +} + +class _AccountGiftProviderElement + extends AutoDisposeFutureProviderElement + with AccountGiftRef { + _AccountGiftProviderElement(super.provider); + + @override + String get giftId => (origin as AccountGiftProvider).giftId; +} + // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/widgets/post/compose_card.dart b/lib/widgets/post/compose_card.dart index cbb566e8..846a3fdf 100644 --- a/lib/widgets/post/compose_card.dart +++ b/lib/widgets/post/compose_card.dart @@ -3,11 +3,13 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; +import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/file.dart'; import 'package:island/models/post.dart'; +import 'package:island/models/publisher.dart'; import 'package:island/pods/network.dart'; -import 'package:island/screens/creators/publishers.dart'; +import 'package:island/screens/creators/publishers_form.dart'; import 'package:island/screens/posts/compose.dart'; import 'package:island/services/compose_storage_db.dart'; import 'package:island/services/responsive.dart'; @@ -31,6 +33,7 @@ class PostComposeCard extends HookConsumerWidget { final VoidCallback? onCancel; final Function(SnPost)? onSubmit; final Function(ComposeState)? onStateChanged; + final bool isInDialog; const PostComposeCard({ super.key, @@ -39,6 +42,7 @@ class PostComposeCard extends HookConsumerWidget { this.onCancel, this.onSubmit, this.onStateChanged, + this.isInDialog = false, }); @override @@ -441,7 +445,11 @@ class PostComposeCard extends HookConsumerWidget { ), ), IconButton( - onPressed: state.submitting.value ? null : performSubmit, + onPressed: + (state.submitting.value || + state.currentPublisher.value == null) + ? null + : performSubmit, icon: state.submitting.value ? SizedBox( @@ -515,16 +523,31 @@ class PostComposeCard extends HookConsumerWidget { : null, ), onTap: () { - showModalBottomSheet( - isScrollControlled: true, - useRootNavigator: true, - context: context, - builder: (context) => const PublisherModal(), - ).then((value) { - if (value != null) { - state.currentPublisher.value = value; + if (state.currentPublisher.value == null) { + // No publisher loaded, guide user to create one + if (isInDialog) { + Navigator.of(context).pop(); } - }); + context.pushNamed('creatorNew').then((value) { + if (value != null) { + state.currentPublisher.value = + value as SnPublisher; + ref.invalidate(publishersManagedProvider); + } + }); + } else { + // Show modal to select from existing publishers + showModalBottomSheet( + isScrollControlled: true, + useRootNavigator: true, + context: context, + builder: (context) => const PublisherModal(), + ).then((value) { + if (value != null) { + state.currentPublisher.value = value; + } + }); + } }, ).padding(top: 8), @@ -533,8 +556,43 @@ class PostComposeCard extends HookConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + if (state.currentPublisher.value == null) + Container( + padding: const EdgeInsets.all(12), + margin: const EdgeInsets.only(bottom: 8), + decoration: BoxDecoration( + color: + theme.colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Icon( + Symbols.info, + size: 16, + color: theme.colorScheme.primary, + ), + const Gap(8), + Expanded( + child: Text( + 'Tap the avatar to create a publisher and start composing.', + style: theme.textTheme.bodySmall + ?.copyWith( + color: + theme + .colorScheme + .onSurfaceVariant, + ), + ), + ), + ], + ), + ), TextField( controller: state.titleController, + enabled: state.currentPublisher.value != null, decoration: InputDecoration( hintText: 'postTitle'.tr(), border: InputBorder.none, @@ -552,6 +610,7 @@ class PostComposeCard extends HookConsumerWidget { ), TextField( controller: state.descriptionController, + enabled: state.currentPublisher.value != null, decoration: InputDecoration( hintText: 'postDescription'.tr(), border: InputBorder.none, @@ -573,6 +632,7 @@ class PostComposeCard extends HookConsumerWidget { ), TextField( controller: state.contentController, + enabled: state.currentPublisher.value != null, style: theme.textTheme.bodyMedium, decoration: InputDecoration( border: InputBorder.none, diff --git a/lib/widgets/post/compose_dialog.dart b/lib/widgets/post/compose_dialog.dart index 5d376c81..a1164789 100644 --- a/lib/widgets/post/compose_dialog.dart +++ b/lib/widgets/post/compose_dialog.dart @@ -70,6 +70,7 @@ class PostComposeDialog extends HookConsumerWidget { initialState: restoredInitialState.value ?? initialState, onCancel: () => Navigator.of(context).pop(), onSubmit: (post) => Navigator.of(context).pop(post), + isInDialog: true, ), ), ); diff --git a/lib/widgets/post/post_quick_reply.dart b/lib/widgets/post/post_quick_reply.dart index e304e252..dea5575d 100644 --- a/lib/widgets/post/post_quick_reply.dart +++ b/lib/widgets/post/post_quick_reply.dart @@ -6,7 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/post.dart'; import 'package:island/models/publisher.dart'; import 'package:island/pods/network.dart'; -import 'package:island/screens/creators/publishers.dart'; +import 'package:island/screens/creators/publishers_form.dart'; import 'package:island/screens/posts/compose.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/content/cloud_files.dart'; diff --git a/lib/widgets/post/publishers_modal.dart b/lib/widgets/post/publishers_modal.dart index 31571e37..fc68c189 100644 --- a/lib/widgets/post/publishers_modal.dart +++ b/lib/widgets/post/publishers_modal.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:island/screens/creators/publishers.dart'; +import 'package:island/screens/creators/publishers_form.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -43,13 +43,15 @@ class PublisherModal extends HookConsumerWidget { const Gap(12), ElevatedButton( onPressed: () { - context.pushNamed('creatorNew').then((value) { - if (value != null) { - ref.invalidate( - publishersManagedProvider, - ); - } - }); + context.pushNamed('creatorNew').then(( + value, + ) { + if (value != null) { + ref.invalidate( + publishersManagedProvider, + ); + } + }); }, child: Text('createPublisher').tr(), ),