Compare commits
	
		
			15 Commits
		
	
	
		
			3.1.0+116
			...
			f00135c4bf
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| f00135c4bf | |||
| 30b8a6c30f | |||
| b9c4ee31b1 | |||
| 87870af866 | |||
| b83cb0fb0b | |||
| 7fd1fe34e5 | |||
| 1c18330891 | |||
| d320879ad0 | |||
| 950150e119 | |||
| 3c4a9767e1 | |||
| 5df2445f3f | |||
| 56543d7b4c | |||
| 4c6fea1242 | |||
| fff43de9e3 | |||
| b31a915544 | 
| @@ -761,5 +761,12 @@ | ||||
|   "publisher": "Publisher", | ||||
|   "publisherHint": "Enter the publisher name", | ||||
|   "publisherCannotBeEmpty": "Publisher cannot be empty", | ||||
|   "operationFailed": "Operation failed: {}" | ||||
|   "operationFailed": "Operation failed: {}", | ||||
|   "stickerMarketplace": "Sticker Marketplace", | ||||
|   "stickerPackAdded": "Sticker pack added to your collection", | ||||
|   "stickerPackRemoved": "Sticker pack removed from your collection", | ||||
|   "addPack": "Add Pack", | ||||
|   "removePack": "Remove Pack", | ||||
|   "browseAndAddStickers": "Browse and add sticker packs", | ||||
|   "stickerPack": "Sticker Pack" | ||||
| } | ||||
|   | ||||
| @@ -3,25 +3,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
| part 'embed.freezed.dart'; | ||||
| part 'embed.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| sealed class SnEmbedLink with _$SnEmbedLink { | ||||
|   const factory SnEmbedLink({ | ||||
|     @JsonKey(name: 'Type') required String type, | ||||
|     @JsonKey(name: 'Url') required String url, | ||||
|     @JsonKey(name: 'Title') required String title, | ||||
|     @JsonKey(name: 'Description') required String? description, | ||||
|     @JsonKey(name: 'ImageUrl') required String? imageUrl, | ||||
|     @JsonKey(name: 'FaviconUrl') @Default("") String faviconUrl, | ||||
|     @JsonKey(name: 'SiteName') @Default("") String siteName, | ||||
|     @JsonKey(name: 'ContentType') required String? contentType, | ||||
|     @JsonKey(name: 'Author') required String? author, | ||||
|     @JsonKey(name: 'PublishedDate') required DateTime? publishedDate, | ||||
|   }) = _SnEmbedLink; | ||||
|  | ||||
|   factory SnEmbedLink.fromJson(Map<String, dynamic> json) => | ||||
|       _$SnEmbedLinkFromJson(json); | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| sealed class SnScrappedLink with _$SnScrappedLink { | ||||
|   const factory SnScrappedLink({ | ||||
|   | ||||
| @@ -12,290 +12,6 @@ part of 'embed.dart'; | ||||
| // dart format off | ||||
| T _$identity<T>(T value) => value; | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$SnEmbedLink { | ||||
|  | ||||
| @JsonKey(name: 'Type') String get type;@JsonKey(name: 'Url') String get url;@JsonKey(name: 'Title') String get title;@JsonKey(name: 'Description') String? get description;@JsonKey(name: 'ImageUrl') String? get imageUrl;@JsonKey(name: 'FaviconUrl') String get faviconUrl;@JsonKey(name: 'SiteName') String get siteName;@JsonKey(name: 'ContentType') String? get contentType;@JsonKey(name: 'Author') String? get author;@JsonKey(name: 'PublishedDate') DateTime? get publishedDate; | ||||
| /// Create a copy of SnEmbedLink | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnEmbedLinkCopyWith<SnEmbedLink> get copyWith => _$SnEmbedLinkCopyWithImpl<SnEmbedLink>(this as SnEmbedLink, _$identity); | ||||
|  | ||||
|   /// Serializes this SnEmbedLink to a JSON map. | ||||
|   Map<String, dynamic> toJson(); | ||||
|  | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnEmbedLink&&(identical(other.type, type) || other.type == type)&&(identical(other.url, url) || other.url == url)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.faviconUrl, faviconUrl) || other.faviconUrl == faviconUrl)&&(identical(other.siteName, siteName) || other.siteName == siteName)&&(identical(other.contentType, contentType) || other.contentType == contentType)&&(identical(other.author, author) || other.author == author)&&(identical(other.publishedDate, publishedDate) || other.publishedDate == publishedDate)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,type,url,title,description,imageUrl,faviconUrl,siteName,contentType,author,publishedDate); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnEmbedLink(type: $type, url: $url, title: $title, description: $description, imageUrl: $imageUrl, faviconUrl: $faviconUrl, siteName: $siteName, contentType: $contentType, author: $author, publishedDate: $publishedDate)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class $SnEmbedLinkCopyWith<$Res>  { | ||||
|   factory $SnEmbedLinkCopyWith(SnEmbedLink value, $Res Function(SnEmbedLink) _then) = _$SnEmbedLinkCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
| @JsonKey(name: 'Type') String type,@JsonKey(name: 'Url') String url,@JsonKey(name: 'Title') String title,@JsonKey(name: 'Description') String? description,@JsonKey(name: 'ImageUrl') String? imageUrl,@JsonKey(name: 'FaviconUrl') String faviconUrl,@JsonKey(name: 'SiteName') String siteName,@JsonKey(name: 'ContentType') String? contentType,@JsonKey(name: 'Author') String? author,@JsonKey(name: 'PublishedDate') DateTime? publishedDate | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class _$SnEmbedLinkCopyWithImpl<$Res> | ||||
|     implements $SnEmbedLinkCopyWith<$Res> { | ||||
|   _$SnEmbedLinkCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final SnEmbedLink _self; | ||||
|   final $Res Function(SnEmbedLink) _then; | ||||
|  | ||||
| /// Create a copy of SnEmbedLink | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? url = null,Object? title = null,Object? description = freezed,Object? imageUrl = freezed,Object? faviconUrl = null,Object? siteName = null,Object? contentType = freezed,Object? author = freezed,Object? publishedDate = freezed,}) { | ||||
|   return _then(_self.copyWith( | ||||
| type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | ||||
| as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable | ||||
| as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable | ||||
| as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable | ||||
| as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable | ||||
| as String?,faviconUrl: null == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable | ||||
| as String,siteName: null == siteName ? _self.siteName : siteName // ignore: cast_nullable_to_non_nullable | ||||
| as String,contentType: freezed == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable | ||||
| as String?,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable | ||||
| as String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime?, | ||||
|   )); | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /// Adds pattern-matching-related methods to [SnEmbedLink]. | ||||
| extension SnEmbedLinkPatterns on SnEmbedLink { | ||||
| /// A variant of `map` that fallback to returning `orElse`. | ||||
| /// | ||||
| /// It is equivalent to doing: | ||||
| /// ```dart | ||||
| /// switch (sealedClass) { | ||||
| ///   case final Subclass value: | ||||
| ///     return ...; | ||||
| ///   case _: | ||||
| ///     return orElse(); | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnEmbedLink value)?  $default,{required TResult orElse(),}){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _SnEmbedLink() when $default != null: | ||||
| return $default(_that);case _: | ||||
|   return orElse(); | ||||
|  | ||||
| } | ||||
| } | ||||
| /// A `switch`-like method, using callbacks. | ||||
| /// | ||||
| /// Callbacks receives the raw object, upcasted. | ||||
| /// It is equivalent to doing: | ||||
| /// ```dart | ||||
| /// switch (sealedClass) { | ||||
| ///   case final Subclass value: | ||||
| ///     return ...; | ||||
| ///   case final Subclass2 value: | ||||
| ///     return ...; | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnEmbedLink value)  $default,){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _SnEmbedLink(): | ||||
| return $default(_that);} | ||||
| } | ||||
| /// A variant of `map` that fallback to returning `null`. | ||||
| /// | ||||
| /// It is equivalent to doing: | ||||
| /// ```dart | ||||
| /// switch (sealedClass) { | ||||
| ///   case final Subclass value: | ||||
| ///     return ...; | ||||
| ///   case _: | ||||
| ///     return null; | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnEmbedLink value)?  $default,){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _SnEmbedLink() when $default != null: | ||||
| return $default(_that);case _: | ||||
|   return null; | ||||
|  | ||||
| } | ||||
| } | ||||
| /// A variant of `when` that fallback to an `orElse` callback. | ||||
| /// | ||||
| /// It is equivalent to doing: | ||||
| /// ```dart | ||||
| /// switch (sealedClass) { | ||||
| ///   case Subclass(:final field): | ||||
| ///     return ...; | ||||
| ///   case _: | ||||
| ///     return orElse(); | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@JsonKey(name: 'Type')  String type, @JsonKey(name: 'Url')  String url, @JsonKey(name: 'Title')  String title, @JsonKey(name: 'Description')  String? description, @JsonKey(name: 'ImageUrl')  String? imageUrl, @JsonKey(name: 'FaviconUrl')  String faviconUrl, @JsonKey(name: 'SiteName')  String siteName, @JsonKey(name: 'ContentType')  String? contentType, @JsonKey(name: 'Author')  String? author, @JsonKey(name: 'PublishedDate')  DateTime? publishedDate)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnEmbedLink() when $default != null: | ||||
| return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);case _: | ||||
|   return orElse(); | ||||
|  | ||||
| } | ||||
| } | ||||
| /// A `switch`-like method, using callbacks. | ||||
| /// | ||||
| /// As opposed to `map`, this offers destructuring. | ||||
| /// It is equivalent to doing: | ||||
| /// ```dart | ||||
| /// switch (sealedClass) { | ||||
| ///   case Subclass(:final field): | ||||
| ///     return ...; | ||||
| ///   case Subclass2(:final field2): | ||||
| ///     return ...; | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@JsonKey(name: 'Type')  String type, @JsonKey(name: 'Url')  String url, @JsonKey(name: 'Title')  String title, @JsonKey(name: 'Description')  String? description, @JsonKey(name: 'ImageUrl')  String? imageUrl, @JsonKey(name: 'FaviconUrl')  String faviconUrl, @JsonKey(name: 'SiteName')  String siteName, @JsonKey(name: 'ContentType')  String? contentType, @JsonKey(name: 'Author')  String? author, @JsonKey(name: 'PublishedDate')  DateTime? publishedDate)  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnEmbedLink(): | ||||
| return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);} | ||||
| } | ||||
| /// A variant of `when` that fallback to returning `null` | ||||
| /// | ||||
| /// It is equivalent to doing: | ||||
| /// ```dart | ||||
| /// switch (sealedClass) { | ||||
| ///   case Subclass(:final field): | ||||
| ///     return ...; | ||||
| ///   case _: | ||||
| ///     return null; | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@JsonKey(name: 'Type')  String type, @JsonKey(name: 'Url')  String url, @JsonKey(name: 'Title')  String title, @JsonKey(name: 'Description')  String? description, @JsonKey(name: 'ImageUrl')  String? imageUrl, @JsonKey(name: 'FaviconUrl')  String faviconUrl, @JsonKey(name: 'SiteName')  String siteName, @JsonKey(name: 'ContentType')  String? contentType, @JsonKey(name: 'Author')  String? author, @JsonKey(name: 'PublishedDate')  DateTime? publishedDate)?  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnEmbedLink() when $default != null: | ||||
| return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);case _: | ||||
|   return null; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _SnEmbedLink implements SnEmbedLink { | ||||
|   const _SnEmbedLink({@JsonKey(name: 'Type') required this.type, @JsonKey(name: 'Url') required this.url, @JsonKey(name: 'Title') required this.title, @JsonKey(name: 'Description') required this.description, @JsonKey(name: 'ImageUrl') required this.imageUrl, @JsonKey(name: 'FaviconUrl') this.faviconUrl = "", @JsonKey(name: 'SiteName') this.siteName = "", @JsonKey(name: 'ContentType') required this.contentType, @JsonKey(name: 'Author') required this.author, @JsonKey(name: 'PublishedDate') required this.publishedDate}); | ||||
|   factory _SnEmbedLink.fromJson(Map<String, dynamic> json) => _$SnEmbedLinkFromJson(json); | ||||
|  | ||||
| @override@JsonKey(name: 'Type') final  String type; | ||||
| @override@JsonKey(name: 'Url') final  String url; | ||||
| @override@JsonKey(name: 'Title') final  String title; | ||||
| @override@JsonKey(name: 'Description') final  String? description; | ||||
| @override@JsonKey(name: 'ImageUrl') final  String? imageUrl; | ||||
| @override@JsonKey(name: 'FaviconUrl') final  String faviconUrl; | ||||
| @override@JsonKey(name: 'SiteName') final  String siteName; | ||||
| @override@JsonKey(name: 'ContentType') final  String? contentType; | ||||
| @override@JsonKey(name: 'Author') final  String? author; | ||||
| @override@JsonKey(name: 'PublishedDate') final  DateTime? publishedDate; | ||||
|  | ||||
| /// Create a copy of SnEmbedLink | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| _$SnEmbedLinkCopyWith<_SnEmbedLink> get copyWith => __$SnEmbedLinkCopyWithImpl<_SnEmbedLink>(this, _$identity); | ||||
|  | ||||
| @override | ||||
| Map<String, dynamic> toJson() { | ||||
|   return _$SnEmbedLinkToJson(this, ); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnEmbedLink&&(identical(other.type, type) || other.type == type)&&(identical(other.url, url) || other.url == url)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.faviconUrl, faviconUrl) || other.faviconUrl == faviconUrl)&&(identical(other.siteName, siteName) || other.siteName == siteName)&&(identical(other.contentType, contentType) || other.contentType == contentType)&&(identical(other.author, author) || other.author == author)&&(identical(other.publishedDate, publishedDate) || other.publishedDate == publishedDate)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,type,url,title,description,imageUrl,faviconUrl,siteName,contentType,author,publishedDate); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnEmbedLink(type: $type, url: $url, title: $title, description: $description, imageUrl: $imageUrl, faviconUrl: $faviconUrl, siteName: $siteName, contentType: $contentType, author: $author, publishedDate: $publishedDate)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class _$SnEmbedLinkCopyWith<$Res> implements $SnEmbedLinkCopyWith<$Res> { | ||||
|   factory _$SnEmbedLinkCopyWith(_SnEmbedLink value, $Res Function(_SnEmbedLink) _then) = __$SnEmbedLinkCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
| @JsonKey(name: 'Type') String type,@JsonKey(name: 'Url') String url,@JsonKey(name: 'Title') String title,@JsonKey(name: 'Description') String? description,@JsonKey(name: 'ImageUrl') String? imageUrl,@JsonKey(name: 'FaviconUrl') String faviconUrl,@JsonKey(name: 'SiteName') String siteName,@JsonKey(name: 'ContentType') String? contentType,@JsonKey(name: 'Author') String? author,@JsonKey(name: 'PublishedDate') DateTime? publishedDate | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class __$SnEmbedLinkCopyWithImpl<$Res> | ||||
|     implements _$SnEmbedLinkCopyWith<$Res> { | ||||
|   __$SnEmbedLinkCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final _SnEmbedLink _self; | ||||
|   final $Res Function(_SnEmbedLink) _then; | ||||
|  | ||||
| /// Create a copy of SnEmbedLink | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? url = null,Object? title = null,Object? description = freezed,Object? imageUrl = freezed,Object? faviconUrl = null,Object? siteName = null,Object? contentType = freezed,Object? author = freezed,Object? publishedDate = freezed,}) { | ||||
|   return _then(_SnEmbedLink( | ||||
| type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | ||||
| as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable | ||||
| as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable | ||||
| as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable | ||||
| as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable | ||||
| as String?,faviconUrl: null == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable | ||||
| as String,siteName: null == siteName ? _self.siteName : siteName // ignore: cast_nullable_to_non_nullable | ||||
| as String,contentType: freezed == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable | ||||
| as String?,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable | ||||
| as String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime?, | ||||
|   )); | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$SnScrappedLink { | ||||
|  | ||||
|   | ||||
| @@ -6,36 +6,6 @@ part of 'embed.dart'; | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _SnEmbedLink _$SnEmbedLinkFromJson(Map<String, dynamic> json) => _SnEmbedLink( | ||||
|   type: json['Type'] as String, | ||||
|   url: json['Url'] as String, | ||||
|   title: json['Title'] as String, | ||||
|   description: json['Description'] as String?, | ||||
|   imageUrl: json['ImageUrl'] as String?, | ||||
|   faviconUrl: json['FaviconUrl'] as String? ?? "", | ||||
|   siteName: json['SiteName'] as String? ?? "", | ||||
|   contentType: json['ContentType'] as String?, | ||||
|   author: json['Author'] as String?, | ||||
|   publishedDate: | ||||
|       json['PublishedDate'] == null | ||||
|           ? null | ||||
|           : DateTime.parse(json['PublishedDate'] as String), | ||||
| ); | ||||
|  | ||||
| Map<String, dynamic> _$SnEmbedLinkToJson(_SnEmbedLink instance) => | ||||
|     <String, dynamic>{ | ||||
|       'Type': instance.type, | ||||
|       'Url': instance.url, | ||||
|       'Title': instance.title, | ||||
|       'Description': instance.description, | ||||
|       'ImageUrl': instance.imageUrl, | ||||
|       'FaviconUrl': instance.faviconUrl, | ||||
|       'SiteName': instance.siteName, | ||||
|       'ContentType': instance.contentType, | ||||
|       'Author': instance.author, | ||||
|       'PublishedDate': instance.publishedDate?.toIso8601String(), | ||||
|     }; | ||||
|  | ||||
| _SnScrappedLink _$SnScrappedLinkFromJson(Map<String, dynamic> json) => | ||||
|     _SnScrappedLink( | ||||
|       type: json['type'] as String, | ||||
|   | ||||
| @@ -90,3 +90,19 @@ enum SnPollQuestionType { | ||||
|   @JsonValue(4) | ||||
|   freeText, | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| sealed class SnPollAnswer with _$SnPollAnswer { | ||||
|   const factory SnPollAnswer({ | ||||
|     required String id, | ||||
|     required Map<String, dynamic> answer, | ||||
|     required String accountId, | ||||
|     required String pollId, | ||||
|     required DateTime createdAt, | ||||
|     required DateTime updatedAt, | ||||
|     required DateTime? deletedAt, | ||||
|   }) = _SnPollAnswer; | ||||
|  | ||||
|   factory SnPollAnswer.fromJson(Map<String, dynamic> json) => | ||||
|       _$SnPollAnswerFromJson(json); | ||||
| } | ||||
|   | ||||
| @@ -1181,6 +1181,287 @@ as int, | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$SnPollAnswer { | ||||
|  | ||||
|  String get id; Map<String, dynamic> get answer; String get accountId; String get pollId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||
| /// Create a copy of SnPollAnswer | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnPollAnswerCopyWith<SnPollAnswer> get copyWith => _$SnPollAnswerCopyWithImpl<SnPollAnswer>(this as SnPollAnswer, _$identity); | ||||
|  | ||||
|   /// Serializes this SnPollAnswer to a JSON map. | ||||
|   Map<String, dynamic> toJson(); | ||||
|  | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPollAnswer&&(identical(other.id, id) || other.id == id)&&const DeepCollectionEquality().equals(other.answer, answer)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.pollId, pollId) || other.pollId == pollId)&&(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,const DeepCollectionEquality().hash(answer),accountId,pollId,createdAt,updatedAt,deletedAt); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnPollAnswer(id: $id, answer: $answer, accountId: $accountId, pollId: $pollId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class $SnPollAnswerCopyWith<$Res>  { | ||||
|   factory $SnPollAnswerCopyWith(SnPollAnswer value, $Res Function(SnPollAnswer) _then) = _$SnPollAnswerCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String id, Map<String, dynamic> answer, String accountId, String pollId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class _$SnPollAnswerCopyWithImpl<$Res> | ||||
|     implements $SnPollAnswerCopyWith<$Res> { | ||||
|   _$SnPollAnswerCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final SnPollAnswer _self; | ||||
|   final $Res Function(SnPollAnswer) _then; | ||||
|  | ||||
| /// Create a copy of SnPollAnswer | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? answer = null,Object? accountId = null,Object? pollId = null,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,answer: null == answer ? _self.answer : answer // ignore: cast_nullable_to_non_nullable | ||||
| as Map<String, dynamic>,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | ||||
| as String,pollId: null == pollId ? _self.pollId : pollId // ignore: cast_nullable_to_non_nullable | ||||
| as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime?, | ||||
|   )); | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /// Adds pattern-matching-related methods to [SnPollAnswer]. | ||||
| extension SnPollAnswerPatterns on SnPollAnswer { | ||||
| /// A variant of `map` that fallback to returning `orElse`. | ||||
| /// | ||||
| /// It is equivalent to doing: | ||||
| /// ```dart | ||||
| /// switch (sealedClass) { | ||||
| ///   case final Subclass value: | ||||
| ///     return ...; | ||||
| ///   case _: | ||||
| ///     return orElse(); | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnPollAnswer value)?  $default,{required TResult orElse(),}){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _SnPollAnswer() when $default != null: | ||||
| return $default(_that);case _: | ||||
|   return orElse(); | ||||
|  | ||||
| } | ||||
| } | ||||
| /// A `switch`-like method, using callbacks. | ||||
| /// | ||||
| /// Callbacks receives the raw object, upcasted. | ||||
| /// It is equivalent to doing: | ||||
| /// ```dart | ||||
| /// switch (sealedClass) { | ||||
| ///   case final Subclass value: | ||||
| ///     return ...; | ||||
| ///   case final Subclass2 value: | ||||
| ///     return ...; | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnPollAnswer value)  $default,){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _SnPollAnswer(): | ||||
| return $default(_that);} | ||||
| } | ||||
| /// A variant of `map` that fallback to returning `null`. | ||||
| /// | ||||
| /// It is equivalent to doing: | ||||
| /// ```dart | ||||
| /// switch (sealedClass) { | ||||
| ///   case final Subclass value: | ||||
| ///     return ...; | ||||
| ///   case _: | ||||
| ///     return null; | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnPollAnswer value)?  $default,){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _SnPollAnswer() when $default != null: | ||||
| return $default(_that);case _: | ||||
|   return null; | ||||
|  | ||||
| } | ||||
| } | ||||
| /// A variant of `when` that fallback to an `orElse` callback. | ||||
| /// | ||||
| /// It is equivalent to doing: | ||||
| /// ```dart | ||||
| /// switch (sealedClass) { | ||||
| ///   case Subclass(:final field): | ||||
| ///     return ...; | ||||
| ///   case _: | ||||
| ///     return orElse(); | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  Map<String, dynamic> answer,  String accountId,  String pollId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnPollAnswer() when $default != null: | ||||
| return $default(_that.id,_that.answer,_that.accountId,_that.pollId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||
|   return orElse(); | ||||
|  | ||||
| } | ||||
| } | ||||
| /// A `switch`-like method, using callbacks. | ||||
| /// | ||||
| /// As opposed to `map`, this offers destructuring. | ||||
| /// It is equivalent to doing: | ||||
| /// ```dart | ||||
| /// switch (sealedClass) { | ||||
| ///   case Subclass(:final field): | ||||
| ///     return ...; | ||||
| ///   case Subclass2(:final field2): | ||||
| ///     return ...; | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  Map<String, dynamic> answer,  String accountId,  String pollId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnPollAnswer(): | ||||
| return $default(_that.id,_that.answer,_that.accountId,_that.pollId,_that.createdAt,_that.updatedAt,_that.deletedAt);} | ||||
| } | ||||
| /// A variant of `when` that fallback to returning `null` | ||||
| /// | ||||
| /// It is equivalent to doing: | ||||
| /// ```dart | ||||
| /// switch (sealedClass) { | ||||
| ///   case Subclass(:final field): | ||||
| ///     return ...; | ||||
| ///   case _: | ||||
| ///     return null; | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  Map<String, dynamic> answer,  String accountId,  String pollId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnPollAnswer() when $default != null: | ||||
| return $default(_that.id,_that.answer,_that.accountId,_that.pollId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||
|   return null; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _SnPollAnswer implements SnPollAnswer { | ||||
|   const _SnPollAnswer({required this.id, required final  Map<String, dynamic> answer, required this.accountId, required this.pollId, required this.createdAt, required this.updatedAt, required this.deletedAt}): _answer = answer; | ||||
|   factory _SnPollAnswer.fromJson(Map<String, dynamic> json) => _$SnPollAnswerFromJson(json); | ||||
|  | ||||
| @override final  String id; | ||||
|  final  Map<String, dynamic> _answer; | ||||
| @override Map<String, dynamic> get answer { | ||||
|   if (_answer is EqualUnmodifiableMapView) return _answer; | ||||
|   // ignore: implicit_dynamic_type | ||||
|   return EqualUnmodifiableMapView(_answer); | ||||
| } | ||||
|  | ||||
| @override final  String accountId; | ||||
| @override final  String pollId; | ||||
| @override final  DateTime createdAt; | ||||
| @override final  DateTime updatedAt; | ||||
| @override final  DateTime? deletedAt; | ||||
|  | ||||
| /// Create a copy of SnPollAnswer | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| _$SnPollAnswerCopyWith<_SnPollAnswer> get copyWith => __$SnPollAnswerCopyWithImpl<_SnPollAnswer>(this, _$identity); | ||||
|  | ||||
| @override | ||||
| Map<String, dynamic> toJson() { | ||||
|   return _$SnPollAnswerToJson(this, ); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPollAnswer&&(identical(other.id, id) || other.id == id)&&const DeepCollectionEquality().equals(other._answer, _answer)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.pollId, pollId) || other.pollId == pollId)&&(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,const DeepCollectionEquality().hash(_answer),accountId,pollId,createdAt,updatedAt,deletedAt); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnPollAnswer(id: $id, answer: $answer, accountId: $accountId, pollId: $pollId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class _$SnPollAnswerCopyWith<$Res> implements $SnPollAnswerCopyWith<$Res> { | ||||
|   factory _$SnPollAnswerCopyWith(_SnPollAnswer value, $Res Function(_SnPollAnswer) _then) = __$SnPollAnswerCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String id, Map<String, dynamic> answer, String accountId, String pollId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class __$SnPollAnswerCopyWithImpl<$Res> | ||||
|     implements _$SnPollAnswerCopyWith<$Res> { | ||||
|   __$SnPollAnswerCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final _SnPollAnswer _self; | ||||
|   final $Res Function(_SnPollAnswer) _then; | ||||
|  | ||||
| /// Create a copy of SnPollAnswer | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? answer = null,Object? accountId = null,Object? pollId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||
|   return _then(_SnPollAnswer( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,answer: null == answer ? _self._answer : answer // ignore: cast_nullable_to_non_nullable | ||||
| as Map<String, dynamic>,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | ||||
| as String,pollId: null == pollId ? _self.pollId : pollId // ignore: cast_nullable_to_non_nullable | ||||
| as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime?, | ||||
|   )); | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| // dart format on | ||||
|   | ||||
| @@ -131,3 +131,28 @@ Map<String, dynamic> _$SnPollOptionToJson(_SnPollOption instance) => | ||||
|       'description': instance.description, | ||||
|       'order': instance.order, | ||||
|     }; | ||||
|  | ||||
| _SnPollAnswer _$SnPollAnswerFromJson(Map<String, dynamic> json) => | ||||
|     _SnPollAnswer( | ||||
|       id: json['id'] as String, | ||||
|       answer: json['answer'] as Map<String, dynamic>, | ||||
|       accountId: json['account_id'] as String, | ||||
|       pollId: json['poll_id'] as String, | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
|       deletedAt: | ||||
|           json['deleted_at'] == null | ||||
|               ? null | ||||
|               : DateTime.parse(json['deleted_at'] as String), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$SnPollAnswerToJson(_SnPollAnswer instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'answer': instance.answer, | ||||
|       'account_id': instance.accountId, | ||||
|       'poll_id': instance.pollId, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|       'updated_at': instance.updatedAt.toIso8601String(), | ||||
|       'deleted_at': instance.deletedAt?.toIso8601String(), | ||||
|     }; | ||||
|   | ||||
| @@ -35,6 +35,7 @@ sealed class SnStickerPack with _$SnStickerPack { | ||||
|     required DateTime createdAt, | ||||
|     required DateTime updatedAt, | ||||
|     required DateTime? deletedAt, | ||||
|     @Default([]) List<SnSticker> stickers, | ||||
|   }) = _SnStickerPack; | ||||
|  | ||||
|   factory SnStickerPack.fromJson(Map<String, dynamic> json) => | ||||
|   | ||||
| @@ -338,7 +338,7 @@ $SnStickerPackCopyWith<$Res>? get pack { | ||||
| /// @nodoc | ||||
| mixin _$SnStickerPack { | ||||
|  | ||||
|  String get id; String get name; String get description; String get prefix; String get publisherId; SnPublisher? get publisher; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||
|  String get id; String get name; String get description; String get prefix; String get publisherId; SnPublisher? get publisher; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; List<SnSticker> get stickers; | ||||
| /// Create a copy of SnStickerPack | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @@ -351,16 +351,16 @@ $SnStickerPackCopyWith<SnStickerPack> get copyWith => _$SnStickerPackCopyWithImp | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnStickerPack&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.prefix, prefix) || other.prefix == prefix)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnStickerPack&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.prefix, prefix) || other.prefix == prefix)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other.stickers, stickers)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,id,name,description,prefix,publisherId,publisher,createdAt,updatedAt,deletedAt); | ||||
| int get hashCode => Object.hash(runtimeType,id,name,description,prefix,publisherId,publisher,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(stickers)); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnStickerPack(id: $id, name: $name, description: $description, prefix: $prefix, publisherId: $publisherId, publisher: $publisher, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||
|   return 'SnStickerPack(id: $id, name: $name, description: $description, prefix: $prefix, publisherId: $publisherId, publisher: $publisher, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, stickers: $stickers)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -371,7 +371,7 @@ abstract mixin class $SnStickerPackCopyWith<$Res>  { | ||||
|   factory $SnStickerPackCopyWith(SnStickerPack value, $Res Function(SnStickerPack) _then) = _$SnStickerPackCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
|  String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnSticker> stickers | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -388,7 +388,7 @@ class _$SnStickerPackCopyWithImpl<$Res> | ||||
|  | ||||
| /// Create a copy of SnStickerPack | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = null,Object? prefix = null,Object? publisherId = null,Object? publisher = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = null,Object? prefix = null,Object? publisherId = null,Object? publisher = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? stickers = null,}) { | ||||
|   return _then(_self.copyWith( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||
| @@ -399,7 +399,8 @@ as String,publisher: freezed == publisher ? _self.publisher : publisher // ignor | ||||
| as SnPublisher?,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?, | ||||
| as DateTime?,stickers: null == stickers ? _self.stickers : stickers // ignore: cast_nullable_to_non_nullable | ||||
| as List<SnSticker>, | ||||
|   )); | ||||
| } | ||||
| /// Create a copy of SnStickerPack | ||||
| @@ -493,10 +494,10 @@ return $default(_that);case _: | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String name,  String description,  String prefix,  String publisherId,  SnPublisher? publisher,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String name,  String description,  String prefix,  String publisherId,  SnPublisher? publisher,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt,  List<SnSticker> stickers)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnStickerPack() when $default != null: | ||||
| return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||
| return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.stickers);case _: | ||||
|   return orElse(); | ||||
|  | ||||
| } | ||||
| @@ -514,10 +515,10 @@ return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publish | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String name,  String description,  String prefix,  String publisherId,  SnPublisher? publisher,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String name,  String description,  String prefix,  String publisherId,  SnPublisher? publisher,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt,  List<SnSticker> stickers)  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnStickerPack(): | ||||
| return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt);} | ||||
| return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.stickers);} | ||||
| } | ||||
| /// A variant of `when` that fallback to returning `null` | ||||
| /// | ||||
| @@ -531,10 +532,10 @@ return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publish | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String name,  String description,  String prefix,  String publisherId,  SnPublisher? publisher,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String name,  String description,  String prefix,  String publisherId,  SnPublisher? publisher,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt,  List<SnSticker> stickers)?  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnStickerPack() when $default != null: | ||||
| return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||
| return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.stickers);case _: | ||||
|   return null; | ||||
|  | ||||
| } | ||||
| @@ -546,7 +547,7 @@ return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publish | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _SnStickerPack implements SnStickerPack { | ||||
|   const _SnStickerPack({required this.id, required this.name, required this.description, required this.prefix, required this.publisherId, required this.publisher, required this.createdAt, required this.updatedAt, required this.deletedAt}); | ||||
|   const _SnStickerPack({required this.id, required this.name, required this.description, required this.prefix, required this.publisherId, required this.publisher, required this.createdAt, required this.updatedAt, required this.deletedAt, final  List<SnSticker> stickers = const []}): _stickers = stickers; | ||||
|   factory _SnStickerPack.fromJson(Map<String, dynamic> json) => _$SnStickerPackFromJson(json); | ||||
|  | ||||
| @override final  String id; | ||||
| @@ -558,6 +559,13 @@ class _SnStickerPack implements SnStickerPack { | ||||
| @override final  DateTime createdAt; | ||||
| @override final  DateTime updatedAt; | ||||
| @override final  DateTime? deletedAt; | ||||
|  final  List<SnSticker> _stickers; | ||||
| @override@JsonKey() List<SnSticker> get stickers { | ||||
|   if (_stickers is EqualUnmodifiableListView) return _stickers; | ||||
|   // ignore: implicit_dynamic_type | ||||
|   return EqualUnmodifiableListView(_stickers); | ||||
| } | ||||
|  | ||||
|  | ||||
| /// Create a copy of SnStickerPack | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @@ -572,16 +580,16 @@ Map<String, dynamic> toJson() { | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnStickerPack&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.prefix, prefix) || other.prefix == prefix)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnStickerPack&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.prefix, prefix) || other.prefix == prefix)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other._stickers, _stickers)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,id,name,description,prefix,publisherId,publisher,createdAt,updatedAt,deletedAt); | ||||
| int get hashCode => Object.hash(runtimeType,id,name,description,prefix,publisherId,publisher,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(_stickers)); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnStickerPack(id: $id, name: $name, description: $description, prefix: $prefix, publisherId: $publisherId, publisher: $publisher, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||
|   return 'SnStickerPack(id: $id, name: $name, description: $description, prefix: $prefix, publisherId: $publisherId, publisher: $publisher, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, stickers: $stickers)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -592,7 +600,7 @@ abstract mixin class _$SnStickerPackCopyWith<$Res> implements $SnStickerPackCopy | ||||
|   factory _$SnStickerPackCopyWith(_SnStickerPack value, $Res Function(_SnStickerPack) _then) = __$SnStickerPackCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
|  String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnSticker> stickers | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -609,7 +617,7 @@ class __$SnStickerPackCopyWithImpl<$Res> | ||||
|  | ||||
| /// Create a copy of SnStickerPack | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = null,Object? prefix = null,Object? publisherId = null,Object? publisher = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = null,Object? prefix = null,Object? publisherId = null,Object? publisher = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? stickers = null,}) { | ||||
|   return _then(_SnStickerPack( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||
| @@ -620,7 +628,8 @@ as String,publisher: freezed == publisher ? _self.publisher : publisher // ignor | ||||
| as SnPublisher?,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?, | ||||
| as DateTime?,stickers: null == stickers ? _self._stickers : stickers // ignore: cast_nullable_to_non_nullable | ||||
| as List<SnSticker>, | ||||
|   )); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -54,6 +54,11 @@ _SnStickerPack _$SnStickerPackFromJson(Map<String, dynamic> json) => | ||||
|           json['deleted_at'] == null | ||||
|               ? null | ||||
|               : DateTime.parse(json['deleted_at'] as String), | ||||
|       stickers: | ||||
|           (json['stickers'] as List<dynamic>?) | ||||
|               ?.map((e) => SnSticker.fromJson(e as Map<String, dynamic>)) | ||||
|               .toList() ?? | ||||
|           const [], | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$SnStickerPackToJson(_SnStickerPack instance) => | ||||
| @@ -67,4 +72,5 @@ Map<String, dynamic> _$SnStickerPackToJson(_SnStickerPack instance) => | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|       'updated_at': instance.updatedAt.toIso8601String(), | ||||
|       'deleted_at': instance.deletedAt?.toIso8601String(), | ||||
|       'stickers': instance.stickers.map((e) => e.toJson()).toList(), | ||||
|     }; | ||||
|   | ||||
| @@ -28,6 +28,8 @@ import 'package:island/screens/creators/hub.dart'; | ||||
| import 'package:island/screens/creators/posts/post_manage_list.dart'; | ||||
| import 'package:island/screens/creators/stickers/stickers.dart'; | ||||
| import 'package:island/screens/creators/stickers/pack_detail.dart'; | ||||
| import 'package:island/screens/stickers/marketplace.dart'; | ||||
| import 'package:island/screens/stickers/pack_detail.dart'; | ||||
| import 'package:island/screens/creators/poll/poll_list.dart'; | ||||
| import 'package:island/screens/creators/publishers.dart'; | ||||
| import 'package:island/screens/creators/webfeed/webfeed_list.dart'; | ||||
| @@ -451,6 +453,23 @@ final routerProvider = Provider<GoRouter>((ref) { | ||||
|                     path: '/account', | ||||
|                     builder: (context, state) => const AccountScreen(), | ||||
|                   ), | ||||
|                   // Sticker marketplace (user-facing, no publisher) | ||||
|                   GoRoute( | ||||
|                     name: 'stickerMarketplace', | ||||
|                     path: '/stickers', | ||||
|                     builder: | ||||
|                         (context, state) => const MarketplaceStickersScreen(), | ||||
|                     routes: [ | ||||
|                       GoRoute( | ||||
|                         name: 'stickerPackDetail', | ||||
|                         path: ':packId', | ||||
|                         builder: (context, state) { | ||||
|                           final packId = state.pathParameters['packId']!; | ||||
|                           return MarketplaceStickerPackDetailScreen(id: packId); | ||||
|                         }, | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     name: 'notifications', | ||||
|                     path: '/account/notifications', | ||||
|   | ||||
| @@ -205,7 +205,7 @@ class _AboutScreenState extends ConsumerState<AboutScreen> { | ||||
|                           title: 'aboutScreenTermsOfServiceTitle'.tr(), | ||||
|                           onTap: | ||||
|                               () => _launchURL( | ||||
|                                 'https://solsynth.dev/terms/basic-law', | ||||
|                                 'https://solsynth.dev/terms/user-agreement', | ||||
|                               ), | ||||
|                         ), | ||||
|                         _buildListTile( | ||||
|   | ||||
| @@ -189,7 +189,6 @@ class AccountScreen extends HookConsumerWidget { | ||||
|                 ), | ||||
|               ], | ||||
|             ).padding(horizontal: 8), | ||||
|             const Gap(8), | ||||
|             ListTile( | ||||
|               minTileHeight: 48, | ||||
|               leading: const Icon(Symbols.notifications), | ||||
| @@ -228,6 +227,16 @@ class AccountScreen extends HookConsumerWidget { | ||||
|                 context.pushNamed('relationships'); | ||||
|               }, | ||||
|             ), | ||||
|             ListTile( | ||||
|               minTileHeight: 48, | ||||
|               leading: const Icon(Symbols.emoji_emotions), | ||||
|               trailing: const Icon(Symbols.chevron_right), | ||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|               title: Text('stickers').tr(), | ||||
|               onTap: () { | ||||
|                 context.pushNamed('stickerMarketplace'); | ||||
|               }, | ||||
|             ), | ||||
|             ListTile( | ||||
|               minTileHeight: 48, | ||||
|               title: Text('abuseReports').tr(), | ||||
|   | ||||
| @@ -35,6 +35,7 @@ import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'chat.dart'; | ||||
| import 'package:island/widgets/chat/call_button.dart'; | ||||
| import 'package:island/widgets/stickers/picker.dart'; | ||||
|  | ||||
| part 'room.g.dart'; | ||||
|  | ||||
| @@ -1060,21 +1061,25 @@ class _ChatInput extends HookConsumerWidget { | ||||
|         children: [ | ||||
|           if (attachments.isNotEmpty) | ||||
|             SizedBox( | ||||
|               height: 280, | ||||
|               height: 324, | ||||
|               child: ListView.separated( | ||||
|                 padding: EdgeInsets.symmetric(horizontal: 12), | ||||
|                 scrollDirection: Axis.horizontal, | ||||
|                 itemCount: attachments.length, | ||||
|                 itemBuilder: (context, idx) { | ||||
|                   return AttachmentPreview( | ||||
|                     item: attachments[idx], | ||||
|                     onRequestUpload: () => onUploadAttachment(idx), | ||||
|                     onDelete: () => onDeleteAttachment(idx), | ||||
|                     onUpdate: (value) { | ||||
|                       attachments[idx] = value; | ||||
|                       onAttachmentsChanged(attachments); | ||||
|                     }, | ||||
|                     onMove: (delta) => onMoveAttachment(idx, delta), | ||||
|                   return SizedBox( | ||||
|                     height: 320, | ||||
|                     width: 280, | ||||
|                     child: AttachmentPreview( | ||||
|                       item: attachments[idx], | ||||
|                       onRequestUpload: () => onUploadAttachment(idx), | ||||
|                       onDelete: () => onDeleteAttachment(idx), | ||||
|                       onUpdate: (value) { | ||||
|                         attachments[idx] = value; | ||||
|                         onAttachmentsChanged(attachments); | ||||
|                       }, | ||||
|                       onMove: (delta) => onMoveAttachment(idx, delta), | ||||
|                     ), | ||||
|                   ); | ||||
|                 }, | ||||
|                 separatorBuilder: (_, _) => const Gap(8), | ||||
| @@ -1129,31 +1134,76 @@ class _ChatInput extends HookConsumerWidget { | ||||
|             padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), | ||||
|             child: Row( | ||||
|               children: [ | ||||
|                 PopupMenuButton( | ||||
|                   icon: const Icon(Symbols.photo_library), | ||||
|                   itemBuilder: | ||||
|                       (context) => [ | ||||
|                         PopupMenuItem( | ||||
|                           onTap: () => onPickFile(true), | ||||
|                           child: Row( | ||||
|                             spacing: 12, | ||||
|                             children: [ | ||||
|                               const Icon(Symbols.photo), | ||||
|                               Text('addPhoto').tr(), | ||||
|                             ], | ||||
|                 Row( | ||||
|                   mainAxisSize: MainAxisSize.min, | ||||
|                   children: [ | ||||
|                     IconButton( | ||||
|                       tooltip: 'stickers'.tr(), | ||||
|                       icon: const Icon(Symbols.emoji_symbols), | ||||
|                       onPressed: () { | ||||
|                         final size = MediaQuery.of(context).size; | ||||
|                         showStickerPickerPopover( | ||||
|                           context, | ||||
|                           Offset( | ||||
|                             20, | ||||
|                             size.height - | ||||
|                                 480 - | ||||
|                                 MediaQuery.of(context).padding.bottom, | ||||
|                           ), | ||||
|                         ), | ||||
|                         PopupMenuItem( | ||||
|                           onTap: () => onPickFile(false), | ||||
|                           child: Row( | ||||
|                             spacing: 12, | ||||
|                             children: [ | ||||
|                               const Icon(Symbols.video_call), | ||||
|                               Text('addVideo').tr(), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ), | ||||
|                       ], | ||||
|                           onPick: (placeholder) { | ||||
|                             // Insert placeholder at current cursor position | ||||
|                             final text = messageController.text; | ||||
|                             final selection = messageController.selection; | ||||
|                             final start = | ||||
|                                 selection.start >= 0 | ||||
|                                     ? selection.start | ||||
|                                     : text.length; | ||||
|                             final end = | ||||
|                                 selection.end >= 0 | ||||
|                                     ? selection.end | ||||
|                                     : text.length; | ||||
|                             final newText = text.replaceRange( | ||||
|                               start, | ||||
|                               end, | ||||
|                               placeholder, | ||||
|                             ); | ||||
|                             messageController.value = TextEditingValue( | ||||
|                               text: newText, | ||||
|                               selection: TextSelection.collapsed( | ||||
|                                 offset: start + placeholder.length, | ||||
|                               ), | ||||
|                             ); | ||||
|                           }, | ||||
|                         ); | ||||
|                       }, | ||||
|                     ), | ||||
|                     PopupMenuButton( | ||||
|                       icon: const Icon(Symbols.photo_library), | ||||
|                       itemBuilder: | ||||
|                           (context) => [ | ||||
|                             PopupMenuItem( | ||||
|                               onTap: () => onPickFile(true), | ||||
|                               child: Row( | ||||
|                                 spacing: 12, | ||||
|                                 children: [ | ||||
|                                   const Icon(Symbols.photo), | ||||
|                                   Text('addPhoto').tr(), | ||||
|                                 ], | ||||
|                               ), | ||||
|                             ), | ||||
|                             PopupMenuItem( | ||||
|                               onTap: () => onPickFile(false), | ||||
|                               child: Row( | ||||
|                                 spacing: 12, | ||||
|                                 children: [ | ||||
|                                   const Icon(Symbols.video_call), | ||||
|                                   Text('addVideo').tr(), | ||||
|                                 ], | ||||
|                               ), | ||||
|                             ), | ||||
|                           ], | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|                 Expanded( | ||||
|                   child: RawKeyboardListener( | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/poll.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/widgets/poll/poll_feedback.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | ||||
| @@ -164,10 +165,13 @@ class _CreatorPollItem extends StatelessWidget { | ||||
|               ], | ||||
|         ), | ||||
|         onTap: () { | ||||
|           // Open editor for edit | ||||
|           // Navigator push by path to keep consistency with rest of app: | ||||
|           // Note: pub name string may be required in route; when absent, route may need query or pick later. | ||||
|           // For safety, just do nothing if no publisher in list item. | ||||
|           showModalBottomSheet( | ||||
|             context: context, | ||||
|             useRootNavigator: true, | ||||
|             isScrollControlled: true, | ||||
|             builder: | ||||
|                 (context) => PollFeedbackSheet(pollId: poll.id, poll: poll), | ||||
|           ); | ||||
|         }, | ||||
|       ), | ||||
|     ); | ||||
|   | ||||
| @@ -218,6 +218,11 @@ class ExploreScreen extends HookConsumerWidget { | ||||
|                               right: 12, | ||||
|                               top: 16, | ||||
|                             ), | ||||
|                             onChecked: () { | ||||
|                               ref.invalidate( | ||||
|                                 eventCalendarProvider(query.value), | ||||
|                               ); | ||||
|                             }, | ||||
|                           ), | ||||
|                           Card( | ||||
|                             margin: EdgeInsets.only(left: 8, right: 12, top: 8), | ||||
|   | ||||
| @@ -207,8 +207,6 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|         isScrollControlled: true, | ||||
|         builder: | ||||
|             (context) => ComposeSettingsSheet( | ||||
|               titleController: state.titleController, | ||||
|               descriptionController: state.descriptionController, | ||||
|               visibility: state.visibility, | ||||
|               tagsController: state.tagsController, | ||||
|               categoriesController: state.categoriesController, | ||||
| @@ -370,14 +368,52 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|                     // Post content form | ||||
|                     Expanded( | ||||
|                       child: SingleChildScrollView( | ||||
|                         padding: const EdgeInsets.symmetric(vertical: 12), | ||||
|                         padding: const EdgeInsets.symmetric(vertical: 16), | ||||
|                         child: Column( | ||||
|                           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                           children: [ | ||||
|                             TextField( | ||||
|                               controller: state.titleController, | ||||
|                               decoration: InputDecoration( | ||||
|                                 hintText: 'postTitle'.tr(), | ||||
|                                 border: InputBorder.none, | ||||
|                                 isCollapsed: true, | ||||
|                                 contentPadding: const EdgeInsets.symmetric( | ||||
|                                   vertical: 8, | ||||
|                                   horizontal: 8, | ||||
|                                 ), | ||||
|                               ), | ||||
|                               style: theme.textTheme.titleMedium, | ||||
|                               onTapOutside: | ||||
|                                   (_) => | ||||
|                                       FocusManager.instance.primaryFocus | ||||
|                                           ?.unfocus(), | ||||
|                             ), | ||||
|                             TextField( | ||||
|                               controller: state.descriptionController, | ||||
|                               decoration: InputDecoration( | ||||
|                                 hintText: 'postDescription'.tr(), | ||||
|                                 border: InputBorder.none, | ||||
|                                 isCollapsed: true, | ||||
|                                 contentPadding: const EdgeInsets.fromLTRB( | ||||
|                                   8, | ||||
|                                   4, | ||||
|                                   8, | ||||
|                                   12, | ||||
|                                 ), | ||||
|                               ), | ||||
|                               style: theme.textTheme.bodyMedium, | ||||
|                               minLines: 1, | ||||
|                               maxLines: 3, | ||||
|                               onTapOutside: | ||||
|                                   (_) => | ||||
|                                       FocusManager.instance.primaryFocus | ||||
|                                           ?.unfocus(), | ||||
|                             ), | ||||
|                             // Content field with borderless design | ||||
|                             RawKeyboardListener( | ||||
|                             KeyboardListener( | ||||
|                               focusNode: FocusNode(), | ||||
|                               onKey: | ||||
|                               onKeyEvent: | ||||
|                                   (event) => ComposeLogic.handleKeyPress( | ||||
|                                     event, | ||||
|                                     state, | ||||
| @@ -393,7 +429,11 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|                                 decoration: InputDecoration( | ||||
|                                   border: InputBorder.none, | ||||
|                                   hintText: 'postContent'.tr(), | ||||
|                                   contentPadding: const EdgeInsets.all(8), | ||||
|                                   isCollapsed: true, | ||||
|                                   contentPadding: const EdgeInsets.symmetric( | ||||
|                                     vertical: 8, | ||||
|                                     horizontal: 8, | ||||
|                                   ), | ||||
|                                 ), | ||||
|                                 maxLines: null, | ||||
|                                 onTapOutside: | ||||
|   | ||||
| @@ -140,8 +140,6 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|         isScrollControlled: true, | ||||
|         builder: | ||||
|             (context) => ComposeSettingsSheet( | ||||
|               titleController: state.titleController, | ||||
|               descriptionController: state.descriptionController, | ||||
|               visibility: state.visibility, | ||||
|               tagsController: state.tagsController, | ||||
|               categoriesController: state.categoriesController, | ||||
| @@ -242,10 +240,39 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|           child: Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             children: [ | ||||
|               TextField( | ||||
|                 controller: state.titleController, | ||||
|                 decoration: InputDecoration( | ||||
|                   hintText: 'postTitle'.tr(), | ||||
|                   border: InputBorder.none, | ||||
|                   isCollapsed: true, | ||||
|                   contentPadding: const EdgeInsets.symmetric( | ||||
|                     vertical: 8, | ||||
|                     horizontal: 8, | ||||
|                   ), | ||||
|                 ), | ||||
|                 style: theme.textTheme.titleMedium, | ||||
|                 onTapOutside: | ||||
|                     (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|               ), | ||||
|               TextField( | ||||
|                 controller: state.descriptionController, | ||||
|                 decoration: InputDecoration( | ||||
|                   hintText: 'postDescription'.tr(), | ||||
|                   border: InputBorder.none, | ||||
|                   isCollapsed: true, | ||||
|                   contentPadding: const EdgeInsets.fromLTRB(8, 4, 8, 12), | ||||
|                 ), | ||||
|                 style: theme.textTheme.bodyMedium, | ||||
|                 minLines: 1, | ||||
|                 maxLines: 3, | ||||
|                 onTapOutside: | ||||
|                     (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|               ), | ||||
|               Expanded( | ||||
|                 child: RawKeyboardListener( | ||||
|                 child: KeyboardListener( | ||||
|                   focusNode: FocusNode(), | ||||
|                   onKey: | ||||
|                   onKeyEvent: | ||||
|                       (event) => _handleKeyPress( | ||||
|                         event, | ||||
|                         state, | ||||
| @@ -454,7 +481,7 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|                               flex: showPreview.value ? 1 : 2, | ||||
|                               child: buildEditorPane(), | ||||
|                             ), | ||||
|                             const VerticalDivider(), | ||||
|                             if (showPreview.value) const VerticalDivider(), | ||||
|                             if (showPreview.value) | ||||
|                               Expanded(child: buildPreviewPane()), | ||||
|                           ], | ||||
| @@ -475,7 +502,7 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|  | ||||
|   // Helper method to handle keyboard shortcuts | ||||
|   void _handleKeyPress( | ||||
|     RawKeyEvent event, | ||||
|     KeyEvent event, | ||||
|     ComposeState state, | ||||
|     WidgetRef ref, | ||||
|     BuildContext context, { | ||||
| @@ -485,7 +512,9 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|  | ||||
|     final isPaste = event.logicalKey == LogicalKeyboardKey.keyV; | ||||
|     final isSave = event.logicalKey == LogicalKeyboardKey.keyS; | ||||
|     final isModifierPressed = event.isMetaPressed || event.isControlPressed; | ||||
|     final isModifierPressed = | ||||
|         HardwareKeyboard.instance.isMetaPressed || | ||||
|         HardwareKeyboard.instance.isControlPressed; | ||||
|     final isSubmit = event.logicalKey == LogicalKeyboardKey.enter; | ||||
|  | ||||
|     if (isPaste && isModifierPressed) { | ||||
|   | ||||
| @@ -87,6 +87,12 @@ class PublisherProfileScreen extends HookConsumerWidget { | ||||
|       publisherAppbarForcegroundColorProvider(name), | ||||
|     ); | ||||
|  | ||||
|     final categoryTabController = useTabController(initialLength: 3); | ||||
|     final categoryTab = useState(0); | ||||
|     categoryTabController.addListener(() { | ||||
|       categoryTab.value = categoryTabController.index; | ||||
|     }); | ||||
|  | ||||
|     final subscribing = useState(false); | ||||
|  | ||||
|     Future<void> subscribe() async { | ||||
| @@ -268,6 +274,16 @@ class PublisherProfileScreen extends HookConsumerWidget { | ||||
|       ).padding(horizontal: 20, vertical: 16), | ||||
|     ); | ||||
|  | ||||
|     Widget publisherCategoryTabWidget() => Card( | ||||
|       margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||
|       child: TabBar( | ||||
|         controller: categoryTabController, | ||||
|         dividerColor: Colors.transparent, | ||||
|         splashBorderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|         tabs: [Tab(text: 'All'), Tab(text: 'Posts'), Tab(text: 'Articles')], | ||||
|       ), | ||||
|     ); | ||||
|  | ||||
|     return publisher.when( | ||||
|       data: | ||||
|           (data) => AppScaffold( | ||||
| @@ -321,7 +337,18 @@ class PublisherProfileScreen extends HookConsumerWidget { | ||||
|                           child: CustomScrollView( | ||||
|                             slivers: [ | ||||
|                               SliverGap(16), | ||||
|                               SliverPostList(pubName: name), | ||||
|                               SliverToBoxAdapter( | ||||
|                                 child: publisherCategoryTabWidget(), | ||||
|                               ), | ||||
|                               SliverPostList( | ||||
|                                 key: ValueKey(categoryTab.value), | ||||
|                                 pubName: name, | ||||
|                                 type: switch (categoryTab.value) { | ||||
|                                   1 => 0, | ||||
|                                   2 => 1, | ||||
|                                   _ => null, | ||||
|                                 }, | ||||
|                               ), | ||||
|                               SliverGap( | ||||
|                                 MediaQuery.of(context).padding.bottom + 16, | ||||
|                               ), | ||||
| @@ -334,9 +361,9 @@ class PublisherProfileScreen extends HookConsumerWidget { | ||||
|                             alignment: Alignment.topLeft, | ||||
|                             child: SingleChildScrollView( | ||||
|                               child: Column( | ||||
|                                 crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                                 crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                                 children: [ | ||||
|                                   publisherBasisWidget(data), | ||||
|                                   publisherBasisWidget(data).padding(bottom: 8), | ||||
|                                   publisherBadgesWidget(data), | ||||
|                                   publisherVerificationWidget(data), | ||||
|                                   publisherBioWidget(data), | ||||
| @@ -398,7 +425,16 @@ class PublisherProfileScreen extends HookConsumerWidget { | ||||
|                           child: publisherVerificationWidget(data), | ||||
|                         ), | ||||
|                         SliverToBoxAdapter(child: publisherBioWidget(data)), | ||||
|                         SliverPostList(pubName: name), | ||||
|                         SliverToBoxAdapter(child: publisherCategoryTabWidget()), | ||||
|                         SliverPostList( | ||||
|                           key: ValueKey(categoryTab.value), | ||||
|                           pubName: name, | ||||
|                           type: switch (categoryTab.value) { | ||||
|                             1 => 0, | ||||
|                             2 => 1, | ||||
|                             _ => null, | ||||
|                           }, | ||||
|                         ), | ||||
|                         SliverGap(MediaQuery.of(context).padding.bottom + 16), | ||||
|                       ], | ||||
|                     ), | ||||
|   | ||||
							
								
								
									
										103
									
								
								lib/screens/stickers/marketplace.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								lib/screens/stickers/marketplace.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/sticker.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | ||||
|  | ||||
| part 'marketplace.g.dart'; | ||||
|  | ||||
| @riverpod | ||||
| class MarketplaceStickerPacksNotifier extends _$MarketplaceStickerPacksNotifier | ||||
|     with CursorPagingNotifierMixin<SnStickerPack> { | ||||
|   @override | ||||
|   Future<CursorPagingData<SnStickerPack>> build() { | ||||
|     return fetch(cursor: null); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<CursorPagingData<SnStickerPack>> fetch({ | ||||
|     required String? cursor, | ||||
|   }) async { | ||||
|     final client = ref.read(apiClientProvider); | ||||
|     final offset = cursor == null ? 0 : int.parse(cursor); | ||||
|  | ||||
|     final response = await client.get( | ||||
|       '/sphere/stickers', | ||||
|       queryParameters: {'offset': offset, 'take': 20}, | ||||
|     ); | ||||
|  | ||||
|     final total = int.parse(response.headers.value('X-Total') ?? '0'); | ||||
|     final List<dynamic> data = response.data; | ||||
|     final stickers = data.map((e) => SnStickerPack.fromJson(e)).toList(); | ||||
|  | ||||
|     final hasMore = offset + stickers.length < total; | ||||
|     final nextCursor = hasMore ? (offset + stickers.length).toString() : null; | ||||
|  | ||||
|     return CursorPagingData( | ||||
|       items: stickers, | ||||
|       hasMore: hasMore, | ||||
|       nextCursor: nextCursor, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// User-facing marketplace screen for browsing sticker packs. | ||||
| /// This version does NOT rely on publisher name (no pubName). | ||||
| class MarketplaceStickersScreen extends HookConsumerWidget { | ||||
|   const MarketplaceStickersScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         title: const Text('stickers').tr(), | ||||
|         actions: const [Gap(8)], | ||||
|       ), | ||||
|       body: const SliverMarketplaceStickerPacksList(), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class SliverMarketplaceStickerPacksList extends HookConsumerWidget { | ||||
|   const SliverMarketplaceStickerPacksList({super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     return PagingHelperView( | ||||
|       provider: marketplaceStickerPacksNotifierProvider, | ||||
|       futureRefreshable: marketplaceStickerPacksNotifierProvider.future, | ||||
|       notifierRefreshable: marketplaceStickerPacksNotifierProvider.notifier, | ||||
|       contentBuilder: | ||||
|           (data, widgetCount, endItemView) => ListView.builder( | ||||
|             padding: EdgeInsets.zero, | ||||
|             itemCount: widgetCount, | ||||
|             itemBuilder: (context, index) { | ||||
|               if (index == widgetCount - 1) { | ||||
|                 return endItemView; | ||||
|               } | ||||
|  | ||||
|               final pack = data.items[index]; | ||||
|               return ListTile( | ||||
|                 title: Text(pack.name), | ||||
|                 subtitle: Text(pack.description), | ||||
|                 trailing: const Icon(Symbols.chevron_right), | ||||
|                 onTap: () { | ||||
|                   // Navigate to user-facing sticker pack detail page. | ||||
|                   // Adjust the route name/parameters if your app uses different ones. | ||||
|                   context.pushNamed( | ||||
|                     'stickerPackDetail', | ||||
|                     pathParameters: {'packId': pack.id}, | ||||
|                   ); | ||||
|                 }, | ||||
|               ); | ||||
|             }, | ||||
|           ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										32
									
								
								lib/screens/stickers/marketplace.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								lib/screens/stickers/marketplace.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'marketplace.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$marketplaceStickerPacksNotifierHash() => | ||||
|     r'b62ae8b7f5c4f8bb3be8c17fc005ea26da355187'; | ||||
|  | ||||
| /// See also [MarketplaceStickerPacksNotifier]. | ||||
| @ProviderFor(MarketplaceStickerPacksNotifier) | ||||
| final marketplaceStickerPacksNotifierProvider = | ||||
|     AutoDisposeAsyncNotifierProvider< | ||||
|       MarketplaceStickerPacksNotifier, | ||||
|       CursorPagingData<SnStickerPack> | ||||
|     >.internal( | ||||
|       MarketplaceStickerPacksNotifier.new, | ||||
|       name: r'marketplaceStickerPacksNotifierProvider', | ||||
|       debugGetCreateSourceHash: | ||||
|           const bool.fromEnvironment('dart.vm.product') | ||||
|               ? null | ||||
|               : _$marketplaceStickerPacksNotifierHash, | ||||
|       dependencies: null, | ||||
|       allTransitiveDependencies: null, | ||||
|     ); | ||||
|  | ||||
| typedef _$MarketplaceStickerPacksNotifier = | ||||
|     AutoDisposeAsyncNotifier<CursorPagingData<SnStickerPack>>; | ||||
| // ignore_for_file: type=lint | ||||
| // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package | ||||
							
								
								
									
										230
									
								
								lib/screens/stickers/pack_detail.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										230
									
								
								lib/screens/stickers/pack_detail.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,230 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:google_fonts/google_fonts.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/sticker.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/screens/creators/stickers/stickers.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:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| part 'pack_detail.g.dart'; // generated by riverpod_annotation build_runner | ||||
|  | ||||
| /// Marketplace version of sticker pack detail page (no publisher dependency). | ||||
| /// Shows all stickers in the pack and provides a button to add the sticker. | ||||
| /// API interactions are intentionally left blank per request. | ||||
| @riverpod | ||||
| Future<List<SnSticker>> marketplaceStickerPackContent( | ||||
|   Ref ref, { | ||||
|   required String packId, | ||||
| }) async { | ||||
|   final apiClient = ref.watch(apiClientProvider); | ||||
|   final resp = await apiClient.get('/sphere/stickers/$packId/content'); | ||||
|   return (resp.data as List).map((e) => SnSticker.fromJson(e)).toList(); | ||||
| } | ||||
|  | ||||
| @riverpod | ||||
| Future<bool> marketplaceStickerPackOwnership( | ||||
|   Ref ref, { | ||||
|   required String packId, | ||||
| }) async { | ||||
|   final api = ref.watch(apiClientProvider); | ||||
|   try { | ||||
|     await api.get('/sphere/stickers/$packId/own'); | ||||
|     // If not 404, consider owned | ||||
|     return true; | ||||
|   } on Object catch (e) { | ||||
|     // Dio error handling agnostic: treat 404 as not-owned, rethrow others | ||||
|     final msg = e.toString(); | ||||
|     if (msg.contains('404')) return false; | ||||
|     rethrow; | ||||
|   } | ||||
| } | ||||
|  | ||||
| class MarketplaceStickerPackDetailScreen extends HookConsumerWidget { | ||||
|   final String id; | ||||
|   const MarketplaceStickerPackDetailScreen({super.key, required this.id}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     // Pack metadata provider exists globally in creators file; reuse it. | ||||
|     final pack = ref.watch(stickerPackProvider(id)); | ||||
|     final packContent = ref.watch( | ||||
|       marketplaceStickerPackContentProvider(packId: id), | ||||
|     ); | ||||
|     final owned = ref.watch( | ||||
|       marketplaceStickerPackOwnershipProvider(packId: id), | ||||
|     ); | ||||
|  | ||||
|     // Add entire pack to user's collection | ||||
|     Future<void> addPackToMyCollection() async { | ||||
|       final apiClient = ref.watch(apiClientProvider); | ||||
|       await apiClient.post('/sphere/stickers/$id/own'); | ||||
|       HapticFeedback.selectionClick(); | ||||
|       ref.invalidate(marketplaceStickerPackOwnershipProvider(packId: id)); | ||||
|       if (!context.mounted) return; | ||||
|       showSnackBar('stickerPackAdded'.tr()); | ||||
|     } | ||||
|  | ||||
|     // Remove ownership of the pack | ||||
|     Future<void> removePackFromMyCollection() async { | ||||
|       final apiClient = ref.watch(apiClientProvider); | ||||
|       await apiClient.delete('/sphere/stickers/$id/own'); | ||||
|       HapticFeedback.selectionClick(); | ||||
|       ref.invalidate(marketplaceStickerPackOwnershipProvider(packId: id)); | ||||
|       if (!context.mounted) return; | ||||
|       showSnackBar('stickerPackRemoved'.tr()); | ||||
|     } | ||||
|  | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar(title: Text(pack.value?.name ?? 'loading'.tr())), | ||||
|       body: pack.when( | ||||
|         data: (p) { | ||||
|           return Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|             children: [ | ||||
|               // Pack meta | ||||
|               Column( | ||||
|                 crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                 children: [ | ||||
|                   Text(p?.description ?? ''), | ||||
|                   Row( | ||||
|                     spacing: 4, | ||||
|                     children: [ | ||||
|                       const Icon(Symbols.folder, size: 16), | ||||
|                       Text( | ||||
|                         '${packContent.value?.length ?? 0}/24', | ||||
|                         style: GoogleFonts.robotoMono(), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ).opacity(0.85), | ||||
|                   Row( | ||||
|                     spacing: 4, | ||||
|                     children: [ | ||||
|                       const Icon(Symbols.sell, size: 16), | ||||
|                       Text(p?.prefix ?? '', style: GoogleFonts.robotoMono()), | ||||
|                     ], | ||||
|                   ).opacity(0.85), | ||||
|                   Row( | ||||
|                     spacing: 4, | ||||
|                     children: [ | ||||
|                       const Icon(Symbols.tag, size: 16), | ||||
|                       SelectableText( | ||||
|                         p?.id ?? id, | ||||
|                         style: GoogleFonts.robotoMono(), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ).opacity(0.85), | ||||
|                 ], | ||||
|               ).padding(horizontal: 24, vertical: 24), | ||||
|               const Divider(height: 1), | ||||
|               // Stickers grid | ||||
|               Expanded( | ||||
|                 child: packContent.when( | ||||
|                   data: | ||||
|                       (stickers) => RefreshIndicator( | ||||
|                         onRefresh: | ||||
|                             () => ref.refresh( | ||||
|                               marketplaceStickerPackContentProvider( | ||||
|                                 packId: id, | ||||
|                               ).future, | ||||
|                             ), | ||||
|                         child: GridView.builder( | ||||
|                           padding: const EdgeInsets.symmetric( | ||||
|                             horizontal: 24, | ||||
|                             vertical: 20, | ||||
|                           ), | ||||
|                           gridDelegate: | ||||
|                               const SliverGridDelegateWithMaxCrossAxisExtent( | ||||
|                                 maxCrossAxisExtent: 96, | ||||
|                                 mainAxisSpacing: 12, | ||||
|                                 crossAxisSpacing: 12, | ||||
|                               ), | ||||
|                           itemCount: stickers.length, | ||||
|                           itemBuilder: (context, index) { | ||||
|                             final sticker = stickers[index]; | ||||
|                             return Tooltip( | ||||
|                               message: ':${p?.prefix ?? ''}${sticker.slug}:', | ||||
|                               child: ClipRRect( | ||||
|                                 borderRadius: const BorderRadius.all( | ||||
|                                   Radius.circular(8), | ||||
|                                 ), | ||||
|                                 child: Container( | ||||
|                                   decoration: BoxDecoration( | ||||
|                                     color: | ||||
|                                         Theme.of( | ||||
|                                           context, | ||||
|                                         ).colorScheme.surfaceContainer, | ||||
|                                     borderRadius: const BorderRadius.all( | ||||
|                                       Radius.circular(8), | ||||
|                                     ), | ||||
|                                   ), | ||||
|                                   child: AspectRatio( | ||||
|                                     aspectRatio: 1, | ||||
|                                     child: CloudImageWidget( | ||||
|                                       fileId: sticker.imageId, | ||||
|                                       fit: BoxFit.contain, | ||||
|                                     ), | ||||
|                                   ), | ||||
|                                 ), | ||||
|                               ), | ||||
|                             ); | ||||
|                           }, | ||||
|                         ), | ||||
|                       ), | ||||
|                   error: | ||||
|                       (err, _) => | ||||
|                           Text( | ||||
|                             'Error: $err', | ||||
|                           ).textAlignment(TextAlign.center).center(), | ||||
|                   loading: () => const CircularProgressIndicator().center(), | ||||
|                 ), | ||||
|               ), | ||||
|               Padding( | ||||
|                 padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8), | ||||
|                 child: owned.when( | ||||
|                   data: | ||||
|                       (isOwned) => FilledButton.icon( | ||||
|                         onPressed: | ||||
|                             isOwned | ||||
|                                 ? removePackFromMyCollection | ||||
|                                 : addPackToMyCollection, | ||||
|                         icon: Icon( | ||||
|                           isOwned ? Symbols.remove_circle : Symbols.add_circle, | ||||
|                         ), | ||||
|                         label: Text( | ||||
|                           isOwned ? 'removePack'.tr() : 'addPack'.tr(), | ||||
|                         ), | ||||
|                       ), | ||||
|                   loading: | ||||
|                       () => const SizedBox( | ||||
|                         height: 32, | ||||
|                         width: 32, | ||||
|                         child: CircularProgressIndicator(strokeWidth: 2), | ||||
|                       ), | ||||
|                   error: | ||||
|                       (_, _) => OutlinedButton.icon( | ||||
|                         onPressed: addPackToMyCollection, | ||||
|                         icon: const Icon(Symbols.add_circle), | ||||
|                         label: Text('addPack').tr(), | ||||
|                       ), | ||||
|                 ), | ||||
|               ), | ||||
|               Gap(MediaQuery.of(context).padding.bottom), | ||||
|             ], | ||||
|           ); | ||||
|         }, | ||||
|         error: | ||||
|             (err, _) => | ||||
|                 Text('Error: $err').textAlignment(TextAlign.center).center(), | ||||
|         loading: () => const CircularProgressIndicator().center(), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										317
									
								
								lib/screens/stickers/pack_detail.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										317
									
								
								lib/screens/stickers/pack_detail.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,317 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'pack_detail.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$marketplaceStickerPackContentHash() => | ||||
|     r'886f8305c978dbea6e5d990a7d555048ac704a5d'; | ||||
|  | ||||
| /// 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)); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// Marketplace version of sticker pack detail page (no publisher dependency). | ||||
| /// Shows all stickers in the pack and provides a button to add the sticker. | ||||
| /// API interactions are intentionally left blank per request. | ||||
| /// | ||||
| /// Copied from [marketplaceStickerPackContent]. | ||||
| @ProviderFor(marketplaceStickerPackContent) | ||||
| const marketplaceStickerPackContentProvider = | ||||
|     MarketplaceStickerPackContentFamily(); | ||||
|  | ||||
| /// Marketplace version of sticker pack detail page (no publisher dependency). | ||||
| /// Shows all stickers in the pack and provides a button to add the sticker. | ||||
| /// API interactions are intentionally left blank per request. | ||||
| /// | ||||
| /// Copied from [marketplaceStickerPackContent]. | ||||
| class MarketplaceStickerPackContentFamily | ||||
|     extends Family<AsyncValue<List<SnSticker>>> { | ||||
|   /// Marketplace version of sticker pack detail page (no publisher dependency). | ||||
|   /// Shows all stickers in the pack and provides a button to add the sticker. | ||||
|   /// API interactions are intentionally left blank per request. | ||||
|   /// | ||||
|   /// Copied from [marketplaceStickerPackContent]. | ||||
|   const MarketplaceStickerPackContentFamily(); | ||||
|  | ||||
|   /// Marketplace version of sticker pack detail page (no publisher dependency). | ||||
|   /// Shows all stickers in the pack and provides a button to add the sticker. | ||||
|   /// API interactions are intentionally left blank per request. | ||||
|   /// | ||||
|   /// Copied from [marketplaceStickerPackContent]. | ||||
|   MarketplaceStickerPackContentProvider call({required String packId}) { | ||||
|     return MarketplaceStickerPackContentProvider(packId: packId); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   MarketplaceStickerPackContentProvider getProviderOverride( | ||||
|     covariant MarketplaceStickerPackContentProvider provider, | ||||
|   ) { | ||||
|     return call(packId: provider.packId); | ||||
|   } | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _dependencies = null; | ||||
|  | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get dependencies => _dependencies; | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null; | ||||
|  | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get allTransitiveDependencies => | ||||
|       _allTransitiveDependencies; | ||||
|  | ||||
|   @override | ||||
|   String? get name => r'marketplaceStickerPackContentProvider'; | ||||
| } | ||||
|  | ||||
| /// Marketplace version of sticker pack detail page (no publisher dependency). | ||||
| /// Shows all stickers in the pack and provides a button to add the sticker. | ||||
| /// API interactions are intentionally left blank per request. | ||||
| /// | ||||
| /// Copied from [marketplaceStickerPackContent]. | ||||
| class MarketplaceStickerPackContentProvider | ||||
|     extends AutoDisposeFutureProvider<List<SnSticker>> { | ||||
|   /// Marketplace version of sticker pack detail page (no publisher dependency). | ||||
|   /// Shows all stickers in the pack and provides a button to add the sticker. | ||||
|   /// API interactions are intentionally left blank per request. | ||||
|   /// | ||||
|   /// Copied from [marketplaceStickerPackContent]. | ||||
|   MarketplaceStickerPackContentProvider({required String packId}) | ||||
|     : this._internal( | ||||
|         (ref) => marketplaceStickerPackContent( | ||||
|           ref as MarketplaceStickerPackContentRef, | ||||
|           packId: packId, | ||||
|         ), | ||||
|         from: marketplaceStickerPackContentProvider, | ||||
|         name: r'marketplaceStickerPackContentProvider', | ||||
|         debugGetCreateSourceHash: | ||||
|             const bool.fromEnvironment('dart.vm.product') | ||||
|                 ? null | ||||
|                 : _$marketplaceStickerPackContentHash, | ||||
|         dependencies: MarketplaceStickerPackContentFamily._dependencies, | ||||
|         allTransitiveDependencies: | ||||
|             MarketplaceStickerPackContentFamily._allTransitiveDependencies, | ||||
|         packId: packId, | ||||
|       ); | ||||
|  | ||||
|   MarketplaceStickerPackContentProvider._internal( | ||||
|     super._createNotifier, { | ||||
|     required super.name, | ||||
|     required super.dependencies, | ||||
|     required super.allTransitiveDependencies, | ||||
|     required super.debugGetCreateSourceHash, | ||||
|     required super.from, | ||||
|     required this.packId, | ||||
|   }) : super.internal(); | ||||
|  | ||||
|   final String packId; | ||||
|  | ||||
|   @override | ||||
|   Override overrideWith( | ||||
|     FutureOr<List<SnSticker>> Function( | ||||
|       MarketplaceStickerPackContentRef provider, | ||||
|     ) | ||||
|     create, | ||||
|   ) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: MarketplaceStickerPackContentProvider._internal( | ||||
|         (ref) => create(ref as MarketplaceStickerPackContentRef), | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         packId: packId, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeFutureProviderElement<List<SnSticker>> createElement() { | ||||
|     return _MarketplaceStickerPackContentProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is MarketplaceStickerPackContentProvider && | ||||
|         other.packId == packId; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||
|     hash = _SystemHash.combine(hash, packId.hashCode); | ||||
|  | ||||
|     return _SystemHash.finish(hash); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| mixin MarketplaceStickerPackContentRef | ||||
|     on AutoDisposeFutureProviderRef<List<SnSticker>> { | ||||
|   /// The parameter `packId` of this provider. | ||||
|   String get packId; | ||||
| } | ||||
|  | ||||
| class _MarketplaceStickerPackContentProviderElement | ||||
|     extends AutoDisposeFutureProviderElement<List<SnSticker>> | ||||
|     with MarketplaceStickerPackContentRef { | ||||
|   _MarketplaceStickerPackContentProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   String get packId => (origin as MarketplaceStickerPackContentProvider).packId; | ||||
| } | ||||
|  | ||||
| String _$marketplaceStickerPackOwnershipHash() => | ||||
|     r'e5dd301c309fac958729d13d984ce7a77edbe7e6'; | ||||
|  | ||||
| /// See also [marketplaceStickerPackOwnership]. | ||||
| @ProviderFor(marketplaceStickerPackOwnership) | ||||
| const marketplaceStickerPackOwnershipProvider = | ||||
|     MarketplaceStickerPackOwnershipFamily(); | ||||
|  | ||||
| /// See also [marketplaceStickerPackOwnership]. | ||||
| class MarketplaceStickerPackOwnershipFamily extends Family<AsyncValue<bool>> { | ||||
|   /// See also [marketplaceStickerPackOwnership]. | ||||
|   const MarketplaceStickerPackOwnershipFamily(); | ||||
|  | ||||
|   /// See also [marketplaceStickerPackOwnership]. | ||||
|   MarketplaceStickerPackOwnershipProvider call({required String packId}) { | ||||
|     return MarketplaceStickerPackOwnershipProvider(packId: packId); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   MarketplaceStickerPackOwnershipProvider getProviderOverride( | ||||
|     covariant MarketplaceStickerPackOwnershipProvider provider, | ||||
|   ) { | ||||
|     return call(packId: provider.packId); | ||||
|   } | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _dependencies = null; | ||||
|  | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get dependencies => _dependencies; | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null; | ||||
|  | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get allTransitiveDependencies => | ||||
|       _allTransitiveDependencies; | ||||
|  | ||||
|   @override | ||||
|   String? get name => r'marketplaceStickerPackOwnershipProvider'; | ||||
| } | ||||
|  | ||||
| /// See also [marketplaceStickerPackOwnership]. | ||||
| class MarketplaceStickerPackOwnershipProvider | ||||
|     extends AutoDisposeFutureProvider<bool> { | ||||
|   /// See also [marketplaceStickerPackOwnership]. | ||||
|   MarketplaceStickerPackOwnershipProvider({required String packId}) | ||||
|     : this._internal( | ||||
|         (ref) => marketplaceStickerPackOwnership( | ||||
|           ref as MarketplaceStickerPackOwnershipRef, | ||||
|           packId: packId, | ||||
|         ), | ||||
|         from: marketplaceStickerPackOwnershipProvider, | ||||
|         name: r'marketplaceStickerPackOwnershipProvider', | ||||
|         debugGetCreateSourceHash: | ||||
|             const bool.fromEnvironment('dart.vm.product') | ||||
|                 ? null | ||||
|                 : _$marketplaceStickerPackOwnershipHash, | ||||
|         dependencies: MarketplaceStickerPackOwnershipFamily._dependencies, | ||||
|         allTransitiveDependencies: | ||||
|             MarketplaceStickerPackOwnershipFamily._allTransitiveDependencies, | ||||
|         packId: packId, | ||||
|       ); | ||||
|  | ||||
|   MarketplaceStickerPackOwnershipProvider._internal( | ||||
|     super._createNotifier, { | ||||
|     required super.name, | ||||
|     required super.dependencies, | ||||
|     required super.allTransitiveDependencies, | ||||
|     required super.debugGetCreateSourceHash, | ||||
|     required super.from, | ||||
|     required this.packId, | ||||
|   }) : super.internal(); | ||||
|  | ||||
|   final String packId; | ||||
|  | ||||
|   @override | ||||
|   Override overrideWith( | ||||
|     FutureOr<bool> Function(MarketplaceStickerPackOwnershipRef provider) create, | ||||
|   ) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: MarketplaceStickerPackOwnershipProvider._internal( | ||||
|         (ref) => create(ref as MarketplaceStickerPackOwnershipRef), | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         packId: packId, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeFutureProviderElement<bool> createElement() { | ||||
|     return _MarketplaceStickerPackOwnershipProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is MarketplaceStickerPackOwnershipProvider && | ||||
|         other.packId == packId; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||
|     hash = _SystemHash.combine(hash, packId.hashCode); | ||||
|  | ||||
|     return _SystemHash.finish(hash); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| mixin MarketplaceStickerPackOwnershipRef on AutoDisposeFutureProviderRef<bool> { | ||||
|   /// The parameter `packId` of this provider. | ||||
|   String get packId; | ||||
| } | ||||
|  | ||||
| class _MarketplaceStickerPackOwnershipProviderElement | ||||
|     extends AutoDisposeFutureProviderElement<bool> | ||||
|     with MarketplaceStickerPackOwnershipRef { | ||||
|   _MarketplaceStickerPackOwnershipProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   String get packId => | ||||
|       (origin as MarketplaceStickerPackOwnershipProvider).packId; | ||||
| } | ||||
|  | ||||
| // 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 | ||||
							
								
								
									
										30
									
								
								lib/utils/mapping.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								lib/utils/mapping.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| String _upperCamelToLowerSnake(String input) { | ||||
|   final regex = RegExp(r'(?<=[a-z0-9])([A-Z])'); | ||||
|   return input | ||||
|       .replaceAllMapped(regex, (match) => '_${match.group(0)}') | ||||
|       .toLowerCase(); | ||||
| } | ||||
|  | ||||
| Map<String, dynamic> convertMapKeysToSnakeCase(Map<String, dynamic> input) { | ||||
|   final result = <String, dynamic>{}; | ||||
|  | ||||
|   input.forEach((key, value) { | ||||
|     final newKey = _upperCamelToLowerSnake(key); | ||||
|  | ||||
|     if (value is Map<String, dynamic>) { | ||||
|       result[newKey] = convertMapKeysToSnakeCase(value); | ||||
|     } else if (value is List) { | ||||
|       result[newKey] = | ||||
|           value.map((item) { | ||||
|             if (item is Map<String, dynamic>) { | ||||
|               return convertMapKeysToSnakeCase(item); | ||||
|             } | ||||
|             return item; | ||||
|           }).toList(); | ||||
|     } else { | ||||
|       result[newKey] = value; | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   return result; | ||||
| } | ||||
| @@ -130,9 +130,22 @@ class AccountStatusWidget extends HookConsumerWidget { | ||||
|               size: 16, | ||||
|             ).padding(right: 4), | ||||
|           if (status.value?.isCustomized ?? false) | ||||
|             Text(status.value?.label ?? 'unknown'.tr()) | ||||
|             Flexible( | ||||
|               child: Text( | ||||
|                 status.value?.label ?? 'unknown'.tr(), | ||||
|                 maxLines: 1, | ||||
|                 overflow: TextOverflow.ellipsis, | ||||
|               ), | ||||
|             ) | ||||
|           else | ||||
|             Text((status.value?.label ?? 'offline').toLowerCase()).tr(), | ||||
|             Flexible( | ||||
|               child: | ||||
|                   Text( | ||||
|                     (status.value?.label ?? 'offline').toLowerCase(), | ||||
|                     maxLines: 1, | ||||
|                     overflow: TextOverflow.ellipsis, | ||||
|                   ).tr(), | ||||
|             ), | ||||
|           if (!(status.value?.isOnline ?? false) && | ||||
|               account.value?.profile.lastSeenAt != null) | ||||
|             Flexible( | ||||
|   | ||||
| @@ -14,6 +14,7 @@ import 'package:island/models/embed.dart'; | ||||
| import 'package:island/pods/call.dart'; | ||||
| import 'package:island/pods/translate.dart'; | ||||
| import 'package:island/screens/chat/room.dart'; | ||||
| import 'package:island/utils/mapping.dart'; | ||||
| import 'package:island/widgets/account/account_name.dart'; | ||||
| import 'package:island/widgets/account/account_pfc.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| @@ -292,12 +293,11 @@ class MessageItem extends HookConsumerWidget { | ||||
|                             ), | ||||
|                           if (remoteMessage.meta['embeds'] != null) | ||||
|                             ...((remoteMessage.meta['embeds'] as List<dynamic>) | ||||
|                                 .where((embed) => embed['Type'] == 'link') | ||||
|                                 .map( | ||||
|                                   (embed) => SnEmbedLink.fromJson( | ||||
|                                     embed as Map<String, dynamic>, | ||||
|                                   ), | ||||
|                                   (embed) => convertMapKeysToSnakeCase(embed), | ||||
|                                 ) | ||||
|                                 .where((embed) => embed['type'] == 'link') | ||||
|                                 .map((embed) => SnScrappedLink.fromJson(embed)) | ||||
|                                 .map( | ||||
|                                   (link) => LayoutBuilder( | ||||
|                                     builder: (context, constraints) { | ||||
|   | ||||
| @@ -36,7 +36,8 @@ Future<SnCheckInResult?> checkInResultToday(Ref ref) async { | ||||
|  | ||||
| class CheckInWidget extends HookConsumerWidget { | ||||
|   final EdgeInsets? margin; | ||||
|   const CheckInWidget({super.key, this.margin}); | ||||
|   final VoidCallback? onChecked; | ||||
|   const CheckInWidget({super.key, this.margin, this.onChecked}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -52,6 +53,7 @@ class CheckInWidget extends HookConsumerWidget { | ||||
|         ref.invalidate(checkInResultTodayProvider); | ||||
|         final userNotifier = ref.read(userInfoProvider.notifier); | ||||
|         userNotifier.fetchUser(); | ||||
|         onChecked?.call(); | ||||
|       } catch (err) { | ||||
|         if (err is DioException) { | ||||
|           if (err.response?.statusCode == 423 && context.mounted) { | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| import 'dart:math' as math; | ||||
| import 'dart:ui'; | ||||
|  | ||||
| import 'package:cached_network_image/cached_network_image.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| @@ -103,6 +102,7 @@ class CloudVideoWidget extends HookConsumerWidget { | ||||
|                 Symbols.play_arrow, | ||||
|                 fill: 1, | ||||
|                 size: 32, | ||||
|                 color: Colors.white, | ||||
|                 shadows: [ | ||||
|                   BoxShadow( | ||||
|                     color: Colors.black54, | ||||
| @@ -114,6 +114,26 @@ class CloudVideoWidget extends HookConsumerWidget { | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|           Positioned( | ||||
|             bottom: 0, | ||||
|             left: 0, | ||||
|             right: 0, | ||||
|             child: IgnorePointer( | ||||
|               child: Container( | ||||
|                 height: 100, | ||||
|                 decoration: BoxDecoration( | ||||
|                   gradient: LinearGradient( | ||||
|                     begin: Alignment.bottomCenter, | ||||
|                     end: Alignment.topCenter, | ||||
|                     colors: [ | ||||
|                       Theme.of(context).colorScheme.surface.withOpacity(0.85), | ||||
|                       Colors.transparent, | ||||
|                     ], | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|           Positioned( | ||||
|             bottom: 0, | ||||
|             left: 0, | ||||
| @@ -133,6 +153,7 @@ class CloudVideoWidget extends HookConsumerWidget { | ||||
|                                   .toInt(), | ||||
|                         ).formatDuration(), | ||||
|                         style: TextStyle( | ||||
|                           color: Colors.white, | ||||
|                           shadows: [ | ||||
|                             BoxShadow( | ||||
|                               color: Colors.black54, | ||||
| @@ -147,6 +168,7 @@ class CloudVideoWidget extends HookConsumerWidget { | ||||
|                       Text( | ||||
|                         '${int.parse(item.fileMeta?['bit_rate'] as String) ~/ 1000} Kbps', | ||||
|                         style: TextStyle( | ||||
|                           color: Colors.white, | ||||
|                           shadows: [ | ||||
|                             BoxShadow( | ||||
|                               color: Colors.black54, | ||||
| @@ -161,7 +183,10 @@ class CloudVideoWidget extends HookConsumerWidget { | ||||
|                 ), | ||||
|                 Text( | ||||
|                   item.name, | ||||
|                   maxLines: 1, | ||||
|                   overflow: TextOverflow.ellipsis, | ||||
|                   style: TextStyle( | ||||
|                     color: Colors.white, | ||||
|                     fontWeight: FontWeight.bold, | ||||
|                     shadows: [ | ||||
|                       BoxShadow( | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:url_launcher/url_launcher.dart'; | ||||
|  | ||||
| class EmbedLinkWidget extends StatelessWidget { | ||||
|   final SnEmbedLink link; | ||||
|   final SnScrappedLink link; | ||||
|   final double? maxWidth; | ||||
|   final EdgeInsetsGeometry? margin; | ||||
|  | ||||
| @@ -116,7 +116,8 @@ class EmbedLinkWidget extends StatelessWidget { | ||||
|                     ], | ||||
|  | ||||
|                     // Description | ||||
|                     if (link.description != null && link.description!.isNotEmpty) ...[ | ||||
|                     if (link.description != null && | ||||
|                         link.description!.isNotEmpty) ...[ | ||||
|                       Text( | ||||
|                         link.description!, | ||||
|                         style: theme.textTheme.bodyMedium?.copyWith( | ||||
| @@ -191,7 +192,7 @@ class EmbedLinkWidget extends StatelessWidget { | ||||
|     try { | ||||
|       final now = DateTime.now(); | ||||
|       final difference = now.difference(date); | ||||
|        | ||||
|  | ||||
|       if (difference.inDays == 0) { | ||||
|         return 'Today'; | ||||
|       } else if (difference.inDays == 1) { | ||||
|   | ||||
							
								
								
									
										236
									
								
								lib/widgets/poll/poll_feedback.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								lib/widgets/poll/poll_feedback.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,236 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/poll.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/services/time.dart'; | ||||
| import 'package:island/widgets/content/sheet.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| part 'poll_feedback.g.dart'; | ||||
|  | ||||
| @riverpod | ||||
| class PollFeedbackNotifier extends _$PollFeedbackNotifier | ||||
|     with CursorPagingNotifierMixin<SnPollAnswer> { | ||||
|   static const int _pageSize = 20; | ||||
|  | ||||
|   @override | ||||
|   Future<CursorPagingData<SnPollAnswer>> build(String id) { | ||||
|     // immediately load first page | ||||
|     return fetch(cursor: null); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<CursorPagingData<SnPollAnswer>> fetch({ | ||||
|     required String? cursor, | ||||
|   }) async { | ||||
|     final client = ref.read(apiClientProvider); | ||||
|     final offset = cursor == null ? 0 : int.parse(cursor); | ||||
|  | ||||
|     final queryParams = {'offset': offset, 'take': _pageSize}; | ||||
|  | ||||
|     final response = await client.get( | ||||
|       '/sphere/polls/$id/feedback', | ||||
|       queryParameters: queryParams, | ||||
|     ); | ||||
|     final total = int.parse(response.headers.value('X-Total') ?? '0'); | ||||
|     final List<dynamic> data = response.data; | ||||
|     final items = data.map((json) => SnPollAnswer.fromJson(json)).toList(); | ||||
|  | ||||
|     final hasMore = offset + items.length < total; | ||||
|     final nextCursor = hasMore ? (offset + items.length).toString() : null; | ||||
|  | ||||
|     return CursorPagingData( | ||||
|       items: items, | ||||
|       hasMore: hasMore, | ||||
|       nextCursor: nextCursor, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class PollFeedbackSheet extends HookConsumerWidget { | ||||
|   final String pollId; | ||||
|   final String? title; | ||||
|   final SnPoll poll; | ||||
|   final Map<String, dynamic>? stats; // stats object similar to PollSubmit | ||||
|   const PollFeedbackSheet({ | ||||
|     super.key, | ||||
|     required this.pollId, | ||||
|     required this.poll, | ||||
|     this.title, | ||||
|     this.stats, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     return SheetScaffold( | ||||
|       titleText: title ?? 'Poll feedback', | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|         children: [ | ||||
|           _PollHeader(poll: poll, stats: stats), | ||||
|           const Divider(height: 1), | ||||
|           Expanded( | ||||
|             child: PagingHelperView( | ||||
|               provider: pollFeedbackNotifierProvider(pollId), | ||||
|               futureRefreshable: pollFeedbackNotifierProvider(pollId).future, | ||||
|               notifierRefreshable: | ||||
|                   pollFeedbackNotifierProvider(pollId).notifier, | ||||
|               contentBuilder: | ||||
|                   (data, widgetCount, endItemView) => ListView.separated( | ||||
|                     padding: const EdgeInsets.symmetric(vertical: 4), | ||||
|                     itemCount: widgetCount, | ||||
|                     itemBuilder: (context, index) { | ||||
|                       if (index == widgetCount - 1) { | ||||
|                         // Provided by PagingHelperView to indicate end/loading | ||||
|                         return endItemView; | ||||
|                       } | ||||
|                       final answer = data.items[index]; | ||||
|                       return _PollAnswerTile(answer: answer, poll: poll); | ||||
|                     }, | ||||
|                     separatorBuilder: | ||||
|                         (context, index) => | ||||
|                             const Divider(height: 1).padding(vertical: 4), | ||||
|                   ), | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _PollHeader extends StatelessWidget { | ||||
|   const _PollHeader({required this.poll, this.stats}); | ||||
|   final SnPoll poll; | ||||
|   final Map<String, dynamic>? stats; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final theme = Theme.of(context); | ||||
|  | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: [ | ||||
|         if (poll.title != null) | ||||
|           Text(poll.title!, style: theme.textTheme.titleLarge), | ||||
|         if (poll.description != null) | ||||
|           Padding( | ||||
|             padding: const EdgeInsets.only(top: 2), | ||||
|             child: Text( | ||||
|               poll.description!, | ||||
|               style: theme.textTheme.bodyMedium?.copyWith( | ||||
|                 color: theme.textTheme.bodyMedium?.color?.withOpacity(0.7), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|       ], | ||||
|     ).padding(horizontal: 20, vertical: 16); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _PollAnswerTile extends StatelessWidget { | ||||
|   final SnPollAnswer answer; | ||||
|   final SnPoll poll; | ||||
|   const _PollAnswerTile({required this.answer, required this.poll}); | ||||
|  | ||||
|   String _formatPerQuestionAnswer( | ||||
|     SnPollQuestion q, | ||||
|     Map<String, dynamic> ansMap, | ||||
|   ) { | ||||
|     switch (q.type) { | ||||
|       case SnPollQuestionType.singleChoice: | ||||
|         final val = ansMap[q.id]; | ||||
|         if (val is String) { | ||||
|           final opt = q.options?.firstWhere( | ||||
|             (o) => o.id == val, | ||||
|             orElse: () => SnPollOption(id: val, label: '#$val', order: 0), | ||||
|           ); | ||||
|           return opt?.label ?? '#$val'; | ||||
|         } | ||||
|         return '—'; | ||||
|       case SnPollQuestionType.multipleChoice: | ||||
|         final val = ansMap[q.id]; | ||||
|         if (val is List) { | ||||
|           final ids = val.whereType<String>().toList(); | ||||
|           if (ids.isEmpty) return '—'; | ||||
|           final labels = | ||||
|               ids.map((id) { | ||||
|                 final opt = q.options?.firstWhere( | ||||
|                   (o) => o.id == id, | ||||
|                   orElse: () => SnPollOption(id: id, label: '#$id', order: 0), | ||||
|                 ); | ||||
|                 return opt?.label ?? '#$id'; | ||||
|               }).toList(); | ||||
|           return labels.join(', '); | ||||
|         } | ||||
|         return '—'; | ||||
|       case SnPollQuestionType.yesNo: | ||||
|         final val = ansMap[q.id]; | ||||
|         if (val is bool) { | ||||
|           return val ? 'Yes' : 'No'; | ||||
|         } | ||||
|         return '—'; | ||||
|       case SnPollQuestionType.rating: | ||||
|         final val = ansMap[q.id]; | ||||
|         if (val is int) return val.toString(); | ||||
|         if (val is num) return val.toString(); | ||||
|         return '—'; | ||||
|       case SnPollQuestionType.freeText: | ||||
|         final val = ansMap[q.id]; | ||||
|         if (val is String && val.trim().isNotEmpty) return val; | ||||
|         return '—'; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     // Submit date/time (title) | ||||
|     final submitText = answer.createdAt.formatSystem(); | ||||
|  | ||||
|     // Compose content from poll questions if provided, otherwise fallback to joined key-values | ||||
|     String content; | ||||
|     if (poll.questions.isNotEmpty) { | ||||
|       final questions = [...poll.questions] | ||||
|         ..sort((a, b) => a.order.compareTo(b.order)); | ||||
|       final buffer = StringBuffer(); | ||||
|       for (final q in questions) { | ||||
|         final formatted = _formatPerQuestionAnswer(q, answer.answer); | ||||
|         buffer.writeln('${q.title}: $formatted'); | ||||
|       } | ||||
|       content = buffer.toString().trimRight(); | ||||
|     } else { | ||||
|       // Fallback formatting without poll context. We still want to show the question title | ||||
|       // instead of the raw question id key if we can derive it from the answer map itself. | ||||
|       // Since we don't have poll metadata here, we cannot resolve the title; therefore we | ||||
|       // will show only values line-by-line without exposing the raw id. | ||||
|       if (answer.answer.isEmpty) { | ||||
|         content = '—'; | ||||
|       } else { | ||||
|         final parts = <String>[]; | ||||
|         answer.answer.forEach((key, value) { | ||||
|           var question = poll.questions.firstWhere((q) => q.id == key); | ||||
|           if (value is List) { | ||||
|             parts.add('${question.title}: ${value.join(', ')}'); | ||||
|           } else { | ||||
|             parts.add('${question.title}: $value'); | ||||
|           } | ||||
|         }); | ||||
|         content = parts.join('\n'); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return ListTile( | ||||
|       contentPadding: const EdgeInsets.symmetric(horizontal: 20), | ||||
|       isThreeLine: true, | ||||
|       leading: const CircleAvatar( | ||||
|         radius: 16, | ||||
|         child: Icon(Icons.how_to_vote, size: 16), | ||||
|       ), | ||||
|       title: Text(submitText), | ||||
|       subtitle: Text(content), | ||||
|       trailing: null, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										180
									
								
								lib/widgets/poll/poll_feedback.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								lib/widgets/poll/poll_feedback.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,180 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'poll_feedback.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$pollFeedbackNotifierHash() => | ||||
|     r'1bf3925b5b751cfd1a9abafb75274f1e95e7f27e'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
|   _SystemHash._(); | ||||
|  | ||||
|   static int combine(int hash, int value) { | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + value); | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); | ||||
|     return hash ^ (hash >> 6); | ||||
|   } | ||||
|  | ||||
|   static int finish(int hash) { | ||||
|     // ignore: parameter_assignments | ||||
|     hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); | ||||
|     // ignore: parameter_assignments | ||||
|     hash = hash ^ (hash >> 11); | ||||
|     return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); | ||||
|   } | ||||
| } | ||||
|  | ||||
| abstract class _$PollFeedbackNotifier | ||||
|     extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPollAnswer>> { | ||||
|   late final String id; | ||||
|  | ||||
|   FutureOr<CursorPagingData<SnPollAnswer>> build(String id); | ||||
| } | ||||
|  | ||||
| /// See also [PollFeedbackNotifier]. | ||||
| @ProviderFor(PollFeedbackNotifier) | ||||
| const pollFeedbackNotifierProvider = PollFeedbackNotifierFamily(); | ||||
|  | ||||
| /// See also [PollFeedbackNotifier]. | ||||
| class PollFeedbackNotifierFamily | ||||
|     extends Family<AsyncValue<CursorPagingData<SnPollAnswer>>> { | ||||
|   /// See also [PollFeedbackNotifier]. | ||||
|   const PollFeedbackNotifierFamily(); | ||||
|  | ||||
|   /// See also [PollFeedbackNotifier]. | ||||
|   PollFeedbackNotifierProvider call(String id) { | ||||
|     return PollFeedbackNotifierProvider(id); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   PollFeedbackNotifierProvider getProviderOverride( | ||||
|     covariant PollFeedbackNotifierProvider provider, | ||||
|   ) { | ||||
|     return call(provider.id); | ||||
|   } | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _dependencies = null; | ||||
|  | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get dependencies => _dependencies; | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null; | ||||
|  | ||||
|   @override | ||||
|   Iterable<ProviderOrFamily>? get allTransitiveDependencies => | ||||
|       _allTransitiveDependencies; | ||||
|  | ||||
|   @override | ||||
|   String? get name => r'pollFeedbackNotifierProvider'; | ||||
| } | ||||
|  | ||||
| /// See also [PollFeedbackNotifier]. | ||||
| class PollFeedbackNotifierProvider | ||||
|     extends | ||||
|         AutoDisposeAsyncNotifierProviderImpl< | ||||
|           PollFeedbackNotifier, | ||||
|           CursorPagingData<SnPollAnswer> | ||||
|         > { | ||||
|   /// See also [PollFeedbackNotifier]. | ||||
|   PollFeedbackNotifierProvider(String id) | ||||
|     : this._internal( | ||||
|         () => PollFeedbackNotifier()..id = id, | ||||
|         from: pollFeedbackNotifierProvider, | ||||
|         name: r'pollFeedbackNotifierProvider', | ||||
|         debugGetCreateSourceHash: | ||||
|             const bool.fromEnvironment('dart.vm.product') | ||||
|                 ? null | ||||
|                 : _$pollFeedbackNotifierHash, | ||||
|         dependencies: PollFeedbackNotifierFamily._dependencies, | ||||
|         allTransitiveDependencies: | ||||
|             PollFeedbackNotifierFamily._allTransitiveDependencies, | ||||
|         id: id, | ||||
|       ); | ||||
|  | ||||
|   PollFeedbackNotifierProvider._internal( | ||||
|     super._createNotifier, { | ||||
|     required super.name, | ||||
|     required super.dependencies, | ||||
|     required super.allTransitiveDependencies, | ||||
|     required super.debugGetCreateSourceHash, | ||||
|     required super.from, | ||||
|     required this.id, | ||||
|   }) : super.internal(); | ||||
|  | ||||
|   final String id; | ||||
|  | ||||
|   @override | ||||
|   FutureOr<CursorPagingData<SnPollAnswer>> runNotifierBuild( | ||||
|     covariant PollFeedbackNotifier notifier, | ||||
|   ) { | ||||
|     return notifier.build(id); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Override overrideWith(PollFeedbackNotifier Function() create) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: PollFeedbackNotifierProvider._internal( | ||||
|         () => create()..id = id, | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         id: id, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeAsyncNotifierProviderElement< | ||||
|     PollFeedbackNotifier, | ||||
|     CursorPagingData<SnPollAnswer> | ||||
|   > | ||||
|   createElement() { | ||||
|     return _PollFeedbackNotifierProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is PollFeedbackNotifierProvider && other.id == id; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||
|     hash = _SystemHash.combine(hash, id.hashCode); | ||||
|  | ||||
|     return _SystemHash.finish(hash); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| mixin PollFeedbackNotifierRef | ||||
|     on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnPollAnswer>> { | ||||
|   /// The parameter `id` of this provider. | ||||
|   String get id; | ||||
| } | ||||
|  | ||||
| class _PollFeedbackNotifierProviderElement | ||||
|     extends | ||||
|         AutoDisposeAsyncNotifierProviderElement< | ||||
|           PollFeedbackNotifier, | ||||
|           CursorPagingData<SnPollAnswer> | ||||
|         > | ||||
|     with PollFeedbackNotifierRef { | ||||
|   _PollFeedbackNotifierProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   String get id => (origin as PollFeedbackNotifierProvider).id; | ||||
| } | ||||
|  | ||||
| // ignore_for_file: type=lint | ||||
| // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package | ||||
| @@ -3,21 +3,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||||
| import 'package:island/models/poll.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
|  | ||||
| /// A poll answering widget that shows one question at a time and collects answers. | ||||
| /// | ||||
| /// Usage: | ||||
| /// PollSubmit( | ||||
| ///   poll: poll, | ||||
| ///   onSubmit: (answers) { | ||||
| ///     // answers is Map<String, dynamic>: questionId -> answer | ||||
| ///     // answer types by question: | ||||
| ///     // - singleChoice: String optionId | ||||
| ///     // - multipleChoice: List<String> optionIds | ||||
| ///     // - yesNo: bool | ||||
| ///     // - rating: int (1..5) | ||||
| ///     // - freeText: String | ||||
| ///   }, | ||||
| /// ) | ||||
| class PollSubmit extends ConsumerStatefulWidget { | ||||
|   const PollSubmit({ | ||||
|     super.key, | ||||
|   | ||||
| @@ -189,8 +189,8 @@ class ComposePollSheet extends HookConsumerWidget { | ||||
|   Widget? _buildPollSubtitle(SnPoll poll) { | ||||
|     try { | ||||
|       final SnPoll dyn = poll; | ||||
|       final List<SnPollQuestion>? options = dyn.questions; | ||||
|       if (options == null || options.isEmpty) return null; | ||||
|       final List<SnPollQuestion> options = dyn.questions; | ||||
|       if (options.isEmpty) return null; | ||||
|       final preview = options.take(3).map((e) => e.title).join(' · '); | ||||
|       if (preview.trim().isEmpty) return null; | ||||
|       return Text(preview); | ||||
|   | ||||
| @@ -99,8 +99,6 @@ class ChipTagInputField extends StatelessWidget { | ||||
| } | ||||
|  | ||||
| class ComposeSettingsSheet extends HookWidget { | ||||
|   final TextEditingController titleController; | ||||
|   final TextEditingController descriptionController; | ||||
|   final ValueNotifier<int> visibility; | ||||
|   final VoidCallback? onVisibilityChanged; | ||||
|   final StringTagController tagsController; | ||||
| @@ -108,8 +106,6 @@ class ComposeSettingsSheet extends HookWidget { | ||||
|  | ||||
|   const ComposeSettingsSheet({ | ||||
|     super.key, | ||||
|     required this.titleController, | ||||
|     required this.descriptionController, | ||||
|     required this.visibility, | ||||
|     this.onVisibilityChanged, | ||||
|     required this.tagsController, | ||||
| @@ -216,39 +212,6 @@ class ComposeSettingsSheet extends HookWidget { | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           spacing: 16, | ||||
|           children: [ | ||||
|             // Title field | ||||
|             TextField( | ||||
|               controller: titleController, | ||||
|               decoration: InputDecoration( | ||||
|                 labelText: 'postTitle'.tr(), | ||||
|                 hintText: 'postTitle'.tr(), | ||||
|                 border: OutlineInputBorder( | ||||
|                   borderRadius: BorderRadius.circular(12), | ||||
|                 ), | ||||
|                 contentPadding: const EdgeInsets.all(16), | ||||
|               ), | ||||
|               style: theme.textTheme.titleMedium, | ||||
|               onTapOutside: | ||||
|                   (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|             ), | ||||
|  | ||||
|             // Description field | ||||
|             TextField( | ||||
|               controller: descriptionController, | ||||
|               decoration: InputDecoration( | ||||
|                 labelText: 'postDescription'.tr(), | ||||
|                 hintText: 'postDescription'.tr(), | ||||
|                 border: OutlineInputBorder( | ||||
|                   borderRadius: BorderRadius.circular(12), | ||||
|                 ), | ||||
|                 contentPadding: const EdgeInsets.all(16), | ||||
|               ), | ||||
|               style: theme.textTheme.bodyMedium, | ||||
|               maxLines: 3, | ||||
|               onTapOutside: | ||||
|                   (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|             ), | ||||
|  | ||||
|             // Tags field | ||||
|             TextFieldTags( | ||||
|               textfieldTagsController: tagsController, | ||||
|   | ||||
| @@ -689,7 +689,7 @@ class ComposeLogic { | ||||
|   } | ||||
|  | ||||
|   static void handleKeyPress( | ||||
|     RawKeyEvent event, | ||||
|     KeyEvent event, | ||||
|     ComposeState state, | ||||
|     WidgetRef ref, | ||||
|     BuildContext context, { | ||||
| @@ -701,7 +701,9 @@ class ComposeLogic { | ||||
|  | ||||
|     final isPaste = event.logicalKey == LogicalKeyboardKey.keyV; | ||||
|     final isSave = event.logicalKey == LogicalKeyboardKey.keyS; | ||||
|     final isModifierPressed = event.isMetaPressed || event.isControlPressed; | ||||
|     final isModifierPressed = | ||||
|         HardwareKeyboard.instance.isMetaPressed || | ||||
|         HardwareKeyboard.instance.isControlPressed; | ||||
|     final isSubmit = event.logicalKey == LogicalKeyboardKey.enter; | ||||
|  | ||||
|     if (isPaste && isModifierPressed) { | ||||
|   | ||||
| @@ -63,6 +63,7 @@ class ComposeToolbar extends HookConsumerWidget { | ||||
|  | ||||
|     return Material( | ||||
|       elevation: 4, | ||||
|       color: Theme.of(context).colorScheme.surfaceContainerLow, | ||||
|       child: Center( | ||||
|         child: ConstrainedBox( | ||||
|           constraints: const BoxConstraints(maxWidth: 560), | ||||
|   | ||||
| @@ -535,7 +535,7 @@ class PostItem extends HookConsumerWidget { | ||||
|               right: renderingPadding.horizontal, | ||||
|             ), | ||||
|           ), | ||||
|         if (item.attachments.isNotEmpty) | ||||
|         if (item.attachments.isNotEmpty && item.type != 1) | ||||
|           CloudFileList( | ||||
|             files: item.attachments, | ||||
|             padding: EdgeInsets.symmetric( | ||||
| @@ -547,7 +547,9 @@ class PostItem extends HookConsumerWidget { | ||||
|           ...((item.meta!['embeds'] as List<dynamic>).map( | ||||
|             (embedData) => switch (embedData['type']) { | ||||
|               'link' => EmbedLinkWidget( | ||||
|                 link: SnEmbedLink.fromJson(embedData as Map<String, dynamic>), | ||||
|                 link: SnScrappedLink.fromJson( | ||||
|                   embedData as Map<String, dynamic>, | ||||
|                 ), | ||||
|                 maxWidth: math.min( | ||||
|                   MediaQuery.of(context).size.width, | ||||
|                   kWideScreenWidth, | ||||
| @@ -770,7 +772,7 @@ class PostReplyPreview extends HookConsumerWidget { | ||||
|     final posts = useState<List<SnPost>>([]); | ||||
|     final loading = useState(false); | ||||
|  | ||||
|     Future<void> fetchMoreReplies({int pageSize = 1}) async { | ||||
|     Future<void> fetchMoreReplies({int pageSize = 3}) async { | ||||
|       final client = ref.read(apiClientProvider); | ||||
|       loading.value = true; | ||||
|  | ||||
| @@ -877,38 +879,40 @@ class PostReplyPreview extends HookConsumerWidget { | ||||
|                   ), | ||||
|               ], | ||||
|             ) | ||||
|             : featuredReply!.when( | ||||
|             : (featuredReply!).map( | ||||
|               data: | ||||
|                   (value) => Row( | ||||
|                   (data) => Row( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                     spacing: 8, | ||||
|                     children: [ | ||||
|                       ProfilePictureWidget( | ||||
|                         file: value?.publisher.picture, | ||||
|                         file: data.value?.publisher.picture, | ||||
|                         radius: 12, | ||||
|                       ).padding(top: 4), | ||||
|                       if (value?.content?.isNotEmpty ?? false) | ||||
|                       if (data.value?.content?.isNotEmpty ?? false) | ||||
|                         Expanded( | ||||
|                           child: MarkdownTextContent(content: value!.content!), | ||||
|                           child: MarkdownTextContent( | ||||
|                             content: data.value!.content!, | ||||
|                           ), | ||||
|                         ) | ||||
|                       else | ||||
|                         Expanded( | ||||
|                           child: Text( | ||||
|                             'postHasAttachments', | ||||
|                           ).plural(value?.attachments.length ?? 0), | ||||
|                           ).plural(data.value?.attachments.length ?? 0), | ||||
|                         ), | ||||
|                     ], | ||||
|                   ), | ||||
|               error: | ||||
|                   (error, _) => Row( | ||||
|                   (e) => Row( | ||||
|                     spacing: 8, | ||||
|                     children: [ | ||||
|                       const Icon(Symbols.close, size: 18), | ||||
|                       Text(error.toString()), | ||||
|                       Text(e.error.toString()), | ||||
|                     ], | ||||
|                   ), | ||||
|               loading: | ||||
|                   () => Row( | ||||
|                   (_) => Row( | ||||
|                     spacing: 8, | ||||
|                     children: [ | ||||
|                       SizedBox( | ||||
| @@ -939,7 +943,6 @@ class PostReplyPreview extends HookConsumerWidget { | ||||
|                 children: [ | ||||
|                   Text('repliesCount') | ||||
|                       .plural(parent.repliesCount) | ||||
|                       .tr() | ||||
|                       .fontSize(15) | ||||
|                       .bold() | ||||
|                       .padding(horizontal: 5), | ||||
|   | ||||
| @@ -15,7 +15,7 @@ class PostListNotifier extends _$PostListNotifier | ||||
|   static const int _pageSize = 20; | ||||
|  | ||||
|   @override | ||||
|   Future<CursorPagingData<SnPost>> build(String? pubName) { | ||||
|   Future<CursorPagingData<SnPost>> build(String? pubName, int? type) { | ||||
|     return fetch(cursor: null); | ||||
|   } | ||||
|  | ||||
| @@ -28,6 +28,7 @@ class PostListNotifier extends _$PostListNotifier | ||||
|       'offset': offset, | ||||
|       'take': _pageSize, | ||||
|       if (pubName != null) 'pub': pubName, | ||||
|       if (type != null) 'type': type, | ||||
|     }; | ||||
|  | ||||
|     final response = await client.get( | ||||
| @@ -60,6 +61,7 @@ enum PostItemType { | ||||
|  | ||||
| class SliverPostList extends HookConsumerWidget { | ||||
|   final String? pubName; | ||||
|   final int? type; | ||||
|   final PostItemType itemType; | ||||
|   final Color? backgroundColor; | ||||
|   final EdgeInsets? padding; | ||||
| @@ -70,6 +72,7 @@ class SliverPostList extends HookConsumerWidget { | ||||
|   const SliverPostList({ | ||||
|     super.key, | ||||
|     this.pubName, | ||||
|     this.type, | ||||
|     this.itemType = PostItemType.regular, | ||||
|     this.backgroundColor, | ||||
|     this.padding, | ||||
| @@ -81,9 +84,9 @@ class SliverPostList extends HookConsumerWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     return PagingHelperSliverView( | ||||
|       provider: postListNotifierProvider(pubName), | ||||
|       futureRefreshable: postListNotifierProvider(pubName).future, | ||||
|       notifierRefreshable: postListNotifierProvider(pubName).notifier, | ||||
|       provider: postListNotifierProvider(pubName, type), | ||||
|       futureRefreshable: postListNotifierProvider(pubName, type).future, | ||||
|       notifierRefreshable: postListNotifierProvider(pubName, type).notifier, | ||||
|       contentBuilder: | ||||
|           (data, widgetCount, endItemView) => SliverList.builder( | ||||
|             itemCount: widgetCount, | ||||
|   | ||||
| @@ -6,7 +6,7 @@ part of 'post_list.dart'; | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$postListNotifierHash() => r'2e4fb36123d3f97ac1edf9945043251d4eb519a2'; | ||||
| String _$postListNotifierHash() => r'78222d62957f85713d17aecd95af0305b764e86c'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
| @@ -32,8 +32,9 @@ class _SystemHash { | ||||
| abstract class _$PostListNotifier | ||||
|     extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPost>> { | ||||
|   late final String? pubName; | ||||
|   late final int? type; | ||||
|  | ||||
|   FutureOr<CursorPagingData<SnPost>> build(String? pubName); | ||||
|   FutureOr<CursorPagingData<SnPost>> build(String? pubName, int? type); | ||||
| } | ||||
|  | ||||
| /// See also [PostListNotifier]. | ||||
| @@ -47,15 +48,15 @@ class PostListNotifierFamily | ||||
|   const PostListNotifierFamily(); | ||||
|  | ||||
|   /// See also [PostListNotifier]. | ||||
|   PostListNotifierProvider call(String? pubName) { | ||||
|     return PostListNotifierProvider(pubName); | ||||
|   PostListNotifierProvider call(String? pubName, int? type) { | ||||
|     return PostListNotifierProvider(pubName, type); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   PostListNotifierProvider getProviderOverride( | ||||
|     covariant PostListNotifierProvider provider, | ||||
|   ) { | ||||
|     return call(provider.pubName); | ||||
|     return call(provider.pubName, provider.type); | ||||
|   } | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _dependencies = null; | ||||
| @@ -81,9 +82,12 @@ class PostListNotifierProvider | ||||
|           CursorPagingData<SnPost> | ||||
|         > { | ||||
|   /// See also [PostListNotifier]. | ||||
|   PostListNotifierProvider(String? pubName) | ||||
|   PostListNotifierProvider(String? pubName, int? type) | ||||
|     : this._internal( | ||||
|         () => PostListNotifier()..pubName = pubName, | ||||
|         () => | ||||
|             PostListNotifier() | ||||
|               ..pubName = pubName | ||||
|               ..type = type, | ||||
|         from: postListNotifierProvider, | ||||
|         name: r'postListNotifierProvider', | ||||
|         debugGetCreateSourceHash: | ||||
| @@ -94,6 +98,7 @@ class PostListNotifierProvider | ||||
|         allTransitiveDependencies: | ||||
|             PostListNotifierFamily._allTransitiveDependencies, | ||||
|         pubName: pubName, | ||||
|         type: type, | ||||
|       ); | ||||
|  | ||||
|   PostListNotifierProvider._internal( | ||||
| @@ -104,15 +109,17 @@ class PostListNotifierProvider | ||||
|     required super.debugGetCreateSourceHash, | ||||
|     required super.from, | ||||
|     required this.pubName, | ||||
|     required this.type, | ||||
|   }) : super.internal(); | ||||
|  | ||||
|   final String? pubName; | ||||
|   final int? type; | ||||
|  | ||||
|   @override | ||||
|   FutureOr<CursorPagingData<SnPost>> runNotifierBuild( | ||||
|     covariant PostListNotifier notifier, | ||||
|   ) { | ||||
|     return notifier.build(pubName); | ||||
|     return notifier.build(pubName, type); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -120,13 +127,17 @@ class PostListNotifierProvider | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: PostListNotifierProvider._internal( | ||||
|         () => create()..pubName = pubName, | ||||
|         () => | ||||
|             create() | ||||
|               ..pubName = pubName | ||||
|               ..type = type, | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         pubName: pubName, | ||||
|         type: type, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| @@ -142,13 +153,16 @@ class PostListNotifierProvider | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is PostListNotifierProvider && other.pubName == pubName; | ||||
|     return other is PostListNotifierProvider && | ||||
|         other.pubName == pubName && | ||||
|         other.type == type; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||
|     hash = _SystemHash.combine(hash, pubName.hashCode); | ||||
|     hash = _SystemHash.combine(hash, type.hashCode); | ||||
|  | ||||
|     return _SystemHash.finish(hash); | ||||
|   } | ||||
| @@ -160,6 +174,9 @@ mixin PostListNotifierRef | ||||
|     on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnPost>> { | ||||
|   /// The parameter `pubName` of this provider. | ||||
|   String? get pubName; | ||||
|  | ||||
|   /// The parameter `type` of this provider. | ||||
|   int? get type; | ||||
| } | ||||
|  | ||||
| class _PostListNotifierProviderElement | ||||
| @@ -173,6 +190,8 @@ class _PostListNotifierProviderElement | ||||
|  | ||||
|   @override | ||||
|   String? get pubName => (origin as PostListNotifierProvider).pubName; | ||||
|   @override | ||||
|   int? get type => (origin as PostListNotifierProvider).type; | ||||
| } | ||||
|  | ||||
| // ignore_for_file: type=lint | ||||
|   | ||||
							
								
								
									
										307
									
								
								lib/widgets/stickers/picker.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										307
									
								
								lib/widgets/stickers/picker.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,307 @@ | ||||
| import 'dart:math' as math; | ||||
|  | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/sticker.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/widgets/content/cloud_files.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:flutter_popup_card/flutter_popup_card.dart'; | ||||
|  | ||||
| part 'picker.g.dart'; | ||||
|  | ||||
| /// Fetch user-added sticker packs (with stickers) from API: | ||||
| /// GET /sphere/stickers/me | ||||
| @riverpod | ||||
| Future<List<SnStickerPack>> myStickerPacks(Ref ref) async { | ||||
|   final api = ref.watch(apiClientProvider); | ||||
|   final resp = await api.get('/sphere/stickers/me'); | ||||
|   final data = resp.data; | ||||
|   if (data is List) { | ||||
|     return data | ||||
|         .map((e) => SnStickerPack.fromJson(e as Map<String, dynamic>)) | ||||
|         .toList(); | ||||
|   } | ||||
|   return const <SnStickerPack>[]; | ||||
| } | ||||
|  | ||||
| /// Sticker Picker popover dialog | ||||
| /// - Displays user-owned sticker packs as tabs (chips) | ||||
| /// - Shows grid of stickers in selected pack | ||||
| /// - On tap, returns placeholder string :{prefix}{slug}: via onPick callback | ||||
| class StickerPicker extends HookConsumerWidget { | ||||
|   final void Function(String placeholder) onPick; | ||||
|  | ||||
|   const StickerPicker({super.key, required this.onPick}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final packsAsync = ref.watch(myStickerPacksProvider); | ||||
|  | ||||
|     return PopupCard( | ||||
|       elevation: 8, | ||||
|       shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)), | ||||
|       child: ConstrainedBox( | ||||
|         constraints: const BoxConstraints(maxWidth: 520, maxHeight: 520), | ||||
|         child: packsAsync.when( | ||||
|           data: (packs) { | ||||
|             if (packs.isEmpty) { | ||||
|               return _EmptyState( | ||||
|                 onRefresh: () async { | ||||
|                   ref.invalidate(myStickerPacksProvider); | ||||
|                 }, | ||||
|               ); | ||||
|             } | ||||
|  | ||||
|             // Maintain selected index locally with a ValueNotifier to avoid hooks dependency | ||||
|             return _PackSwitcher( | ||||
|               packs: packs, | ||||
|               onPick: (pack, sticker) { | ||||
|                 final placeholder = ':${pack.prefix}${sticker.slug}:'; | ||||
|                 HapticFeedback.selectionClick(); | ||||
|                 onPick(placeholder); | ||||
|                 if (Navigator.of(context).canPop()) { | ||||
|                   Navigator.of(context).pop(); | ||||
|                 } | ||||
|               }, | ||||
|               onRefresh: () async { | ||||
|                 ref.invalidate(myStickerPacksProvider); | ||||
|               }, | ||||
|             ); | ||||
|           }, | ||||
|           loading: | ||||
|               () => const SizedBox( | ||||
|                 width: 320, | ||||
|                 height: 320, | ||||
|                 child: Center(child: CircularProgressIndicator()), | ||||
|               ), | ||||
|           error: | ||||
|               (err, _) => SizedBox( | ||||
|                 width: 360, | ||||
|                 height: 200, | ||||
|                 child: Column( | ||||
|                   mainAxisAlignment: MainAxisAlignment.center, | ||||
|                   children: [ | ||||
|                     const Icon(Symbols.error, size: 28), | ||||
|                     const Gap(8), | ||||
|                     Text('Error: $err', textAlign: TextAlign.center), | ||||
|                     const Gap(12), | ||||
|                     FilledButton.icon( | ||||
|                       onPressed: () => ref.invalidate(myStickerPacksProvider), | ||||
|                       icon: const Icon(Symbols.refresh), | ||||
|                       label: Text('retry').tr(), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ).padding(all: 16), | ||||
|               ), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _EmptyState extends StatelessWidget { | ||||
|   final Future<void> Function() onRefresh; | ||||
|   const _EmptyState({required this.onRefresh}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return SizedBox( | ||||
|       width: 360, | ||||
|       height: 220, | ||||
|       child: Column( | ||||
|         mainAxisAlignment: MainAxisAlignment.center, | ||||
|         children: [ | ||||
|           const Icon(Symbols.emoji_symbols, size: 28), | ||||
|           const Gap(8), | ||||
|           Text('noStickerPacks'.tr(), textAlign: TextAlign.center), | ||||
|           const Gap(12), | ||||
|           OutlinedButton.icon( | ||||
|             onPressed: onRefresh, | ||||
|             icon: const Icon(Symbols.refresh), | ||||
|             label: Text('refresh').tr(), | ||||
|           ), | ||||
|         ], | ||||
|       ).padding(all: 16), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _PackSwitcher extends StatefulWidget { | ||||
|   final List<SnStickerPack> packs; | ||||
|   final void Function(SnStickerPack pack, SnSticker sticker) onPick; | ||||
|   final Future<void> Function() onRefresh; | ||||
|  | ||||
|   const _PackSwitcher({ | ||||
|     required this.packs, | ||||
|     required this.onPick, | ||||
|     required this.onRefresh, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   State<_PackSwitcher> createState() => _PackSwitcherState(); | ||||
| } | ||||
|  | ||||
| class _PackSwitcherState extends State<_PackSwitcher> { | ||||
|   int _index = 0; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final packs = widget.packs; | ||||
|     _index = _index.clamp(0, packs.length - 1); | ||||
|  | ||||
|     final selectedPack = packs[_index]; | ||||
|  | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|       children: [ | ||||
|         // Header | ||||
|         Row( | ||||
|           children: [ | ||||
|             const Icon(Symbols.sticky_note_2, size: 20), | ||||
|             const Gap(8), | ||||
|             Text( | ||||
|               'stickers'.tr(), | ||||
|               style: Theme.of(context).textTheme.titleMedium, | ||||
|             ), | ||||
|             const Spacer(), | ||||
|             IconButton( | ||||
|               padding: EdgeInsets.zero, | ||||
|               visualDensity: VisualDensity.compact, | ||||
|               tooltip: 'close'.tr(), | ||||
|               onPressed: () => Navigator.of(context).maybePop(), | ||||
|               icon: const Icon(Symbols.close), | ||||
|             ), | ||||
|           ], | ||||
|         ).padding(horizontal: 12, top: 8), | ||||
|  | ||||
|         // Vertical, scrollable packs rail like common emoji pickers | ||||
|         SizedBox( | ||||
|           height: 48, | ||||
|           child: ListView.separated( | ||||
|             padding: const EdgeInsets.symmetric(horizontal: 8), | ||||
|             scrollDirection: Axis.horizontal, | ||||
|             itemCount: packs.length, | ||||
|             separatorBuilder: (_, _) => const Gap(4), | ||||
|             itemBuilder: (context, i) { | ||||
|               final selected = _index == i; | ||||
|               return Tooltip( | ||||
|                 message: packs[i].name, | ||||
|                 child: FilterChip( | ||||
|                   label: Text(packs[i].name, overflow: TextOverflow.ellipsis), | ||||
|                   selected: selected, | ||||
|                   onSelected: (_) { | ||||
|                     setState(() => _index = i); | ||||
|                     HapticFeedback.selectionClick(); | ||||
|                   }, | ||||
|                 ), | ||||
|               ); | ||||
|             }, | ||||
|           ), | ||||
|         ).padding(bottom: 8), | ||||
|         const Divider(height: 1), | ||||
|  | ||||
|         // Content | ||||
|         Expanded( | ||||
|           child: RefreshIndicator( | ||||
|             onRefresh: widget.onRefresh, | ||||
|             child: _StickersGrid( | ||||
|               pack: selectedPack, | ||||
|               onPick: (sticker) => widget.onPick(selectedPack, sticker), | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _StickersGrid extends StatelessWidget { | ||||
|   final SnStickerPack pack; | ||||
|   final void Function(SnSticker sticker) onPick; | ||||
|  | ||||
|   const _StickersGrid({required this.pack, required this.onPick}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final stickers = pack.stickers; | ||||
|  | ||||
|     if (stickers.isEmpty) { | ||||
|       return Center(child: Text('noStickersInPack'.tr())); | ||||
|     } | ||||
|  | ||||
|     return GridView.builder( | ||||
|       physics: const AlwaysScrollableScrollPhysics(), | ||||
|       padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), | ||||
|       gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( | ||||
|         maxCrossAxisExtent: 96, | ||||
|         mainAxisSpacing: 12, | ||||
|         crossAxisSpacing: 12, | ||||
|       ), | ||||
|       itemCount: stickers.length, | ||||
|       itemBuilder: (context, index) { | ||||
|         final sticker = stickers[index]; | ||||
|         final placeholder = ':${pack.prefix}${sticker.slug}:'; | ||||
|         return Tooltip( | ||||
|           message: placeholder, | ||||
|           child: InkWell( | ||||
|             borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|             onTap: () => onPick(sticker), | ||||
|             child: ClipRRect( | ||||
|               borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|               child: DecoratedBox( | ||||
|                 decoration: BoxDecoration( | ||||
|                   color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                   borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                 ), | ||||
|                 child: AspectRatio( | ||||
|                   aspectRatio: 1, | ||||
|                   child: CloudImageWidget( | ||||
|                     fileId: sticker.imageId, | ||||
|                     fit: BoxFit.contain, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// Helper to show sticker picker as an anchored popover near the trigger. | ||||
| /// Provide the button's BuildContext (typically from the onPressed closure). | ||||
| /// Fallbacks to dialog if overlay cannot be found (e.g., during tests). | ||||
| Future<void> showStickerPickerPopover( | ||||
|   BuildContext context, | ||||
|   Offset offset, { | ||||
|   required void Function(String placeholder) onPick, | ||||
| }) async { | ||||
|   // Use flutter_popup_card to present the anchored popup near trigger. | ||||
|   await showPopupCard<void>( | ||||
|     context: context, | ||||
|     offset: offset, | ||||
|     alignment: Alignment.topLeft, | ||||
|     dimBackground: true, | ||||
|     builder: | ||||
|         (ctx) => SizedBox( | ||||
|           width: math.min(480, MediaQuery.of(context).size.width * 0.9), | ||||
|           height: 480, | ||||
|           child: ProviderScope( | ||||
|             parent: ProviderScope.containerOf(context), | ||||
|             child: StickerPicker( | ||||
|               onPick: (ph) { | ||||
|                 onPick(ph); | ||||
|                 Navigator.of(ctx).maybePop(); | ||||
|               }, | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										32
									
								
								lib/widgets/stickers/picker.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								lib/widgets/stickers/picker.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'picker.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$myStickerPacksHash() => r'1e19832e8ab1cb139ad18aebfa5aebdf4fdea499'; | ||||
|  | ||||
| /// Fetch user-added sticker packs (with stickers) from API: | ||||
| /// GET /sphere/stickers/me | ||||
| /// | ||||
| /// Copied from [myStickerPacks]. | ||||
| @ProviderFor(myStickerPacks) | ||||
| final myStickerPacksProvider = | ||||
|     AutoDisposeFutureProvider<List<SnStickerPack>>.internal( | ||||
|       myStickerPacks, | ||||
|       name: r'myStickerPacksProvider', | ||||
|       debugGetCreateSourceHash: | ||||
|           const bool.fromEnvironment('dart.vm.product') | ||||
|               ? null | ||||
|               : _$myStickerPacksHash, | ||||
|       dependencies: null, | ||||
|       allTransitiveDependencies: null, | ||||
|     ); | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| typedef MyStickerPacksRef = AutoDisposeFutureProviderRef<List<SnStickerPack>>; | ||||
| // ignore_for_file: type=lint | ||||
| // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package | ||||
		Reference in New Issue
	
	Block a user