diff --git a/lib/models/sticker.dart b/lib/models/sticker.dart index 3021e12..7c6afa8 100644 --- a/lib/models/sticker.dart +++ b/lib/models/sticker.dart @@ -35,6 +35,7 @@ sealed class SnStickerPack with _$SnStickerPack { required DateTime createdAt, required DateTime updatedAt, required DateTime? deletedAt, + @Default([]) List stickers, }) = _SnStickerPack; factory SnStickerPack.fromJson(Map json) => diff --git a/lib/models/sticker.freezed.dart b/lib/models/sticker.freezed.dart index aaebb39..a2059ea 100644 --- a/lib/models/sticker.freezed.dart +++ b/lib/models/sticker.freezed.dart @@ -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 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 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 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, )); } /// Create a copy of SnStickerPack @@ -493,10 +494,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(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 Function( String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List 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 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 Function( String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List 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? 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? Function( String id, String name, String description, String prefix, String publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List 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 stickers = const []}): _stickers = stickers; factory _SnStickerPack.fromJson(Map 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 _stickers; +@override@JsonKey() List 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 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 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, )); } diff --git a/lib/models/sticker.g.dart b/lib/models/sticker.g.dart index 92d35ce..ace697a 100644 --- a/lib/models/sticker.g.dart +++ b/lib/models/sticker.g.dart @@ -54,6 +54,11 @@ _SnStickerPack _$SnStickerPackFromJson(Map json) => json['deleted_at'] == null ? null : DateTime.parse(json['deleted_at'] as String), + stickers: + (json['stickers'] as List?) + ?.map((e) => SnSticker.fromJson(e as Map)) + .toList() ?? + const [], ); Map _$SnStickerPackToJson(_SnStickerPack instance) => @@ -67,4 +72,5 @@ Map _$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(), }; diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index f231027..da0851f 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -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,69 @@ 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(), - ], - ), - ), - PopupMenuItem( - onTap: () => onPickFile(false), - child: Row( - spacing: 12, - children: [ - const Icon(Symbols.video_call), - Text('addVideo').tr(), - ], - ), - ), - ], + 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( + 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( diff --git a/lib/widgets/stickers/picker.dart b/lib/widgets/stickers/picker.dart new file mode 100644 index 0000000..87ae76c --- /dev/null +++ b/lib/widgets/stickers/picker.dart @@ -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> 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)) + .toList(); + } + return const []; +} + +/// 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 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 packs; + final void Function(SnStickerPack pack, SnSticker sticker) onPick; + final Future 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 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), + ); + }, + ); +} diff --git a/lib/widgets/stickers/picker.g.dart b/lib/widgets/stickers/picker.g.dart new file mode 100644 index 0000000..6b67928 --- /dev/null +++ b/lib/widgets/stickers/picker.g.dart @@ -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>.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>; +// 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