✨ Create new sticker to pack
This commit is contained in:
		| @@ -701,5 +701,20 @@ | ||||
|   "stickersRemoved": "Sticker pack has been removed, you can add it again anytime.", | ||||
|   "stickersReload": "Reload Stickers", | ||||
|   "stickersReloadDescription": "Reload stickers from the server, update the sticker picker.", | ||||
|   "stickersReloaded": "Sticker packs has been reloaded." | ||||
|   "stickersReloaded": "Sticker packs has been reloaded.", | ||||
|   "stickersPackDelete": "Delete Pack {}", | ||||
|   "stickersPackDeleteDescription": "Are you sure you want to delete this sticker pack? This operation is irreversible.", | ||||
|   "stickersPackDeleted": "Sticker pack has been deleted.", | ||||
|   "stickersDelete": "Delete Sticker {}", | ||||
|   "stickersDeleteDescription": "Are you sure you want to delete this sticker? This operation is irreversible.", | ||||
|   "stickersDeleted": "Sticker has been deleted.", | ||||
|   "fieldStickerName": "Sticker Name", | ||||
|   "fieldStickerAlias": "Sticker Alias", | ||||
|   "fieldStickerAliasHint": "The unique sticker placeholder with the pack prefix.", | ||||
|   "fieldStickerPackName": "Name", | ||||
|   "fieldStickerPackDescription": "Description", | ||||
|   "fieldStickerPackPrefix": "Prefix", | ||||
|   "fieldStickerAttachment": "Attachment", | ||||
|   "stickersNew": "New Sticker", | ||||
|   "stickersNewDescription": "Create a new sticker belongs to this pack." | ||||
| } | ||||
|   | ||||
| @@ -699,5 +699,20 @@ | ||||
|   "stickersRemoved": "贴图包已被移除,你可以随时再次添加回来。", | ||||
|   "stickersReload": "重载贴图包", | ||||
|   "stickersReloadDescription": "从服务器重新加载添加过的贴图,更新贴图选择器。", | ||||
|   "stickersReloaded": "贴图包已重载。" | ||||
|   "stickersReloaded": "贴图包已重载。", | ||||
|   "stickersPackDelete": "删除贴图包 {}", | ||||
|   "stickersPackDeleteDescription": "你确定要删除这个贴图包吗?这个操作不可撤销。", | ||||
|   "stickersPackDeleted": "贴图包已被删除。", | ||||
|   "stickersDelete": "删除贴图 {}", | ||||
|   "stickersDeleteDescription": "你确定要删除这个贴图吗?这个操作不可撤销。", | ||||
|   "stickersDeleted": "贴图已被删除。", | ||||
|   "fieldStickerName": "贴图名称", | ||||
|   "fieldStickerAlias": "贴图别名", | ||||
|   "fieldStickerAliasHint": "和贴图包前缀组合成为本贴图的唯一占位符。", | ||||
|   "fieldStickerPackName": "名称", | ||||
|   "fieldStickerPackDescription": "描述", | ||||
|   "fieldStickerPackPrefix": "贴图包前缀", | ||||
|   "fieldStickerAttachment": "附件", | ||||
|   "stickersNew": "新建贴图", | ||||
|   "stickersNewDescription": "创建一个新的贴图。" | ||||
| } | ||||
|   | ||||
| @@ -35,6 +35,7 @@ import 'package:surface/screens/realm/realm_discovery.dart'; | ||||
| import 'package:surface/screens/settings.dart'; | ||||
| import 'package:surface/screens/sharing.dart'; | ||||
| import 'package:surface/screens/stickers.dart'; | ||||
| import 'package:surface/screens/stickers/pack_detail.dart'; | ||||
| import 'package:surface/screens/wallet.dart'; | ||||
| import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/widgets/about.dart'; | ||||
| @@ -238,6 +239,15 @@ final _appRoutes = [ | ||||
|     path: '/stickers', | ||||
|     name: 'stickers', | ||||
|     builder: (context, state) => const StickerScreen(), | ||||
|     routes: [ | ||||
|       GoRoute( | ||||
|         path: '/packs/:id', | ||||
|         name: 'stickerPack', | ||||
|         builder: (context, state) => StickerPackScreen( | ||||
|           id: int.tryParse(state.pathParameters['id']!)!, | ||||
|         ), | ||||
|       ), | ||||
|     ], | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/album', | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| @@ -83,6 +84,29 @@ class _StickerScreenState extends State<StickerScreen> | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _deletePack(SnStickerPack pack) async { | ||||
|     final confirm = await context.showConfirmDialog( | ||||
|       'stickersPackDelete'.tr(args: [pack.name]), | ||||
|       'stickersPackDeleteDescription'.tr(), | ||||
|     ); | ||||
|     if (!confirm) return; | ||||
|     if (!mounted) return; | ||||
|  | ||||
|     try { | ||||
|       setState(() => _isBusy = true); | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       await sn.client.delete('/cgi/uc/stickers/packs/${pack.id}'); | ||||
|       if (!mounted) return; | ||||
|       context.showSnackbar('stickersDeleted'.tr()); | ||||
|       _refreshPacks(); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } finally { | ||||
|       setState(() => _isBusy = false); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _refreshPacks() async { | ||||
|     _packs.clear(); | ||||
|     _totalCount = null; | ||||
| @@ -160,16 +184,32 @@ class _StickerScreenState extends State<StickerScreen> | ||||
|                         }, | ||||
|                         icon: const Icon(Symbols.remove), | ||||
|                       ) | ||||
|                     : null, | ||||
|                     : _tabController.index == 2 | ||||
|                         ? IconButton( | ||||
|                             onPressed: () { | ||||
|                               _deletePack(pack); | ||||
|                             }, | ||||
|                             icon: const Icon(Symbols.delete), | ||||
|                           ) | ||||
|                         : null, | ||||
|                 onTap: () { | ||||
|                   showModalBottomSheet( | ||||
|                     context: context, | ||||
|                     builder: (context) => _StickerPackAddPopup(pack: pack), | ||||
|                   ).then((value) { | ||||
|                     if (value == true && _tabController.index == 1) { | ||||
|                       _refreshPacks(); | ||||
|                     } | ||||
|                   }); | ||||
|                   if (_tabController.index == 0) { | ||||
|                     showModalBottomSheet( | ||||
|                       context: context, | ||||
|                       builder: (context) => _StickerPackAddPopup(pack: pack), | ||||
|                     ).then((value) { | ||||
|                       if (value == true && _tabController.index == 1) { | ||||
|                         _refreshPacks(); | ||||
|                       } | ||||
|                     }); | ||||
|                   } else { | ||||
|                     GoRouter.of(context).pushNamed( | ||||
|                       'stickerPack', | ||||
|                       pathParameters: { | ||||
|                         'id': pack.id.toString(), | ||||
|                       }, | ||||
|                     ); | ||||
|                   } | ||||
|                 }, | ||||
|               ); | ||||
|             }, | ||||
|   | ||||
							
								
								
									
										230
									
								
								lib/screens/stickers/pack_detail.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										230
									
								
								lib/screens/stickers/pack_detail.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,230 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/types/attachment.dart'; | ||||
