✨ Create sticker
✨ Single file mode attachment editor and more options
This commit is contained in:
parent
bbea4b4359
commit
ea434815cf
@ -141,7 +141,7 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
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<BootstrapperShell> {
|
||||
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,
|
||||
|
@ -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<Attachment> performSingleTask(int queueIndex) async {
|
||||
Future<Attachment?> 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;
|
||||
},
|
||||
);
|
||||
|
||||
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<String, dynamic>? 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<Attachment> uploadAttachment(
|
||||
Future<Attachment?> uploadAttachment(
|
||||
Uint8List data,
|
||||
String path,
|
||||
String usage,
|
||||
@ -175,9 +191,9 @@ class AttachmentUploaderController extends GetxController {
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<Attachment> _rawUploadAttachment(
|
||||
Future<Attachment?> _rawUploadAttachment(
|
||||
Uint8List data, String path, String usage, Map<String, dynamic>? 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,8 @@ class AttachmentProvider extends GetConnect {
|
||||
List<int> id, {
|
||||
noCache = false,
|
||||
}) async {
|
||||
if (id.isEmpty) return List.empty();
|
||||
|
||||
List<Attachment?> result = List.filled(id.length, null);
|
||||
List<int> pendingQuery = List.empty(growable: true);
|
||||
if (!noCache) {
|
||||
|
5
lib/providers/stickers.dart
Normal file
5
lib/providers/stickers.dart
Normal file
@ -0,0 +1,5 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class StickerProvider extends GetxController {
|
||||
|
||||
}
|
@ -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',
|
||||
|
@ -46,6 +46,11 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
'accountFriend'.tr,
|
||||
'accountFriend',
|
||||
),
|
||||
(
|
||||
const Icon(Icons.emoji_symbols),
|
||||
'accountStickers'.tr,
|
||||
'accountStickers',
|
||||
),
|
||||
];
|
||||
|
||||
final AuthProvider auth = Get.find();
|
||||
|
@ -43,7 +43,7 @@ class _FriendScreenState extends State<FriendScreen>
|
||||
_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<FriendScreen>
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.add),
|
||||
onPressed: () => promptAddFriend(),
|
||||
onPressed: () => _promptAddFriend(),
|
||||
),
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
|
@ -86,11 +86,17 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
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<PersonalizeScreen> {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _BannerCropAspectRatioPreset extends CropAspectRatioPresetData {
|
||||
@override
|
||||
(int, int)? get data => (16, 7);
|
||||
|
||||
@override
|
||||
String get name => '16x7';
|
||||
}
|
||||
|
86
lib/screens/account/stickers.dart
Normal file
86
lib/screens/account/stickers.dart
Normal file
@ -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<StickerScreen> createState() => _StickerScreenState();
|
||||
}
|
||||
|
||||
class _StickerScreenState extends State<StickerScreen> {
|
||||
final PagingController<int, dynamic> _pagingController =
|
||||
PagingController(firstPageKey: 0);
|
||||
|
||||
Future<bool?> _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();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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.',
|
||||
};
|
||||
|
@ -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': '在贴图选择界面提供给用户的人类友好名称。',
|
||||
};
|
||||
|
@ -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 (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);
|
||||
}));
|
||||
} 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 ||
|
||||
if ((PlatformInfo.isDesktop ||
|
||||
PlatformInfo.isIOS ||
|
||||
PlatformInfo.isWeb)
|
||||
PlatformInfo.isWeb) &&
|
||||
!widget.imageOnly)
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.paste),
|
||||
label: Text('attachmentAddClipboard'.tr),
|
||||
@ -701,6 +744,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
style: const ButtonStyle(visualDensity: density),
|
||||
onPressed: () => _pickPhotoToUpload(),
|
||||
),
|
||||
if (!widget.imageOnly)
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.add_road),
|
||||
label: Text('attachmentAddGalleryVideo'.tr),
|
||||
@ -713,18 +757,21 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
style: const ButtonStyle(visualDensity: density),
|
||||
onPressed: () => _takeMediaToUpload(false),
|
||||
),
|
||||
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),
|
||||
|
@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user