✨ Basic sticker management
This commit is contained in:
		
							
								
								
									
										122
									
								
								lib/models/stickers.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								lib/models/stickers.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | ||||
| import 'package:solian/models/account.dart'; | ||||
| import 'package:solian/models/attachment.dart'; | ||||
|  | ||||
| class Sticker { | ||||
|   int id; | ||||
|   DateTime createdAt; | ||||
|   DateTime updatedAt; | ||||
|   DateTime? deletedAt; | ||||
|   String alias; | ||||
|   String name; | ||||
|   int attachmentId; | ||||
|   Attachment attachment; | ||||
|   int packId; | ||||
|   StickerPack? pack; | ||||
|   int accountId; | ||||
|   Account account; | ||||
|  | ||||
|   Sticker({ | ||||
|     required this.id, | ||||
|     required this.createdAt, | ||||
|     required this.updatedAt, | ||||
|     required this.deletedAt, | ||||
|     required this.alias, | ||||
|     required this.name, | ||||
|     required this.attachmentId, | ||||
|     required this.attachment, | ||||
|     required this.packId, | ||||
|     required this.pack, | ||||
|     required this.accountId, | ||||
|     required this.account, | ||||
|   }); | ||||
|  | ||||
|   factory Sticker.fromJson(Map<String, dynamic> json) => Sticker( | ||||
|         id: json['id'], | ||||
|         createdAt: DateTime.parse(json['created_at']), | ||||
|         updatedAt: DateTime.parse(json['updated_at']), | ||||
|         deletedAt: json['deleted_at'] != null | ||||
|             ? DateTime.parse(json['deleted_at']) | ||||
|             : json['deleted_at'], | ||||
|         alias: json['alias'], | ||||
|         name: json['name'], | ||||
|         attachmentId: json['attachment_id'], | ||||
|         attachment: Attachment.fromJson(json['attachment']), | ||||
|         packId: json['pack_id'], | ||||
|         pack: json['pack'] != null ? StickerPack.fromJson(json['pack']) : null, | ||||
|         accountId: json['account_id'], | ||||
|         account: Account.fromJson(json['account']), | ||||
|       ); | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
|         'id': id, | ||||
|         'created_at': createdAt.toIso8601String(), | ||||
|         'updated_at': updatedAt.toIso8601String(), | ||||
|         'deleted_at': deletedAt?.toIso8601String(), | ||||
|         'alias': alias, | ||||
|         'name': name, | ||||
|         'attachment_id': attachmentId, | ||||
|         'attachment': attachment.toJson(), | ||||
|         'pack_id': packId, | ||||
|         'account_id': accountId, | ||||
|         'account': account.toJson(), | ||||
|       }; | ||||
| } | ||||
|  | ||||
| class StickerPack { | ||||
|   int id; | ||||
|   DateTime createdAt; | ||||
|   DateTime updatedAt; | ||||
|   DateTime? deletedAt; | ||||
|   String prefix; | ||||
|   String name; | ||||
|   String description; | ||||
|   List<Sticker>? stickers; | ||||
|   int accountId; | ||||
|   Account account; | ||||
|  | ||||
|   StickerPack({ | ||||
|     required this.id, | ||||
|     required this.createdAt, | ||||
|     required this.updatedAt, | ||||
|     required this.deletedAt, | ||||
|     required this.prefix, | ||||
|     required this.name, | ||||
|     required this.description, | ||||
|     required this.stickers, | ||||
|     required this.accountId, | ||||
|     required this.account, | ||||
|   }); | ||||
|  | ||||
|   factory StickerPack.fromJson(Map<String, dynamic> json) => StickerPack( | ||||
|         id: json['id'], | ||||
|         createdAt: DateTime.parse(json['created_at']), | ||||
|         updatedAt: DateTime.parse(json['updated_at']), | ||||
|         deletedAt: json['deleted_at'] != null | ||||
|             ? DateTime.parse(json['deleted_at']) | ||||
|             : json['deleted_at'], | ||||
|         prefix: json['prefix'], | ||||
|         name: json['name'], | ||||
|         description: json['description'], | ||||
|         stickers: json['stickers'] == null | ||||
|             ? [] | ||||
|             : List<Sticker>.from( | ||||
|                 json['stickers']!.map((x) => Sticker.fromJson(x))), | ||||
|         accountId: json['account_id'], | ||||
|         account: Account.fromJson(json['account']), | ||||
|       ); | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
|         'id': id, | ||||
|         'created_at': createdAt.toIso8601String(), | ||||
|         'updated_at': updatedAt.toIso8601String(), | ||||
|         'deleted_at': deletedAt?.toIso8601String(), | ||||
|         'prefix': prefix, | ||||
|         'name': name, | ||||
|         'description': description, | ||||
|         'stickers': stickers == null | ||||
|             ? [] | ||||
|             : List<dynamic>.from(stickers!.map((x) => x.toJson())), | ||||
|         'account_id': accountId, | ||||
|         'account': account.toJson(), | ||||
|       }; | ||||
| } | ||||
| @@ -1,7 +1,10 @@ | ||||
| import 'package:cached_network_image/cached_network_image.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; | ||||
| import 'package:solian/models/pagination.dart'; | ||||
| import 'package:solian/models/stickers.dart'; | ||||
| import 'package:solian/platform.dart'; | ||||
| import 'package:solian/providers/auth.dart'; | ||||
| import 'package:solian/services.dart'; | ||||
| import 'package:solian/widgets/stickers/sticker_uploader.dart'; | ||||
| @@ -14,13 +17,92 @@ class StickerScreen extends StatefulWidget { | ||||
| } | ||||
|  | ||||
| class _StickerScreenState extends State<StickerScreen> { | ||||
|   final PagingController<int, dynamic> _pagingController = | ||||
|   final PagingController<int, StickerPack> _pagingController = | ||||
|       PagingController(firstPageKey: 0); | ||||
|  | ||||
|   Future<bool?> _promptUploadSticker() { | ||||
|   Future<bool> _promptDelete(Sticker item, String prefix) async { | ||||
|     final AuthProvider auth = Get.find(); | ||||
|     if (auth.isAuthorized.isFalse) return false; | ||||
|  | ||||
|     final confirm = await showDialog( | ||||
|       context: context, | ||||
|       builder: (context) => AlertDialog( | ||||
|         title: Text('stickerDeletionConfirm'.tr), | ||||
|         content: Text( | ||||
|           'stickerDeletionConfirmCaption'.trParams({ | ||||
|             'name': ':${'$prefix${item.alias}'.camelCase}:', | ||||
|           }), | ||||
|         ), | ||||
|         actions: <Widget>[ | ||||
|           TextButton( | ||||
|             onPressed: () => Navigator.pop(context), | ||||
|             child: Text('cancel'.tr), | ||||
|           ), | ||||
|           TextButton( | ||||
|             onPressed: () => Navigator.pop(context, true), | ||||
|             child: Text('confirm'.tr), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|     if (confirm != true) return false; | ||||
|  | ||||
|     final client = auth.configureClient('files'); | ||||
|     final resp = await client.delete('/stickers/${item.id}'); | ||||
|  | ||||
|     return resp.statusCode == 200; | ||||
|   } | ||||
|  | ||||
|   Future<bool?> _promptUploadSticker({Sticker? edit}) { | ||||
|     return showDialog( | ||||
|       context: context, | ||||
|       builder: (context) => const StickerUploadDialog(), | ||||
|       builder: (context) => StickerUploadDialog( | ||||
|         edit: edit, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildEmoteEntry(Sticker item, String prefix) { | ||||
|     final imageUrl = ServiceFinder.buildUrl( | ||||
|       'files', | ||||
|       '/attachments/${item.attachmentId}', | ||||
|     ); | ||||
|     return ListTile( | ||||
|       title: Text(item.name), | ||||
|       subtitle: Text(':${'$prefix${item.alias}'.camelCase}:'), | ||||
|       contentPadding: const EdgeInsets.only(left: 16, right: 14), | ||||
|       trailing: Row( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         children: [ | ||||
|           IconButton( | ||||
|             icon: const Icon(Icons.edit_square), | ||||
|             onPressed: () { | ||||
|               _promptUploadSticker(edit: item).then((value) { | ||||
|                 if (value == true) _pagingController.refresh(); | ||||
|               }); | ||||
|             }, | ||||
|           ), | ||||
|           IconButton( | ||||
|             icon: const Icon(Icons.delete), | ||||
|             onPressed: () { | ||||
|               _promptDelete(item, prefix).then((value) { | ||||
|                 if (value == true) _pagingController.refresh(); | ||||
|               }); | ||||
|             }, | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|       leading: PlatformInfo.canCacheImage | ||||
|           ? CachedNetworkImage( | ||||
|               imageUrl: imageUrl, | ||||
|               width: 28, | ||||
|               height: 28, | ||||
|             ) | ||||
|           : Image.network( | ||||
|               imageUrl, | ||||
|               width: 28, | ||||
|               height: 28, | ||||
|             ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @@ -30,13 +112,12 @@ class _StickerScreenState extends State<StickerScreen> { | ||||
|     final name = auth.userProfile.value!['name']; | ||||
|     _pagingController.addPageRequestListener((pageKey) async { | ||||
|       final client = ServiceFinder.configureClient('files'); | ||||
|       final resp = | ||||
|           await client.get('/stickers?take=10&offset=$pageKey&author=$name'); | ||||
|       final resp = await client.get( | ||||
|         '/stickers/manifest?take=10&offset=$pageKey&author=$name', | ||||
|       ); | ||||
|       if (resp.statusCode == 200) { | ||||
|         final result = PaginationResult.fromJson(resp.body); | ||||
|         final out = result.data | ||||
|             ?.map((e) => e) // TODO transform object | ||||
|             .toList(); | ||||
|         final out = result.data?.map((e) => StickerPack.fromJson(e)).toList(); | ||||
|         if (out != null && result.data!.length >= 10) { | ||||
|           _pagingController.appendPage(out, pageKey + out.length); | ||||
|         } else if (out != null) { | ||||
| @@ -70,11 +151,22 @@ class _StickerScreenState extends State<StickerScreen> { | ||||
|         onRefresh: () => Future.sync(() => _pagingController.refresh()), | ||||
|         child: CustomScrollView( | ||||
|           slivers: [ | ||||
|             PagedSliverList( | ||||
|             PagedSliverList<int, StickerPack>( | ||||
|               pagingController: _pagingController, | ||||
|               builderDelegate: PagedChildBuilderDelegate( | ||||
|                 itemBuilder: (BuildContext context, item, int index) { | ||||
|                   return const SizedBox(); | ||||
|                   return ExpansionTile( | ||||
|                     title: Text(item.name), | ||||
|                     subtitle: Text( | ||||
|                       item.description, | ||||
|                       maxLines: 1, | ||||
|                       overflow: TextOverflow.ellipsis, | ||||
|                     ), | ||||
|                     children: item.stickers | ||||
|                             ?.map((x) => _buildEmoteEntry(x, item.prefix)) | ||||
|                             .toList() ?? | ||||
|                         List.empty(), | ||||
|                   ); | ||||
|                 }, | ||||
|               ), | ||||
|             ), | ||||
|   | ||||
| @@ -163,9 +163,11 @@ const i18nEnglish = { | ||||
|   'attachmentAutoUpload': 'Auto Upload', | ||||
|   'attachmentUploadQueue': 'Upload Queue', | ||||
|   'attachmentUploadQueueStart': 'Start All', | ||||
|   'attachmentUploadInProgress': 'There are attachments being uploaded. Please wait until all attachments have been uploaded before proceeding...', | ||||
|   'attachmentUploadInProgress': | ||||
|       'There are attachments being uploaded. Please wait until all attachments have been uploaded before proceeding...', | ||||
|   'attachmentAttached': 'Exists Files', | ||||
|   'attachmentUploadBlocked': 'Upload blocked, there is currently a task in progress...', | ||||
|   'attachmentUploadBlocked': | ||||
|       'Upload blocked, there is currently a task in progress...', | ||||
|   'attachmentAdd': 'Attach attachments', | ||||
|   'attachmentAddGalleryPhoto': 'Gallery photo', | ||||
|   'attachmentAddGalleryVideo': 'Gallery video', | ||||
| @@ -174,7 +176,8 @@ const i18nEnglish = { | ||||
|   'attachmentAddClipboard': 'Paste file', | ||||
|   'attachmentAddFile': 'Attach file', | ||||
|   'attachmentAddLink': 'Link attachments', | ||||
|   'attachmentAddLinkHint': 'Enter attachment serial number to link that attachment', | ||||
|   'attachmentAddLinkHint': | ||||
|       'Enter attachment serial number to link that attachment', | ||||
|   'attachmentAddLinkInput': 'Serial number', | ||||
|   'attachmentSetting': 'Adjust attachment', | ||||
|   'attachmentAlt': 'Alternative text', | ||||
| @@ -318,8 +321,10 @@ const i18nEnglish = { | ||||
|   'bsCheckForUpdate': 'Checking For Updates', | ||||
|   'bsCheckForUpdateFailed': 'Unable to Check Updates', | ||||
|   'bsCheckForUpdateNew': 'Found New Version', | ||||
|   'bsCheckForUpdateDescApple': 'Please head to TestFlight and update your app to latest version to prevent error happens and get latest functions.', | ||||
|   'bsCheckForUpdateDescCommon': 'Please head to our website download and install latest version of application to prevent error happens and get latest functions.', | ||||
|   'bsCheckForUpdateDescApple': | ||||
|       'Please head to TestFlight and update your app to latest version to prevent error happens and get latest functions.', | ||||
|   'bsCheckForUpdateDescCommon': | ||||
|       'Please head to our website download and install latest version of application to prevent error happens and get latest functions.', | ||||
|   'bsCheckingServer': 'Checking Server Status', | ||||
|   'bsCheckingServerFail': | ||||
|       'Unable connect to server, check your network connection', | ||||
| @@ -337,6 +342,9 @@ const i18nEnglish = { | ||||
|   'themeColorMiku': 'Miku Blue', | ||||
|   'themeColorKagamine': 'Kagamine Yellow', | ||||
|   'themeColorLuka': 'Luka Pink', | ||||
|   'stickerDeletionConfirm': 'Confirm sticker delete', | ||||
|   'stickerDeletionConfirmCaption': | ||||
|       'Are you sure to delete sticker @name? This action cannot be undo.', | ||||
|   'themeColorApplied': 'Global theme color has been applied.', | ||||
|   'attachmentSaved': 'Attachment saved to your system album.', | ||||
|   'cropImage': 'Crop Image', | ||||
| @@ -344,9 +352,12 @@ const i18nEnglish = { | ||||
|   'stickerUploaderAttachmentNew': 'Upload new attachment', | ||||
|   'stickerUploaderAttachment': 'Attachment serial number', | ||||
|   'stickerUploaderPack': 'Sticker pack serial number', | ||||
|   'stickerUploaderPackHint': 'Don\'t have pack id? Head to creator platform and create one!', | ||||
|   'stickerUploaderPackHint': | ||||
|       'Don\'t have pack id? Head to creator platform and create one!', | ||||
|   'stickerUploaderAlias': 'Alias', | ||||
|   'stickerUploaderAliasHint': 'Will be used as a placeholder with the sticker pack prefix when entered.', | ||||
|   'stickerUploaderAliasHint': | ||||
|       'Will be used as a placeholder with the sticker pack prefix when entered.', | ||||
|   'stickerUploaderName': 'Name', | ||||
|   'stickerUploaderNameHint': 'A human-friendly name given to the user in the sticker selection interface.', | ||||
|   'stickerUploaderNameHint': | ||||
|       'A human-friendly name given to the user in the sticker selection interface.', | ||||
| }; | ||||
|   | ||||
| @@ -315,6 +315,8 @@ const i18nSimplifiedChinese = { | ||||
|   'themeColorKagamine': '镜音黄', | ||||
|   'themeColorLuka': '流音粉', | ||||
|   'themeColorApplied': '全局主题颜色已应用', | ||||
|   'stickerDeletionConfirm': '确认删除贴图', | ||||
|   'stickerDeletionConfirmCaption': '你确认要删除贴图 @name 吗?该操作不可撤销。', | ||||
|   'attachmentSaved': '附件已保存到系统相册', | ||||
|   'cropImage': '裁剪图片', | ||||
|   'stickerUploader': '上传贴图', | ||||
|   | ||||
| @@ -1,11 +1,14 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:solian/exts.dart'; | ||||
| import 'package:solian/models/stickers.dart'; | ||||
| import 'package:solian/providers/auth.dart'; | ||||
| import 'package:solian/widgets/attachments/attachment_editor.dart'; | ||||
|  | ||||
| class StickerUploadDialog extends StatefulWidget { | ||||
|   const StickerUploadDialog({super.key}); | ||||
|   final Sticker? edit; | ||||
|  | ||||
|   const StickerUploadDialog({super.key, this.edit}); | ||||
|  | ||||
|   @override | ||||
|   State<StickerUploadDialog> createState() => _StickerUploadDialogState(); | ||||
| @@ -56,13 +59,27 @@ class _StickerUploadDialogState extends State<StickerUploadDialog> { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     setState(() => _isBusy = true); | ||||
|  | ||||
|     Response resp; | ||||
|     final client = auth.configureClient('files'); | ||||
|     final resp = await client.post('/stickers', { | ||||
|     if (widget.edit == null) { | ||||
|       resp = await client.post('/stickers', { | ||||
|         'name': _nameController.text, | ||||
|         'alias': _aliasController.text, | ||||
|         'pack_id': int.tryParse(_packController.text), | ||||
|         'attachment_id': int.tryParse(_attachmentController.text), | ||||
|       }); | ||||
|     } else { | ||||
|       resp = await client.put('/stickers/${widget.edit!.id}', { | ||||
|         'name': _nameController.text, | ||||
|         'alias': _aliasController.text, | ||||
|         'pack_id': int.tryParse(_packController.text), | ||||
|         'attachment_id': int.tryParse(_attachmentController.text), | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     setState(() => _isBusy = false); | ||||
|  | ||||
|     if (resp.statusCode != 200) { | ||||
|       context.showErrorDialog(resp.bodyString); | ||||
| @@ -71,6 +88,17 @@ class _StickerUploadDialogState extends State<StickerUploadDialog> { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     if (widget.edit != null) { | ||||
|       _attachmentController.text = widget.edit!.attachmentId.toString(); | ||||
|       _packController.text = widget.edit!.packId.toString(); | ||||
|       _aliasController.text = widget.edit!.alias; | ||||
|       _nameController.text = widget.edit!.name; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     _attachmentController.dispose(); | ||||
| @@ -164,7 +192,7 @@ class _StickerUploadDialogState extends State<StickerUploadDialog> { | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|       actions: <Widget>[ | ||||
|       actions: [ | ||||
|         TextButton( | ||||
|           style: TextButton.styleFrom( | ||||
|             foregroundColor: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user