✨ Sticker picker basis
This commit is contained in:
@@ -35,6 +35,7 @@ sealed class SnStickerPack with _$SnStickerPack {
|
|||||||
required DateTime createdAt,
|
required DateTime createdAt,
|
||||||
required DateTime updatedAt,
|
required DateTime updatedAt,
|
||||||
required DateTime? deletedAt,
|
required DateTime? deletedAt,
|
||||||
|
@Default([]) List<SnSticker> stickers,
|
||||||
}) = _SnStickerPack;
|
}) = _SnStickerPack;
|
||||||
|
|
||||||
factory SnStickerPack.fromJson(Map<String, dynamic> json) =>
|
factory SnStickerPack.fromJson(Map<String, dynamic> json) =>
|
||||||
|
@@ -338,7 +338,7 @@ $SnStickerPackCopyWith<$Res>? get pack {
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$SnStickerPack {
|
mixin _$SnStickerPack {
|
||||||
|
|
||||||
String get id; String get name; String get description; String get prefix; String get publisherId; SnPublisher? get publisher; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
String get id; String get name; String get description; String get prefix; String get publisherId; SnPublisher? get publisher; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; List<SnSticker> get stickers;
|
||||||
/// Create a copy of SnStickerPack
|
/// Create a copy of SnStickerPack
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@@ -351,16 +351,16 @@ $SnStickerPackCopyWith<SnStickerPack> get copyWith => _$SnStickerPackCopyWithImp
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnStickerPack&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.prefix, prefix) || other.prefix == prefix)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnStickerPack&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.prefix, prefix) || other.prefix == prefix)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other.stickers, stickers));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,id,name,description,prefix,publisherId,publisher,createdAt,updatedAt,deletedAt);
|
int get hashCode => Object.hash(runtimeType,id,name,description,prefix,publisherId,publisher,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(stickers));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnStickerPack(id: $id, name: $name, description: $description, prefix: $prefix, publisherId: $publisherId, publisher: $publisher, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
return 'SnStickerPack(id: $id, name: $name, description: $description, prefix: $prefix, publisherId: $publisherId, publisher: $publisher, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, stickers: $stickers)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -371,7 +371,7 @@ abstract mixin class $SnStickerPackCopyWith<$Res> {
|
|||||||
factory $SnStickerPackCopyWith(SnStickerPack value, $Res Function(SnStickerPack) _then) = _$SnStickerPackCopyWithImpl;
|
factory $SnStickerPackCopyWith(SnStickerPack value, $Res Function(SnStickerPack) _then) = _$SnStickerPackCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnSticker> stickers
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -388,7 +388,7 @@ class _$SnStickerPackCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of SnStickerPack
|
/// Create a copy of SnStickerPack
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = null,Object? prefix = null,Object? publisherId = null,Object? publisher = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = null,Object? prefix = null,Object? publisherId = null,Object? publisher = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? stickers = null,}) {
|
||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -399,7 +399,8 @@ as String,publisher: freezed == publisher ? _self.publisher : publisher // ignor
|
|||||||
as SnPublisher?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
as SnPublisher?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
as DateTime?,
|
as DateTime?,stickers: null == stickers ? _self.stickers : stickers // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<SnSticker>,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
/// Create a copy of SnStickerPack
|
/// Create a copy of SnStickerPack
|
||||||
@@ -493,10 +494,10 @@ return $default(_that);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnSticker> stickers)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _SnStickerPack() when $default != null:
|
case _SnStickerPack() when $default != null:
|
||||||
return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.stickers);case _:
|
||||||
return orElse();
|
return orElse();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -514,10 +515,10 @@ return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publish
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnSticker> stickers) $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _SnStickerPack():
|
case _SnStickerPack():
|
||||||
return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.stickers);}
|
||||||
}
|
}
|
||||||
/// A variant of `when` that fallback to returning `null`
|
/// A variant of `when` that fallback to returning `null`
|
||||||
///
|
///
|
||||||
@@ -531,10 +532,10 @@ return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publish
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnSticker> stickers)? $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _SnStickerPack() when $default != null:
|
case _SnStickerPack() when $default != null:
|
||||||
return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.stickers);case _:
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -546,7 +547,7 @@ return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publish
|
|||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
|
|
||||||
class _SnStickerPack implements SnStickerPack {
|
class _SnStickerPack implements SnStickerPack {
|
||||||
const _SnStickerPack({required this.id, required this.name, required this.description, required this.prefix, required this.publisherId, required this.publisher, required this.createdAt, required this.updatedAt, required this.deletedAt});
|
const _SnStickerPack({required this.id, required this.name, required this.description, required this.prefix, required this.publisherId, required this.publisher, required this.createdAt, required this.updatedAt, required this.deletedAt, final List<SnSticker> stickers = const []}): _stickers = stickers;
|
||||||
factory _SnStickerPack.fromJson(Map<String, dynamic> json) => _$SnStickerPackFromJson(json);
|
factory _SnStickerPack.fromJson(Map<String, dynamic> json) => _$SnStickerPackFromJson(json);
|
||||||
|
|
||||||
@override final String id;
|
@override final String id;
|
||||||
@@ -558,6 +559,13 @@ class _SnStickerPack implements SnStickerPack {
|
|||||||
@override final DateTime createdAt;
|
@override final DateTime createdAt;
|
||||||
@override final DateTime updatedAt;
|
@override final DateTime updatedAt;
|
||||||
@override final DateTime? deletedAt;
|
@override final DateTime? deletedAt;
|
||||||
|
final List<SnSticker> _stickers;
|
||||||
|
@override@JsonKey() List<SnSticker> get stickers {
|
||||||
|
if (_stickers is EqualUnmodifiableListView) return _stickers;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(_stickers);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Create a copy of SnStickerPack
|
/// Create a copy of SnStickerPack
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@@ -572,16 +580,16 @@ Map<String, dynamic> toJson() {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnStickerPack&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.prefix, prefix) || other.prefix == prefix)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnStickerPack&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.prefix, prefix) || other.prefix == prefix)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other._stickers, _stickers));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,id,name,description,prefix,publisherId,publisher,createdAt,updatedAt,deletedAt);
|
int get hashCode => Object.hash(runtimeType,id,name,description,prefix,publisherId,publisher,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(_stickers));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SnStickerPack(id: $id, name: $name, description: $description, prefix: $prefix, publisherId: $publisherId, publisher: $publisher, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
return 'SnStickerPack(id: $id, name: $name, description: $description, prefix: $prefix, publisherId: $publisherId, publisher: $publisher, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, stickers: $stickers)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -592,7 +600,7 @@ abstract mixin class _$SnStickerPackCopyWith<$Res> implements $SnStickerPackCopy
|
|||||||
factory _$SnStickerPackCopyWith(_SnStickerPack value, $Res Function(_SnStickerPack) _then) = __$SnStickerPackCopyWithImpl;
|
factory _$SnStickerPackCopyWith(_SnStickerPack value, $Res Function(_SnStickerPack) _then) = __$SnStickerPackCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnSticker> stickers
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -609,7 +617,7 @@ class __$SnStickerPackCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of SnStickerPack
|
/// Create a copy of SnStickerPack
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = null,Object? prefix = null,Object? publisherId = null,Object? publisher = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = null,Object? prefix = null,Object? publisherId = null,Object? publisher = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? stickers = null,}) {
|
||||||
return _then(_SnStickerPack(
|
return _then(_SnStickerPack(
|
||||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -620,7 +628,8 @@ as String,publisher: freezed == publisher ? _self.publisher : publisher // ignor
|
|||||||
as SnPublisher?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
as SnPublisher?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
as DateTime?,
|
as DateTime?,stickers: null == stickers ? _self._stickers : stickers // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<SnSticker>,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -54,6 +54,11 @@ _SnStickerPack _$SnStickerPackFromJson(Map<String, dynamic> json) =>
|
|||||||
json['deleted_at'] == null
|
json['deleted_at'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json['deleted_at'] as String),
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
|
stickers:
|
||||||
|
(json['stickers'] as List<dynamic>?)
|
||||||
|
?.map((e) => SnSticker.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList() ??
|
||||||
|
const [],
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnStickerPackToJson(_SnStickerPack instance) =>
|
Map<String, dynamic> _$SnStickerPackToJson(_SnStickerPack instance) =>
|
||||||
@@ -67,4 +72,5 @@ Map<String, dynamic> _$SnStickerPackToJson(_SnStickerPack instance) =>
|
|||||||
'created_at': instance.createdAt.toIso8601String(),
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
'updated_at': instance.updatedAt.toIso8601String(),
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
'stickers': instance.stickers.map((e) => e.toJson()).toList(),
|
||||||
};
|
};
|
||||||
|
@@ -35,6 +35,7 @@ import 'package:material_symbols_icons/symbols.dart';
|
|||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'chat.dart';
|
import 'chat.dart';
|
||||||
import 'package:island/widgets/chat/call_button.dart';
|
import 'package:island/widgets/chat/call_button.dart';
|
||||||
|
import 'package:island/widgets/stickers/picker.dart';
|
||||||
|
|
||||||
part 'room.g.dart';
|
part 'room.g.dart';
|
||||||
|
|
||||||
@@ -1133,6 +1134,42 @@ class _ChatInput extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'stickers'.tr(),
|
||||||
|
icon: const Icon(Symbols.emoji_symbols),
|
||||||
|
onPressed: () {
|
||||||
|
showStickerPickerPopover(
|
||||||
|
context,
|
||||||
|
onPick: (placeholder) {
|
||||||
|
// Insert placeholder at current cursor position
|
||||||
|
final text = messageController.text;
|
||||||
|
final selection = messageController.selection;
|
||||||
|
final start =
|
||||||
|
selection.start >= 0
|
||||||
|
? selection.start
|
||||||
|
: text.length;
|
||||||
|
final end =
|
||||||
|
selection.end >= 0
|
||||||
|
? selection.end
|
||||||
|
: text.length;
|
||||||
|
final newText = text.replaceRange(
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
placeholder,
|
||||||
|
);
|
||||||
|
messageController.value = TextEditingValue(
|
||||||
|
text: newText,
|
||||||
|
selection: TextSelection.collapsed(
|
||||||
|
offset: start + placeholder.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
icon: const Icon(Symbols.photo_library),
|
icon: const Icon(Symbols.photo_library),
|
||||||
itemBuilder:
|
itemBuilder:
|
||||||
@@ -1159,6 +1196,8 @@ class _ChatInput extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: RawKeyboardListener(
|
child: RawKeyboardListener(
|
||||||
focusNode: FocusNode(),
|
focusNode: FocusNode(),
|
||||||
|
289
lib/widgets/stickers/picker.dart
Normal file
289
lib/widgets/stickers/picker.dart
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
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 Dialog(
|
||||||
|
insetPadding: const EdgeInsets.all(12),
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 520, maxHeight: 520),
|
||||||
|
child: packsAsync.when(
|
||||||
|
data: (packs) {
|
||||||
|
if (packs.isEmpty) {
|
||||||
|
return _EmptyState(
|
||||||
|
onRefresh: () async {
|
||||||
|
ref.invalidate(myStickerPacksProvider);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maintain selected index locally with a ValueNotifier to avoid hooks dependency
|
||||||
|
return _PackSwitcher(
|
||||||
|
packs: packs,
|
||||||
|
onPick: (pack, sticker) {
|
||||||
|
final placeholder = ':${pack.prefix}${sticker.slug}:';
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
onPick(placeholder);
|
||||||
|
if (Navigator.of(context).canPop()) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRefresh: () async {
|
||||||
|
ref.invalidate(myStickerPacksProvider);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading:
|
||||||
|
() => const SizedBox(
|
||||||
|
width: 320,
|
||||||
|
height: 320,
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
|
error:
|
||||||
|
(err, _) => SizedBox(
|
||||||
|
width: 360,
|
||||||
|
height: 200,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.error, size: 28),
|
||||||
|
const Gap(8),
|
||||||
|
Text('Error: $err', textAlign: TextAlign.center),
|
||||||
|
const Gap(12),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: () => ref.invalidate(myStickerPacksProvider),
|
||||||
|
icon: const Icon(Symbols.refresh),
|
||||||
|
label: Text('retry').tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(all: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EmptyState extends StatelessWidget {
|
||||||
|
final Future<void> Function() onRefresh;
|
||||||
|
const _EmptyState({required this.onRefresh});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 360,
|
||||||
|
height: 220,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.emoji_symbols, size: 28),
|
||||||
|
const Gap(8),
|
||||||
|
Text('noStickerPacks'.tr(), textAlign: TextAlign.center),
|
||||||
|
const Gap(12),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: onRefresh,
|
||||||
|
icon: const Icon(Symbols.refresh),
|
||||||
|
label: Text('refresh').tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(all: 16),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PackSwitcher extends StatefulWidget {
|
||||||
|
final List<SnStickerPack> packs;
|
||||||
|
final void Function(SnStickerPack pack, SnSticker sticker) onPick;
|
||||||
|
final Future<void> Function() onRefresh;
|
||||||
|
|
||||||
|
const _PackSwitcher({
|
||||||
|
required this.packs,
|
||||||
|
required this.onPick,
|
||||||
|
required this.onRefresh,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_PackSwitcher> createState() => _PackSwitcherState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PackSwitcherState extends State<_PackSwitcher> {
|
||||||
|
int _index = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final packs = widget.packs;
|
||||||
|
_index = _index.clamp(0, packs.length - 1);
|
||||||
|
|
||||||
|
final selectedPack = packs[_index];
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.sticky_note_2, size: 20),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
'stickers'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'close'.tr(),
|
||||||
|
onPressed: () => Navigator.of(context).maybePop(),
|
||||||
|
icon: const Icon(Symbols.close),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 12, top: 8, bottom: 4),
|
||||||
|
|
||||||
|
// Pack chips
|
||||||
|
SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
for (var i = 0; i < packs.length; i++)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
child: ChoiceChip(
|
||||||
|
label: Text(packs[i].name),
|
||||||
|
selected: _index == i,
|
||||||
|
onSelected: (v) {
|
||||||
|
if (!v) return;
|
||||||
|
setState(() => _index = i);
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
|
||||||
|
// Content
|
||||||
|
Expanded(
|
||||||
|
child: RefreshIndicator(
|
||||||
|
onRefresh: widget.onRefresh,
|
||||||
|
child: _StickersGrid(
|
||||||
|
pack: selectedPack,
|
||||||
|
onPick: (sticker) => widget.onPick(selectedPack, sticker),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Gap(MediaQuery.of(context).padding.bottom),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StickersGrid extends StatelessWidget {
|
||||||
|
final SnStickerPack pack;
|
||||||
|
final void Function(SnSticker sticker) onPick;
|
||||||
|
|
||||||
|
const _StickersGrid({required this.pack, required this.onPick});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final stickers = pack.stickers;
|
||||||
|
|
||||||
|
if (stickers.isEmpty) {
|
||||||
|
return Center(child: Text('noStickersInPack'.tr()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return GridView.builder(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||||
|
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
|
maxCrossAxisExtent: 96,
|
||||||
|
mainAxisSpacing: 12,
|
||||||
|
crossAxisSpacing: 12,
|
||||||
|
),
|
||||||
|
itemCount: stickers.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final sticker = stickers[index];
|
||||||
|
final placeholder = ':${pack.prefix}${sticker.slug}:';
|
||||||
|
return Tooltip(
|
||||||
|
message: placeholder,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
onTap: () => onPick(sticker),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 1,
|
||||||
|
child: CloudImageWidget(
|
||||||
|
fileId: sticker.imageId,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to show sticker picker as a popover dialog.
|
||||||
|
/// Usage:
|
||||||
|
/// await showStickerPickerPopover(context, onPick: (placeholder) { ... });
|
||||||
|
Future<void> showStickerPickerPopover(
|
||||||
|
BuildContext context, {
|
||||||
|
required void Function(String placeholder) onPick,
|
||||||
|
}) async {
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: true,
|
||||||
|
builder: (ctx) {
|
||||||
|
return ProviderScope(
|
||||||
|
parent: ProviderScope.containerOf(context),
|
||||||
|
child: StickerPicker(onPick: onPick),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
32
lib/widgets/stickers/picker.g.dart
Normal file
32
lib/widgets/stickers/picker.g.dart
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'picker.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$myStickerPacksHash() => r'1e19832e8ab1cb139ad18aebfa5aebdf4fdea499';
|
||||||
|
|
||||||
|
/// Fetch user-added sticker packs (with stickers) from API:
|
||||||
|
/// GET /sphere/stickers/me
|
||||||
|
///
|
||||||
|
/// Copied from [myStickerPacks].
|
||||||
|
@ProviderFor(myStickerPacks)
|
||||||
|
final myStickerPacksProvider =
|
||||||
|
AutoDisposeFutureProvider<List<SnStickerPack>>.internal(
|
||||||
|
myStickerPacks,
|
||||||
|
name: r'myStickerPacksProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$myStickerPacksHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
typedef MyStickerPacksRef = AutoDisposeFutureProviderRef<List<SnStickerPack>>;
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
Reference in New Issue
Block a user