Compare commits

...

15 Commits

Author SHA1 Message Date
f00135c4bf 🐛 Fix room input 2025-08-07 12:04:47 +08:00
30b8a6c30f 🐛 Fix status overflow 2025-08-07 11:56:37 +08:00
b9c4ee31b1 Adjust picker size 2025-08-07 11:55:16 +08:00
87870af866 Sticker picker basis 2025-08-07 03:22:56 +08:00
b83cb0fb0b ♻️ Replace the snackbar usage 2025-08-07 03:03:15 +08:00
7fd1fe34e5 Stickers marketplace 2025-08-07 03:00:29 +08:00
1c18330891 🐛 Fixes poll feedback 2025-08-07 02:17:03 +08:00
d320879ad0 Poll feedback 2025-08-07 02:01:15 +08:00
950150e119 🐛 Fix large screen have no post category filter 2025-08-06 21:42:23 +08:00
3c4a9767e1 ♻️ Move the title, description out of the settings sheet 2025-08-06 20:55:13 +08:00
5df2445f3f 📝 Update user agreement 2025-08-06 20:30:59 +08:00
56543d7b4c Publisher page category filter 2025-08-06 20:17:07 +08:00
4c6fea1242 💄 Optimize cloud video 2025-08-06 18:49:04 +08:00
fff43de9e3 🐛 Dozens of bug fixes 2025-08-06 18:15:13 +08:00
b31a915544 🐛 Bug fixes 2025-08-06 14:57:42 +08:00
41 changed files with 2193 additions and 505 deletions

View File

@@ -761,5 +761,12 @@
"publisher": "Publisher",
"publisherHint": "Enter the publisher name",
"publisherCannotBeEmpty": "Publisher cannot be empty",
"operationFailed": "Operation failed: {}"
"operationFailed": "Operation failed: {}",
"stickerMarketplace": "Sticker Marketplace",
"stickerPackAdded": "Sticker pack added to your collection",
"stickerPackRemoved": "Sticker pack removed from your collection",
"addPack": "Add Pack",
"removePack": "Remove Pack",
"browseAndAddStickers": "Browse and add sticker packs",
"stickerPack": "Sticker Pack"
}

View File

