Compare commits
14 Commits
3.1.0+116
...
30b8a6c30f
| Author | SHA1 | Date | |
|---|---|---|---|
| 30b8a6c30f | |||
| b9c4ee31b1 | |||
| 87870af866 | |||
| b83cb0fb0b | |||
| 7fd1fe34e5 | |||
| 1c18330891 | |||
| d320879ad0 | |||
| 950150e119 | |||
| 3c4a9767e1 | |||
| 5df2445f3f | |||
| 56543d7b4c | |||
| 4c6fea1242 | |||
| fff43de9e3 | |||
| b31a915544 |
@@ -761,5 +761,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';
|
||||||
|
|
||||||
@@ -1066,15 +1067,19 @@ class _ChatInput extends HookConsumerWidget {
|
|||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: attachments.length,
|
itemCount: attachments.length,
|
||||||
itemBuilder: (context, idx) {
|
itemBuilder: (context, idx) {
|
||||||
return AttachmentPreview(
|
return SizedBox(
|
||||||
item: attachments[idx],
|
height: 280,
|
||||||
onRequestUpload: () => onUploadAttachment(idx),
|
width: 280,
|
||||||
onDelete: () => onDeleteAttachment(idx),
|
child: AttachmentPreview(
|
||||||
onUpdate: (value) {
|
item: attachments[idx],
|
||||||
attachments[idx] = value;
|
onRequestUpload: () => onUploadAttachment(idx),
|
||||||
onAttachmentsChanged(attachments);
|
onDelete: () => onDeleteAttachment(idx),
|
||||||
},
|
onUpdate: (value) {
|
||||||
onMove: (delta) => onMoveAttachment(idx, delta),
|
attachments[idx] = value;
|
||||||
|
onAttachmentsChanged(attachments);
|
||||||
|
},
|
||||||
|
onMove: (delta) => onMoveAttachment(idx, delta),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
separatorBuilder: (_, _) => const Gap(8),
|
separatorBuilder: (_, _) => const Gap(8),
|
||||||
@@ -1129,31 +1134,76 @@ 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: [
|
||||||
PopupMenuButton(
|
Row(
|
||||||
icon: const Icon(Symbols.photo_library),
|
mainAxisSize: MainAxisSize.min,
|
||||||
itemBuilder:
|
children: [
|
||||||
(context) => [
|
IconButton(
|
||||||
PopupMenuItem(
|
tooltip: 'stickers'.tr(),
|
||||||
onTap: () => onPickFile(true),
|
icon: const Icon(Symbols.emoji_symbols),
|
||||||
child: Row(
|
onPressed: () {
|
||||||
spacing: 12,
|
final size = MediaQuery.of(context).size;
|
||||||
children: [
|
showStickerPickerPopover(
|
||||||
const Icon(Symbols.photo),
|
context,
|
||||||
Text('addPhoto').tr(),
|
Offset(
|
||||||
],
|
20,
|
||||||
|
size.height -
|
||||||
|
480 -
|
||||||
|
MediaQuery.of(context).padding.bottom,
|
||||||
),
|
),
|
||||||
),
|
onPick: (placeholder) {
|
||||||
PopupMenuItem(
|
// Insert placeholder at current cursor position
|
||||||
onTap: () => onPickFile(false),
|
final text = messageController.text;
|
||||||
child: Row(
|
final selection = messageController.selection;
|
||||||
spacing: 12,
|
final start =
|
||||||
children: [
|
selection.start >= 0
|
||||||
const Icon(Symbols.video_call),
|
? selection.start
|
||||||
Text('addVideo').tr(),
|
: text.length;
|
||||||
],
|
final end =
|
||||||
),
|
selection.end >= 0
|
||||||
),
|
? selection.end
|
||||||
],
|
: text.length;
|
||||||
|
final newText = text.replaceRange(
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
placeholder,
|
||||||
|
);
|
||||||
|
messageController.value = TextEditingValue(
|
||||||
|
text: newText,
|
||||||
|
selection: TextSelection.collapsed(
|
||||||
|
offset: start + placeholder.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
PopupMenuButton(
|
||||||
|
icon: const Icon(Symbols.photo_library),
|
||||||
|
itemBuilder:
|
||||||
|
(context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
onTap: () => onPickFile(true),
|
||||||
|
child: Row(
|
||||||
|
spacing: 12,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.photo),
|
||||||
|
Text('addPhoto').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
onTap: () => onPickFile(false),
|
||||||
|
child: Row(
|
||||||
|
spacing: 12,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.video_call),
|
||||||
|
Text('addVideo').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: RawKeyboardListener(
|
child: RawKeyboardListener(
|
||||||
|
|||||||
@@ -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(
|
||||||
@@ -191,7 +192,7 @@ class EmbedLinkWidget extends StatelessWidget {
|
|||||||
try {
|
try {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final difference = now.difference(date);
|
final difference = now.difference(date);
|
||||||
|
|
||||||
if (difference.inDays == 0) {
|
if (difference.inDays == 0) {
|
||||||
return 'Today';
|
return 'Today';
|
||||||
} else if (difference.inDays == 1) {
|
} else if (difference.inDays == 1) {
|
||||||
|
|||||||
236
lib/widgets/poll/poll_feedback.dart
Normal file
236
lib/widgets/poll/poll_feedback.dart
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/poll.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/services/time.dart';
|
||||||
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
part 'poll_feedback.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class PollFeedbackNotifier extends _$PollFeedbackNotifier
|
||||||
|
with CursorPagingNotifierMixin<SnPollAnswer> {
|
||||||
|
static const int _pageSize = 20;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<CursorPagingData<SnPollAnswer>> build(String id) {
|
||||||
|
// immediately load first page
|
||||||
|
return fetch(cursor: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<CursorPagingData<SnPollAnswer>> fetch({
|
||||||
|
required String? cursor,
|
||||||
|
}) async {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final offset = cursor == null ? 0 : int.parse(cursor);
|
||||||
|
|
||||||
|
final queryParams = {'offset': offset, 'take': _pageSize};
|
||||||
|
|
||||||
|
final response = await client.get(
|
||||||
|
'/sphere/polls/$id/feedback',
|
||||||
|
queryParameters: queryParams,
|
||||||
|
);
|
||||||
|
final total = int.parse(response.headers.value('X-Total') ?? '0');
|
||||||
|
final List<dynamic> data = response.data;
|
||||||
|
final items = data.map((json) => SnPollAnswer.fromJson(json)).toList();
|
||||||
|
|
||||||
|
final hasMore = offset + items.length < total;
|
||||||
|
final nextCursor = hasMore ? (offset + items.length).toString() : null;
|
||||||
|
|
||||||
|
return CursorPagingData(
|
||||||
|
items: items,
|
||||||
|
hasMore: hasMore,
|
||||||
|
nextCursor: nextCursor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PollFeedbackSheet extends HookConsumerWidget {
|
||||||
|
final String pollId;
|
||||||
|
final String? title;
|
||||||
|
final SnPoll poll;
|
||||||
|
final Map<String, dynamic>? stats; // stats object similar to PollSubmit
|
||||||
|
const PollFeedbackSheet({
|
||||||
|
super.key,
|
||||||
|
required this.pollId,
|
||||||
|
required this.poll,
|
||||||
|
this.title,
|
||||||
|
this.stats,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return SheetScaffold(
|
||||||
|
titleText: title ?? 'Poll feedback',
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
_PollHeader(poll: poll, stats: stats),
|
||||||
|
const Divider(height: 1),
|
||||||
|
Expanded(
|
||||||
|
child: PagingHelperView(
|
||||||
|
provider: pollFeedbackNotifierProvider(pollId),
|
||||||
|
futureRefreshable: pollFeedbackNotifierProvider(pollId).future,
|
||||||
|
notifierRefreshable:
|
||||||
|
pollFeedbackNotifierProvider(pollId).notifier,
|
||||||
|
contentBuilder:
|
||||||
|
(data, widgetCount, endItemView) => ListView.separated(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
itemCount: widgetCount,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (index == widgetCount - 1) {
|
||||||
|
// Provided by PagingHelperView to indicate end/loading
|
||||||
|
return endItemView;
|
||||||
|
}
|
||||||
|
final answer = data.items[index];
|
||||||
|
return _PollAnswerTile(answer: answer, poll: poll);
|
||||||
|
},
|
||||||
|
separatorBuilder:
|
||||||
|
(context, index) =>
|
||||||
|
const Divider(height: 1).padding(vertical: 4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PollHeader extends StatelessWidget {
|
||||||
|
const _PollHeader({required this.poll, this.stats});
|
||||||
|
final SnPoll poll;
|
||||||
|
final Map<String, dynamic>? stats;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (poll.title != null)
|
||||||
|
Text(poll.title!, style: theme.textTheme.titleLarge),
|
||||||
|
if (poll.description != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 2),
|
||||||
|
child: Text(
|
||||||
|
poll.description!,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.textTheme.bodyMedium?.color?.withOpacity(0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 20, vertical: 16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PollAnswerTile extends StatelessWidget {
|
||||||
|
final SnPollAnswer answer;
|
||||||
|
final SnPoll poll;
|
||||||
|
const _PollAnswerTile({required this.answer, required this.poll});
|
||||||
|
|
||||||
|
String _formatPerQuestionAnswer(
|
||||||
|
SnPollQuestion q,
|
||||||
|
Map<String, dynamic> ansMap,
|
||||||
|
) {
|
||||||
|
switch (q.type) {
|
||||||
|
case SnPollQuestionType.singleChoice:
|
||||||
|
final val = ansMap[q.id];
|
||||||
|
if (val is String) {
|
||||||
|
final opt = q.options?.firstWhere(
|
||||||
|
(o) => o.id == val,
|
||||||
|
orElse: () => SnPollOption(id: val, label: '#$val', order: 0),
|
||||||
|
);
|
||||||
|
return opt?.label ?? '#$val';
|
||||||
|
}
|
||||||
|
return '—';
|
||||||
|
case SnPollQuestionType.multipleChoice:
|
||||||
|
final val = ansMap[q.id];
|
||||||
|
if (val is List) {
|
||||||
|
final ids = val.whereType<String>().toList();
|
||||||
|
if (ids.isEmpty) return '—';
|
||||||
|
final labels =
|
||||||
|
ids.map((id) {
|
||||||
|
final opt = q.options?.firstWhere(
|
||||||
|
(o) => o.id == id,
|
||||||
|
orElse: () => SnPollOption(id: id, label: '#$id', order: 0),
|
||||||
|
);
|
||||||
|
return opt?.label ?? '#$id';
|
||||||
|
}).toList();
|
||||||
|
return labels.join(', ');
|
||||||
|
}
|
||||||
|
return '—';
|
||||||
|
case SnPollQuestionType.yesNo:
|
||||||
|
final val = ansMap[q.id];
|
||||||
|
if (val is bool) {
|
||||||
|
return val ? 'Yes' : 'No';
|
||||||
|
}
|
||||||
|
return '—';
|
||||||
|
case SnPollQuestionType.rating:
|
||||||
|
final val = ansMap[q.id];
|
||||||
|
if (val is int) return val.toString();
|
||||||
|
if (val is num) return val.toString();
|
||||||
|
return '—';
|
||||||
|
case SnPollQuestionType.freeText:
|
||||||
|
final val = ansMap[q.id];
|
||||||
|
if (val is String && val.trim().isNotEmpty) return val;
|
||||||
|
return '—';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Submit date/time (title)
|
||||||
|
final submitText = answer.createdAt.formatSystem();
|
||||||
|
|
||||||
|
// Compose content from poll questions if provided, otherwise fallback to joined key-values
|
||||||
|
String content;
|
||||||
|
if (poll.questions.isNotEmpty) {
|
||||||
|
final questions = [...poll.questions]
|
||||||
|
..sort((a, b) => a.order.compareTo(b.order));
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
for (final q in questions) {
|
||||||
|
final formatted = _formatPerQuestionAnswer(q, answer.answer);
|
||||||
|
buffer.writeln('${q.title}: $formatted');
|
||||||
|
}
|
||||||
|
content = buffer.toString().trimRight();
|
||||||
|
} else {
|
||||||
|
// Fallback formatting without poll context. We still want to show the question title
|
||||||
|
// instead of the raw question id key if we can derive it from the answer map itself.
|
||||||
|
// Since we don't have poll metadata here, we cannot resolve the title; therefore we
|
||||||
|
// will show only values line-by-line without exposing the raw id.
|
||||||
|
if (answer.answer.isEmpty) {
|
||||||
|
content = '—';
|
||||||
|
} else {
|
||||||
|
final parts = <String>[];
|
||||||
|
answer.answer.forEach((key, value) {
|
||||||
|
var question = poll.questions.firstWhere((q) => q.id == key);
|
||||||
|
if (value is List) {
|
||||||
|
parts.add('${question.title}: ${value.join(', ')}');
|
||||||
|
} else {
|
||||||
|
parts.add('${question.title}: $value');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
content = parts.join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
isThreeLine: true,
|
||||||
|
leading: const CircleAvatar(
|
||||||
|
radius: 16,
|
||||||
|
child: Icon(Icons.how_to_vote, size: 16),
|
||||||
|
),
|
||||||
|
title: Text(submitText),
|
||||||
|
subtitle: Text(content),
|
||||||
|
trailing: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
180
lib/widgets/poll/poll_feedback.g.dart
Normal file
180
lib/widgets/poll/poll_feedback.g.dart
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'poll_feedback.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$pollFeedbackNotifierHash() =>
|
||||||
|
r'1bf3925b5b751cfd1a9abafb75274f1e95e7f27e';
|
||||||
|
|
||||||
|
/// Copied from Dart SDK
|
||||||
|
class _SystemHash {
|
||||||
|
_SystemHash._();
|
||||||
|
|
||||||
|
static int combine(int hash, int value) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + value);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||||
|
return hash ^ (hash >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish(int hash) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = hash ^ (hash >> 11);
|
||||||
|
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _$PollFeedbackNotifier
|
||||||
|
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPollAnswer>> {
|
||||||
|
late final String id;
|
||||||
|
|
||||||
|
FutureOr<CursorPagingData<SnPollAnswer>> build(String id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [PollFeedbackNotifier].
|
||||||
|
@ProviderFor(PollFeedbackNotifier)
|
||||||
|
const pollFeedbackNotifierProvider = PollFeedbackNotifierFamily();
|
||||||
|
|
||||||
|
/// See also [PollFeedbackNotifier].
|
||||||
|
class PollFeedbackNotifierFamily
|
||||||
|
extends Family<AsyncValue<CursorPagingData<SnPollAnswer>>> {
|
||||||
|
/// See also [PollFeedbackNotifier].
|
||||||
|
const PollFeedbackNotifierFamily();
|
||||||
|
|
||||||
|
/// See also [PollFeedbackNotifier].
|
||||||
|
PollFeedbackNotifierProvider call(String id) {
|
||||||
|
return PollFeedbackNotifierProvider(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
PollFeedbackNotifierProvider getProviderOverride(
|
||||||
|
covariant PollFeedbackNotifierProvider provider,
|
||||||
|
) {
|
||||||
|
return call(provider.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'pollFeedbackNotifierProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [PollFeedbackNotifier].
|
||||||
|
class PollFeedbackNotifierProvider
|
||||||
|
extends
|
||||||
|
AutoDisposeAsyncNotifierProviderImpl<
|
||||||
|
PollFeedbackNotifier,
|
||||||
|
CursorPagingData<SnPollAnswer>
|
||||||
|
> {
|
||||||
|
/// See also [PollFeedbackNotifier].
|
||||||
|
PollFeedbackNotifierProvider(String id)
|
||||||
|
: this._internal(
|
||||||
|
() => PollFeedbackNotifier()..id = id,
|
||||||
|
from: pollFeedbackNotifierProvider,
|
||||||
|
name: r'pollFeedbackNotifierProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$pollFeedbackNotifierHash,
|
||||||
|
dependencies: PollFeedbackNotifierFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
PollFeedbackNotifierFamily._allTransitiveDependencies,
|
||||||
|
id: id,
|
||||||
|
);
|
||||||
|
|
||||||
|
PollFeedbackNotifierProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.id,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<CursorPagingData<SnPollAnswer>> runNotifierBuild(
|
||||||
|
covariant PollFeedbackNotifier notifier,
|
||||||
|
) {
|
||||||
|
return notifier.build(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(PollFeedbackNotifier Function() create) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: PollFeedbackNotifierProvider._internal(
|
||||||
|
() => create()..id = id,
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
id: id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeAsyncNotifierProviderElement<
|
||||||
|
PollFeedbackNotifier,
|
||||||
|
CursorPagingData<SnPollAnswer>
|
||||||
|
>
|
||||||
|
createElement() {
|
||||||
|
return _PollFeedbackNotifierProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is PollFeedbackNotifierProvider && other.id == id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, id.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin PollFeedbackNotifierRef
|
||||||
|
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnPollAnswer>> {
|
||||||
|
/// The parameter `id` of this provider.
|
||||||
|
String get id;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PollFeedbackNotifierProviderElement
|
||||||
|
extends
|
||||||
|
AutoDisposeAsyncNotifierProviderElement<
|
||||||
|
PollFeedbackNotifier,
|
||||||
|
CursorPagingData<SnPollAnswer>
|
||||||
|
>
|
||||||
|
with PollFeedbackNotifierRef {
|
||||||
|
_PollFeedbackNotifierProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get id => (origin as PollFeedbackNotifierProvider).id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
@@ -3,21 +3,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:island/models/poll.dart';
|
import 'package:island/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),
|
||||||
|
|||||||
@@ -535,7 +535,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 +547,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 +772,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;
|
||||||
|
|
||||||
@@ -877,38 +879,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 +943,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
|
||||||
|
|||||||
306
lib/widgets/stickers/picker.dart
Normal file
306
lib/widgets/stickers/picker.dart
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
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(
|
||||||
|
tooltip: 'close'.tr(),
|
||||||
|
onPressed: () => Navigator.of(context).maybePop(),
|
||||||
|
icon: const Icon(Symbols.close),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 12, top: 8, bottom: 4),
|
||||||
|
|
||||||
|
// Vertical, scrollable packs rail like common emoji pickers
|
||||||
|
SizedBox(
|
||||||
|
height: 52,
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
|
||||||
|
// Content
|
||||||
|
Expanded(
|
||||||
|
child: RefreshIndicator(
|
||||||
|
onRefresh: widget.onRefresh,
|
||||||
|
child: _StickersGrid(
|
||||||
|
pack: selectedPack,
|
||||||
|
onPick: (sticker) => widget.onPick(selectedPack, sticker),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Gap(MediaQuery.of(context).padding.bottom),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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