💄 Unified design of single attachment uploader

This commit is contained in:
LittleSheep 2024-09-16 20:33:34 +08:00
parent ad1dc064e6
commit 152efd97a0
10 changed files with 39 additions and 428 deletions

View File

@ -157,6 +157,9 @@
"postListNews": "News", "postListNews": "News",
"postListFriends": "Friends", "postListFriends": "Friends",
"postListShuffle": "Random", "postListShuffle": "Random",
"attachmentThumbnail": "Thumbnail",
"attachmentThumbnailAttachmentNew": "Upload thumbnail",
"attachmentThumbnailAttachment": "Attachment serial number",
"postEditorModeStory": "Post a post", "postEditorModeStory": "Post a post",
"postEditorModeArticle": "Post an article", "postEditorModeArticle": "Post an article",
"postEditor": "Post editor", "postEditor": "Post editor",

View File

@ -168,6 +168,9 @@
"postListNews": "新鲜事", "postListNews": "新鲜事",
"postListFriends": "好友圈", "postListFriends": "好友圈",
"postListShuffle": "打乱看", "postListShuffle": "打乱看",
"attachmentThumbnail": "附件缩略图",
"attachmentThumbnailAttachmentNew": "上传附件作为缩略图",
"attachmentThumbnailAttachment": "附件序列号",
"postNew": "创建新帖子", "postNew": "创建新帖子",
"postNewInRealmHint": "在领域 @realm 里发表新帖子", "postNewInRealmHint": "在领域 @realm 里发表新帖子",
"postAction": "发表", "postAction": "发表",

View File

@ -9,7 +9,6 @@ import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/channel.dart'; import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/content/realm.dart'; import 'package:solian/providers/content/realm.dart';
import 'package:solian/providers/relation.dart'; import 'package:solian/providers/relation.dart';
import 'package:solian/providers/stickers.dart';
import 'package:solian/providers/theme_switcher.dart'; import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/providers/websocket.dart'; import 'package:solian/providers/websocket.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
@ -115,7 +114,6 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
try { try {
await Future.wait([ await Future.wait([
Get.find<StickerProvider>().refreshAvailableStickers(),
if (auth.isAuthorized.isTrue) if (auth.isAuthorized.isTrue)
Get.find<ChannelProvider>().refreshAvailableChannel(), Get.find<ChannelProvider>().refreshAvailableChannel(),
if (auth.isAuthorized.isTrue) if (auth.isAuthorized.isTrue)

View File

@ -1,4 +1,5 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/models/pagination.dart'; import 'package:solian/models/pagination.dart';
import 'package:solian/models/stickers.dart'; import 'package:solian/models/stickers.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
@ -31,4 +32,16 @@ class StickerProvider extends GetxController {
} }
availableStickers.refresh(); availableStickers.refresh();
} }
Future<Sticker?> getStickerByAlias(String alias) async {
final client = await ServiceFinder.configureClient('files');
final resp = await client.get(
'/stickers/lookup/$alias',
);
if (resp.statusCode != 200) {
throw RequestException(resp);
}
return Sticker.fromJson(resp.body);
}
} }

View File

@ -7,7 +7,6 @@ import 'package:solian/screens/account.dart';
import 'package:solian/screens/account/friend.dart'; import 'package:solian/screens/account/friend.dart';
import 'package:solian/screens/account/personalize.dart'; import 'package:solian/screens/account/personalize.dart';
import 'package:solian/screens/account/profile_page.dart'; import 'package:solian/screens/account/profile_page.dart';
import 'package:solian/screens/account/stickers.dart';
import 'package:solian/screens/auth/signin.dart'; import 'package:solian/screens/auth/signin.dart';
import 'package:solian/screens/auth/signup.dart'; import 'package:solian/screens/auth/signup.dart';
import 'package:solian/screens/channel/channel_chat.dart'; import 'package:solian/screens/channel/channel_chat.dart';
@ -238,14 +237,6 @@ abstract class AppRouter {
name: 'accountFriend', name: 'accountFriend',
builder: (context, state) => const FriendScreen(), builder: (context, state) => const FriendScreen(),
), ),
GoRoute(
path: '/account/stickers',
name: 'accountStickers',
builder: (context, state) => TitleShell(
state: state,
child: const StickerScreen(),
),
),
GoRoute( GoRoute(
path: '/account/personalize', path: '/account/personalize',
name: 'accountProfile', name: 'accountProfile',

View File

@ -45,11 +45,6 @@ class _AccountScreenState extends State<AccountScreen> {
'accountFriend'.tr, 'accountFriend'.tr,
'accountFriend', 'accountFriend',
), ),
(
const Icon(Icons.emoji_symbols),
'accountStickers'.tr,
'accountStickers',
),
]; ];
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();

