Compare commits
	
		
			17 Commits
		
	
	
		
			3.1.0+116
			...
			e2dc520012
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e2dc520012 | |||
| cff9c15e31 | |||
| f00135c4bf | |||
| 30b8a6c30f | |||
| b9c4ee31b1 | |||
| 87870af866 | |||
| b83cb0fb0b | |||
| 7fd1fe34e5 | |||
| 1c18330891 | |||
| d320879ad0 | |||
| 950150e119 | |||
| 3c4a9767e1 | |||
| 5df2445f3f | |||
| 56543d7b4c | |||
| 4c6fea1242 | |||
| fff43de9e3 | |||
| b31a915544 | 
| @@ -144,6 +144,7 @@ | |||||||
|     "other": "{} attachments" |     "other": "{} attachments" | ||||||
|   }, |   }, | ||||||
|   "edited": "Edited", |   "edited": "Edited", | ||||||
|  |   "editedAt": "Edited at {}", | ||||||
|   "addVideo": "Add video", |   "addVideo": "Add video", | ||||||
|   "addPhoto": "Add photo", |   "addPhoto": "Add photo", | ||||||
|   "addAudio": "Add audio", |   "addAudio": "Add audio", | ||||||
| @@ -761,5 +762,12 @@ | |||||||
|   "publisher": "Publisher", |   "publisher": "Publisher", | ||||||
|   "publisherHint": "Enter the publisher name", |   "publisherHint": "Enter the publisher name", | ||||||
|   "publisherCannotBeEmpty": "Publisher cannot be empty", |   "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.freezed.dart'; | ||||||
| part 'embed.g.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 | @freezed | ||||||
| sealed class SnScrappedLink with _$SnScrappedLink { | sealed class SnScrappedLink with _$SnScrappedLink { | ||||||
|   const factory SnScrappedLink({ |   const factory SnScrappedLink({ | ||||||
|   | |||||||
| @@ -12,290 +12,6 @@ part of 'embed.dart'; | |||||||
| // dart format off | // dart format off | ||||||
| T _$identity<T>(T value) => value; | 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 | /// @nodoc | ||||||
| mixin _$SnScrappedLink { | mixin _$SnScrappedLink { | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,36 +6,6 @@ part of 'embed.dart'; | |||||||
| // JsonSerializableGenerator | // 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 _$SnScrappedLinkFromJson(Map<String, dynamic> json) => | ||||||
|     _SnScrappedLink( |     _SnScrappedLink( | ||||||
|       type: json['type'] as String, |       type: json['type'] as String, | ||||||
|   | |||||||
| @@ -90,3 +90,19 @@ enum SnPollQuestionType { | |||||||
|   @JsonValue(4) |   @JsonValue(4) | ||||||
|   freeText, |   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 | // dart format on | ||||||
|   | |||||||
| @@ -131,3 +131,28 @@ Map<String, dynamic> _$SnPollOptionToJson(_SnPollOption instance) => | |||||||
|       'description': instance.description, |       'description': instance.description, | ||||||
|       'order': instance.order, |       '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 createdAt, | ||||||
|     required DateTime updatedAt, |     required DateTime updatedAt, | ||||||
|     required DateTime? deletedAt, |     required DateTime? deletedAt, | ||||||
|  |     @Default([]) List<SnSticker> stickers, | ||||||
|   }) = _SnStickerPack; |   }) = _SnStickerPack; | ||||||
|  |  | ||||||
|   factory SnStickerPack.fromJson(Map<String, dynamic> json) => |   factory SnStickerPack.fromJson(Map<String, dynamic> json) => | ||||||
|   | |||||||
| @@ -338,7 +338,7 @@ $SnStickerPackCopyWith<$Res>? get pack { | |||||||
| /// @nodoc | /// @nodoc | ||||||
| mixin _$SnStickerPack { | 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 | /// Create a copy of SnStickerPack | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @@ -351,16 +351,16 @@ $SnStickerPackCopyWith<SnStickerPack> get copyWith => _$SnStickerPackCopyWithImp | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | 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) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @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 | @override | ||||||
| String toString() { | 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; |   factory $SnStickerPackCopyWith(SnStickerPack value, $Res Function(SnStickerPack) _then) = _$SnStickerPackCopyWithImpl; | ||||||
| @useResult | @useResult | ||||||
| $Res call({ | $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 | /// Create a copy of SnStickerPack | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// 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( |   return _then(_self.copyWith( | ||||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | 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 | 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 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,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,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 | /// 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) { | switch (_that) { | ||||||
| case _SnStickerPack() when $default != null: | 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(); |   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) { | switch (_that) { | ||||||
| case _SnStickerPack(): | 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` | /// 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) { | switch (_that) { | ||||||
| case _SnStickerPack() when $default != null: | 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; |   return null; | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -546,7 +547,7 @@ return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publish | |||||||
| @JsonSerializable() | @JsonSerializable() | ||||||
|  |  | ||||||
| class _SnStickerPack implements SnStickerPack { | 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); |   factory _SnStickerPack.fromJson(Map<String, dynamic> json) => _$SnStickerPackFromJson(json); | ||||||
|  |  | ||||||
| @override final  String id; | @override final  String id; | ||||||
| @@ -558,6 +559,13 @@ class _SnStickerPack implements SnStickerPack { | |||||||
| @override final  DateTime createdAt; | @override final  DateTime createdAt; | ||||||
| @override final  DateTime updatedAt; | @override final  DateTime updatedAt; | ||||||
| @override final  DateTime? deletedAt; | @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 | /// Create a copy of SnStickerPack | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @@ -572,16 +580,16 @@ Map<String, dynamic> toJson() { | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | 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) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @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 | @override | ||||||
| String toString() { | 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; |   factory _$SnStickerPackCopyWith(_SnStickerPack value, $Res Function(_SnStickerPack) _then) = __$SnStickerPackCopyWithImpl; | ||||||
| @override @useResult | @override @useResult | ||||||
| $Res call({ | $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 | /// Create a copy of SnStickerPack | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// 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( |   return _then(_SnStickerPack( | ||||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | 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 | 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 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,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,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 |           json['deleted_at'] == null | ||||||
|               ? null |               ? null | ||||||
|               : DateTime.parse(json['deleted_at'] as String), |               : 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) => | Map<String, dynamic> _$SnStickerPackToJson(_SnStickerPack instance) => | ||||||
| @@ -67,4 +72,5 @@ Map<String, dynamic> _$SnStickerPackToJson(_SnStickerPack instance) => | |||||||
|       'created_at': instance.createdAt.toIso8601String(), |       'created_at': instance.createdAt.toIso8601String(), | ||||||
|       'updated_at': instance.updatedAt.toIso8601String(), |       'updated_at': instance.updatedAt.toIso8601String(), | ||||||
|       'deleted_at': instance.deletedAt?.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/posts/post_manage_list.dart'; | ||||||
| import 'package:island/screens/creators/stickers/stickers.dart'; | import 'package:island/screens/creators/stickers/stickers.dart'; | ||||||
| import 'package:island/screens/creators/stickers/pack_detail.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/poll/poll_list.dart'; | ||||||
| import 'package:island/screens/creators/publishers.dart'; | import 'package:island/screens/creators/publishers.dart'; | ||||||
| import 'package:island/screens/creators/webfeed/webfeed_list.dart'; | import 'package:island/screens/creators/webfeed/webfeed_list.dart'; | ||||||
| @@ -451,6 +453,23 @@ final routerProvider = Provider<GoRouter>((ref) { | |||||||
|                     path: '/account', |                     path: '/account', | ||||||
|                     builder: (context, state) => const AccountScreen(), |                     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( |                   GoRoute( | ||||||
|                     name: 'notifications', |                     name: 'notifications', | ||||||
|                     path: '/account/notifications', |                     path: '/account/notifications', | ||||||
|   | |||||||
| @@ -205,7 +205,7 @@ class _AboutScreenState extends ConsumerState<AboutScreen> { | |||||||
|                           title: 'aboutScreenTermsOfServiceTitle'.tr(), |                           title: 'aboutScreenTermsOfServiceTitle'.tr(), | ||||||
|                           onTap: |                           onTap: | ||||||
|                               () => _launchURL( |                               () => _launchURL( | ||||||
|                                 'https://solsynth.dev/terms/basic-law', |                                 'https://solsynth.dev/terms/user-agreement', | ||||||
|                               ), |                               ), | ||||||
|                         ), |                         ), | ||||||
|                         _buildListTile( |                         _buildListTile( | ||||||
|   | |||||||
| @@ -189,7 +189,6 @@ class AccountScreen extends HookConsumerWidget { | |||||||
|                 ), |                 ), | ||||||
|               ], |               ], | ||||||
|             ).padding(horizontal: 8), |             ).padding(horizontal: 8), | ||||||
|             const Gap(8), |  | ||||||
|             ListTile( |             ListTile( | ||||||
|               minTileHeight: 48, |               minTileHeight: 48, | ||||||
|               leading: const Icon(Symbols.notifications), |               leading: const Icon(Symbols.notifications), | ||||||
| @@ -228,6 +227,16 @@ class AccountScreen extends HookConsumerWidget { | |||||||
|                 context.pushNamed('relationships'); |                 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( |             ListTile( | ||||||
|               minTileHeight: 48, |               minTileHeight: 48, | ||||||
|               title: Text('abuseReports').tr(), |               title: Text('abuseReports').tr(), | ||||||
|   | |||||||
| @@ -35,6 +35,7 @@ import 'package:material_symbols_icons/symbols.dart'; | |||||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
| import 'chat.dart'; | import 'chat.dart'; | ||||||
| import 'package:island/widgets/chat/call_button.dart'; | import 'package:island/widgets/chat/call_button.dart'; | ||||||
|  | import 'package:island/widgets/stickers/picker.dart'; | ||||||
|  |  | ||||||
| part 'room.g.dart'; | part 'room.g.dart'; | ||||||
|  |  | ||||||
| @@ -1060,13 +1061,16 @@ class _ChatInput extends HookConsumerWidget { | |||||||
|         children: [ |         children: [ | ||||||
|           if (attachments.isNotEmpty) |           if (attachments.isNotEmpty) | ||||||
|             SizedBox( |             SizedBox( | ||||||
|               height: 280, |               height: 324, | ||||||
|               child: ListView.separated( |               child: ListView.separated( | ||||||
|                 padding: EdgeInsets.symmetric(horizontal: 12), |                 padding: EdgeInsets.symmetric(horizontal: 12), | ||||||
|                 scrollDirection: Axis.horizontal, |                 scrollDirection: Axis.horizontal, | ||||||
|                 itemCount: attachments.length, |                 itemCount: attachments.length, | ||||||
|                 itemBuilder: (context, idx) { |                 itemBuilder: (context, idx) { | ||||||
|                   return AttachmentPreview( |                   return SizedBox( | ||||||
|  |                     height: 320, | ||||||
|  |                     width: 280, | ||||||
|  |                     child: AttachmentPreview( | ||||||
|                       item: attachments[idx], |                       item: attachments[idx], | ||||||
|                       onRequestUpload: () => onUploadAttachment(idx), |                       onRequestUpload: () => onUploadAttachment(idx), | ||||||
|                       onDelete: () => onDeleteAttachment(idx), |                       onDelete: () => onDeleteAttachment(idx), | ||||||
| @@ -1075,6 +1079,7 @@ class _ChatInput extends HookConsumerWidget { | |||||||
|                         onAttachmentsChanged(attachments); |                         onAttachmentsChanged(attachments); | ||||||
|                       }, |                       }, | ||||||
|                       onMove: (delta) => onMoveAttachment(idx, delta), |                       onMove: (delta) => onMoveAttachment(idx, delta), | ||||||
|  |                     ), | ||||||
|                   ); |                   ); | ||||||
|                 }, |                 }, | ||||||
|                 separatorBuilder: (_, _) => const Gap(8), |                 separatorBuilder: (_, _) => const Gap(8), | ||||||
| @@ -1129,6 +1134,49 @@ class _ChatInput extends HookConsumerWidget { | |||||||
|             padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), |             padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), | ||||||
|             child: Row( |             child: Row( | ||||||
|               children: [ |               children: [ | ||||||
|  |                 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, | ||||||
|  |                           ), | ||||||
|  |                           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( |                     PopupMenuButton( | ||||||
|                       icon: const Icon(Symbols.photo_library), |                       icon: const Icon(Symbols.photo_library), | ||||||
|                       itemBuilder: |                       itemBuilder: | ||||||
| @@ -1155,6 +1203,8 @@ class _ChatInput extends HookConsumerWidget { | |||||||
|                             ), |                             ), | ||||||
|                           ], |                           ], | ||||||
|                     ), |                     ), | ||||||
|  |                   ], | ||||||
|  |                 ), | ||||||
|                 Expanded( |                 Expanded( | ||||||
|                   child: RawKeyboardListener( |                   child: RawKeyboardListener( | ||||||
|                     focusNode: FocusNode(), |                     focusNode: FocusNode(), | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/models/poll.dart'; | import 'package:island/models/poll.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
|  | import 'package:island/widgets/poll/poll_feedback.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
| import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | ||||||
| @@ -164,10 +165,13 @@ class _CreatorPollItem extends StatelessWidget { | |||||||
|               ], |               ], | ||||||
|         ), |         ), | ||||||
|         onTap: () { |         onTap: () { | ||||||
|           // Open editor for edit |           showModalBottomSheet( | ||||||
|           // Navigator push by path to keep consistency with rest of app: |             context: context, | ||||||
|           // Note: pub name string may be required in route; when absent, route may need query or pick later. |             useRootNavigator: true, | ||||||
|           // For safety, just do nothing if no publisher in list item. |             isScrollControlled: true, | ||||||
|  |             builder: | ||||||
|  |                 (context) => PollFeedbackSheet(pollId: poll.id, poll: poll), | ||||||
|  |           ); | ||||||
|         }, |         }, | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|   | |||||||
| @@ -218,6 +218,11 @@ class ExploreScreen extends HookConsumerWidget { | |||||||
|                               right: 12, |                               right: 12, | ||||||
|                               top: 16, |                               top: 16, | ||||||
|                             ), |                             ), | ||||||
|  |                             onChecked: () { | ||||||
|  |                               ref.invalidate( | ||||||
|  |                                 eventCalendarProvider(query.value), | ||||||
|  |                               ); | ||||||
|  |                             }, | ||||||
|                           ), |                           ), | ||||||
|                           Card( |                           Card( | ||||||
|                             margin: EdgeInsets.only(left: 8, right: 12, top: 8), |                             margin: EdgeInsets.only(left: 8, right: 12, top: 8), | ||||||
|   | |||||||
| @@ -207,8 +207,6 @@ class PostComposeScreen extends HookConsumerWidget { | |||||||
|         isScrollControlled: true, |         isScrollControlled: true, | ||||||
|         builder: |         builder: | ||||||
|             (context) => ComposeSettingsSheet( |             (context) => ComposeSettingsSheet( | ||||||
|               titleController: state.titleController, |  | ||||||
|               descriptionController: state.descriptionController, |  | ||||||
|               visibility: state.visibility, |               visibility: state.visibility, | ||||||
|               tagsController: state.tagsController, |               tagsController: state.tagsController, | ||||||
|               categoriesController: state.categoriesController, |               categoriesController: state.categoriesController, | ||||||
| @@ -370,14 +368,52 @@ class PostComposeScreen extends HookConsumerWidget { | |||||||
|                     // Post content form |                     // Post content form | ||||||
|                     Expanded( |                     Expanded( | ||||||
|                       child: SingleChildScrollView( |                       child: SingleChildScrollView( | ||||||
|                         padding: const EdgeInsets.symmetric(vertical: 12), |                         padding: const EdgeInsets.symmetric(vertical: 16), | ||||||
|                         child: Column( |                         child: Column( | ||||||
|                           crossAxisAlignment: CrossAxisAlignment.start, |                           crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|                           children: [ |                           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 |                             // Content field with borderless design | ||||||
|                             RawKeyboardListener( |                             KeyboardListener( | ||||||
|                               focusNode: FocusNode(), |                               focusNode: FocusNode(), | ||||||
|                               onKey: |                               onKeyEvent: | ||||||
|                                   (event) => ComposeLogic.handleKeyPress( |                                   (event) => ComposeLogic.handleKeyPress( | ||||||
|                                     event, |                                     event, | ||||||
|                                     state, |                                     state, | ||||||
| @@ -393,7 +429,11 @@ class PostComposeScreen extends HookConsumerWidget { | |||||||
|                                 decoration: InputDecoration( |                                 decoration: InputDecoration( | ||||||
|                                   border: InputBorder.none, |                                   border: InputBorder.none, | ||||||
|                                   hintText: 'postContent'.tr(), |                                   hintText: 'postContent'.tr(), | ||||||
|                                   contentPadding: const EdgeInsets.all(8), |                                   isCollapsed: true, | ||||||
|  |                                   contentPadding: const EdgeInsets.symmetric( | ||||||
|  |                                     vertical: 8, | ||||||
|  |                                     horizontal: 8, | ||||||
|  |                                   ), | ||||||
|                                 ), |                                 ), | ||||||
|                                 maxLines: null, |                                 maxLines: null, | ||||||
|                                 onTapOutside: |                                 onTapOutside: | ||||||
|   | |||||||
| @@ -140,8 +140,6 @@ class ArticleComposeScreen extends HookConsumerWidget { | |||||||
|         isScrollControlled: true, |         isScrollControlled: true, | ||||||
|         builder: |         builder: | ||||||
|             (context) => ComposeSettingsSheet( |             (context) => ComposeSettingsSheet( | ||||||
|               titleController: state.titleController, |  | ||||||
|               descriptionController: state.descriptionController, |  | ||||||
|               visibility: state.visibility, |               visibility: state.visibility, | ||||||
|               tagsController: state.tagsController, |               tagsController: state.tagsController, | ||||||
|               categoriesController: state.categoriesController, |               categoriesController: state.categoriesController, | ||||||
| @@ -242,10 +240,39 @@ class ArticleComposeScreen extends HookConsumerWidget { | |||||||
|           child: Column( |           child: Column( | ||||||
|             crossAxisAlignment: CrossAxisAlignment.start, |             crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|             children: [ |             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( |               Expanded( | ||||||
|                 child: RawKeyboardListener( |                 child: KeyboardListener( | ||||||
|                   focusNode: FocusNode(), |                   focusNode: FocusNode(), | ||||||
|                   onKey: |                   onKeyEvent: | ||||||
|                       (event) => _handleKeyPress( |                       (event) => _handleKeyPress( | ||||||
|                         event, |                         event, | ||||||
|                         state, |                         state, | ||||||
| @@ -454,7 +481,7 @@ class ArticleComposeScreen extends HookConsumerWidget { | |||||||
|                               flex: showPreview.value ? 1 : 2, |                               flex: showPreview.value ? 1 : 2, | ||||||
|                               child: buildEditorPane(), |                               child: buildEditorPane(), | ||||||
|                             ), |                             ), | ||||||
|                             const VerticalDivider(), |                             if (showPreview.value) const VerticalDivider(), | ||||||
|                             if (showPreview.value) |                             if (showPreview.value) | ||||||
|                               Expanded(child: buildPreviewPane()), |                               Expanded(child: buildPreviewPane()), | ||||||
|                           ], |                           ], | ||||||
| @@ -475,7 +502,7 @@ class ArticleComposeScreen extends HookConsumerWidget { | |||||||
|  |  | ||||||
|   // Helper method to handle keyboard shortcuts |   // Helper method to handle keyboard shortcuts | ||||||
|   void _handleKeyPress( |   void _handleKeyPress( | ||||||
|     RawKeyEvent event, |     KeyEvent event, | ||||||
|     ComposeState state, |     ComposeState state, | ||||||
|     WidgetRef ref, |     WidgetRef ref, | ||||||
|     BuildContext context, { |     BuildContext context, { | ||||||
| @@ -485,7 +512,9 @@ class ArticleComposeScreen extends HookConsumerWidget { | |||||||
|  |  | ||||||
|     final isPaste = event.logicalKey == LogicalKeyboardKey.keyV; |     final isPaste = event.logicalKey == LogicalKeyboardKey.keyV; | ||||||
|     final isSave = event.logicalKey == LogicalKeyboardKey.keyS; |     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; |     final isSubmit = event.logicalKey == LogicalKeyboardKey.enter; | ||||||
|  |  | ||||||
|     if (isPaste && isModifierPressed) { |     if (isPaste && isModifierPressed) { | ||||||
|   | |||||||
| @@ -87,6 +87,12 @@ class PublisherProfileScreen extends HookConsumerWidget { | |||||||
|       publisherAppbarForcegroundColorProvider(name), |       publisherAppbarForcegroundColorProvider(name), | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  |     final categoryTabController = useTabController(initialLength: 3); | ||||||
|  |     final categoryTab = useState(0); | ||||||
|  |     categoryTabController.addListener(() { | ||||||
|  |       categoryTab.value = categoryTabController.index; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|     final subscribing = useState(false); |     final subscribing = useState(false); | ||||||
|  |  | ||||||
|     Future<void> subscribe() async { |     Future<void> subscribe() async { | ||||||
| @@ -268,6 +274,16 @@ class PublisherProfileScreen extends HookConsumerWidget { | |||||||
|       ).padding(horizontal: 20, vertical: 16), |       ).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( |     return publisher.when( | ||||||
|       data: |       data: | ||||||
|           (data) => AppScaffold( |           (data) => AppScaffold( | ||||||
| @@ -321,7 +337,18 @@ class PublisherProfileScreen extends HookConsumerWidget { | |||||||
|                           child: CustomScrollView( |                           child: CustomScrollView( | ||||||
|                             slivers: [ |                             slivers: [ | ||||||
|                               SliverGap(16), |                               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( |                               SliverGap( | ||||||
|                                 MediaQuery.of(context).padding.bottom + 16, |                                 MediaQuery.of(context).padding.bottom + 16, | ||||||
|                               ), |                               ), | ||||||
| @@ -334,9 +361,9 @@ class PublisherProfileScreen extends HookConsumerWidget { | |||||||
|                             alignment: Alignment.topLeft, |                             alignment: Alignment.topLeft, | ||||||
|                             child: SingleChildScrollView( |                             child: SingleChildScrollView( | ||||||
|                               child: Column( |                               child: Column( | ||||||
|                                 crossAxisAlignment: CrossAxisAlignment.start, |                                 crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|                                 children: [ |                                 children: [ | ||||||
|                                   publisherBasisWidget(data), |                                   publisherBasisWidget(data).padding(bottom: 8), | ||||||
|                                   publisherBadgesWidget(data), |                                   publisherBadgesWidget(data), | ||||||
|                                   publisherVerificationWidget(data), |                                   publisherVerificationWidget(data), | ||||||
|                                   publisherBioWidget(data), |                                   publisherBioWidget(data), | ||||||
| @@ -398,7 +425,16 @@ class PublisherProfileScreen extends HookConsumerWidget { | |||||||
|                           child: publisherVerificationWidget(data), |                           child: publisherVerificationWidget(data), | ||||||
|                         ), |                         ), | ||||||
|                         SliverToBoxAdapter(child: publisherBioWidget(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), |                         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, |               size: 16, | ||||||
|             ).padding(right: 4), |             ).padding(right: 4), | ||||||
|           if (status.value?.isCustomized ?? false) |           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 |           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) && |           if (!(status.value?.isOnline ?? false) && | ||||||
|               account.value?.profile.lastSeenAt != null) |               account.value?.profile.lastSeenAt != null) | ||||||
|             Flexible( |             Flexible( | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ import 'package:island/models/embed.dart'; | |||||||
| import 'package:island/pods/call.dart'; | import 'package:island/pods/call.dart'; | ||||||
| import 'package:island/pods/translate.dart'; | import 'package:island/pods/translate.dart'; | ||||||
| import 'package:island/screens/chat/room.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_name.dart'; | ||||||
| import 'package:island/widgets/account/account_pfc.dart'; | import 'package:island/widgets/account/account_pfc.dart'; | ||||||
| import 'package:island/widgets/app_scaffold.dart'; | import 'package:island/widgets/app_scaffold.dart'; | ||||||
| @@ -292,12 +293,11 @@ class MessageItem extends HookConsumerWidget { | |||||||
|                             ), |                             ), | ||||||
|                           if (remoteMessage.meta['embeds'] != null) |                           if (remoteMessage.meta['embeds'] != null) | ||||||
|                             ...((remoteMessage.meta['embeds'] as List<dynamic>) |                             ...((remoteMessage.meta['embeds'] as List<dynamic>) | ||||||
|                                 .where((embed) => embed['Type'] == 'link') |  | ||||||
|                                 .map( |                                 .map( | ||||||
|                                   (embed) => SnEmbedLink.fromJson( |                                   (embed) => convertMapKeysToSnakeCase(embed), | ||||||
|                                     embed as Map<String, dynamic>, |  | ||||||
|                                   ), |  | ||||||
|                                 ) |                                 ) | ||||||
|  |                                 .where((embed) => embed['type'] == 'link') | ||||||
|  |                                 .map((embed) => SnScrappedLink.fromJson(embed)) | ||||||
|                                 .map( |                                 .map( | ||||||
|                                   (link) => LayoutBuilder( |                                   (link) => LayoutBuilder( | ||||||
|                                     builder: (context, constraints) { |                                     builder: (context, constraints) { | ||||||
|   | |||||||
| @@ -36,7 +36,8 @@ Future<SnCheckInResult?> checkInResultToday(Ref ref) async { | |||||||
|  |  | ||||||
| class CheckInWidget extends HookConsumerWidget { | class CheckInWidget extends HookConsumerWidget { | ||||||
|   final EdgeInsets? margin; |   final EdgeInsets? margin; | ||||||
|   const CheckInWidget({super.key, this.margin}); |   final VoidCallback? onChecked; | ||||||
|  |   const CheckInWidget({super.key, this.margin, this.onChecked}); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
| @@ -52,6 +53,7 @@ class CheckInWidget extends HookConsumerWidget { | |||||||
|         ref.invalidate(checkInResultTodayProvider); |         ref.invalidate(checkInResultTodayProvider); | ||||||
|         final userNotifier = ref.read(userInfoProvider.notifier); |         final userNotifier = ref.read(userInfoProvider.notifier); | ||||||
|         userNotifier.fetchUser(); |         userNotifier.fetchUser(); | ||||||
|  |         onChecked?.call(); | ||||||
|       } catch (err) { |       } catch (err) { | ||||||
|         if (err is DioException) { |         if (err is DioException) { | ||||||
|           if (err.response?.statusCode == 423 && context.mounted) { |           if (err.response?.statusCode == 423 && context.mounted) { | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import 'dart:math' as math; | import 'dart:math' as math; | ||||||
| import 'dart:ui'; |  | ||||||
|  |  | ||||||
| import 'package:cached_network_image/cached_network_image.dart'; | import 'package:cached_network_image/cached_network_image.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| @@ -103,6 +102,7 @@ class CloudVideoWidget extends HookConsumerWidget { | |||||||
|                 Symbols.play_arrow, |                 Symbols.play_arrow, | ||||||
|                 fill: 1, |                 fill: 1, | ||||||
|                 size: 32, |                 size: 32, | ||||||
|  |                 color: Colors.white, | ||||||
|                 shadows: [ |                 shadows: [ | ||||||
|                   BoxShadow( |                   BoxShadow( | ||||||
|                     color: Colors.black54, |                     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( |           Positioned( | ||||||
|             bottom: 0, |             bottom: 0, | ||||||
|             left: 0, |             left: 0, | ||||||
| @@ -133,6 +153,7 @@ class CloudVideoWidget extends HookConsumerWidget { | |||||||
|                                   .toInt(), |                                   .toInt(), | ||||||
|                         ).formatDuration(), |                         ).formatDuration(), | ||||||
|                         style: TextStyle( |                         style: TextStyle( | ||||||
|  |                           color: Colors.white, | ||||||
|                           shadows: [ |                           shadows: [ | ||||||
|                             BoxShadow( |                             BoxShadow( | ||||||
|                               color: Colors.black54, |                               color: Colors.black54, | ||||||
| @@ -147,6 +168,7 @@ class CloudVideoWidget extends HookConsumerWidget { | |||||||
|                       Text( |                       Text( | ||||||
|                         '${int.parse(item.fileMeta?['bit_rate'] as String) ~/ 1000} Kbps', |                         '${int.parse(item.fileMeta?['bit_rate'] as String) ~/ 1000} Kbps', | ||||||
|                         style: TextStyle( |                         style: TextStyle( | ||||||
|  |                           color: Colors.white, | ||||||
|                           shadows: [ |                           shadows: [ | ||||||
|                             BoxShadow( |                             BoxShadow( | ||||||
|                               color: Colors.black54, |                               color: Colors.black54, | ||||||
| @@ -161,7 +183,10 @@ class CloudVideoWidget extends HookConsumerWidget { | |||||||
|                 ), |                 ), | ||||||
|                 Text( |                 Text( | ||||||
|                   item.name, |                   item.name, | ||||||
|  |                   maxLines: 1, | ||||||
|  |                   overflow: TextOverflow.ellipsis, | ||||||
|                   style: TextStyle( |                   style: TextStyle( | ||||||
|  |                     color: Colors.white, | ||||||
|                     fontWeight: FontWeight.bold, |                     fontWeight: FontWeight.bold, | ||||||
|                     shadows: [ |                     shadows: [ | ||||||
|                       BoxShadow( |                       BoxShadow( | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ import 'package:material_symbols_icons/symbols.dart'; | |||||||
| import 'package:url_launcher/url_launcher.dart'; | import 'package:url_launcher/url_launcher.dart'; | ||||||
|  |  | ||||||
| class EmbedLinkWidget extends StatelessWidget { | class EmbedLinkWidget extends StatelessWidget { | ||||||
|   final SnEmbedLink link; |   final SnScrappedLink link; | ||||||
|   final double? maxWidth; |   final double? maxWidth; | ||||||
|   final EdgeInsetsGeometry? margin; |   final EdgeInsetsGeometry? margin; | ||||||
|  |  | ||||||
| @@ -116,7 +116,8 @@ class EmbedLinkWidget extends StatelessWidget { | |||||||
|                     ], |                     ], | ||||||
|  |  | ||||||
|                     // Description |                     // Description | ||||||
|                     if (link.description != null && link.description!.isNotEmpty) ...[ |                     if (link.description != null && | ||||||
|  |                         link.description!.isNotEmpty) ...[ | ||||||
|                       Text( |                       Text( | ||||||
|                         link.description!, |                         link.description!, | ||||||
|                         style: theme.textTheme.bodyMedium?.copyWith( |                         style: theme.textTheme.bodyMedium?.copyWith( | ||||||
|   | |||||||
							
								
								
									
										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/models/poll.dart'; | ||||||
| import 'package:island/pods/network.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 { | class PollSubmit extends ConsumerStatefulWidget { | ||||||
|   const PollSubmit({ |   const PollSubmit({ | ||||||
|     super.key, |     super.key, | ||||||
|   | |||||||
| @@ -189,8 +189,8 @@ class ComposePollSheet extends HookConsumerWidget { | |||||||
|   Widget? _buildPollSubtitle(SnPoll poll) { |   Widget? _buildPollSubtitle(SnPoll poll) { | ||||||
|     try { |     try { | ||||||
|       final SnPoll dyn = poll; |       final SnPoll dyn = poll; | ||||||
|       final List<SnPollQuestion>? options = dyn.questions; |       final List<SnPollQuestion> options = dyn.questions; | ||||||
|       if (options == null || options.isEmpty) return null; |       if (options.isEmpty) return null; | ||||||
|       final preview = options.take(3).map((e) => e.title).join(' · '); |       final preview = options.take(3).map((e) => e.title).join(' · '); | ||||||
|       if (preview.trim().isEmpty) return null; |       if (preview.trim().isEmpty) return null; | ||||||
|       return Text(preview); |       return Text(preview); | ||||||
|   | |||||||
| @@ -99,8 +99,6 @@ class ChipTagInputField extends StatelessWidget { | |||||||
| } | } | ||||||
|  |  | ||||||
| class ComposeSettingsSheet extends HookWidget { | class ComposeSettingsSheet extends HookWidget { | ||||||
|   final TextEditingController titleController; |  | ||||||
|   final TextEditingController descriptionController; |  | ||||||
|   final ValueNotifier<int> visibility; |   final ValueNotifier<int> visibility; | ||||||
|   final VoidCallback? onVisibilityChanged; |   final VoidCallback? onVisibilityChanged; | ||||||
|   final StringTagController tagsController; |   final StringTagController tagsController; | ||||||
| @@ -108,8 +106,6 @@ class ComposeSettingsSheet extends HookWidget { | |||||||
|  |  | ||||||
|   const ComposeSettingsSheet({ |   const ComposeSettingsSheet({ | ||||||
|     super.key, |     super.key, | ||||||
|     required this.titleController, |  | ||||||
|     required this.descriptionController, |  | ||||||
|     required this.visibility, |     required this.visibility, | ||||||
|     this.onVisibilityChanged, |     this.onVisibilityChanged, | ||||||
|     required this.tagsController, |     required this.tagsController, | ||||||
| @@ -216,39 +212,6 @@ class ComposeSettingsSheet extends HookWidget { | |||||||
|           crossAxisAlignment: CrossAxisAlignment.start, |           crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|           spacing: 16, |           spacing: 16, | ||||||
|           children: [ |           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 |             // Tags field | ||||||
|             TextFieldTags( |             TextFieldTags( | ||||||
|               textfieldTagsController: tagsController, |               textfieldTagsController: tagsController, | ||||||
|   | |||||||
| @@ -689,7 +689,7 @@ class ComposeLogic { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   static void handleKeyPress( |   static void handleKeyPress( | ||||||
|     RawKeyEvent event, |     KeyEvent event, | ||||||
|     ComposeState state, |     ComposeState state, | ||||||
|     WidgetRef ref, |     WidgetRef ref, | ||||||
|     BuildContext context, { |     BuildContext context, { | ||||||
| @@ -701,7 +701,9 @@ class ComposeLogic { | |||||||
|  |  | ||||||
|     final isPaste = event.logicalKey == LogicalKeyboardKey.keyV; |     final isPaste = event.logicalKey == LogicalKeyboardKey.keyV; | ||||||
|     final isSave = event.logicalKey == LogicalKeyboardKey.keyS; |     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; |     final isSubmit = event.logicalKey == LogicalKeyboardKey.enter; | ||||||
|  |  | ||||||
|     if (isPaste && isModifierPressed) { |     if (isPaste && isModifierPressed) { | ||||||
|   | |||||||
| @@ -63,6 +63,7 @@ class ComposeToolbar extends HookConsumerWidget { | |||||||
|  |  | ||||||
|     return Material( |     return Material( | ||||||
|       elevation: 4, |       elevation: 4, | ||||||
|  |       color: Theme.of(context).colorScheme.surfaceContainerLow, | ||||||
|       child: Center( |       child: Center( | ||||||
|         child: ConstrainedBox( |         child: ConstrainedBox( | ||||||
|           constraints: const BoxConstraints(maxWidth: 560), |           constraints: const BoxConstraints(maxWidth: 560), | ||||||
|   | |||||||
| @@ -279,18 +279,14 @@ class _DraftItem extends StatelessWidget { | |||||||
|  |  | ||||||
|   String _parseVisibility(int visibility) { |   String _parseVisibility(int visibility) { | ||||||
|     switch (visibility) { |     switch (visibility) { | ||||||
|       case 0: |  | ||||||
|         return 'public'.tr(); |  | ||||||
|       case 1: |       case 1: | ||||||
|         return 'unlisted'.tr(); |         return 'postVisibilityFriends'; | ||||||
|       case 2: |       case 2: | ||||||
|         return 'friends'.tr(); |         return 'postVisibilityUnlisted'; | ||||||
|       case 3: |       case 3: | ||||||
|         return 'selected'.tr(); |         return 'postVisibilityPrivate'; | ||||||
|       case 4: |  | ||||||
|         return 'private'.tr(); |  | ||||||
|       default: |       default: | ||||||
|         return 'unknown'.tr(); |         return 'postVisibilityPublic'; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -314,6 +314,19 @@ class PostItem extends HookConsumerWidget { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     String _parseVisibility(int visibility) { | ||||||
|  |       switch (visibility) { | ||||||
|  |         case 1: | ||||||
|  |           return 'postVisibilityFriends'; | ||||||
|  |         case 2: | ||||||
|  |           return 'postVisibilityUnlisted'; | ||||||
|  |         case 3: | ||||||
|  |           return 'postVisibilityPrivate'; | ||||||
|  |         default: | ||||||
|  |           return 'postVisibilityPublic'; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     return Column( |     return Column( | ||||||
|       mainAxisSize: MainAxisSize.min, |       mainAxisSize: MainAxisSize.min, | ||||||
|       crossAxisAlignment: CrossAxisAlignment.start, |       crossAxisAlignment: CrossAxisAlignment.start, | ||||||
| @@ -349,13 +362,29 @@ class PostItem extends HookConsumerWidget { | |||||||
|                       Text('@${item.publisher.name}').fontSize(11), |                       Text('@${item.publisher.name}').fontSize(11), | ||||||
|                     ], |                     ], | ||||||
|                   ), |                   ), | ||||||
|  |                   Row( | ||||||
|  |                     spacing: 6, | ||||||
|  |                     crossAxisAlignment: CrossAxisAlignment.end, | ||||||
|  |                     children: [ | ||||||
|                       Text( |                       Text( | ||||||
|                         isFullPost |                         isFullPost | ||||||
|                         ? (item.publishedAt ?? item.createdAt)!.formatSystem() |                             ? (item.publishedAt ?? item.createdAt)! | ||||||
|                         : (item.publishedAt ?? item.createdAt)!.formatRelative( |                                 .formatSystem() | ||||||
|                           context, |                             : (item.publishedAt ?? item.createdAt)! | ||||||
|                         ), |                                 .formatRelative(context), | ||||||
|                       ).fontSize(10), |                       ).fontSize(10), | ||||||
|  |                       if (item.editedAt != null) | ||||||
|  |                         Text( | ||||||
|  |                           'editedAt'.tr(args: [item.editedAt!.formatSystem()]), | ||||||
|  |                           style: TextStyle(height: 1.2), | ||||||
|  |                         ).fontSize(10), | ||||||
|  |                       if (item.visibility != 0) | ||||||
|  |                         Text( | ||||||
|  |                           _parseVisibility(item.visibility).tr(), | ||||||
|  |                           style: TextStyle(height: 1.45), | ||||||
|  |                         ).fontSize(10), | ||||||
|  |                     ], | ||||||
|  |                   ), | ||||||
|                 ], |                 ], | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
| @@ -535,7 +564,7 @@ class PostItem extends HookConsumerWidget { | |||||||
|               right: renderingPadding.horizontal, |               right: renderingPadding.horizontal, | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|         if (item.attachments.isNotEmpty) |         if (item.attachments.isNotEmpty && item.type != 1) | ||||||
|           CloudFileList( |           CloudFileList( | ||||||
|             files: item.attachments, |             files: item.attachments, | ||||||
|             padding: EdgeInsets.symmetric( |             padding: EdgeInsets.symmetric( | ||||||
| @@ -547,7 +576,9 @@ class PostItem extends HookConsumerWidget { | |||||||
|           ...((item.meta!['embeds'] as List<dynamic>).map( |           ...((item.meta!['embeds'] as List<dynamic>).map( | ||||||
|             (embedData) => switch (embedData['type']) { |             (embedData) => switch (embedData['type']) { | ||||||
|               'link' => EmbedLinkWidget( |               'link' => EmbedLinkWidget( | ||||||
|                 link: SnEmbedLink.fromJson(embedData as Map<String, dynamic>), |                 link: SnScrappedLink.fromJson( | ||||||
|  |                   embedData as Map<String, dynamic>, | ||||||
|  |                 ), | ||||||
|                 maxWidth: math.min( |                 maxWidth: math.min( | ||||||
|                   MediaQuery.of(context).size.width, |                   MediaQuery.of(context).size.width, | ||||||
|                   kWideScreenWidth, |                   kWideScreenWidth, | ||||||
| @@ -770,7 +801,7 @@ class PostReplyPreview extends HookConsumerWidget { | |||||||
|     final posts = useState<List<SnPost>>([]); |     final posts = useState<List<SnPost>>([]); | ||||||
|     final loading = useState(false); |     final loading = useState(false); | ||||||
|  |  | ||||||
|     Future<void> fetchMoreReplies({int pageSize = 1}) async { |     Future<void> fetchMoreReplies({int pageSize = 3}) async { | ||||||
|       final client = ref.read(apiClientProvider); |       final client = ref.read(apiClientProvider); | ||||||
|       loading.value = true; |       loading.value = true; | ||||||
|  |  | ||||||
| @@ -779,10 +810,14 @@ class PostReplyPreview extends HookConsumerWidget { | |||||||
|           '/sphere/posts/${parent.id}/replies', |           '/sphere/posts/${parent.id}/replies', | ||||||
|           queryParameters: {'offset': posts.value.length, 'take': pageSize}, |           queryParameters: {'offset': posts.value.length, 'take': pageSize}, | ||||||
|         ); |         ); | ||||||
|  |         try { | ||||||
|           posts.value = [ |           posts.value = [ | ||||||
|             ...posts.value, |             ...posts.value, | ||||||
|             ...response.data.map((e) => SnPost.fromJson(e)), |             ...response.data.map((e) => SnPost.fromJson(e)), | ||||||
|           ]; |           ]; | ||||||
|  |         } catch (_) { | ||||||
|  |           // ignore disposed | ||||||
|  |         } | ||||||
|       } catch (err) { |       } catch (err) { | ||||||
|         showErrorAlert(err); |         showErrorAlert(err); | ||||||
|       } finally { |       } finally { | ||||||
| @@ -877,38 +912,40 @@ class PostReplyPreview extends HookConsumerWidget { | |||||||
|                   ), |                   ), | ||||||
|               ], |               ], | ||||||
|             ) |             ) | ||||||
|             : featuredReply!.when( |             : (featuredReply!).map( | ||||||
|               data: |               data: | ||||||
|                   (value) => Row( |                   (data) => Row( | ||||||
|                     crossAxisAlignment: CrossAxisAlignment.center, |                     crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|                     spacing: 8, |                     spacing: 8, | ||||||
|                     children: [ |                     children: [ | ||||||
|                       ProfilePictureWidget( |                       ProfilePictureWidget( | ||||||
|                         file: value?.publisher.picture, |                         file: data.value?.publisher.picture, | ||||||
|                         radius: 12, |                         radius: 12, | ||||||
|                       ).padding(top: 4), |                       ).padding(top: 4), | ||||||
|                       if (value?.content?.isNotEmpty ?? false) |                       if (data.value?.content?.isNotEmpty ?? false) | ||||||
|                         Expanded( |                         Expanded( | ||||||
|                           child: MarkdownTextContent(content: value!.content!), |                           child: MarkdownTextContent( | ||||||
|  |                             content: data.value!.content!, | ||||||
|  |                           ), | ||||||
|                         ) |                         ) | ||||||
|                       else |                       else | ||||||
|                         Expanded( |                         Expanded( | ||||||
|                           child: Text( |                           child: Text( | ||||||
|                             'postHasAttachments', |                             'postHasAttachments', | ||||||
|                           ).plural(value?.attachments.length ?? 0), |                           ).plural(data.value?.attachments.length ?? 0), | ||||||
|                         ), |                         ), | ||||||
|                     ], |                     ], | ||||||
|                   ), |                   ), | ||||||
|               error: |               error: | ||||||
|                   (error, _) => Row( |                   (e) => Row( | ||||||
|                     spacing: 8, |                     spacing: 8, | ||||||
|                     children: [ |                     children: [ | ||||||
|                       const Icon(Symbols.close, size: 18), |                       const Icon(Symbols.close, size: 18), | ||||||
|                       Text(error.toString()), |                       Text(e.error.toString()), | ||||||
|                     ], |                     ], | ||||||
|                   ), |                   ), | ||||||
|               loading: |               loading: | ||||||
|                   () => Row( |                   (_) => Row( | ||||||
|                     spacing: 8, |                     spacing: 8, | ||||||
|                     children: [ |                     children: [ | ||||||
|                       SizedBox( |                       SizedBox( | ||||||
| @@ -939,7 +976,6 @@ class PostReplyPreview extends HookConsumerWidget { | |||||||
|                 children: [ |                 children: [ | ||||||
|                   Text('repliesCount') |                   Text('repliesCount') | ||||||
|                       .plural(parent.repliesCount) |                       .plural(parent.repliesCount) | ||||||
|                       .tr() |  | ||||||
|                       .fontSize(15) |                       .fontSize(15) | ||||||
|                       .bold() |                       .bold() | ||||||
|                       .padding(horizontal: 5), |                       .padding(horizontal: 5), | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ class PostListNotifier extends _$PostListNotifier | |||||||
|   static const int _pageSize = 20; |   static const int _pageSize = 20; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Future<CursorPagingData<SnPost>> build(String? pubName) { |   Future<CursorPagingData<SnPost>> build(String? pubName, int? type) { | ||||||
|     return fetch(cursor: null); |     return fetch(cursor: null); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -28,6 +28,7 @@ class PostListNotifier extends _$PostListNotifier | |||||||
|       'offset': offset, |       'offset': offset, | ||||||
|       'take': _pageSize, |       'take': _pageSize, | ||||||
|       if (pubName != null) 'pub': pubName, |       if (pubName != null) 'pub': pubName, | ||||||
|  |       if (type != null) 'type': type, | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     final response = await client.get( |     final response = await client.get( | ||||||
| @@ -60,6 +61,7 @@ enum PostItemType { | |||||||
|  |  | ||||||
| class SliverPostList extends HookConsumerWidget { | class SliverPostList extends HookConsumerWidget { | ||||||
|   final String? pubName; |   final String? pubName; | ||||||
|  |   final int? type; | ||||||
|   final PostItemType itemType; |   final PostItemType itemType; | ||||||
|   final Color? backgroundColor; |   final Color? backgroundColor; | ||||||
|   final EdgeInsets? padding; |   final EdgeInsets? padding; | ||||||
| @@ -70,6 +72,7 @@ class SliverPostList extends HookConsumerWidget { | |||||||
|   const SliverPostList({ |   const SliverPostList({ | ||||||
|     super.key, |     super.key, | ||||||
|     this.pubName, |     this.pubName, | ||||||
|  |     this.type, | ||||||
|     this.itemType = PostItemType.regular, |     this.itemType = PostItemType.regular, | ||||||
|     this.backgroundColor, |     this.backgroundColor, | ||||||
|     this.padding, |     this.padding, | ||||||
| @@ -81,9 +84,9 @@ class SliverPostList extends HookConsumerWidget { | |||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     return PagingHelperSliverView( |     return PagingHelperSliverView( | ||||||
|       provider: postListNotifierProvider(pubName), |       provider: postListNotifierProvider(pubName, type), | ||||||
|       futureRefreshable: postListNotifierProvider(pubName).future, |       futureRefreshable: postListNotifierProvider(pubName, type).future, | ||||||
|       notifierRefreshable: postListNotifierProvider(pubName).notifier, |       notifierRefreshable: postListNotifierProvider(pubName, type).notifier, | ||||||
|       contentBuilder: |       contentBuilder: | ||||||
|           (data, widgetCount, endItemView) => SliverList.builder( |           (data, widgetCount, endItemView) => SliverList.builder( | ||||||
|             itemCount: widgetCount, |             itemCount: widgetCount, | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ part of 'post_list.dart'; | |||||||
| // RiverpodGenerator | // RiverpodGenerator | ||||||
| // ************************************************************************** | // ************************************************************************** | ||||||
|  |  | ||||||
| String _$postListNotifierHash() => r'2e4fb36123d3f97ac1edf9945043251d4eb519a2'; | String _$postListNotifierHash() => r'78222d62957f85713d17aecd95af0305b764e86c'; | ||||||
|  |  | ||||||
| /// Copied from Dart SDK | /// Copied from Dart SDK | ||||||
| class _SystemHash { | class _SystemHash { | ||||||
| @@ -32,8 +32,9 @@ class _SystemHash { | |||||||
| abstract class _$PostListNotifier | abstract class _$PostListNotifier | ||||||
|     extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPost>> { |     extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPost>> { | ||||||
|   late final String? pubName; |   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]. | /// See also [PostListNotifier]. | ||||||
| @@ -47,15 +48,15 @@ class PostListNotifierFamily | |||||||
|   const PostListNotifierFamily(); |   const PostListNotifierFamily(); | ||||||
|  |  | ||||||
|   /// See also [PostListNotifier]. |   /// See also [PostListNotifier]. | ||||||
|   PostListNotifierProvider call(String? pubName) { |   PostListNotifierProvider call(String? pubName, int? type) { | ||||||
|     return PostListNotifierProvider(pubName); |     return PostListNotifierProvider(pubName, type); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   PostListNotifierProvider getProviderOverride( |   PostListNotifierProvider getProviderOverride( | ||||||
|     covariant PostListNotifierProvider provider, |     covariant PostListNotifierProvider provider, | ||||||
|   ) { |   ) { | ||||||
|     return call(provider.pubName); |     return call(provider.pubName, provider.type); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static const Iterable<ProviderOrFamily>? _dependencies = null; |   static const Iterable<ProviderOrFamily>? _dependencies = null; | ||||||
| @@ -81,9 +82,12 @@ class PostListNotifierProvider | |||||||
|           CursorPagingData<SnPost> |           CursorPagingData<SnPost> | ||||||
|         > { |         > { | ||||||
|   /// See also [PostListNotifier]. |   /// See also [PostListNotifier]. | ||||||
|   PostListNotifierProvider(String? pubName) |   PostListNotifierProvider(String? pubName, int? type) | ||||||
|     : this._internal( |     : this._internal( | ||||||
|         () => PostListNotifier()..pubName = pubName, |         () => | ||||||
|  |             PostListNotifier() | ||||||
|  |               ..pubName = pubName | ||||||
|  |               ..type = type, | ||||||
|         from: postListNotifierProvider, |         from: postListNotifierProvider, | ||||||
|         name: r'postListNotifierProvider', |         name: r'postListNotifierProvider', | ||||||
|         debugGetCreateSourceHash: |         debugGetCreateSourceHash: | ||||||
| @@ -94,6 +98,7 @@ class PostListNotifierProvider | |||||||
|         allTransitiveDependencies: |         allTransitiveDependencies: | ||||||
|             PostListNotifierFamily._allTransitiveDependencies, |             PostListNotifierFamily._allTransitiveDependencies, | ||||||
|         pubName: pubName, |         pubName: pubName, | ||||||
|  |         type: type, | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|   PostListNotifierProvider._internal( |   PostListNotifierProvider._internal( | ||||||
| @@ -104,15 +109,17 @@ class PostListNotifierProvider | |||||||
|     required super.debugGetCreateSourceHash, |     required super.debugGetCreateSourceHash, | ||||||
|     required super.from, |     required super.from, | ||||||
|     required this.pubName, |     required this.pubName, | ||||||
|  |     required this.type, | ||||||
|   }) : super.internal(); |   }) : super.internal(); | ||||||
|  |  | ||||||
|   final String? pubName; |   final String? pubName; | ||||||
|  |   final int? type; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   FutureOr<CursorPagingData<SnPost>> runNotifierBuild( |   FutureOr<CursorPagingData<SnPost>> runNotifierBuild( | ||||||
|     covariant PostListNotifier notifier, |     covariant PostListNotifier notifier, | ||||||
|   ) { |   ) { | ||||||
|     return notifier.build(pubName); |     return notifier.build(pubName, type); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -120,13 +127,17 @@ class PostListNotifierProvider | |||||||
|     return ProviderOverride( |     return ProviderOverride( | ||||||
|       origin: this, |       origin: this, | ||||||
|       override: PostListNotifierProvider._internal( |       override: PostListNotifierProvider._internal( | ||||||
|         () => create()..pubName = pubName, |         () => | ||||||
|  |             create() | ||||||
|  |               ..pubName = pubName | ||||||
|  |               ..type = type, | ||||||
|         from: from, |         from: from, | ||||||
|         name: null, |         name: null, | ||||||
|         dependencies: null, |         dependencies: null, | ||||||
|         allTransitiveDependencies: null, |         allTransitiveDependencies: null, | ||||||
|         debugGetCreateSourceHash: null, |         debugGetCreateSourceHash: null, | ||||||
|         pubName: pubName, |         pubName: pubName, | ||||||
|  |         type: type, | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| @@ -142,13 +153,16 @@ class PostListNotifierProvider | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   bool operator ==(Object other) { |   bool operator ==(Object other) { | ||||||
|     return other is PostListNotifierProvider && other.pubName == pubName; |     return other is PostListNotifierProvider && | ||||||
|  |         other.pubName == pubName && | ||||||
|  |         other.type == type; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   int get hashCode { |   int get hashCode { | ||||||
|     var hash = _SystemHash.combine(0, runtimeType.hashCode); |     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||||
|     hash = _SystemHash.combine(hash, pubName.hashCode); |     hash = _SystemHash.combine(hash, pubName.hashCode); | ||||||
|  |     hash = _SystemHash.combine(hash, type.hashCode); | ||||||
|  |  | ||||||
|     return _SystemHash.finish(hash); |     return _SystemHash.finish(hash); | ||||||
|   } |   } | ||||||
| @@ -160,6 +174,9 @@ mixin PostListNotifierRef | |||||||
|     on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnPost>> { |     on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnPost>> { | ||||||
|   /// The parameter `pubName` of this provider. |   /// The parameter `pubName` of this provider. | ||||||
|   String? get pubName; |   String? get pubName; | ||||||
|  |  | ||||||
|  |   /// The parameter `type` of this provider. | ||||||
|  |   int? get type; | ||||||
| } | } | ||||||
|  |  | ||||||
| class _PostListNotifierProviderElement | class _PostListNotifierProviderElement | ||||||
| @@ -173,6 +190,8 @@ class _PostListNotifierProviderElement | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   String? get pubName => (origin as PostListNotifierProvider).pubName; |   String? get pubName => (origin as PostListNotifierProvider).pubName; | ||||||
|  |   @override | ||||||
|  |   int? get type => (origin as PostListNotifierProvider).type; | ||||||
| } | } | ||||||
|  |  | ||||||
| // ignore_for_file: type=lint | // 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