diff --git a/lib/bootstrapper.dart b/lib/bootstrapper.dart index 1e6cbd7..7aabad0 100644 --- a/lib/bootstrapper.dart +++ b/lib/bootstrapper.dart @@ -141,7 +141,7 @@ class _BootstrapperShellState extends State { try { for (var idx = 0; idx < _periods.length; idx++) { await _periods[idx].action(); - if (_isErrored) break; + if (_isErrored && !_isDismissable) break; if (_periodCursor < _periods.length - 1) { setState(() => _periodCursor++); } @@ -179,11 +179,11 @@ class _BootstrapperShellState extends State { GestureDetector( child: Column( children: [ - if (_isErrored && !_isDismissable) + if (_isErrored && !_isDismissable && !_isBusy) const Icon(Icons.cancel, size: 24), if (_isErrored && _isDismissable) const Icon(Icons.warning, size: 24), - if (!_isErrored && _isBusy) + if ((_isErrored && _isDismissable && _isBusy) || _isBusy) const SizedBox( width: 24, height: 24, diff --git a/lib/providers/attachment_uploader.dart b/lib/providers/attachment_uploader.dart index ed50ddb..5c675e7 100644 --- a/lib/providers/attachment_uploader.dart +++ b/lib/providers/attachment_uploader.dart @@ -14,6 +14,7 @@ class AttachmentUploadTask { double progress = 0; bool isUploading = false; bool isCompleted = false; + dynamic error; AttachmentUploadTask({ required this.file, @@ -66,7 +67,7 @@ class AttachmentUploaderController extends GetxController { queueOfUpload.remove(task); } - Future performSingleTask(int queueIndex) async { + Future performSingleTask(int queueIndex) async { isUploading.value = true; progressOfUpload.value = 0; @@ -83,9 +84,15 @@ class AttachmentUploaderController extends GetxController { queueOfUpload[queueIndex].progress = value; _progressOfUpload = value; }, + onError: (err) { + queueOfUpload[queueIndex].error = err; + queueOfUpload[queueIndex].isUploading = false; + }, ); - queueOfUpload.removeAt(queueIndex); + if (queueOfUpload[queueIndex].error == null) { + queueOfUpload.removeAt(queueIndex); + } _stopProgressSyncTimer(); _syncProgress(); @@ -103,6 +110,10 @@ class AttachmentUploaderController extends GetxController { _startProgressSyncTimer(); for (var idx = 0; idx < queueOfUpload.length; idx++) { + if (queueOfUpload[idx].isUploading || queueOfUpload[idx].error != null) { + continue; + } + queueOfUpload[idx].isUploading = true; final task = queueOfUpload[idx]; @@ -115,15 +126,20 @@ class AttachmentUploaderController extends GetxController { queueOfUpload[idx].progress = value; _progressOfUpload = (idx + value) / queueOfUpload.length; }, + onError: (err) { + queueOfUpload[idx].error = err; + queueOfUpload[idx].isUploading = false; + }, ); _progressOfUpload = (idx + 1) / queueOfUpload.length; - onData(result); + if (result != null) onData(result); queueOfUpload[idx].isUploading = false; - queueOfUpload[idx].isCompleted = false; + queueOfUpload[idx].isCompleted = true; } - queueOfUpload.clear(); + queueOfUpload.value = + queueOfUpload.where((x) => x.error == null).toList(growable: true); _stopProgressSyncTimer(); _syncProgress(); @@ -135,7 +151,7 @@ class AttachmentUploaderController extends GetxController { String path, String usage, Map? metadata, - Function(Attachment) callback, + Function(Attachment?) callback, ) async { if (isUploading.value) throw Exception('uploading blocked'); @@ -153,7 +169,7 @@ class AttachmentUploaderController extends GetxController { callback(result); } - Future uploadAttachment( + Future uploadAttachment( Uint8List data, String path, String usage, @@ -175,9 +191,9 @@ class AttachmentUploaderController extends GetxController { return result; } - Future _rawUploadAttachment( + Future _rawUploadAttachment( Uint8List data, String path, String usage, Map? metadata, - {Function(double)? onProgress}) async { + {Function(double)? onProgress, Function(dynamic err)? onError}) async { final AttachmentProvider provider = Get.find(); try { final result = await provider.createAttachment( @@ -189,7 +205,10 @@ class AttachmentUploaderController extends GetxController { ); return result; } catch (err) { - rethrow; + if (onError != null) { + onError(err); + } + return null; } } } diff --git a/lib/providers/content/attachment.dart b/lib/providers/content/attachment.dart index 6f7300f..edea50c 100644 --- a/lib/providers/content/attachment.dart +++ b/lib/providers/content/attachment.dart @@ -26,6 +26,8 @@ class AttachmentProvider extends GetConnect { List id, { noCache = false, }) async { + if (id.isEmpty) return List.empty(); + List result = List.filled(id.length, null); List pendingQuery = List.empty(growable: true); if (!noCache) { diff --git a/lib/providers/stickers.dart b/lib/providers/stickers.dart new file mode 100644 index 0000000..1920f8e --- /dev/null +++ b/lib/providers/stickers.dart @@ -0,0 +1,5 @@ +import 'package:get/get.dart'; + +class StickerProvider extends GetxController { + +} \ No newline at end of file diff --git a/lib/router.dart b/lib/router.dart index 12cfea3..d7ba60f 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -7,6 +7,7 @@ import 'package:solian/screens/account.dart'; import 'package:solian/screens/account/friend.dart'; import 'package:solian/screens/account/personalize.dart'; import 'package:solian/screens/account/profile_page.dart'; +import 'package:solian/screens/account/stickers.dart'; import 'package:solian/screens/channel/channel_chat.dart'; import 'package:solian/screens/channel/channel_detail.dart'; import 'package:solian/screens/channel/channel_organize.dart'; @@ -226,6 +227,14 @@ abstract class AppRouter { name: 'accountFriend', builder: (context, state) => const FriendScreen(), ), + GoRoute( + path: '/account/stickers', + name: 'accountStickers', + builder: (context, state) => TitleShell( + state: state, + child: const StickerScreen(), + ), + ), GoRoute( path: '/account/personalize', name: 'accountPersonalize', diff --git a/lib/screens/account.dart b/lib/screens/account.dart index 31df715..3ea03a3 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -46,6 +46,11 @@ class _AccountScreenState extends State { 'accountFriend'.tr, 'accountFriend', ), + ( + const Icon(Icons.emoji_symbols), + 'accountStickers'.tr, + 'accountStickers', + ), ]; final AuthProvider auth = Get.find(); diff --git a/lib/screens/account/friend.dart b/lib/screens/account/friend.dart index 3e78e8b..802fb75 100644 --- a/lib/screens/account/friend.dart +++ b/lib/screens/account/friend.dart @@ -43,7 +43,7 @@ class _FriendScreenState extends State _relations.where((x) => x.status == 0).length; } - void promptAddFriend() async { + void _promptAddFriend() async { final RelationshipProvider provider = Get.find(); final controller = TextEditingController(); @@ -146,7 +146,7 @@ class _FriendScreenState extends State ), floatingActionButton: FloatingActionButton( child: const Icon(Icons.add), - onPressed: () => promptAddFriend(), + onPressed: () => _promptAddFriend(), ), body: TabBarView( controller: _tabController, diff --git a/lib/screens/account/personalize.dart b/lib/screens/account/personalize.dart index 1f0cf53..22a726d 100644 --- a/lib/screens/account/personalize.dart +++ b/lib/screens/account/personalize.dart @@ -86,11 +86,17 @@ class _PersonalizeScreenState extends State { toolbarTitle: 'cropImage'.tr, toolbarColor: Theme.of(context).colorScheme.primary, toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary, - aspectRatioPresets: [CropAspectRatioPreset.square], + aspectRatioPresets: [ + if (position == 'avatar') CropAspectRatioPreset.square, + if (position == 'banner') _BannerCropAspectRatioPreset(), + ], ), IOSUiSettings( title: 'cropImage'.tr, - aspectRatioPresets: [CropAspectRatioPreset.square], + aspectRatioPresets: [ + if (position == 'avatar') CropAspectRatioPreset.square, + if (position == 'banner') _BannerCropAspectRatioPreset(), + ], ), WebUiSettings( context: context, @@ -346,3 +352,11 @@ class _PersonalizeScreenState extends State { super.dispose(); } } + +class _BannerCropAspectRatioPreset extends CropAspectRatioPresetData { + @override + (int, int)? get data => (16, 7); + + @override + String get name => '16x7'; +} diff --git a/lib/screens/account/stickers.dart b/lib/screens/account/stickers.dart new file mode 100644 index 0000000..acc3bf1 --- /dev/null +++ b/lib/screens/account/stickers.dart @@ -0,0 +1,86 @@ +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/providers/auth.dart'; +import 'package:solian/services.dart'; +import 'package:solian/widgets/stickers/sticker_uploader.dart'; + +class StickerScreen extends StatefulWidget { + const StickerScreen({super.key}); + + @override + State createState() => _StickerScreenState(); +} + +class _StickerScreenState extends State { + final PagingController _pagingController = + PagingController(firstPageKey: 0); + + Future _promptUploadSticker() { + return showDialog( + context: context, + builder: (context) => const StickerUploadDialog(), + ); + } + + @override + void initState() { + final AuthProvider auth = Get.find(); + 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'); + if (resp.statusCode == 200) { + final result = PaginationResult.fromJson(resp.body); + final out = result.data + ?.map((e) => e) // TODO transform object + .toList(); + if (out != null && result.data!.length >= 10) { + _pagingController.appendPage(out, pageKey + out.length); + } else if (out != null) { + _pagingController.appendLastPage(out); + } + } else { + _pagingController.error = resp.bodyString; + } + }); + super.initState(); + } + + @override + void dispose() { + _pagingController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + floatingActionButton: FloatingActionButton( + child: const Icon(Icons.add), + onPressed: () { + _promptUploadSticker().then((value) { + if (value == true) _pagingController.refresh(); + }); + }, + ), + body: RefreshIndicator( + onRefresh: () => Future.sync(() => _pagingController.refresh()), + child: CustomScrollView( + slivers: [ + PagedSliverList( + pagingController: _pagingController, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (BuildContext context, item, int index) { + return const SizedBox(); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/translations/en_us.dart b/lib/translations/en_us.dart index c3933c7..c5bc4bb 100644 --- a/lib/translations/en_us.dart +++ b/lib/translations/en_us.dart @@ -52,6 +52,7 @@ const i18nEnglish = { 'account': 'Account', 'accountPersonalize': 'Personalize', 'accountPersonalizeApplied': 'Account personalize settings has been saved.', + 'accountStickers': 'Stickers', 'accountFriend': 'Friend', 'accountFriendNew': 'New friend', 'accountFriendNewHint': @@ -339,4 +340,13 @@ const i18nEnglish = { 'themeColorApplied': 'Global theme color has been applied.', 'attachmentSaved': 'Attachment saved to your system album.', 'cropImage': 'Crop Image', + 'stickerUploader': 'Upload sticker', + '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!', + 'stickerUploaderAlias': 'Alias', + '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.', }; diff --git a/lib/translations/zh_cn.dart b/lib/translations/zh_cn.dart index 072e807..40e9695 100644 --- a/lib/translations/zh_cn.dart +++ b/lib/translations/zh_cn.dart @@ -52,6 +52,7 @@ const i18nSimplifiedChinese = { 'account': '账号', 'accountPersonalize': '个性化', 'accountPersonalizeApplied': '账户的个性化设置已保存。', + 'accountStickers': '贴图', 'accountFriend': '好友', 'accountFriendNew': '添加好友', 'accountFriendNewHint': '使用他人的用户名来发送一个好友请求吧!', @@ -316,4 +317,13 @@ const i18nSimplifiedChinese = { 'themeColorApplied': '全局主题颜色已应用', 'attachmentSaved': '附件已保存到系统相册', 'cropImage': '裁剪图片', + 'stickerUploader': '上传贴图', + 'stickerUploaderAttachmentNew': '上传附件', + 'stickerUploaderAttachment': '附件序列号', + 'stickerUploaderPack': '贴图包序号', + 'stickerUploaderPackHint': '没有该序号?请转到我们的创作者平台创建一个贴图包。', + 'stickerUploaderAlias': '贴图别名', + 'stickerUploaderAliasHint': '将会在输入时使用和贴图包前缀组成占位符。', + 'stickerUploaderName': '贴图名称', + 'stickerUploaderNameHint': '在贴图选择界面提供给用户的人类友好名称。', }; diff --git a/lib/widgets/account/account_status_action.dart b/lib/widgets/account/account_status_action.dart index 6d8c533..a5e4ce6 100644 --- a/lib/widgets/account/account_status_action.dart +++ b/lib/widgets/account/account_status_action.dart @@ -226,7 +226,7 @@ class _AccountStatusEditorDialogState extends State { onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), - const SizedBox(height: 5), + const SizedBox(height: 8), TextField( controller: _clearAtController, readOnly: true, @@ -238,7 +238,7 @@ class _AccountStatusEditorDialogState extends State { ), onTap: () => selectClearAt(), ), - const SizedBox(height: 5), + const SizedBox(height: 8), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Wrap( @@ -281,7 +281,7 @@ class _AccountStatusEditorDialogState extends State { ], ), ), - const SizedBox(height: 5), + const SizedBox(height: 8), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Wrap( diff --git a/lib/widgets/attachments/attachment_editor.dart b/lib/widgets/attachments/attachment_editor.dart index 0fbcfc5..7537b9a 100644 --- a/lib/widgets/attachments/attachment_editor.dart +++ b/lib/widgets/attachments/attachment_editor.dart @@ -23,16 +23,26 @@ import 'package:solian/widgets/attachments/attachment_fullscreen.dart'; class AttachmentEditorPopup extends StatefulWidget { final String usage; - final List initialAttachments; + final bool singleMode; + final bool imageOnly; + final bool autoUpload; + final double? imageMaxWidth; + final double? imageMaxHeight; + final List? initialAttachments; final void Function(int) onAdd; final void Function(int) onRemove; const AttachmentEditorPopup({ super.key, required this.usage, - required this.initialAttachments, required this.onAdd, required this.onRemove, + this.singleMode = false, + this.imageOnly = false, + this.autoUpload = false, + this.imageMaxWidth, + this.imageMaxHeight, + this.initialAttachments, }); @override @@ -43,7 +53,7 @@ class _AttachmentEditorPopupState extends State { final _imagePicker = ImagePicker(); final AttachmentUploaderController _uploadController = Get.find(); - bool _isAutoUpload = false; + late bool _isAutoUpload = widget.autoUpload; bool _isBusy = false; bool _isFirstTimeBusy = true; @@ -54,13 +64,28 @@ class _AttachmentEditorPopupState extends State { final AuthProvider auth = Get.find(); if (auth.isAuthorized.isFalse) return; - final medias = await _imagePicker.pickMultiImage(); - if (medias.isEmpty) return; + if (widget.singleMode) { + final medias = await _imagePicker.pickMultiImage( + maxWidth: widget.imageMaxWidth, + maxHeight: widget.imageMaxHeight, + ); + if (medias.isEmpty) return; - _enqueueTaskBatch(medias.map((x) { - final file = File(x.path); - return AttachmentUploadTask(file: file, usage: widget.usage); - })); + _enqueueTaskBatch(medias.map((x) { + final file = File(x.path); + return AttachmentUploadTask(file: file, usage: widget.usage); + })); + } else { + final media = await _imagePicker.pickMedia( + maxWidth: widget.imageMaxWidth, + maxHeight: widget.imageMaxHeight, + ); + if (media == null) return; + + _enqueueTask( + AttachmentUploadTask(file: File(media.path), usage: widget.usage), + ); + } } Future _pickVideoToUpload() async { @@ -164,6 +189,7 @@ class _AttachmentEditorPopupState extends State { if (result != null) { widget.onAdd(result.id); setState(() => _attachments.add(result)); + if (widget.singleMode) Navigator.pop(context); } } @@ -179,9 +205,11 @@ class _AttachmentEditorPopupState extends State { widget.usage, null, (item) { + if (item == null) return; widget.onAdd(item.id); if (mounted) { setState(() => _attachments.add(item)); + if (widget.singleMode) Navigator.pop(context); } }, ); @@ -209,12 +237,12 @@ class _AttachmentEditorPopupState extends State { void _revertMetadataList() { final AttachmentProvider attach = Get.find(); - if (widget.initialAttachments.isEmpty) { + if (widget.initialAttachments?.isEmpty ?? true) { _isFirstTimeBusy = false; return; } else { _attachments = List.filled( - widget.initialAttachments.length, + widget.initialAttachments!.length, null, growable: true, ); @@ -222,7 +250,9 @@ class _AttachmentEditorPopupState extends State { setState(() => _isBusy = true); - attach.listMetadata(widget.initialAttachments).then((result) { + attach + .listMetadata(widget.initialAttachments ?? List.empty()) + .then((result) { setState(() { _attachments = result; _isBusy = false; @@ -349,7 +379,13 @@ class _AttachmentEditorPopupState extends State { child: Icon(Icons.check), ), ), - if (!element.isCompleted && canBeCrop) + if (element.error != null) + IconButton( + tooltip: element.error!.toString(), + icon: const Icon(Icons.warning), + onPressed: () {}, + ), + if (!element.isCompleted && element.error == null && canBeCrop) Obx( () => IconButton( color: Colors.teal, @@ -362,7 +398,7 @@ class _AttachmentEditorPopupState extends State { }, ), ), - if (!element.isCompleted && !element.isUploading) + if (!element.isCompleted && !element.isUploading && element.error == null) Obx( () => IconButton( color: Colors.green, @@ -374,9 +410,13 @@ class _AttachmentEditorPopupState extends State { _uploadController .performSingleTask(index) .then((r) { + if (r == null) return; widget.onAdd(r.id); if (mounted) { setState(() => _attachments.add(r)); + if (widget.singleMode) { + Navigator.pop(context); + } } }); }, @@ -519,6 +559,7 @@ class _AttachmentEditorPopupState extends State { widget.onAdd(r.id); if (mounted) { setState(() => _attachments.add(r)); + if (widget.singleMode) Navigator.pop(context); } }); } @@ -670,6 +711,7 @@ class _AttachmentEditorPopupState extends State { ignoring: _uploadController.isUploading.value, child: Container( height: 64, + width: MediaQuery.of(context).size.width, decoration: BoxDecoration( border: Border( top: BorderSide( @@ -686,9 +728,10 @@ class _AttachmentEditorPopupState extends State { alignment: WrapAlignment.center, runAlignment: WrapAlignment.center, children: [ - if (PlatformInfo.isDesktop || - PlatformInfo.isIOS || - PlatformInfo.isWeb) + if ((PlatformInfo.isDesktop || + PlatformInfo.isIOS || + PlatformInfo.isWeb) && + !widget.imageOnly) ElevatedButton.icon( icon: const Icon(Icons.paste), label: Text('attachmentAddClipboard'.tr), @@ -701,36 +744,40 @@ class _AttachmentEditorPopupState extends State { style: const ButtonStyle(visualDensity: density), onPressed: () => _pickPhotoToUpload(), ), - ElevatedButton.icon( - icon: const Icon(Icons.add_road), - label: Text('attachmentAddGalleryVideo'.tr), - style: const ButtonStyle(visualDensity: density), - onPressed: () => _pickVideoToUpload(), - ), + if (!widget.imageOnly) + ElevatedButton.icon( + icon: const Icon(Icons.add_road), + label: Text('attachmentAddGalleryVideo'.tr), + style: const ButtonStyle(visualDensity: density), + onPressed: () => _pickVideoToUpload(), + ), ElevatedButton.icon( icon: const Icon(Icons.photo_camera_back), label: Text('attachmentAddCameraPhoto'.tr), style: const ButtonStyle(visualDensity: density), onPressed: () => _takeMediaToUpload(false), ), - ElevatedButton.icon( - icon: const Icon(Icons.video_camera_back_outlined), - label: Text('attachmentAddCameraVideo'.tr), - style: const ButtonStyle(visualDensity: density), - onPressed: () => _takeMediaToUpload(true), - ), - ElevatedButton.icon( - icon: const Icon(Icons.file_present_rounded), - label: Text('attachmentAddFile'.tr), - style: const ButtonStyle(visualDensity: density), - onPressed: () => _pickFileToUpload(), - ), - ElevatedButton.icon( - icon: const Icon(Icons.link), - label: Text('attachmentAddFile'.tr), - style: const ButtonStyle(visualDensity: density), - onPressed: () => _linkAttachments(), - ), + if (!widget.imageOnly) + ElevatedButton.icon( + icon: const Icon(Icons.video_camera_back_outlined), + label: Text('attachmentAddCameraVideo'.tr), + style: const ButtonStyle(visualDensity: density), + onPressed: () => _takeMediaToUpload(true), + ), + if (!widget.imageOnly) + ElevatedButton.icon( + icon: const Icon(Icons.file_present_rounded), + label: Text('attachmentAddFile'.tr), + style: const ButtonStyle(visualDensity: density), + onPressed: () => _pickFileToUpload(), + ), + if (!widget.imageOnly) + ElevatedButton.icon( + icon: const Icon(Icons.link), + label: Text('attachmentAddFile'.tr), + style: const ButtonStyle(visualDensity: density), + onPressed: () => _linkAttachments(), + ), ], ).paddingSymmetric(horizontal: 12), ), diff --git a/lib/widgets/attachments/attachment_fullscreen.dart b/lib/widgets/attachments/attachment_fullscreen.dart index 8a7849c..85badc0 100644 --- a/lib/widgets/attachments/attachment_fullscreen.dart +++ b/lib/widgets/attachments/attachment_fullscreen.dart @@ -257,6 +257,10 @@ class _AttachmentFullScreenState extends State { child: Wrap( spacing: 6, children: [ + Text( + '#${widget.item.id}', + style: metaTextStyle, + ), if (widget.item.metadata?['width'] != null && widget.item.metadata?['height'] != null) Text( diff --git a/lib/widgets/channel/channel_member.dart b/lib/widgets/channel/channel_member.dart index c73dd18..27a3e7a 100644 --- a/lib/widgets/channel/channel_member.dart +++ b/lib/widgets/channel/channel_member.dart @@ -57,7 +57,7 @@ class _ChannelMemberListPopupState extends State { setState(() => _isBusy = false); } - void promptAddMember() async { + void _promptAddMember() async { final input = await showModalBottomSheet( context: context, builder: (context) { @@ -141,7 +141,7 @@ class _ChannelMemberListPopupState extends State { 'channelMembersAddHint' .trParams({'channel': '#${widget.channel.alias}'}), ), - onTap: () => promptAddMember(), + onTap: () => _promptAddMember(), ), Expanded( child: ListView.builder( diff --git a/lib/widgets/stickers/sticker_uploader.dart b/lib/widgets/stickers/sticker_uploader.dart new file mode 100644 index 0000000..ed44730 --- /dev/null +++ b/lib/widgets/stickers/sticker_uploader.dart @@ -0,0 +1,183 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:solian/exts.dart'; +import 'package:solian/providers/auth.dart'; +import 'package:solian/widgets/attachments/attachment_editor.dart'; + +class StickerUploadDialog extends StatefulWidget { + const StickerUploadDialog({super.key}); + + @override + State createState() => _StickerUploadDialogState(); +} + +class _StickerUploadDialogState extends State { + final TextEditingController _attachmentController = TextEditingController(); + final TextEditingController _packController = TextEditingController(); + final TextEditingController _aliasController = TextEditingController(); + final TextEditingController _nameController = TextEditingController(); + + Color get _unFocusColor => + Theme.of(context).colorScheme.onSurface.withOpacity(0.75); + + bool _isBusy = false; + + void _promptUploadNewAttachment() { + showModalBottomSheet( + context: context, + builder: (context) => AttachmentEditorPopup( + usage: 'sticker', + singleMode: true, + imageOnly: true, + autoUpload: true, + imageMaxHeight: 28, + imageMaxWidth: 28, + onAdd: (value) { + setState(() { + _attachmentController.text = value.toString(); + }); + }, + initialAttachments: const [], + onRemove: (_) {}, + ), + ); + } + + Future _applySticker() async { + final AuthProvider auth = Get.find(); + if (auth.isAuthorized.isFalse) return; + + if ([ + _nameController.text.isEmpty, + _aliasController.text.isEmpty, + _packController.text.isEmpty, + _attachmentController.text.isEmpty, + ].any((x) => x)) { + return; + } + + final client = auth.configureClient('files'); + final resp = await client.post('/stickers', { + 'name': _nameController.text, + 'alias': _aliasController.text, + 'pack_id': int.tryParse(_packController.text), + 'attachment_id': int.tryParse(_attachmentController.text), + }); + + if (resp.statusCode != 200) { + context.showErrorDialog(resp.bodyString); + } else { + Navigator.pop(context, true); + } + } + + @override + void dispose() { + _attachmentController.dispose(); + _packController.dispose(); + _aliasController.dispose(); + _nameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text('stickerUploader'.tr), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile( + title: Text('stickerUploaderAttachmentNew'.tr), + contentPadding: const EdgeInsets.only(left: 16, right: 13), + trailing: const Icon(Icons.chevron_right), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + onTap: () { + _promptUploadNewAttachment(); + }, + ), + const SizedBox(height: 8), + TextField( + controller: _attachmentController, + decoration: InputDecoration( + isDense: true, + border: const OutlineInputBorder(), + prefixText: '#', + labelText: 'stickerUploaderAttachment'.tr, + ), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + const SizedBox(height: 8), + TextField( + controller: _packController, + decoration: InputDecoration( + isDense: true, + border: const OutlineInputBorder(), + prefixText: '#', + labelText: 'stickerUploaderPack'.tr, + ), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + Container( + padding: + const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 6), + child: Text( + 'stickerUploaderPackHint'.tr, + style: TextStyle(color: _unFocusColor), + ), + ), + TextField( + controller: _aliasController, + decoration: InputDecoration( + isDense: true, + border: const OutlineInputBorder(), + labelText: 'stickerUploaderAlias'.tr, + ), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + Container( + padding: + const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 6), + child: Text( + 'stickerUploaderAliasHint'.tr, + style: TextStyle(color: _unFocusColor), + ), + ), + TextField( + controller: _nameController, + decoration: InputDecoration( + isDense: true, + border: const OutlineInputBorder(), + labelText: 'stickerUploaderName'.tr, + ), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + Container( + padding: const EdgeInsets.only(left: 8, right: 8, top: 4), + child: Text( + 'stickerUploaderNameHint'.tr, + style: TextStyle(color: _unFocusColor), + ), + ), + ], + ), + actions: [ + TextButton( + style: TextButton.styleFrom( + foregroundColor: + Theme.of(context).colorScheme.onSurface.withOpacity(0.8), + ), + onPressed: _isBusy ? null : () => Navigator.pop(context), + child: Text('cancel'.tr), + ), + TextButton( + onPressed: _isBusy ? null : () => _applySticker(), + child: Text('apply'.tr), + ), + ], + ); + } +}