Compare commits
	
		
			3 Commits
		
	
	
		
			b83cb0fb0b
			...
			30b8a6c30f
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 30b8a6c30f | |||
| b9c4ee31b1 | |||
| 87870af866 | 
| @@ -35,6 +35,7 @@ sealed class SnStickerPack with _$SnStickerPack { | ||||
|     required DateTime createdAt, | ||||
|     required DateTime updatedAt, | ||||
|     required DateTime? deletedAt, | ||||
|     @Default([]) List<SnSticker> stickers, | ||||
|   }) = _SnStickerPack; | ||||
|  | ||||
|   factory SnStickerPack.fromJson(Map<String, dynamic> json) => | ||||
|   | ||||
| @@ -338,7 +338,7 @@ $SnStickerPackCopyWith<$Res>? get pack { | ||||
| /// @nodoc | ||||
| mixin _$SnStickerPack { | ||||
|  | ||||
|  String get id; String get name; String get description; String get prefix; String get publisherId; SnPublisher? get publisher; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||
|  String get id; String get name; String get description; String get prefix; String get publisherId; SnPublisher? get publisher; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; List<SnSticker> get stickers; | ||||
| /// Create a copy of SnStickerPack | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @@ -351,16 +351,16 @@ $SnStickerPackCopyWith<SnStickerPack> get copyWith => _$SnStickerPackCopyWithImp | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnStickerPack&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.prefix, prefix) || other.prefix == prefix)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnStickerPack&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.prefix, prefix) || other.prefix == prefix)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other.stickers, stickers)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,id,name,description,prefix,publisherId,publisher,createdAt,updatedAt,deletedAt); | ||||
| int get hashCode => Object.hash(runtimeType,id,name,description,prefix,publisherId,publisher,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(stickers)); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnStickerPack(id: $id, name: $name, description: $description, prefix: $prefix, publisherId: $publisherId, publisher: $publisher, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||
|   return 'SnStickerPack(id: $id, name: $name, description: $description, prefix: $prefix, publisherId: $publisherId, publisher: $publisher, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, stickers: $stickers)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -371,7 +371,7 @@ abstract mixin class $SnStickerPackCopyWith<$Res>  { | ||||
|   factory $SnStickerPackCopyWith(SnStickerPack value, $Res Function(SnStickerPack) _then) = _$SnStickerPackCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
|  String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnSticker> stickers | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -388,7 +388,7 @@ class _$SnStickerPackCopyWithImpl<$Res> | ||||
|  | ||||
| /// Create a copy of SnStickerPack | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = null,Object? prefix = null,Object? publisherId = null,Object? publisher = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = null,Object? prefix = null,Object? publisherId = null,Object? publisher = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? stickers = null,}) { | ||||
|   return _then(_self.copyWith( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||
| @@ -399,7 +399,8 @@ as String,publisher: freezed == publisher ? _self.publisher : publisher // ignor | ||||
| as SnPublisher?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime?, | ||||
| as DateTime?,stickers: null == stickers ? _self.stickers : stickers // ignore: cast_nullable_to_non_nullable | ||||
| as List<SnSticker>, | ||||
|   )); | ||||
| } | ||||
| /// Create a copy of SnStickerPack | ||||
| @@ -493,10 +494,10 @@ return $default(_that);case _: | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String name,  String description,  String prefix,  String publisherId,  SnPublisher? publisher,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String name,  String description,  String prefix,  String publisherId,  SnPublisher? publisher,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt,  List<SnSticker> stickers)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnStickerPack() when $default != null: | ||||
| return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||
| return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.stickers);case _: | ||||
|   return orElse(); | ||||
|  | ||||
| } | ||||
| @@ -514,10 +515,10 @@ return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publish | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String name,  String description,  String prefix,  String publisherId,  SnPublisher? publisher,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String name,  String description,  String prefix,  String publisherId,  SnPublisher? publisher,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt,  List<SnSticker> stickers)  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnStickerPack(): | ||||
| return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt);} | ||||
| return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.stickers);} | ||||
| } | ||||
| /// A variant of `when` that fallback to returning `null` | ||||
| /// | ||||
| @@ -531,10 +532,10 @@ return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publish | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String name,  String description,  String prefix,  String publisherId,  SnPublisher? publisher,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String name,  String description,  String prefix,  String publisherId,  SnPublisher? publisher,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt,  List<SnSticker> stickers)?  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnStickerPack() when $default != null: | ||||
| return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||
| return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publisherId,_that.publisher,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.stickers);case _: | ||||
|   return null; | ||||
|  | ||||
| } | ||||
| @@ -546,7 +547,7 @@ return $default(_that.id,_that.name,_that.description,_that.prefix,_that.publish | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _SnStickerPack implements SnStickerPack { | ||||
|   const _SnStickerPack({required this.id, required this.name, required this.description, required this.prefix, required this.publisherId, required this.publisher, required this.createdAt, required this.updatedAt, required this.deletedAt}); | ||||
|   const _SnStickerPack({required this.id, required this.name, required this.description, required this.prefix, required this.publisherId, required this.publisher, required this.createdAt, required this.updatedAt, required this.deletedAt, final  List<SnSticker> stickers = const []}): _stickers = stickers; | ||||
|   factory _SnStickerPack.fromJson(Map<String, dynamic> json) => _$SnStickerPackFromJson(json); | ||||
|  | ||||
| @override final  String id; | ||||
| @@ -558,6 +559,13 @@ class _SnStickerPack implements SnStickerPack { | ||||
| @override final  DateTime createdAt; | ||||
| @override final  DateTime updatedAt; | ||||
| @override final  DateTime? deletedAt; | ||||
|  final  List<SnSticker> _stickers; | ||||
| @override@JsonKey() List<SnSticker> get stickers { | ||||
|   if (_stickers is EqualUnmodifiableListView) return _stickers; | ||||
|   // ignore: implicit_dynamic_type | ||||
|   return EqualUnmodifiableListView(_stickers); | ||||
| } | ||||
|  | ||||
|  | ||||
| /// Create a copy of SnStickerPack | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @@ -572,16 +580,16 @@ Map<String, dynamic> toJson() { | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnStickerPack&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.prefix, prefix) || other.prefix == prefix)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnStickerPack&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.prefix, prefix) || other.prefix == prefix)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other._stickers, _stickers)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,id,name,description,prefix,publisherId,publisher,createdAt,updatedAt,deletedAt); | ||||
| int get hashCode => Object.hash(runtimeType,id,name,description,prefix,publisherId,publisher,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(_stickers)); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnStickerPack(id: $id, name: $name, description: $description, prefix: $prefix, publisherId: $publisherId, publisher: $publisher, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||
|   return 'SnStickerPack(id: $id, name: $name, description: $description, prefix: $prefix, publisherId: $publisherId, publisher: $publisher, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, stickers: $stickers)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -592,7 +600,7 @@ abstract mixin class _$SnStickerPackCopyWith<$Res> implements $SnStickerPackCopy | ||||
|   factory _$SnStickerPackCopyWith(_SnStickerPack value, $Res Function(_SnStickerPack) _then) = __$SnStickerPackCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
|  String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List<SnSticker> stickers | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -609,7 +617,7 @@ class __$SnStickerPackCopyWithImpl<$Res> | ||||
|  | ||||
| /// Create a copy of SnStickerPack | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = null,Object? prefix = null,Object? publisherId = null,Object? publisher = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = null,Object? prefix = null,Object? publisherId = null,Object? publisher = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? stickers = null,}) { | ||||
|   return _then(_SnStickerPack( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||
| @@ -620,7 +628,8 @@ as String,publisher: freezed == publisher ? _self.publisher : publisher // ignor | ||||
| as SnPublisher?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime?, | ||||
| as DateTime?,stickers: null == stickers ? _self._stickers : stickers // ignore: cast_nullable_to_non_nullable | ||||
| as List<SnSticker>, | ||||
|   )); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -54,6 +54,11 @@ _SnStickerPack _$SnStickerPackFromJson(Map<String, dynamic> json) => | ||||
|           json['deleted_at'] == null | ||||
|               ? null | ||||
|               : DateTime.parse(json['deleted_at'] as String), | ||||
|       stickers: | ||||
|           (json['stickers'] as List<dynamic>?) | ||||
|               ?.map((e) => SnSticker.fromJson(e as Map<String, dynamic>)) | ||||
|               .toList() ?? | ||||
|           const [], | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$SnStickerPackToJson(_SnStickerPack instance) => | ||||
| @@ -67,4 +72,5 @@ Map<String, dynamic> _$SnStickerPackToJson(_SnStickerPack instance) => | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|       'updated_at': instance.updatedAt.toIso8601String(), | ||||
|       'deleted_at': instance.deletedAt?.toIso8601String(), | ||||
|       'stickers': instance.stickers.map((e) => e.toJson()).toList(), | ||||
|     }; | ||||
|   | ||||
| @@ -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'; | ||||
|  | ||||
| @@ -1133,31 +1134,76 @@ class _ChatInput extends HookConsumerWidget { | ||||
|             padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), | ||||
|             child: Row( | ||||
|               children: [ | ||||
|                 PopupMenuButton( | ||||
|                   icon: const Icon(Symbols.photo_library), | ||||
|                   itemBuilder: | ||||
|                       (context) => [ | ||||
|                         PopupMenuItem( | ||||
|                           onTap: () => onPickFile(true), | ||||
|                           child: Row( | ||||
|                             spacing: 12, | ||||
|                             children: [ | ||||
|                               const Icon(Symbols.photo), | ||||
|                               Text('addPhoto').tr(), | ||||
|                             ], | ||||
|                 Row( | ||||
|                   mainAxisSize: MainAxisSize.min, | ||||
|                   children: [ | ||||
|                     IconButton( | ||||
|                       tooltip: 'stickers'.tr(), | ||||
|                       icon: const Icon(Symbols.emoji_symbols), | ||||
|                       onPressed: () { | ||||
|                         final size = MediaQuery.of(context).size; | ||||
|                         showStickerPickerPopover( | ||||
|                           context, | ||||
|                           Offset( | ||||
|                             20, | ||||
|                             size.height - | ||||
|                                 480 - | ||||
|                                 MediaQuery.of(context).padding.bottom, | ||||
|                           ), | ||||
|                         ), | ||||
|                         PopupMenuItem( | ||||
|                           onTap: () => onPickFile(false), | ||||
|                           child: Row( | ||||
|                             spacing: 12, | ||||
|                             children: [ | ||||
|                               const Icon(Symbols.video_call), | ||||
|                               Text('addVideo').tr(), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ), | ||||
|                       ], | ||||
|                           onPick: (placeholder) { | ||||
|                             // Insert placeholder at current cursor position | ||||
|                             final text = messageController.text; | ||||
|                             final selection = messageController.selection; | ||||
|                             final start = | ||||
|                                 selection.start >= 0 | ||||
|                                     ? selection.start | ||||
|                                     : text.length; | ||||
|                             final end = | ||||
|                                 selection.end >= 0 | ||||
|                                     ? selection.end | ||||
|                                     : text.length; | ||||
|                             final newText = text.replaceRange( | ||||
|                               start, | ||||
|                               end, | ||||
|                               placeholder, | ||||
|                             ); | ||||
|                             messageController.value = TextEditingValue( | ||||
|                               text: newText, | ||||
|                               selection: TextSelection.collapsed( | ||||
|                                 offset: start + placeholder.length, | ||||
|                               ), | ||||
|                             ); | ||||
|                           }, | ||||
|                         ); | ||||
|                       }, | ||||
|                     ), | ||||
|                     PopupMenuButton( | ||||
|                       icon: const Icon(Symbols.photo_library), | ||||
|                       itemBuilder: | ||||
|                           (context) => [ | ||||
|                             PopupMenuItem( | ||||
|                               onTap: () => onPickFile(true), | ||||
|                               child: Row( | ||||
|                                 spacing: 12, | ||||
|                                 children: [ | ||||
|                                   const Icon(Symbols.photo), | ||||
|                                   Text('addPhoto').tr(), | ||||
|                                 ], | ||||
|                               ), | ||||
|                             ), | ||||
|                             PopupMenuItem( | ||||
|                               onTap: () => onPickFile(false), | ||||
|                               child: Row( | ||||
|                                 spacing: 12, | ||||
|                                 children: [ | ||||
|                                   const Icon(Symbols.video_call), | ||||
|                                   Text('addVideo').tr(), | ||||
|                                 ], | ||||
|                               ), | ||||
|                             ), | ||||
|                           ], | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|                 Expanded( | ||||
|                   child: RawKeyboardListener( | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
							
								
								
									
										306
									
								
								lib/widgets/stickers/picker.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										306
									
								
								lib/widgets/stickers/picker.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,306 @@ | ||||
| import 'dart:math' as math; | ||||
|  | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/sticker.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/widgets/content/cloud_files.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:flutter_popup_card/flutter_popup_card.dart'; | ||||
|  | ||||
| part 'picker.g.dart'; | ||||
|  | ||||
| /// Fetch user-added sticker packs (with stickers) from API: | ||||
| /// GET /sphere/stickers/me | ||||
| @riverpod | ||||
| Future<List<SnStickerPack>> myStickerPacks(Ref ref) async { | ||||
|   final api = ref.watch(apiClientProvider); | ||||
|   final resp = await api.get('/sphere/stickers/me'); | ||||
|   final data = resp.data; | ||||
|   if (data is List) { | ||||
|     return data | ||||
|         .map((e) => SnStickerPack.fromJson(e as Map<String, dynamic>)) | ||||
|         .toList(); | ||||
|   } | ||||
|   return const <SnStickerPack>[]; | ||||
| } | ||||
|  | ||||
| /// Sticker Picker popover dialog | ||||
| /// - Displays user-owned sticker packs as tabs (chips) | ||||
| /// - Shows grid of stickers in selected pack | ||||
| /// - On tap, returns placeholder string :{prefix}{slug}: via onPick callback | ||||
| class StickerPicker extends HookConsumerWidget { | ||||
|   final void Function(String placeholder) onPick; | ||||
|  | ||||
|   const StickerPicker({super.key, required this.onPick}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final packsAsync = ref.watch(myStickerPacksProvider); | ||||
|  | ||||
|     return PopupCard( | ||||
|       elevation: 8, | ||||
|       shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)), | ||||
|       child: ConstrainedBox( | ||||
|         constraints: const BoxConstraints(maxWidth: 520, maxHeight: 520), | ||||
|         child: packsAsync.when( | ||||
|           data: (packs) { | ||||
|             if (packs.isEmpty) { | ||||
|               return _EmptyState( | ||||
|                 onRefresh: () async { | ||||
|                   ref.invalidate(myStickerPacksProvider); | ||||
|                 }, | ||||
|               ); | ||||
|             } | ||||
|  | ||||
|             // Maintain selected index locally with a ValueNotifier to avoid hooks dependency | ||||
|             return _PackSwitcher( | ||||
|               packs: packs, | ||||
|               onPick: (pack, sticker) { | ||||
|                 final placeholder = ':${pack.prefix}${sticker.slug}:'; | ||||
|                 HapticFeedback.selectionClick(); | ||||
|                 onPick(placeholder); | ||||
|                 if (Navigator.of(context).canPop()) { | ||||
|                   Navigator.of(context).pop(); | ||||
|                 } | ||||
|               }, | ||||
|               onRefresh: () async { | ||||
|                 ref.invalidate(myStickerPacksProvider); | ||||
|               }, | ||||
|             ); | ||||
|           }, | ||||
|           loading: | ||||
|               () => const SizedBox( | ||||
|                 width: 320, | ||||
|                 height: 320, | ||||
|                 child: Center(child: CircularProgressIndicator()), | ||||
|               ), | ||||
|           error: | ||||
|               (err, _) => SizedBox( | ||||
|                 width: 360, | ||||
|                 height: 200, | ||||
|                 child: Column( | ||||
|                   mainAxisAlignment: MainAxisAlignment.center, | ||||
|                   children: [ | ||||
|                     const Icon(Symbols.error, size: 28), | ||||
|                     const Gap(8), | ||||
|                     Text('Error: $err', textAlign: TextAlign.center), | ||||
|                     const Gap(12), | ||||
|                     FilledButton.icon( | ||||
|                       onPressed: () => ref.invalidate(myStickerPacksProvider), | ||||
|                       icon: const Icon(Symbols.refresh), | ||||
|                       label: Text('retry').tr(), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ).padding(all: 16), | ||||
|               ), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _EmptyState extends StatelessWidget { | ||||
|   final Future<void> Function() onRefresh; | ||||
|   const _EmptyState({required this.onRefresh}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return SizedBox( | ||||
|       width: 360, | ||||
|       height: 220, | ||||
|       child: Column( | ||||
|         mainAxisAlignment: MainAxisAlignment.center, | ||||
|         children: [ | ||||
|           const Icon(Symbols.emoji_symbols, size: 28), | ||||
|           const Gap(8), | ||||
|           Text('noStickerPacks'.tr(), textAlign: TextAlign.center), | ||||
|           const Gap(12), | ||||
|           OutlinedButton.icon( | ||||
|             onPressed: onRefresh, | ||||
|             icon: const Icon(Symbols.refresh), | ||||
|             label: Text('refresh').tr(), | ||||
|           ), | ||||
|         ], | ||||
|       ).padding(all: 16), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _PackSwitcher extends StatefulWidget { | ||||
|   final List<SnStickerPack> packs; | ||||
|   final void Function(SnStickerPack pack, SnSticker sticker) onPick; | ||||
|   final Future<void> Function() onRefresh; | ||||
|  | ||||
|   const _PackSwitcher({ | ||||
|     required this.packs, | ||||
|     required this.onPick, | ||||
|     required this.onRefresh, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   State<_PackSwitcher> createState() => _PackSwitcherState(); | ||||
| } | ||||
|  | ||||
| class _PackSwitcherState extends State<_PackSwitcher> { | ||||
|   int _index = 0; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final packs = widget.packs; | ||||
|     _index = _index.clamp(0, packs.length - 1); | ||||
|  | ||||
|     final selectedPack = packs[_index]; | ||||
|  | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|       children: [ | ||||
|         // Header | ||||
|         Row( | ||||
|           children: [ | ||||
|             const Icon(Symbols.sticky_note_2, size: 20), | ||||
|             const Gap(8), | ||||
|             Text( | ||||
|               'stickers'.tr(), | ||||
|               style: Theme.of(context).textTheme.titleMedium, | ||||
|             ), | ||||
|             const Spacer(), | ||||
|             IconButton( | ||||
|               tooltip: 'close'.tr(), | ||||
|               onPressed: () => Navigator.of(context).maybePop(), | ||||
|               icon: const Icon(Symbols.close), | ||||
|             ), | ||||
|           ], | ||||
|         ).padding(horizontal: 12, top: 8, bottom: 4), | ||||
|  | ||||
|         // Vertical, scrollable packs rail like common emoji pickers | ||||
|         SizedBox( | ||||
|           height: 52, | ||||
|           child: ListView.separated( | ||||
|             padding: const EdgeInsets.symmetric(horizontal: 8), | ||||
|             scrollDirection: Axis.horizontal, | ||||
|             itemCount: packs.length, | ||||
|             separatorBuilder: (_, __) => const Gap(4), | ||||
|             itemBuilder: (context, i) { | ||||
|               final selected = _index == i; | ||||
|               return Tooltip( | ||||
|                 message: packs[i].name, | ||||
|                 child: FilterChip( | ||||
|                   label: Text(packs[i].name, overflow: TextOverflow.ellipsis), | ||||
|                   selected: selected, | ||||
|                   onSelected: (_) { | ||||
|                     setState(() => _index = i); | ||||
|                     HapticFeedback.selectionClick(); | ||||
|                   }, | ||||
|                 ), | ||||
|               ); | ||||
|             }, | ||||
|           ), | ||||
|         ), | ||||
|         const Divider(height: 1), | ||||
|  | ||||
|         // Content | ||||
|         Expanded( | ||||
|           child: RefreshIndicator( | ||||
|             onRefresh: widget.onRefresh, | ||||
|             child: _StickersGrid( | ||||
|               pack: selectedPack, | ||||
|               onPick: (sticker) => widget.onPick(selectedPack, sticker), | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|         Gap(MediaQuery.of(context).padding.bottom), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _StickersGrid extends StatelessWidget { | ||||
|   final SnStickerPack pack; | ||||
|   final void Function(SnSticker sticker) onPick; | ||||
|  | ||||
|   const _StickersGrid({required this.pack, required this.onPick}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final stickers = pack.stickers; | ||||
|  | ||||
|     if (stickers.isEmpty) { | ||||
|       return Center(child: Text('noStickersInPack'.tr())); | ||||
|     } | ||||
|  | ||||
|     return GridView.builder( | ||||
|       physics: const AlwaysScrollableScrollPhysics(), | ||||
|       padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), | ||||
|       gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( | ||||
|         maxCrossAxisExtent: 96, | ||||
|         mainAxisSpacing: 12, | ||||
|         crossAxisSpacing: 12, | ||||
|       ), | ||||
|       itemCount: stickers.length, | ||||
|       itemBuilder: (context, index) { | ||||
|         final sticker = stickers[index]; | ||||
|         final placeholder = ':${pack.prefix}${sticker.slug}:'; | ||||
|         return Tooltip( | ||||
|           message: placeholder, | ||||
|           child: InkWell( | ||||
|             borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|             onTap: () => onPick(sticker), | ||||
|             child: ClipRRect( | ||||
|               borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|               child: DecoratedBox( | ||||
|                 decoration: BoxDecoration( | ||||
|                   color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                   borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                 ), | ||||
|                 child: AspectRatio( | ||||
|                   aspectRatio: 1, | ||||
|                   child: CloudImageWidget( | ||||
|                     fileId: sticker.imageId, | ||||
|                     fit: BoxFit.contain, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// Helper to show sticker picker as an anchored popover near the trigger. | ||||
| /// Provide the button's BuildContext (typically from the onPressed closure). | ||||
| /// Fallbacks to dialog if overlay cannot be found (e.g., during tests). | ||||
| Future<void> showStickerPickerPopover( | ||||
|   BuildContext context, | ||||
|   Offset offset, { | ||||
|   required void Function(String placeholder) onPick, | ||||
| }) async { | ||||
|   // Use flutter_popup_card to present the anchored popup near trigger. | ||||
|   await showPopupCard<void>( | ||||
|     context: context, | ||||
|     offset: offset, | ||||
|     alignment: Alignment.topLeft, | ||||
|     dimBackground: true, | ||||
|     builder: | ||||
|         (ctx) => SizedBox( | ||||
|           width: math.min(480, MediaQuery.of(context).size.width * 0.9), | ||||
|           height: 480, | ||||
|           child: ProviderScope( | ||||
|             parent: ProviderScope.containerOf(context), | ||||
|             child: StickerPicker( | ||||
|               onPick: (ph) { | ||||
|                 onPick(ph); | ||||
|                 Navigator.of(ctx).maybePop(); | ||||
|               }, | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										32
									
								
								lib/widgets/stickers/picker.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								lib/widgets/stickers/picker.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'picker.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$myStickerPacksHash() => r'1e19832e8ab1cb139ad18aebfa5aebdf4fdea499'; | ||||
|  | ||||
| /// Fetch user-added sticker packs (with stickers) from API: | ||||
| /// GET /sphere/stickers/me | ||||
| /// | ||||
| /// Copied from [myStickerPacks]. | ||||
| @ProviderFor(myStickerPacks) | ||||
| final myStickerPacksProvider = | ||||
|     AutoDisposeFutureProvider<List<SnStickerPack>>.internal( | ||||
|       myStickerPacks, | ||||
|       name: r'myStickerPacksProvider', | ||||
|       debugGetCreateSourceHash: | ||||
|           const bool.fromEnvironment('dart.vm.product') | ||||
|               ? null | ||||
|               : _$myStickerPacksHash, | ||||
|       dependencies: null, | ||||
|       allTransitiveDependencies: null, | ||||
|     ); | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| typedef MyStickerPacksRef = AutoDisposeFutureProviderRef<List<SnStickerPack>>; | ||||
| // ignore_for_file: type=lint | ||||
| // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package | ||||
		Reference in New Issue
	
	Block a user