✨ Create sticker
✨ Single file mode attachment editor and more options
This commit is contained in:
@@ -226,7 +226,7 @@ class _AccountStatusEditorDialogState extends State<AccountStatusEditorDialog> {
|
||||
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<AccountStatusEditorDialog> {
|
||||
),
|
||||
onTap: () => selectClearAt(),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
const SizedBox(height: 8),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Wrap(
|
||||
@@ -281,7 +281,7 @@ class _AccountStatusEditorDialogState extends State<AccountStatusEditorDialog> {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
const SizedBox(height: 8),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Wrap(
|
||||
|
@@ -23,16 +23,26 @@ import 'package:solian/widgets/attachments/attachment_fullscreen.dart';
|
||||
|
||||
class AttachmentEditorPopup extends StatefulWidget {
|
||||
final String usage;
|
||||
final List<int> initialAttachments;
|
||||
final bool singleMode;
|
||||
final bool imageOnly;
|
||||
final bool autoUpload;
|
||||
final double? imageMaxWidth;
|
||||
final double? imageMaxHeight;
|
||||
final List<int>? 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<AttachmentEditorPopup> {
|
||||
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<AttachmentEditorPopup> {
|
||||
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<void> _pickVideoToUpload() async {
|
||||
@@ -164,6 +189,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
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<AttachmentEditorPopup> {
|
||||
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<AttachmentEditorPopup> {
|
||||
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<AttachmentEditorPopup> {
|
||||
|
||||
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<AttachmentEditorPopup> {
|
||||
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<AttachmentEditorPopup> {
|
||||
},
|
||||
),
|
||||
),
|
||||
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<AttachmentEditorPopup> {
|
||||
_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<AttachmentEditorPopup> {
|
||||
widget.onAdd(r.id);
|
||||
if (mounted) {
|
||||
setState(() => _attachments.add(r));
|
||||
if (widget.singleMode) Navigator.pop(context);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -670,6 +711,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
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<AttachmentEditorPopup> {
|
||||
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<AttachmentEditorPopup> {
|
||||
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),
|
||||
),
|
||||
|
@@ -257,6 +257,10 @@ class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
|
||||
child: Wrap(
|
||||
spacing: 6,
|
||||
children: [
|
||||
Text(
|
||||
'#${widget.item.id}',
|
||||
style: metaTextStyle,
|
||||
),
|
||||
if (widget.item.metadata?['width'] != null &&
|
||||
widget.item.metadata?['height'] != null)
|
||||
Text(
|
||||
|
@@ -57,7 +57,7 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
|
||||
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<ChannelMemberListPopup> {
|
||||
'channelMembersAddHint'
|
||||
.trParams({'channel': '#${widget.channel.alias}'}),
|
||||
),
|
||||
onTap: () => promptAddMember(),
|
||||
onTap: () => _promptAddMember(),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
|
183
lib/widgets/stickers/sticker_uploader.dart
Normal file
183
lib/widgets/stickers/sticker_uploader.dart
Normal file
@@ -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<StickerUploadDialog> createState() => _StickerUploadDialogState();
|
||||
}
|
||||
|
||||
class _StickerUploadDialogState extends State<StickerUploadDialog> {
|
||||
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<void> _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: <Widget>[
|
||||
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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user