@@ -3,25 +3,6 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'embed.freezed.dart';
part 'embed.g.dart';
@freezed
sealed class SnEmbedLink with _$SnEmbedLink {
const factory SnEmbedLink({
@JsonKey(name: 'Type') required String type,
@JsonKey(name: 'Url') required String url,
@JsonKey(name: 'Title') required String title,
@JsonKey(name: 'Description') required String? description,
@JsonKey(name: 'ImageUrl') required String? imageUrl,
@JsonKey(name: 'FaviconUrl') @Default("") String faviconUrl,
@JsonKey(name: 'SiteName') @Default("") String siteName,
@JsonKey(name: 'ContentType') required String? contentType,
@JsonKey(name: 'Author') required String? author,
@JsonKey(name: 'PublishedDate') required DateTime? publishedDate,
}) = _SnEmbedLink;
factory SnEmbedLink.fromJson(Map<String, dynamic> json) =>
_$SnEmbedLinkFromJson(json);
}
@freezed
sealed class SnScrappedLink with _$SnScrappedLink {
const factory SnScrappedLink({

View File

@@ -12,290 +12,6 @@ part of 'embed.dart';
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnEmbedLink {
@JsonKey(name: 'Type') String get type;@JsonKey(name: 'Url') String get url;@JsonKey(name: 'Title') String get title;@JsonKey(name: 'Description') String? get description;@JsonKey(name: 'ImageUrl') String? get imageUrl;@JsonKey(name: 'FaviconUrl') String get faviconUrl;@JsonKey(name: 'SiteName') String get siteName;@JsonKey(name: 'ContentType') String? get contentType;@JsonKey(name: 'Author') String? get author;@JsonKey(name: 'PublishedDate') DateTime? get publishedDate;
/// Create a copy of SnEmbedLink
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnEmbedLinkCopyWith<SnEmbedLink> get copyWith => _$SnEmbedLinkCopyWithImpl<SnEmbedLink>(this as SnEmbedLink, _$identity);
/// Serializes this SnEmbedLink to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnEmbedLink&&(identical(other.type, type) || other.type == type)&&(identical(other.url, url) || other.url == url)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.faviconUrl, faviconUrl) || other.faviconUrl == faviconUrl)&&(identical(other.siteName, siteName) || other.siteName == siteName)&&(identical(other.contentType, contentType) || other.contentType == contentType)&&(identical(other.author, author) || other.author == author)&&(identical(other.publishedDate, publishedDate) || other.publishedDate == publishedDate));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,type,url,title,description,imageUrl,faviconUrl,siteName,contentType,author,publishedDate);
@override
String toString() {
return 'SnEmbedLink(type: $type, url: $url, title: $title, description: $description, imageUrl: $imageUrl, faviconUrl: $faviconUrl, siteName: $siteName, contentType: $contentType, author: $author, publishedDate: $publishedDate)';
}
}
/// @nodoc
abstract mixin class $SnEmbedLinkCopyWith<$Res> {
factory $SnEmbedLinkCopyWith(SnEmbedLink value, $Res Function(SnEmbedLink) _then) = _$SnEmbedLinkCopyWithImpl;
@useResult
$Res call({
@JsonKey(name: 'Type') String type,@JsonKey(name: 'Url') String url,@JsonKey(name: 'Title') String title,@JsonKey(name: 'Description') String? description,@JsonKey(name: 'ImageUrl') String? imageUrl,@JsonKey(name: 'FaviconUrl') String faviconUrl,@JsonKey(name: 'SiteName') String siteName,@JsonKey(name: 'ContentType') String? contentType,@JsonKey(name: 'Author') String? author,@JsonKey(name: 'PublishedDate') DateTime? publishedDate
});
}
/// @nodoc
class _$SnEmbedLinkCopyWithImpl<$Res>
implements $SnEmbedLinkCopyWith<$Res> {
_$SnEmbedLinkCopyWithImpl(this._self, this._then);
final SnEmbedLink _self;
final $Res Function(SnEmbedLink) _then;
/// Create a copy of SnEmbedLink
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? url = null,Object? title = null,Object? description = freezed,Object? imageUrl = freezed,Object? faviconUrl = null,Object? siteName = null,Object? contentType = freezed,Object? author = freezed,Object? publishedDate = freezed,}) {
return _then(_self.copyWith(
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable
as String?,faviconUrl: null == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable
as String,siteName: null == siteName ? _self.siteName : siteName // ignore: cast_nullable_to_non_nullable
as String,contentType: freezed == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable
as String?,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable
as String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
/// Adds pattern-matching-related methods to [SnEmbedLink].
extension SnEmbedLinkPatterns on SnEmbedLink {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnEmbedLink value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnEmbedLink() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnEmbedLink value) $default,){
final _that = this;
switch (_that) {
case _SnEmbedLink():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnEmbedLink value)? $default,){
final _that = this;
switch (_that) {
case _SnEmbedLink() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@JsonKey(name: 'Type') String type, @JsonKey(name: 'Url') String url, @JsonKey(name: 'Title') String title, @JsonKey(name: 'Description') String? description, @JsonKey(name: 'ImageUrl') String? imageUrl, @JsonKey(name: 'FaviconUrl') String faviconUrl, @JsonKey(name: 'SiteName') String siteName, @JsonKey(name: 'ContentType') String? contentType, @JsonKey(name: 'Author') String? author, @JsonKey(name: 'PublishedDate') DateTime? publishedDate)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnEmbedLink() when $default != null:
return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@JsonKey(name: 'Type') String type, @JsonKey(name: 'Url') String url, @JsonKey(name: 'Title') String title, @JsonKey(name: 'Description') String? description, @JsonKey(name: 'ImageUrl') String? imageUrl, @JsonKey(name: 'FaviconUrl') String faviconUrl, @JsonKey(name: 'SiteName') String siteName, @JsonKey(name: 'ContentType') String? contentType, @JsonKey(name: 'Author') String? author, @JsonKey(name: 'PublishedDate') DateTime? publishedDate) $default,) {final _that = this;
switch (_that) {
case _SnEmbedLink():
return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@JsonKey(name: 'Type') String type, @JsonKey(name: 'Url') String url, @JsonKey(name: 'Title') String title, @JsonKey(name: 'Description') String? description, @JsonKey(name: 'ImageUrl') String? imageUrl, @JsonKey(name: 'FaviconUrl') String faviconUrl, @JsonKey(name: 'SiteName') String siteName, @JsonKey(name: 'ContentType') String? contentType, @JsonKey(name: 'Author') String? author, @JsonKey(name: 'PublishedDate') DateTime? publishedDate)? $default,) {final _that = this;
switch (_that) {
case _SnEmbedLink() when $default != null:
return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnEmbedLink implements SnEmbedLink {
const _SnEmbedLink({@JsonKey(name: 'Type') required this.type, @JsonKey(name: 'Url') required this.url, @JsonKey(name: 'Title') required this.title, @JsonKey(name: 'Description') required this.description, @JsonKey(name: 'ImageUrl') required this.imageUrl, @JsonKey(name: 'FaviconUrl') this.faviconUrl = "", @JsonKey(name: 'SiteName') this.siteName = "", @JsonKey(name: 'ContentType') required this.contentType, @JsonKey(name: 'Author') required this.author, @JsonKey(name: 'PublishedDate') required this.publishedDate});
factory _SnEmbedLink.fromJson(Map<String, dynamic> json) => _$SnEmbedLinkFromJson(json);
@override@JsonKey(name: 'Type') final String type;
@override@JsonKey(name: 'Url') final String url;
@override@JsonKey(name: 'Title') final String title;
@override@JsonKey(name: 'Description') final String? description;
@override@JsonKey(name: 'ImageUrl') final String? imageUrl;
@override@JsonKey(name: 'FaviconUrl') final String faviconUrl;
@override@JsonKey(name: 'SiteName') final String siteName;
@override@JsonKey(name: 'ContentType') final String? contentType;
@override@JsonKey(name: 'Author') final String? author;
@override@JsonKey(name: 'PublishedDate') final DateTime? publishedDate;
/// Create a copy of SnEmbedLink
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnEmbedLinkCopyWith<_SnEmbedLink> get copyWith => __$SnEmbedLinkCopyWithImpl<_SnEmbedLink>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnEmbedLinkToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnEmbedLink&&(identical(other.type, type) || other.type == type)&&(identical(other.url, url) || other.url == url)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.faviconUrl, faviconUrl) || other.faviconUrl == faviconUrl)&&(identical(other.siteName, siteName) || other.siteName == siteName)&&(identical(other.contentType, contentType) || other.contentType == contentType)&&(identical(other.author, author) || other.author == author)&&(identical(other.publishedDate, publishedDate) || other.publishedDate == publishedDate));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,type,url,title,description,imageUrl,faviconUrl,siteName,contentType,author,publishedDate);
@override
String toString() {
return 'SnEmbedLink(type: $type, url: $url, title: $title, description: $description, imageUrl: $imageUrl, faviconUrl: $faviconUrl, siteName: $siteName, contentType: $contentType, author: $author, publishedDate: $publishedDate)';
}
}
/// @nodoc
abstract mixin class _$SnEmbedLinkCopyWith<$Res> implements $SnEmbedLinkCopyWith<$Res> {
factory _$SnEmbedLinkCopyWith(_SnEmbedLink value, $Res Function(_SnEmbedLink) _then) = __$SnEmbedLinkCopyWithImpl;
@override @useResult
$Res call({
@JsonKey(name: 'Type') String type,@JsonKey(name: 'Url') String url,@JsonKey(name: 'Title') String title,@JsonKey(name: 'Description') String? description,@JsonKey(name: 'ImageUrl') String? imageUrl,@JsonKey(name: 'FaviconUrl') String faviconUrl,@JsonKey(name: 'SiteName') String siteName,@JsonKey(name: 'ContentType') String? contentType,@JsonKey(name: 'Author') String? author,@JsonKey(name: 'PublishedDate') DateTime? publishedDate
});
}
/// @nodoc
class __$SnEmbedLinkCopyWithImpl<$Res>
implements _$SnEmbedLinkCopyWith<$Res> {
__$SnEmbedLinkCopyWithImpl(this._self, this._then);
final _SnEmbedLink _self;
final $Res Function(_SnEmbedLink) _then;
/// Create a copy of SnEmbedLink
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? url = null,Object? title = null,Object? description = freezed,Object? imageUrl = freezed,Object? faviconUrl = null,Object? siteName = null,Object? contentType = freezed,Object? author = freezed,Object? publishedDate = freezed,}) {
return _then(_SnEmbedLink(
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable
as String?,faviconUrl: null == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable
as String,siteName: null == siteName ? _self.siteName : siteName // ignore: cast_nullable_to_non_nullable
as String,contentType: freezed == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable
as String?,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable
as String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
/// @nodoc
mixin _$SnScrappedLink {

View File

@@ -6,36 +6,6 @@ part of 'embed.dart';
// JsonSerializableGenerator
// **************************************************************************
_SnEmbedLink _$SnEmbedLinkFromJson(Map<String, dynamic> json) => _SnEmbedLink(
type: json['Type'] as String,
url: json['Url'] as String,
title: json['Title'] as String,
description: json['Description'] as String?,
imageUrl: json['ImageUrl'] as String?,
faviconUrl: json['FaviconUrl'] as String? ?? "",
siteName: json['SiteName'] as String? ?? "",
contentType: json['ContentType'] as String?,
author: json['Author'] as String?,
publishedDate:
json['PublishedDate'] == null
? null
: DateTime.parse(json['PublishedDate'] as String),
);
Map<String, dynamic> _$SnEmbedLinkToJson(_SnEmbedLink instance) =>
<String, dynamic>{
'Type': instance.type,
'Url': instance.url,
'Title': instance.title,
'Description': instance.description,
'ImageUrl': instance.imageUrl,
'FaviconUrl': instance.faviconUrl,
'SiteName': instance.siteName,
'ContentType': instance.contentType,
'Author': instance.author,
'PublishedDate': instance.publishedDate?.toIso8601String(),
};
_SnScrappedLink _$SnScrappedLinkFromJson(Map<String, dynamic> json) =>
_SnScrappedLink(
type: json['type'] as String,

View File

@@ -90,3 +90,19 @@ enum SnPollQuestionType {
@JsonValue(4)
freeText,
}
@freezed
sealed class SnPollAnswer with _$SnPollAnswer {
const factory SnPollAnswer({
required String id,
required Map<String, dynamic> answer,
required String accountId,
required String pollId,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
}) = _SnPollAnswer;
factory SnPollAnswer.fromJson(Map<String, dynamic> json) =>
_$SnPollAnswerFromJson(json);
}

View File

@@ -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

View File

@@ -131,3 +131,28 @@ Map<String, dynamic> _$SnPollOptionToJson(_SnPollOption instance) =>
'description': instance.description,
'order': instance.order,
};
_SnPollAnswer _$SnPollAnswerFromJson(Map<String, dynamic> json) =>
_SnPollAnswer(
id: json['id'] as String,
answer: json['answer'] as Map<String, dynamic>,
accountId: json['account_id'] as String,
pollId: json['poll_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);
Map<String, dynamic> _$SnPollAnswerToJson(_SnPollAnswer instance) =>
<String, dynamic>{
'id': instance.id,
'answer': instance.answer,
'account_id': instance.accountId,
'poll_id': instance.pollId,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};

View File

@@ -35,6 +35,7 @@ sealed class SnStickerPack with _$SnStickerPack {
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
@Default([]) List<SnSticker> stickers,
}) = _SnStickerPack;
factory SnStickerPack.fromJson(Map<String, dynamic> json) =>

View File

@@ -338,7 +338,7 @@ $SnStickerPackCopyWith<$Res>? get pack {
/// @nodoc
mixin _$SnStickerPack {
String get id; String get name; String get description; String get prefix; String get publisherId; SnPublisher? get publisher; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
String get id; String get name; String get description; String get prefix; String get publisherId; SnPublisher? get publisher; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; List<SnSticker> get stickers;
/// Create a copy of SnStickerPack
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -351,16 +351,16 @@ $SnStickerPackCopyWith<SnStickerPack> get copyWith => _$SnStickerPackCopyWithImp
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnStickerPack&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.prefix, prefix) || other.prefix == prefix)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnStickerPack&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.prefix, prefix) || other.prefix == prefix)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other.stickers, stickers));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,description,prefix,publisherId,publisher,createdAt,updatedAt,deletedAt);
int get hashCode => Object.hash(runtimeType,id,name,description,prefix,publisherId,publisher,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(stickers));
@override
String toString() {
return 'SnStickerPack(id: $id, name: $name, description: $description, prefix: $prefix, publisherId: $publisherId, publisher: $publisher, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnStickerPack(id: $id, name: $name, description: $description, prefix: $prefix, publisherId: $publisherId, publisher: $publisher, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, stickers: $stickers)';
}
@@ -371,7 +371,7 @@ abstract mixin class $SnStickerPackCopyWith<$Res> {
factory $SnStickerPackCopyWith(SnStickerPack value, $Res Function(SnStickerPack) _then) = _$SnStickerPackCopyWithImpl;
@useResult
$Res call({
String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnSticker> stickers
});
@@ -388,7 +388,7 @@ class _$SnStickerPackCopyWithImpl<$Res>
/// Create a copy of SnStickerPack
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = null,Object? prefix = null,Object? publisherId = null,Object? publisher = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = null,Object? prefix = null,Object? publisherId = null,Object? publisher = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? stickers = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
@@ -399,7 +399,8 @@ as String,publisher: freezed == publisher ? _self.publisher : publisher // ignor
as SnPublisher?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
as DateTime?,stickers: null == stickers ? _self.stickers : stickers // ignore: cast_nullable_to_non_nullable
as List<SnSticker>,
));
}
/// Create a copy of SnStickerPack
@@ -493,10 +494,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnSticker> stickers)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnStickerPack() when $default != null:
return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.stickers);case _:
return orElse();
}
@@ -514,10 +515,10 @@ return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publish
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnSticker> stickers) $default,) {final _that = this;
switch (_that) {
case _SnStickerPack():
return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt);}
return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.stickers);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -531,10 +532,10 @@ return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publish
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnSticker> stickers)? $default,) {final _that = this;
switch (_that) {
case _SnStickerPack() when $default != null:
return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.stickers);case _:
return null;
}
@@ -546,7 +547,7 @@ return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publish
@JsonSerializable()
class _SnStickerPack implements SnStickerPack {
const _SnStickerPack({required this.id, required this.name, required this.description, required this.prefix, required this.publisherId, required this.publisher, required this.createdAt, required this.updatedAt, required this.deletedAt});
const _SnStickerPack({required this.id, required this.name, required this.description, required this.prefix, required this.publisherId, required this.publisher, required this.createdAt, required this.updatedAt, required this.deletedAt, final List<SnSticker> stickers = const []}): _stickers = stickers;
factory _SnStickerPack.fromJson(Map<String, dynamic> json) => _$SnStickerPackFromJson(json);
@override final String id;
@@ -558,6 +559,13 @@ class _SnStickerPack implements SnStickerPack {
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
final List<SnSticker> _stickers;
@override@JsonKey() List<SnSticker> get stickers {
if (_stickers is EqualUnmodifiableListView) return _stickers;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_stickers);
}
/// Create a copy of SnStickerPack
/// with the given fields replaced by the non-null parameter values.
@@ -572,16 +580,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnStickerPack&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.prefix, prefix) || other.prefix == prefix)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnStickerPack&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.prefix, prefix) || other.prefix == prefix)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other._stickers, _stickers));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,description,prefix,publisherId,publisher,createdAt,updatedAt,deletedAt);
int get hashCode => Object.hash(runtimeType,id,name,description,prefix,publisherId,publisher,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(_stickers));
@override
String toString() {
return 'SnStickerPack(id: $id, name: $name, description: $description, prefix: $prefix, publisherId: $publisherId, publisher: $publisher, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnStickerPack(id: $id, name: $name, description: $description, prefix: $prefix, publisherId: $publisherId, publisher: $publisher, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, stickers: $stickers)';
}
@@ -592,7 +600,7 @@ abstract mixin class _$SnStickerPackCopyWith<$Res> implements $SnStickerPackCopy
factory _$SnStickerPackCopyWith(_SnStickerPack value, $Res Function(_SnStickerPack) _then) = __$SnStickerPackCopyWithImpl;
@override @useResult
$Res call({
String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnSticker> stickers
});
@@ -609,7 +617,7 @@ class __$SnStickerPackCopyWithImpl<$Res>
/// Create a copy of SnStickerPack
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = null,Object? prefix = null,Object? publisherId = null,Object? publisher = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = null,Object? prefix = null,Object? publisherId = null,Object? publisher = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? stickers = null,}) {
return _then(_SnStickerPack(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
@@ -620,7 +628,8 @@ as String,publisher: freezed == publisher ? _self.publisher : publisher // ignor
as SnPublisher?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
as DateTime?,stickers: null == stickers ? _self._stickers : stickers // ignore: cast_nullable_to_non_nullable
as List<SnSticker>,
));
}