| import 'package:surface/widgets/attachment/attachment_input.dart'; | ||||
| import 'package:surface/widgets/attachment/attachment_item.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
|  | ||||
| class StickerPackScreen extends StatefulWidget { | ||||
|   final int id; | ||||
|   const StickerPackScreen({super.key, required this.id}); | ||||
|  | ||||
|   @override | ||||
|   State<StickerPackScreen> createState() => _StickerPackScreenState(); | ||||
| } | ||||
|  | ||||
| class _StickerPackScreenState extends State<StickerPackScreen> { | ||||
|   SnStickerPack? _pack; | ||||
|  | ||||
|   Future<void> _fetchPack() async { | ||||
|     try { | ||||
|       setState(() => _isBusy = true); | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final resp = await sn.client.get('/cgi/uc/stickers/packs/${widget.id}'); | ||||
|       _pack = SnStickerPack.fromJson(resp.data); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } finally { | ||||
|       setState(() => _isBusy = false); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   bool _isBusy = false; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _fetchPack(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         title: Text(_pack?.name ?? 'loading'.tr()), | ||||
|       ), | ||||
|       body: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           LoadingIndicator(isActive: _isBusy), | ||||
|           if (_pack != null) | ||||
|             Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 Text(_pack!.name).bold(), | ||||
|                 Text( | ||||
|                   _pack!.description, | ||||
|                   maxLines: 2, | ||||
|                   overflow: TextOverflow.ellipsis, | ||||
|                 ), | ||||
|               ], | ||||
|             ).padding(horizontal: 24, vertical: 16), | ||||
|           const Divider(height: 1), | ||||
|           ListTile( | ||||
|             leading: const Icon(Symbols.add), | ||||
|             title: Text('stickersNew').tr(), | ||||
|             subtitle: Text('stickersNewDescription').tr(), | ||||
|             contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|             onTap: () { | ||||
|               showDialog( | ||||
|                 context: context, | ||||
|                 builder: (context) => _StickerCreateDialog(pack: _pack!), | ||||
|               ).then((value) { | ||||
|                 if (value) _fetchPack(); | ||||
|               }); | ||||
|             }, | ||||
|           ), | ||||
|           const Divider(height: 1), | ||||
|           if (_pack?.stickers != null) | ||||
|             Expanded( | ||||
|               child: GridView.extent( | ||||
|                 padding: EdgeInsets.only(left: 20, right: 20, top: 16), | ||||
|                 maxCrossAxisExtent: 48, | ||||
|                 mainAxisSpacing: 8, | ||||
|                 crossAxisSpacing: 8, | ||||
|                 children: _pack!.stickers! | ||||
|                     .map( | ||||
|                       (ele) => ClipRRect( | ||||
|                         borderRadius: BorderRadius.circular(8), | ||||
|                         child: Container( | ||||
|                           color: Theme.of(context) | ||||
|                               .colorScheme | ||||
|                               .surfaceContainerHigh, | ||||
|                           child: AttachmentItem( | ||||
|                             data: ele.attachment, | ||||
|                             heroTag: 'sticker-pack-${ele.attachment.rid}', | ||||
|                             fit: BoxFit.contain, | ||||
|                           ), | ||||
|                         ), | ||||
|                       ), | ||||
|                     ) | ||||
|                     .toList(), | ||||
|               ), | ||||
|             ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _StickerCreateDialog extends StatefulWidget { | ||||
|   final SnStickerPack pack; | ||||
|   const _StickerCreateDialog({required this.pack}); | ||||
|  | ||||
|   @override | ||||
|   State<_StickerCreateDialog> createState() => _StickerCreateDialogState(); | ||||
| } | ||||
|  | ||||
| class _StickerCreateDialogState extends State<_StickerCreateDialog> { | ||||
|   final TextEditingController _nameController = TextEditingController(); | ||||
|   final TextEditingController _aliasController = TextEditingController(); | ||||
|   final TextEditingController _attachmentController = TextEditingController(); | ||||
|  | ||||
|   bool _isBusy = false; | ||||
|  | ||||
|   Future<void> _createSticker() async { | ||||
|     if (_nameController.text.isEmpty || | ||||
|         _aliasController.text.isEmpty || | ||||
|         _attachmentController.text.isEmpty) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     setState(() => _isBusy = true); | ||||
|  | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       await sn.client.post( | ||||
|         '/cgi/uc/stickers', | ||||
|         data: { | ||||
|           'name': _nameController.text, | ||||
|           'alias': _aliasController.text, | ||||
|           'attachment_id': _attachmentController.text, | ||||
|           'pack_id': widget.pack.id, | ||||
|         }, | ||||
|       ); | ||||
|       if (!mounted) return; | ||||
|       Navigator.pop(context, true); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return AlertDialog( | ||||
|       title: Text('stickersNew'.tr()), | ||||
|       content: Column( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           TextField( | ||||
|             controller: _nameController, | ||||
|             decoration: InputDecoration( | ||||
|               border: const UnderlineInputBorder(), | ||||
|               labelText: 'fieldStickerName'.tr(), | ||||
|             ), | ||||
|             onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|           ), | ||||
|           const Gap(4), | ||||
|           TextField( | ||||
|             controller: _aliasController, | ||||
|             decoration: InputDecoration( | ||||
|               border: const UnderlineInputBorder(), | ||||
|               labelText: 'fieldStickerAlias'.tr(), | ||||
|               helperText: 'fieldStickerAliasHint'.tr(), | ||||
|             ), | ||||
|             onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|           ), | ||||
|           const Gap(4), | ||||
|           TextField( | ||||
|             controller: _attachmentController, | ||||
|             decoration: InputDecoration( | ||||
|               border: const UnderlineInputBorder(), | ||||
|               labelText: 'fieldStickerAttachment'.tr(), | ||||
|             ), | ||||
|             readOnly: true, | ||||
|             onTap: () async { | ||||
|               final attachment = await showDialog<SnAttachment?>( | ||||
|                 context: context, | ||||
|                 builder: (context) => AttachmentInputDialog( | ||||
|                   title: 'fieldStickerAttachment'.tr(), | ||||
|                   pool: 'sticker', | ||||
|                   mediaType: SnMediaType.image, | ||||
|                 ), | ||||
|               ); | ||||
|               if (attachment != null) { | ||||
|                 setState(() { | ||||
|                   _attachmentController.text = attachment.rid; | ||||
|                 }); | ||||
|               } | ||||
|             }, | ||||
|             onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|       actions: [ | ||||
|         TextButton( | ||||
|           onPressed: _isBusy | ||||
|               ? null | ||||
|               : () { | ||||
|                   Navigator.pop(context); | ||||
|                 }, | ||||
|           child: Text('dialogDismiss').tr(), | ||||
|         ), | ||||
|         TextButton( | ||||
|           onPressed: _isBusy ? null : () => _createSticker(), | ||||
|           child: Text('dialogConfirm').tr(), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user