From eb29f76b9a39a5df2fbc2a75ba47352e00ee9042 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 23 Feb 2025 14:11:45 +0800 Subject: [PATCH] :sparkles: Create new sticker to pack --- assets/translations/en-US.json | 17 +- assets/translations/zh-CN.json | 17 +- lib/router.dart | 10 ++ lib/screens/stickers.dart | 58 ++++++- lib/screens/stickers/pack_detail.dart | 230 ++++++++++++++++++++++++++ 5 files changed, 321 insertions(+), 11 deletions(-) create mode 100644 lib/screens/stickers/pack_detail.dart diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index a5e9c1b..f57a568 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -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." } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index a1d8ff9..24cbc75 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -699,5 +699,20 @@ "stickersRemoved": "贴图包已被移除,你可以随时再次添加回来。", "stickersReload": "重载贴图包", "stickersReloadDescription": "从服务器重新加载添加过的贴图,更新贴图选择器。", - "stickersReloaded": "贴图包已重载。" + "stickersReloaded": "贴图包已重载。", + "stickersPackDelete": "删除贴图包 {}", + "stickersPackDeleteDescription": "你确定要删除这个贴图包吗?这个操作不可撤销。", + "stickersPackDeleted": "贴图包已被删除。", + "stickersDelete": "删除贴图 {}", + "stickersDeleteDescription": "你确定要删除这个贴图吗?这个操作不可撤销。", + "stickersDeleted": "贴图已被删除。", + "fieldStickerName": "贴图名称", + "fieldStickerAlias": "贴图别名", + "fieldStickerAliasHint": "和贴图包前缀组合成为本贴图的唯一占位符。", + "fieldStickerPackName": "名称", + "fieldStickerPackDescription": "描述", + "fieldStickerPackPrefix": "贴图包前缀", + "fieldStickerAttachment": "附件", + "stickersNew": "新建贴图", + "stickersNewDescription": "创建一个新的贴图。" } diff --git a/lib/router.dart b/lib/router.dart index 57f15ac..c2732e3 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -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', diff --git a/lib/screens/stickers.dart b/lib/screens/stickers.dart index 59a2b1a..1308ba4 100644 --- a/lib/screens/stickers.dart +++ b/lib/screens/stickers.dart @@ -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 } } + Future _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(); + 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 _refreshPacks() async { _packs.clear(); _totalCount = null; @@ -160,16 +184,32 @@ class _StickerScreenState extends State }, 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(), + }, + ); + } }, ); }, diff --git a/lib/screens/stickers/pack_detail.dart b/lib/screens/stickers/pack_detail.dart new file mode 100644 index 0000000..18eafa8 --- /dev/null +++ b/lib/screens/stickers/pack_detail.dart @@ -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 createState() => _StickerPackScreenState(); +} + +class _StickerPackScreenState extends State { + SnStickerPack? _pack; + + Future _fetchPack() async { + try { + setState(() => _isBusy = true); + final sn = context.read(); + 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 _createSticker() async { + if (_nameController.text.isEmpty || + _aliasController.text.isEmpty || + _attachmentController.text.isEmpty) { + return; + } + + setState(() => _isBusy = true); + + try { + final sn = context.read(); + 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( + 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(), + ), + ], + ); + } +}