View File

@@ -54,6 +54,11 @@ _SnStickerPack _$SnStickerPackFromJson(Map<String, dynamic> json) =>
json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
stickers:
(json['stickers'] as List<dynamic>?)
?.map((e) => SnSticker.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
);
Map<String, dynamic> _$SnStickerPackToJson(_SnStickerPack instance) =>
@@ -67,4 +72,5 @@ Map<String, dynamic> _$SnStickerPackToJson(_SnStickerPack instance) =>
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'stickers': instance.stickers.map((e) => e.toJson()).toList(),
};

View File

@@ -28,6 +28,8 @@ import 'package:island/screens/creators/hub.dart';
import 'package:island/screens/creators/posts/post_manage_list.dart';
import 'package:island/screens/creators/stickers/stickers.dart';
import 'package:island/screens/creators/stickers/pack_detail.dart';
import 'package:island/screens/stickers/marketplace.dart';
import 'package:island/screens/stickers/pack_detail.dart';
import 'package:island/screens/creators/poll/poll_list.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/screens/creators/webfeed/webfeed_list.dart';
@@ -451,6 +453,23 @@ final routerProvider = Provider<GoRouter>((ref) {
path: '/account',
builder: (context, state) => const AccountScreen(),
),
// Sticker marketplace (user-facing, no publisher)
GoRoute(
name: 'stickerMarketplace',
path: '/stickers',
builder:
(context, state) => const MarketplaceStickersScreen(),
routes: [
GoRoute(
name: 'stickerPackDetail',
path: ':packId',
builder: (context, state) {
final packId = state.pathParameters['packId']!;
return MarketplaceStickerPackDetailScreen(id: packId);
},
),
],
),
GoRoute(
name: 'notifications',
path: '/account/notifications',

View File

@@ -205,7 +205,7 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
title: 'aboutScreenTermsOfServiceTitle'.tr(),
onTap:
() => _launchURL(
'https://solsynth.dev/terms/basic-law',
'https://solsynth.dev/terms/user-agreement',
),
),
_buildListTile(

View File

@@ -189,7 +189,6 @@ class AccountScreen extends HookConsumerWidget {
),
],
).padding(horizontal: 8),
const Gap(8),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.notifications),
@@ -228,6 +227,16 @@ class AccountScreen extends HookConsumerWidget {
context.pushNamed('relationships');
},
),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.emoji_emotions),
trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('stickers').tr(),
onTap: () {
context.pushNamed('stickerMarketplace');
},
),
ListTile(
minTileHeight: 48,
title: Text('abuseReports').tr(),

View File

@@ -35,6 +35,7 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'chat.dart';
import 'package:island/widgets/chat/call_button.dart';
import 'package:island/widgets/stickers/picker.dart';
part 'room.g.dart';
@@ -1060,13 +1061,16 @@ class _ChatInput extends HookConsumerWidget {
children: [
if (attachments.isNotEmpty)
SizedBox(
height: 280,
height: 324,
child: ListView.separated(
padding: EdgeInsets.symmetric(horizontal: 12),
scrollDirection: Axis.horizontal,
itemCount: attachments.length,
itemBuilder: (context, idx) {
return AttachmentPreview(
return SizedBox(
height: 320,
width: 280,
child: AttachmentPreview(
item: attachments[idx],
onRequestUpload: () => onUploadAttachment(idx),
onDelete: () => onDeleteAttachment(idx),
@@ -1075,6 +1079,7 @@ class _ChatInput extends HookConsumerWidget {
onAttachmentsChanged(attachments);
},
onMove: (delta) => onMoveAttachment(idx, delta),
),
);
},
separatorBuilder: (_, _) => const Gap(8),
@@ -1129,6 +1134,49 @@ class _ChatInput extends HookConsumerWidget {
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
child: Row(
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
tooltip: 'stickers'.tr(),
icon: const Icon(Symbols.emoji_symbols),
onPressed: () {
final size = MediaQuery.of(context).size;
showStickerPickerPopover(
context,
Offset(
20,
size.height -
480 -
MediaQuery.of(context).padding.bottom,
),
onPick: (placeholder) {
// Insert placeholder at current cursor position
final text = messageController.text;
final selection = messageController.selection;
final start =
selection.start >= 0
? selection.start
: text.length;
final end =
selection.end >= 0
? selection.end
: text.length;
final newText = text.replaceRange(
start,
end,
placeholder,
);
messageController.value = TextEditingValue(
text: newText,
selection: TextSelection.collapsed(
offset: start + placeholder.length,
),
);
},
);
},
),
PopupMenuButton(
icon: const Icon(Symbols.photo_library),
itemBuilder:
@@ -1155,6 +1203,8 @@ class _ChatInput extends HookConsumerWidget {
),
],
),
],
),
Expanded(
child: RawKeyboardListener(
focusNode: FocusNode(),

View File

@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/poll.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/poll/poll_feedback.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
@@ -164,10 +165,13 @@ class _CreatorPollItem extends StatelessWidget {
],
),
onTap: () {
// Open editor for edit
// Navigator push by path to keep consistency with rest of app:
// Note: pub name string may be required in route; when absent, route may need query or pick later.
// For safety, just do nothing if no publisher in list item.
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder:
(context) => PollFeedbackSheet(pollId: poll.id, poll: poll),
);
},
),
);

View File

@@ -218,6 +218,11 @@ class ExploreScreen extends HookConsumerWidget {
right: 12,
top: 16,
),
onChecked: () {
ref.invalidate(
eventCalendarProvider(query.value),
);
},
),
Card(
margin: EdgeInsets.only(left: 8, right: 12, top: 8),

View File

@@ -207,8 +207,6 @@ class PostComposeScreen extends HookConsumerWidget {
isScrollControlled: true,
builder:
(context) => ComposeSettingsSheet(
titleController: state.titleController,
descriptionController: state.descriptionController,
visibility: state.visibility,
tagsController: state.tagsController,
categoriesController: state.categoriesController,
@@ -370,14 +368,52 @@ class PostComposeScreen extends HookConsumerWidget {
// Post content form
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 12),
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: state.titleController,
decoration: InputDecoration(
hintText: 'postTitle'.tr(),
border: InputBorder.none,
isCollapsed: true,
contentPadding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
),
style: theme.textTheme.titleMedium,
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
?.unfocus(),
),
TextField(
controller: state.descriptionController,
decoration: InputDecoration(
hintText: 'postDescription'.tr(),
border: InputBorder.none,
isCollapsed: true,
contentPadding: const EdgeInsets.fromLTRB(
8,
4,
8,
12,
),
),
style: theme.textTheme.bodyMedium,
minLines: 1,
maxLines: 3,
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
?.unfocus(),
),
// Content field with borderless design
RawKeyboardListener(
KeyboardListener(
focusNode: FocusNode(),
onKey:
onKeyEvent:
(event) => ComposeLogic.handleKeyPress(
event,
state,
@@ -393,7 +429,11 @@ class PostComposeScreen extends HookConsumerWidget {
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'postContent'.tr(),
contentPadding: const EdgeInsets.all(8),
isCollapsed: true,
contentPadding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
),
maxLines: null,
onTapOutside:

View File

@@ -140,8 +140,6 @@ class ArticleComposeScreen extends HookConsumerWidget {
isScrollControlled: true,
builder:
(context) => ComposeSettingsSheet(
titleController: state.titleController,
descriptionController: state.descriptionController,
visibility: state.visibility,
tagsController: state.tagsController,
categoriesController: state.categoriesController,
@@ -242,10 +240,39 @@ class ArticleComposeScreen extends HookConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: state.titleController,
decoration: InputDecoration(
hintText: 'postTitle'.tr(),
border: InputBorder.none,
isCollapsed: true,
contentPadding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
),
style: theme.textTheme.titleMedium,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
TextField(
controller: state.descriptionController,
decoration: InputDecoration(
hintText: 'postDescription'.tr(),
border: InputBorder.none,
isCollapsed: true,
contentPadding: const EdgeInsets.fromLTRB(8, 4, 8, 12),
),
style: theme.textTheme.bodyMedium,
minLines: 1,
maxLines: 3,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
Expanded(
child: RawKeyboardListener(
child: KeyboardListener(
focusNode: FocusNode(),
onKey:
onKeyEvent:
(event) => _handleKeyPress(
event,
state,
@@ -454,7 +481,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
flex: showPreview.value ? 1 : 2,
child: buildEditorPane(),
),
const VerticalDivider(),
if (showPreview.value) const VerticalDivider(),
if (showPreview.value)
Expanded(child: buildPreviewPane()),
],
@@ -475,7 +502,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
// Helper method to handle keyboard shortcuts
void _handleKeyPress(
RawKeyEvent event,
KeyEvent event,
ComposeState state,
WidgetRef ref,
BuildContext context, {
@@ -485,7 +512,9 @@ class ArticleComposeScreen extends HookConsumerWidget {
final isPaste = event.logicalKey == LogicalKeyboardKey.keyV;
final isSave = event.logicalKey == LogicalKeyboardKey.keyS;
final isModifierPressed = event.isMetaPressed || event.isControlPressed;
final isModifierPressed =
HardwareKeyboard.instance.isMetaPressed ||
HardwareKeyboard.instance.isControlPressed;
final isSubmit = event.logicalKey == LogicalKeyboardKey.enter;
if (isPaste && isModifierPressed) {

View File

@@ -87,6 +87,12 @@ class PublisherProfileScreen extends HookConsumerWidget {
publisherAppbarForcegroundColorProvider(name),
);
final categoryTabController = useTabController(initialLength: 3);
final categoryTab = useState(0);
categoryTabController.addListener(() {
categoryTab.value = categoryTabController.index;
});
final subscribing = useState(false);
Future<void> subscribe() async {
@@ -268,6 +274,16 @@ class PublisherProfileScreen extends HookConsumerWidget {
).padding(horizontal: 20, vertical: 16),
);
Widget publisherCategoryTabWidget() => Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: TabBar(
controller: categoryTabController,
dividerColor: Colors.transparent,
splashBorderRadius: const BorderRadius.all(Radius.circular(8)),
tabs: [Tab(text: 'All'), Tab(text: 'Posts'), Tab(text: 'Articles')],
),
);
return publisher.when(
data:
(data) => AppScaffold(
@@ -321,7 +337,18 @@ class PublisherProfileScreen extends HookConsumerWidget {
child: CustomScrollView(
slivers: [
SliverGap(16),
SliverPostList(pubName: name),
SliverToBoxAdapter(
child: publisherCategoryTabWidget(),
),
SliverPostList(
key: ValueKey(categoryTab.value),
pubName: name,
type: switch (categoryTab.value) {
1 => 0,
2 => 1,
_ => null,
},
),
SliverGap(
MediaQuery.of(context).padding.bottom + 16,
),
@@ -334,9 +361,9 @@ class PublisherProfileScreen extends HookConsumerWidget {
alignment: Alignment.topLeft,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
publisherBasisWidget(data),
publisherBasisWidget(data).padding(bottom: 8),
publisherBadgesWidget(data),
publisherVerificationWidget(data),
publisherBioWidget(data),
@@ -398,7 +425,16 @@ class PublisherProfileScreen extends HookConsumerWidget {
child: publisherVerificationWidget(data),
),
SliverToBoxAdapter(child: publisherBioWidget(data)),
SliverPostList(pubName: name),
SliverToBoxAdapter(child: publisherCategoryTabWidget()),
SliverPostList(
key: ValueKey(categoryTab.value),
pubName: name,
type: switch (categoryTab.value) {
1 => 0,
2 => 1,
_ => null,
},
),
SliverGap(MediaQuery.of(context).padding.bottom + 16),
],
),

View 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},
);
},
);
},
),
);
}
}

View 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

View 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(),
),
);
}
}

View 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
View 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;
}

View File

@@ -130,9 +130,22 @@ class AccountStatusWidget extends HookConsumerWidget {
size: 16,
).padding(right: 4),
if (status.value?.isCustomized ?? false)
Text(status.value?.label ?? 'unknown'.tr())
Flexible(
child: Text(
status.value?.label ?? 'unknown'.tr(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
)
else
Text((status.value?.label ?? 'offline').toLowerCase()).tr(),
Flexible(
child:
Text(
(status.value?.label ?? 'offline').toLowerCase(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
).tr(),
),
if (!(status.value?.isOnline ?? false) &&
account.value?.profile.lastSeenAt != null)
Flexible(

View File

@@ -14,6 +14,7 @@ import 'package:island/models/embed.dart';
import 'package:island/pods/call.dart';
import 'package:island/pods/translate.dart';
import 'package:island/screens/chat/room.dart';
import 'package:island/utils/mapping.dart';
import 'package:island/widgets/account/account_name.dart';
import 'package:island/widgets/account/account_pfc.dart';
import 'package:island/widgets/app_scaffold.dart';
@@ -292,12 +293,11 @@ class MessageItem extends HookConsumerWidget {
),
if (remoteMessage.meta['embeds'] != null)
...((remoteMessage.meta['embeds'] as List<dynamic>)
.where((embed) => embed['Type'] == 'link')
.map(
(embed) => SnEmbedLink.fromJson(
embed as Map<String, dynamic>,
),
(embed) => convertMapKeysToSnakeCase(embed),
)
.where((embed) => embed['type'] == 'link')
.map((embed) => SnScrappedLink.fromJson(embed))
.map(
(link) => LayoutBuilder(
builder: (context, constraints) {

View File

@@ -36,7 +36,8 @@ Future<SnCheckInResult?> checkInResultToday(Ref ref) async {
class CheckInWidget extends HookConsumerWidget {
final EdgeInsets? margin;
const CheckInWidget({super.key, this.margin});
final VoidCallback? onChecked;
const CheckInWidget({super.key, this.margin, this.onChecked});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -52,6 +53,7 @@ class CheckInWidget extends HookConsumerWidget {
ref.invalidate(checkInResultTodayProvider);
final userNotifier = ref.read(userInfoProvider.notifier);
userNotifier.fetchUser();
onChecked?.call();
} catch (err) {
if (err is DioException) {
if (err.response?.statusCode == 423 && context.mounted) {

View File

@@ -1,5 +1,4 @@
import 'dart:math' as math;
import 'dart:ui';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
@@ -103,6 +102,7 @@ class CloudVideoWidget extends HookConsumerWidget {
Symbols.play_arrow,
fill: 1,
size: 32,
color: Colors.white,
shadows: [
BoxShadow(
color: Colors.black54,
@@ -114,6 +114,26 @@ class CloudVideoWidget extends HookConsumerWidget {
),
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: IgnorePointer(
child: Container(
height: 100,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Theme.of(context).colorScheme.surface.withOpacity(0.85),
Colors.transparent,
],
),
),
),
),
),
Positioned(
bottom: 0,
left: 0,
@@ -133,6 +153,7 @@ class CloudVideoWidget extends HookConsumerWidget {
.toInt(),
).formatDuration(),
style: TextStyle(
color: Colors.white,
shadows: [
BoxShadow(
color: Colors.black54,
@@ -147,6 +168,7 @@ class CloudVideoWidget extends HookConsumerWidget {
Text(
'${int.parse(item.fileMeta?['bit_rate'] as String) ~/ 1000} Kbps',
style: TextStyle(
color: Colors.white,
shadows: [
BoxShadow(
color: Colors.black54,
@@ -161,7 +183,10 @@ class CloudVideoWidget extends HookConsumerWidget {
),
Text(
item.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
shadows: [
BoxShadow(

View File

@@ -6,7 +6,7 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:url_launcher/url_launcher.dart';
class EmbedLinkWidget extends StatelessWidget {
final SnEmbedLink link;
final SnScrappedLink link;
final double? maxWidth;
final EdgeInsetsGeometry? margin;
@@ -116,7 +116,8 @@ class EmbedLinkWidget extends StatelessWidget {
],
// Description
if (link.description != null && link.description!.isNotEmpty) ...[
if (link.description != null &&
link.description!.isNotEmpty) ...[
Text(
link.description!,
style: theme.textTheme.bodyMedium?.copyWith(

View 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,
);
}
}

View 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

View File

@@ -3,21 +3,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:island/models/poll.dart';
import 'package:island/pods/network.dart';
/// A poll answering widget that shows one question at a time and collects answers.
///
/// Usage:
/// PollSubmit(
/// poll: poll,
/// onSubmit: (answers) {
/// // answers is Map<String, dynamic>: questionId -> answer
/// // answer types by question:
/// // - singleChoice: String optionId
/// // - multipleChoice: List<String> optionIds
/// // - yesNo: bool
/// // - rating: int (1..5)
/// // - freeText: String
/// },
/// )
class PollSubmit extends ConsumerStatefulWidget {
const PollSubmit({
super.key,

View File

@@ -189,8 +189,8 @@ class ComposePollSheet extends HookConsumerWidget {
Widget? _buildPollSubtitle(SnPoll poll) {
try {
final SnPoll dyn = poll;
final List<SnPollQuestion>? options = dyn.questions;
if (options == null || options.isEmpty) return null;
final List<SnPollQuestion> options = dyn.questions;
if (options.isEmpty) return null;
final preview = options.take(3).map((e) => e.title).join(' · ');
if (preview.trim().isEmpty) return null;
return Text(preview);

View File

@@ -99,8 +99,6 @@ class ChipTagInputField extends StatelessWidget {
}
class ComposeSettingsSheet extends HookWidget {
final TextEditingController titleController;
final TextEditingController descriptionController;
final ValueNotifier<int> visibility;
final VoidCallback? onVisibilityChanged;
final StringTagController tagsController;
@@ -108,8 +106,6 @@ class ComposeSettingsSheet extends HookWidget {
const ComposeSettingsSheet({
super.key,
required this.titleController,
required this.descriptionController,
required this.visibility,
this.onVisibilityChanged,
required this.tagsController,
@@ -216,39 +212,6 @@ class ComposeSettingsSheet extends HookWidget {
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 16,
children: [
// Title field
TextField(
controller: titleController,
decoration: InputDecoration(
labelText: 'postTitle'.tr(),
hintText: 'postTitle'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.all(16),
),
style: theme.textTheme.titleMedium,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
// Description field
TextField(
controller: descriptionController,
decoration: InputDecoration(
labelText: 'postDescription'.tr(),
hintText: 'postDescription'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.all(16),
),
style: theme.textTheme.bodyMedium,
maxLines: 3,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
// Tags field
TextFieldTags(
textfieldTagsController: tagsController,

View File

@@ -689,7 +689,7 @@ class ComposeLogic {
}
static void handleKeyPress(
RawKeyEvent event,
KeyEvent event,
ComposeState state,
WidgetRef ref,
BuildContext context, {
@@ -701,7 +701,9 @@ class ComposeLogic {
final isPaste = event.logicalKey == LogicalKeyboardKey.keyV;
final isSave = event.logicalKey == LogicalKeyboardKey.keyS;
final isModifierPressed = event.isMetaPressed || event.isControlPressed;
final isModifierPressed =
HardwareKeyboard.instance.isMetaPressed ||
HardwareKeyboard.instance.isControlPressed;
final isSubmit = event.logicalKey == LogicalKeyboardKey.enter;
if (isPaste && isModifierPressed) {

View File

@@ -63,6 +63,7 @@ class ComposeToolbar extends HookConsumerWidget {
return Material(
elevation: 4,
color: Theme.of(context).colorScheme.surfaceContainerLow,
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560),

View File

@@ -535,7 +535,7 @@ class PostItem extends HookConsumerWidget {
right: renderingPadding.horizontal,
),
),
if (item.attachments.isNotEmpty)
if (item.attachments.isNotEmpty && item.type != 1)
CloudFileList(
files: item.attachments,
padding: EdgeInsets.symmetric(
@@ -547,7 +547,9 @@ class PostItem extends HookConsumerWidget {
...((item.meta!['embeds'] as List<dynamic>).map(
(embedData) => switch (embedData['type']) {
'link' => EmbedLinkWidget(
link: SnEmbedLink.fromJson(embedData as Map<String, dynamic>),
link: SnScrappedLink.fromJson(
embedData as Map<String, dynamic>,
),
maxWidth: math.min(
MediaQuery.of(context).size.width,
kWideScreenWidth,
@@ -770,7 +772,7 @@ class PostReplyPreview extends HookConsumerWidget {
final posts = useState<List<SnPost>>([]);
final loading = useState(false);
Future<void> fetchMoreReplies({int pageSize = 1}) async {
Future<void> fetchMoreReplies({int pageSize = 3}) async {
final client = ref.read(apiClientProvider);
loading.value = true;
@@ -877,38 +879,40 @@ class PostReplyPreview extends HookConsumerWidget {
),
],
)
: featuredReply!.when(
: (featuredReply!).map(
data:
(value) => Row(
(data) => Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 8,
children: [
ProfilePictureWidget(
file: value?.publisher.picture,
file: data.value?.publisher.picture,
radius: 12,
).padding(top: 4),
if (value?.content?.isNotEmpty ?? false)
if (data.value?.content?.isNotEmpty ?? false)
Expanded(
child: MarkdownTextContent(content: value!.content!),
child: MarkdownTextContent(
content: data.value!.content!,
),
)
else
Expanded(
child: Text(
'postHasAttachments',
).plural(value?.attachments.length ?? 0),
).plural(data.value?.attachments.length ?? 0),
),
],
),
error:
(error, _) => Row(
(e) => Row(
spacing: 8,
children: [
const Icon(Symbols.close, size: 18),
Text(error.toString()),
Text(e.error.toString()),
],
),
loading:
() => Row(
(_) => Row(
spacing: 8,
children: [
SizedBox(
@@ -939,7 +943,6 @@ class PostReplyPreview extends HookConsumerWidget {
children: [
Text('repliesCount')
.plural(parent.repliesCount)
.tr()
.fontSize(15)
.bold()
.padding(horizontal: 5),

View File

@@ -15,7 +15,7 @@ class PostListNotifier extends _$PostListNotifier
static const int _pageSize = 20;
@override
Future<CursorPagingData<SnPost>> build(String? pubName) {
Future<CursorPagingData<SnPost>> build(String? pubName, int? type) {
return fetch(cursor: null);
}
@@ -28,6 +28,7 @@ class PostListNotifier extends _$PostListNotifier
'offset': offset,
'take': _pageSize,
if (pubName != null) 'pub': pubName,
if (type != null) 'type': type,
};
final response = await client.get(
@@ -60,6 +61,7 @@ enum PostItemType {
class SliverPostList extends HookConsumerWidget {
final String? pubName;
final int? type;
final PostItemType itemType;
final Color? backgroundColor;
final EdgeInsets? padding;
@@ -70,6 +72,7 @@ class SliverPostList extends HookConsumerWidget {
const SliverPostList({
super.key,
this.pubName,
this.type,
this.itemType = PostItemType.regular,
this.backgroundColor,
this.padding,
@@ -81,9 +84,9 @@ class SliverPostList extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return PagingHelperSliverView(
provider: postListNotifierProvider(pubName),
futureRefreshable: postListNotifierProvider(pubName).future,
notifierRefreshable: postListNotifierProvider(pubName).notifier,
provider: postListNotifierProvider(pubName, type),
futureRefreshable: postListNotifierProvider(pubName, type).future,
notifierRefreshable: postListNotifierProvider(pubName, type).notifier,
contentBuilder:
(data, widgetCount, endItemView) => SliverList.builder(
itemCount: widgetCount,

View File

@@ -6,7 +6,7 @@ part of 'post_list.dart';
// RiverpodGenerator
// **************************************************************************
String _$postListNotifierHash() => r'2e4fb36123d3f97ac1edf9945043251d4eb519a2';
String _$postListNotifierHash() => r'78222d62957f85713d17aecd95af0305b764e86c';
/// Copied from Dart SDK
class _SystemHash {
@@ -32,8 +32,9 @@ class _SystemHash {
abstract class _$PostListNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPost>> {
late final String? pubName;
late final int? type;
FutureOr<CursorPagingData<SnPost>> build(String? pubName);
FutureOr<CursorPagingData<SnPost>> build(String? pubName, int? type);
}
/// See also [PostListNotifier].
@@ -47,15 +48,15 @@ class PostListNotifierFamily
const PostListNotifierFamily();
/// See also [PostListNotifier].
PostListNotifierProvider call(String? pubName) {
return PostListNotifierProvider(pubName);
PostListNotifierProvider call(String? pubName, int? type) {
return PostListNotifierProvider(pubName, type);
}
@override
PostListNotifierProvider getProviderOverride(
covariant PostListNotifierProvider provider,
) {
return call(provider.pubName);
return call(provider.pubName, provider.type);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@@ -81,9 +82,12 @@ class PostListNotifierProvider
CursorPagingData<SnPost>
> {
/// See also [PostListNotifier].
PostListNotifierProvider(String? pubName)
PostListNotifierProvider(String? pubName, int? type)
: this._internal(
() => PostListNotifier()..pubName = pubName,
() =>
PostListNotifier()
..pubName = pubName
..type = type,
from: postListNotifierProvider,
name: r'postListNotifierProvider',
debugGetCreateSourceHash:
@@ -94,6 +98,7 @@ class PostListNotifierProvider
allTransitiveDependencies:
PostListNotifierFamily._allTransitiveDependencies,
pubName: pubName,
type: type,
);
PostListNotifierProvider._internal(
@@ -104,15 +109,17 @@ class PostListNotifierProvider
required super.debugGetCreateSourceHash,
required super.from,
required this.pubName,
required this.type,
}) : super.internal();
final String? pubName;
final int? type;
@override
FutureOr<CursorPagingData<SnPost>> runNotifierBuild(
covariant PostListNotifier notifier,
) {
return notifier.build(pubName);
return notifier.build(pubName, type);
}
@override
@@ -120,13 +127,17 @@ class PostListNotifierProvider
return ProviderOverride(
origin: this,
override: PostListNotifierProvider._internal(
() => create()..pubName = pubName,
() =>
create()
..pubName = pubName
..type = type,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
pubName: pubName,
type: type,
),
);
}
@@ -142,13 +153,16 @@ class PostListNotifierProvider
@override
bool operator ==(Object other) {
return other is PostListNotifierProvider && other.pubName == pubName;
return other is PostListNotifierProvider &&
other.pubName == pubName &&
other.type == type;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, pubName.hashCode);
hash = _SystemHash.combine(hash, type.hashCode);
return _SystemHash.finish(hash);
}
@@ -160,6 +174,9 @@ mixin PostListNotifierRef
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnPost>> {
/// The parameter `pubName` of this provider.
String? get pubName;
/// The parameter `type` of this provider.
int? get type;
}
class _PostListNotifierProviderElement
@@ -173,6 +190,8 @@ class _PostListNotifierProviderElement
@override
String? get pubName => (origin as PostListNotifierProvider).pubName;
@override
int? get type => (origin as PostListNotifierProvider).type;
}
// ignore_for_file: type=lint

View File

@@ -0,0 +1,307 @@
import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/sticker.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:flutter_popup_card/flutter_popup_card.dart';
part 'picker.g.dart';
/// Fetch user-added sticker packs (with stickers) from API:
/// GET /sphere/stickers/me
@riverpod
Future<List<SnStickerPack>> myStickerPacks(Ref ref) async {
final api = ref.watch(apiClientProvider);
final resp = await api.get('/sphere/stickers/me');
final data = resp.data;
if (data is List) {
return data
.map((e) => SnStickerPack.fromJson(e as Map<String, dynamic>))
.toList();
}
return const <SnStickerPack>[];
}
/// Sticker Picker popover dialog
/// - Displays user-owned sticker packs as tabs (chips)
/// - Shows grid of stickers in selected pack
/// - On tap, returns placeholder string :{prefix}{slug}: via onPick callback
class StickerPicker extends HookConsumerWidget {
final void Function(String placeholder) onPick;
const StickerPicker({super.key, required this.onPick});
@override
Widget build(BuildContext context, WidgetRef ref) {
final packsAsync = ref.watch(myStickerPacksProvider);
return PopupCard(
elevation: 8,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520, maxHeight: 520),
child: packsAsync.when(
data: (packs) {
if (packs.isEmpty) {
return _EmptyState(
onRefresh: () async {
ref.invalidate(myStickerPacksProvider);
},
);
}
// Maintain selected index locally with a ValueNotifier to avoid hooks dependency
return _PackSwitcher(
packs: packs,
onPick: (pack, sticker) {
final placeholder = ':${pack.prefix}${sticker.slug}:';
HapticFeedback.selectionClick();
onPick(placeholder);
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
},
onRefresh: () async {
ref.invalidate(myStickerPacksProvider);
},
);
},
loading:
() => const SizedBox(
width: 320,
height: 320,
child: Center(child: CircularProgressIndicator()),
),
error:
(err, _) => SizedBox(
width: 360,
height: 200,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Symbols.error, size: 28),
const Gap(8),
Text('Error: $err', textAlign: TextAlign.center),
const Gap(12),
FilledButton.icon(
onPressed: () => ref.invalidate(myStickerPacksProvider),
icon: const Icon(Symbols.refresh),
label: Text('retry').tr(),
),
],
).padding(all: 16),
),
),
),
);
}
}
class _EmptyState extends StatelessWidget {
final Future<void> Function() onRefresh;
const _EmptyState({required this.onRefresh});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 360,
height: 220,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Symbols.emoji_symbols, size: 28),
const Gap(8),
Text('noStickerPacks'.tr(), textAlign: TextAlign.center),
const Gap(12),
OutlinedButton.icon(
onPressed: onRefresh,
icon: const Icon(Symbols.refresh),
label: Text('refresh').tr(),
),
],
).padding(all: 16),
);
}
}
class _PackSwitcher extends StatefulWidget {
final List<SnStickerPack> packs;
final void Function(SnStickerPack pack, SnSticker sticker) onPick;
final Future<void> Function() onRefresh;
const _PackSwitcher({
required this.packs,
required this.onPick,
required this.onRefresh,
});
@override
State<_PackSwitcher> createState() => _PackSwitcherState();
}
class _PackSwitcherState extends State<_PackSwitcher> {
int _index = 0;
@override
Widget build(BuildContext context) {
final packs = widget.packs;
_index = _index.clamp(0, packs.length - 1);
final selectedPack = packs[_index];
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Header
Row(
children: [
const Icon(Symbols.sticky_note_2, size: 20),
const Gap(8),
Text(
'stickers'.tr(),
style: Theme.of(context).textTheme.titleMedium,
),
const Spacer(),
IconButton(
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
tooltip: 'close'.tr(),
onPressed: () => Navigator.of(context).maybePop(),
icon: const Icon(Symbols.close),
),
],
).padding(horizontal: 12, top: 8),
// Vertical, scrollable packs rail like common emoji pickers
SizedBox(
height: 48,
child: ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 8),
scrollDirection: Axis.horizontal,
itemCount: packs.length,
separatorBuilder: (_, _) => const Gap(4),
itemBuilder: (context, i) {
final selected = _index == i;
return Tooltip(
message: packs[i].name,
child: FilterChip(
label: Text(packs[i].name, overflow: TextOverflow.ellipsis),
selected: selected,
onSelected: (_) {
setState(() => _index = i);
HapticFeedback.selectionClick();
},
),
);
},
),
).padding(bottom: 8),
const Divider(height: 1),
// Content
Expanded(
child: RefreshIndicator(
onRefresh: widget.onRefresh,
child: _StickersGrid(
pack: selectedPack,
onPick: (sticker) => widget.onPick(selectedPack, sticker),
),
),
),
],
);
}
}
class _StickersGrid extends StatelessWidget {
final SnStickerPack pack;
final void Function(SnSticker sticker) onPick;
const _StickersGrid({required this.pack, required this.onPick});
@override
Widget build(BuildContext context) {
final stickers = pack.stickers;
if (stickers.isEmpty) {
return Center(child: Text('noStickersInPack'.tr()));
}
return GridView.builder(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 96,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
),
itemCount: stickers.length,
itemBuilder: (context, index) {
final sticker = stickers[index];
final placeholder = ':${pack.prefix}${sticker.slug}:';
return Tooltip(
message: placeholder,
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
onTap: () => onPick(sticker),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
child: AspectRatio(
aspectRatio: 1,
child: CloudImageWidget(
fileId: sticker.imageId,
fit: BoxFit.contain,
),
),
),
),
),
);
},
);
}
}
/// Helper to show sticker picker as an anchored popover near the trigger.
/// Provide the button's BuildContext (typically from the onPressed closure).
/// Fallbacks to dialog if overlay cannot be found (e.g., during tests).
Future<void> showStickerPickerPopover(
BuildContext context,
Offset offset, {
required void Function(String placeholder) onPick,
}) async {
// Use flutter_popup_card to present the anchored popup near trigger.
await showPopupCard<void>(
context: context,
offset: offset,
alignment: Alignment.topLeft,
dimBackground: true,
builder:
(ctx) => SizedBox(
width: math.min(480, MediaQuery.of(context).size.width * 0.9),
height: 480,
child: ProviderScope(
parent: ProviderScope.containerOf(context),
child: StickerPicker(
onPick: (ph) {
onPick(ph);
Navigator.of(ctx).maybePop();
},
),
),
),
);
}

View 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