View File

@ -1,186 +0,0 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.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/providers/auth.dart';
import 'package:solian/providers/stickers.dart';
import 'package:solian/services.dart';
import 'package:solian/widgets/auto_cache_image.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, StickerPack> _pagingController =
PagingController(firstPageKey: 0);
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 = await 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) => StickerUploadDialog(
edit: edit,
),
);
}
Widget _buildEmoteEntry(Sticker item, String prefix) {
final imageUrl = ServiceFinder.buildUrl(
'files',
'/attachments/${item.attachment.rid}',
);
return ListTile(
title: Text(item.name),
subtitle: Text(item.textWarpedPlaceholder),
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: AutoCacheImage(
imageUrl,
width: 28,
height: 28,
noErrorWidget: true,
),
);
}
@override
void initState() {
final AuthProvider auth = Get.find();
final name = auth.userProfile.value!['name'];
_pagingController.addPageRequestListener((pageKey) async {
final client = await ServiceFinder.configureClient('files');
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) => StickerPack.fromJson(e)).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() {
final StickerProvider sticker = Get.find();
sticker.refreshAvailableStickers();
_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<int, StickerPack>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate(
itemBuilder: (BuildContext context, item, int index) {
return ExpansionTile(
title: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(item.name),
const Gap(6),
Badge(
label: Text('#${item.id}'),
)
],
),
subtitle: Text(
item.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
children: item.stickers?.map((x) {
x.pack = item;
return _buildEmoteEntry(x, item.prefix);
}).toList() ??
List.empty(),
);
},
),
),
],
),
),
);
}
}

View File

@ -93,14 +93,14 @@ class _AttachmentEditorThumbnailDialogState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text('postThumbnail'.tr), title: Text('attachmentThumbnail'.tr),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Card( Card(
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
child: ListTile( child: ListTile(
title: Text('postThumbnailAttachmentNew'.tr), title: Text('attachmentThumbnailAttachmentNew'.tr),
contentPadding: const EdgeInsets.only(left: 12, right: 9), contentPadding: const EdgeInsets.only(left: 12, right: 9),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
@ -122,7 +122,7 @@ class _AttachmentEditorThumbnailDialogState
isDense: true, isDense: true,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
prefixText: '#', prefixText: '#',
labelText: 'postThumbnailAttachment'.tr, labelText: 'attachmentThumbnailAttachment'.tr,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/controllers/post_editor_controller.dart'; import 'package:solian/controllers/post_editor_controller.dart';
import 'package:solian/widgets/attachments/attachment_editor.dart'; import 'package:solian/widgets/attachments/attachment_editor.dart';
@ -58,9 +57,11 @@ class _PostEditorThumbnailDialogState extends State<PostEditorThumbnailDialog> {
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
ListTile( Card(
margin: EdgeInsets.zero,
child: ListTile(
title: Text('postThumbnailAttachmentNew'.tr), title: Text('postThumbnailAttachmentNew'.tr),
contentPadding: const EdgeInsets.only(left: 16, right: 13), contentPadding: const EdgeInsets.only(left: 12, right: 9),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)), borderRadius: BorderRadius.all(Radius.circular(8)),
@ -69,7 +70,12 @@ class _PostEditorThumbnailDialogState extends State<PostEditorThumbnailDialog> {
_promptUploadNewAttachment(); _promptUploadNewAttachment();
}, },
), ),
const Gap(8), ),
const Row(children: <Widget>[
Expanded(child: Divider()),
Text('OR'),
Expanded(child: Divider()),
]).paddingOnly(top: 12, bottom: 16, left: 16, right: 16),
TextField( TextField(
controller: _attachmentController, controller: _attachmentController,
decoration: InputDecoration( decoration: InputDecoration(

View File

@ -1,212 +0,0 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.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 {
final Sticker? edit;
const StickerUploadDialog({super.key, this.edit});
@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(
pool: '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;
}
setState(() => _isBusy = true);
Response resp;
final client = await auth.configureClient('files');
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);
} else {
Navigator.pop(context, true);
}
}
@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();
_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 Gap(8),
TextField(
controller: _attachmentController,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
prefixText: '#',
labelText: 'stickerUploaderAttachment'.tr,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(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),
),
],
);
}
}