Compare commits

...

3 Commits

5 changed files with 405 additions and 250 deletions

View File

@ -44,7 +44,9 @@ class SnNetworkProvider {
settings: const TalkerDioLoggerSettings(
printRequestHeaders: false,
printResponseHeaders: false,
printResponseMessage: true,
printResponseMessage: false,
printResponseData: false,
printRequestData: false,
),
),
);

View File

@ -95,8 +95,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
);
final beforeId = config.prefs.getInt('int_last_publisher_id');
_writeController
.setPublisher(_publishers?.where((ele) => ele.id == beforeId).firstOrNull ?? _publishers?.firstOrNull);
_writeController.setPublisher(
_publishers?.where((ele) => ele.id == beforeId).firstOrNull ??
_publishers?.firstOrNull);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
@ -125,7 +126,11 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
final HotKey _pasteHotKey = HotKey(
key: PhysicalKeyboardKey.keyV,
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
modifiers: [
(!kIsWeb && Platform.isMacOS)
? HotKeyModifier.meta
: HotKeyModifier.control
],
scope: HotKeyScope.inapp,
);
@ -232,7 +237,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
if (widget.extraProps != null) {
_writeController.contentController.text = widget.extraProps!.text ?? '';
_writeController.titleController.text = widget.extraProps!.title ?? '';
_writeController.descriptionController.text = widget.extraProps!.description ?? '';
_writeController.descriptionController.text =
widget.extraProps!.description ?? '';
_writeController.addAttachments(widget.extraProps!.attachments ?? []);
}
}
@ -253,7 +259,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
textAlign: TextAlign.center,
text: TextSpan(children: [
TextSpan(
text: _writeController.title.isNotEmpty ? _writeController.title : 'untitled'.tr(),
text: _writeController.title.isNotEmpty
? _writeController.title
: 'untitled'.tr(),
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
@ -280,7 +288,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
children: [
if (_writeController.editingPost != null)
Container(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20),
padding: const EdgeInsets.only(
top: 4, bottom: 4, left: 20, right: 20),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
@ -294,13 +303,16 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
children: [
const Icon(Icons.edit, size: 16),
const Gap(10),
Text('postEditingNotice').tr(args: ['@${_writeController.editingPost!.publisher.name}']),
Text('postEditingNotice').tr(args: [
'@${_writeController.editingPost!.publisher.name}'
]),
],
),
),
if (_writeController.replyingPost != null)
Container(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20),
padding: const EdgeInsets.only(
top: 4, bottom: 4, left: 20, right: 20),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
@ -314,7 +326,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
children: [
const Icon(Symbols.reply, size: 16),
const Gap(10),
Text('@${_writeController.replyingPost!.publisher.name}').bold(),
Text('@${_writeController.replyingPost!.publisher.name}')
.bold(),
const Gap(4),
Expanded(
child: Text(
@ -328,7 +341,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
),
if (_writeController.repostingPost != null)
Container(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20),
padding: const EdgeInsets.only(
top: 4, bottom: 4, left: 20, right: 20),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
@ -342,7 +356,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
children: [
const Icon(Symbols.forward, size: 16),
const Gap(10),
Text('@${_writeController.repostingPost!.publisher.name}').bold(),
Text('@${_writeController.repostingPost!.publisher.name}')
.bold(),
const Gap(4),
Expanded(
child: Text(
@ -384,7 +399,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
})
.padding(top: 8),
),
if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null)
if (_writeController.attachments.isNotEmpty ||
_writeController.thumbnail != null)
Positioned(
bottom: 0,
left: 0,
@ -393,16 +409,19 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
attachments: _writeController.attachments,
isBusy: _writeController.isBusy,
onUpload: (int idx) async {
await _writeController.uploadSingleAttachment(context, idx);
await _writeController.uploadSingleAttachment(
context, idx);
},
onInsertLink: (int idx) async {
_writeController.contentController.text +=
'\n![](solink://attachments/${_writeController.attachments[idx].attachment!.rid})';
},
onUpdate: (int idx, PostWriteMedia updatedMedia) async {
onUpdate:
(int idx, PostWriteMedia updatedMedia) async {
_writeController.setIsBusy(true);
try {
_writeController.setAttachmentAt(idx, updatedMedia);
_writeController.setAttachmentAt(
idx, updatedMedia);
} finally {
_writeController.setIsBusy(false);
}
@ -415,7 +434,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
_writeController.setIsBusy(false);
}
},
onUpdateBusy: (state) => _writeController.setIsBusy(state),
onUpdateBusy: (state) =>
_writeController.setIsBusy(state),
).padding(bottom: 8),
),
],
@ -426,11 +446,13 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_writeController.isBusy && _writeController.progress != null)
if (_writeController.isBusy &&
_writeController.progress != null)
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: _writeController.progress),
duration: Duration(milliseconds: 300),
builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2),
builder: (context, value, _) =>
LinearProgressIndicator(value: value, minHeight: 2),
)
else if (_writeController.isBusy)
const LinearProgressIndicator(value: null, minHeight: 2),
@ -439,12 +461,14 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
Container(
child: _writeController.temporaryRestored
? Container(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 28, right: 22),
padding: const EdgeInsets.only(
top: 4, bottom: 4, left: 28, right: 22),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
width: 1 /
MediaQuery.of(context).devicePixelRatio,
),
),
),
@ -453,7 +477,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
children: [
const Icon(Icons.restore, size: 20),
const Gap(8),
Expanded(child: Text('postLocalDraftRestored').tr()),
Expanded(
child:
Text('postLocalDraftRestored').tr()),
InkWell(
child: Text('dialogDismiss').tr(),
onTap: () {
@ -464,8 +490,10 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
))
: const SizedBox.shrink(),
)
.height(_writeController.temporaryRestored ? 32 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.fastLinearToSlowEaseIn),
.height(_writeController.temporaryRestored ? 32 : 0,
animate: true)
.animate(const Duration(milliseconds: 300),
Curves.fastLinearToSlowEaseIn),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@ -485,11 +513,18 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
),
if (_writeController.mode == 'stories')
IconButton(
icon: Icon(Symbols.poll, color: Theme.of(context).colorScheme.primary),
icon: Icon(Symbols.poll,
color: Theme.of(context)
.colorScheme
.primary),
style: ButtonStyle(
backgroundColor: _writeController.poll == null
? null
: WidgetStatePropertyAll(Theme.of(context).colorScheme.surfaceContainer),
backgroundColor:
_writeController.poll == null
? null
: WidgetStatePropertyAll(
Theme.of(context)
.colorScheme
.surfaceContainer),
),
onPressed: () {
_showPollEditorDialog();
@ -497,14 +532,22 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
),
if (_writeController.mode == 'articles')
IconButton(
icon: Icon(Symbols.full_coverage, color: Theme.of(context).colorScheme.primary),
icon: Icon(Symbols.full_coverage,
color: Theme.of(context)
.colorScheme
.primary),
style: ButtonStyle(
backgroundColor: _writeController.thumbnail == null
? null
: WidgetStatePropertyAll(Theme.of(context).colorScheme.surfaceContainer),
backgroundColor:
_writeController.thumbnail == null
? null
: WidgetStatePropertyAll(
Theme.of(context)
.colorScheme
.surfaceContainer),
),
onPressed: () {
if (_writeController.thumbnail != null) {
if (_writeController.thumbnail !=
null) {
_writeController.setThumbnail(null);
return;
}
@ -517,7 +560,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
),
),
TextButton.icon(
onPressed: (_writeController.isBusy || _writeController.publisher == null)
onPressed: (_writeController.isBusy ||
_writeController.publisher == null)
? null
: () {
_writeController.sendPost(context).then((_) {
@ -556,7 +600,8 @@ class _PostPublisherPopup extends StatelessWidget {
final List<SnPublisher>? publishers;
final Function onUpdate;
const _PostPublisherPopup({required this.controller, this.publishers, required this.onUpdate});
const _PostPublisherPopup(
{required this.controller, this.publishers, required this.onUpdate});
@override
Widget build(BuildContext context) {
@ -568,7 +613,9 @@ class _PostPublisherPopup extends StatelessWidget {
children: [
const Icon(Symbols.face, size: 24),
const Gap(16),
Text('accountPublishers', style: Theme.of(context).textTheme.titleLarge).tr(),
Text('accountPublishers',
style: Theme.of(context).textTheme.titleLarge)
.tr(),
],
).padding(horizontal: 20, top: 16, bottom: 12),
ListTile(
@ -612,7 +659,8 @@ class _PostRealmPopup extends StatelessWidget {
final List<SnRealm>? realms;
final Function onUpdate;
const _PostRealmPopup({required this.controller, this.realms, required this.onUpdate});
const _PostRealmPopup(
{required this.controller, this.realms, required this.onUpdate});
@override
Widget build(BuildContext context) {
@ -624,7 +672,8 @@ class _PostRealmPopup extends StatelessWidget {
children: [
const Icon(Symbols.face, size: 24),
const Gap(16),
Text('accountRealms', style: Theme.of(context).textTheme.titleLarge).tr(),
Text('accountRealms', style: Theme.of(context).textTheme.titleLarge)
.tr(),
],
).padding(horizontal: 20, top: 16, bottom: 12),
ListTile(
@ -665,7 +714,8 @@ class _PostStoryEditor extends StatelessWidget {
final Function? onTapPublisher;
final Function? onTapRealm;
const _PostStoryEditor({required this.controller, this.onTapPublisher, this.onTapRealm});
const _PostStoryEditor(
{required this.controller, this.onTapPublisher, this.onTapRealm});
@override
Widget build(BuildContext context) {
@ -717,7 +767,8 @@ class _PostStoryEditor extends StatelessWidget {
border: InputBorder.none,
),
style: Theme.of(context).textTheme.titleLarge,
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 16),
const Gap(8),
TextField(
@ -732,8 +783,10 @@ class _PostStoryEditor extends StatelessWidget {
),
border: InputBorder.none,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
contentInsertionConfiguration: controller.contentInsertionConfiguration,
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
contentInsertionConfiguration:
controller.contentInsertionConfiguration,
),
],
),
@ -749,7 +802,8 @@ class _PostArticleEditor extends StatelessWidget {
final Function? onTapPublisher;
final Function? onTapRealm;
const _PostArticleEditor({required this.controller, this.onTapPublisher, this.onTapRealm});
const _PostArticleEditor(
{required this.controller, this.onTapPublisher, this.onTapRealm});
@override
Widget build(BuildContext context) {
@ -857,8 +911,10 @@ class _PostArticleEditor extends StatelessWidget {
),
border: InputBorder.none,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
contentInsertionConfiguration: controller.contentInsertionConfiguration,
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
contentInsertionConfiguration:
controller.contentInsertionConfiguration,
),
),
const Gap(8),
@ -893,7 +949,8 @@ class _PostArticleEditor extends StatelessWidget {
border: InputBorder.none,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
contentInsertionConfiguration: controller.contentInsertionConfiguration,
contentInsertionConfiguration:
controller.contentInsertionConfiguration,
),
),
],
@ -906,7 +963,8 @@ class _PostQuestionEditor extends StatelessWidget {
final Function? onTapPublisher;
final Function? onTapRealm;
const _PostQuestionEditor({required this.controller, this.onTapPublisher, this.onTapRealm});
const _PostQuestionEditor(
{required this.controller, this.onTapPublisher, this.onTapRealm});
@override
Widget build(BuildContext context) {
@ -958,7 +1016,8 @@ class _PostQuestionEditor extends StatelessWidget {
border: InputBorder.none,
),
style: Theme.of(context).textTheme.titleLarge,
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 16),
const Gap(8),
TextField(
@ -969,7 +1028,8 @@ class _PostQuestionEditor extends StatelessWidget {
border: InputBorder.none,
isCollapsed: true,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 16),
const Gap(8),
TextField(
@ -984,8 +1044,10 @@ class _PostQuestionEditor extends StatelessWidget {
),
border: InputBorder.none,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
contentInsertionConfiguration: controller.contentInsertionConfiguration,
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
contentInsertionConfiguration:
controller.contentInsertionConfiguration,
),
],
),
@ -1001,7 +1063,8 @@ class _PostVideoEditor extends StatelessWidget {
final Function? onTapPublisher;
final Function? onTapRealm;
const _PostVideoEditor({required this.controller, this.onTapPublisher, this.onTapRealm});
const _PostVideoEditor(
{required this.controller, this.onTapPublisher, this.onTapRealm});
void _selectVideo(BuildContext context) async {
final video = await showDialog<SnAttachment?>(
@ -1022,7 +1085,8 @@ class _PostVideoEditor extends StatelessWidget {
final result = await showDialog<SnAttachment?>(
context: context,
builder: (context) => PendingAttachmentAltDialog(media: PostWriteMedia(controller.videoAttachment)),
builder: (context) => PendingAttachmentAltDialog(
media: PostWriteMedia(controller.videoAttachment)),
);
if (result == null) return;
@ -1034,7 +1098,8 @@ class _PostVideoEditor extends StatelessWidget {
final result = await showDialog<SnAttachmentBoost?>(
context: context,
builder: (context) => PendingAttachmentBoostDialog(media: PostWriteMedia(controller.videoAttachment)),
builder: (context) => PendingAttachmentBoostDialog(
media: PostWriteMedia(controller.videoAttachment)),
);
if (result == null) return;
@ -1077,7 +1142,8 @@ class _PostVideoEditor extends StatelessWidget {
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.delete('/cgi/uc/attachments/${controller.videoAttachment!.id}');
await sn.client
.delete('/cgi/uc/attachments/${controller.videoAttachment!.id}');
controller.setVideoAttachment(null);
} catch (err) {
if (!context.mounted) return;
@ -1087,143 +1153,159 @@ class _PostVideoEditor extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
Column(
children: [
Material(
elevation: 2,
borderRadius: const BorderRadius.all(Radius.circular(24)),
child: GestureDetector(
onTap: () {
onTapPublisher?.call();
},
child: AccountImage(
content: controller.publisher?.avatar,
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
constraints: const BoxConstraints(maxWidth: 640),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
children: [
Material(
elevation: 2,
borderRadius: const BorderRadius.all(Radius.circular(24)),
child: GestureDetector(
onTap: () {
onTapPublisher?.call();
},
child: AccountImage(
content: controller.publisher?.avatar,
),
),
),
),
const Gap(11),
Material(
elevation: 1,
borderRadius: const BorderRadius.all(Radius.circular(24)),
child: GestureDetector(
onTap: () {
onTapRealm?.call();
},
child: AccountImage(
content: controller.realm?.avatar,
fallbackWidget: const Icon(Symbols.globe, size: 20),
radius: 14,
const Gap(11),
Material(
elevation: 1,
borderRadius: const BorderRadius.all(Radius.circular(24)),
child: GestureDetector(
onTap: () {
onTapRealm?.call();
},
child: AccountImage(
content: controller.realm?.avatar,
fallbackWidget: const Icon(Symbols.globe, size: 20),
radius: 14,
),
),
),
),
],
),
const Gap(16),
TextField(
controller: controller.titleController,
decoration: InputDecoration.collapsed(
hintText: 'fieldPostTitle'.tr(),
border: InputBorder.none,
],
),
style: Theme.of(context).textTheme.titleLarge,
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 16),
const Gap(8),
TextField(
controller: controller.descriptionController,
decoration: InputDecoration.collapsed(
hintText: 'fieldPostDescription'.tr(),
border: InputBorder.none,
),
maxLines: null,
keyboardType: TextInputType.multiline,
style: Theme.of(context).textTheme.bodyLarge,
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 16),
const Gap(12),
Container(
margin: const EdgeInsets.only(left: 16, right: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Theme.of(context).dividerColor),
),
child: ContextMenuRegion(
contextMenu: ContextMenu(
entries: [
MenuItem(
label: 'attachmentSetAlt'.tr(),
icon: Symbols.description,
onSelected: () {
_setAlt(context);
},
),
MenuItem(
label: 'attachmentBoost'.tr(),
icon: Symbols.bolt,
onSelected: () {
_createBoost(context);
},
),
MenuItem(
label: 'attachmentSetThumbnail'.tr(),
icon: Symbols.image,
onSelected: () {
_setThumbnail(context);
},
),
MenuItem(
label: 'attachmentCopyRandomId'.tr(),
icon: Symbols.content_copy,
onSelected: () {
Clipboard.setData(ClipboardData(text: controller.videoAttachment!.rid));
},
),
MenuItem(
label: 'delete'.tr(),
icon: Symbols.delete,
onSelected: () => _deleteAttachment(context),
),
MenuItem(
label: 'unlink'.tr(),
icon: Symbols.link_off,
onSelected: () {
controller.setVideoAttachment(null);
},
Expanded(
child: Column(
children: [
const Gap(6),
TextField(
controller: controller.titleController,
decoration: InputDecoration.collapsed(
hintText: 'fieldPostTitle'.tr(),
border: InputBorder.none,
),
style: Theme.of(context).textTheme.titleLarge,
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 16),
const Gap(8),
TextField(
controller: controller.descriptionController,
decoration: InputDecoration.collapsed(
hintText: 'fieldPostDescription'.tr(),
border: InputBorder.none,
),
maxLines: null,
keyboardType: TextInputType.multiline,
style: Theme.of(context).textTheme.bodyLarge,
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 16),
const Gap(12),
Container(
margin: const EdgeInsets.only(left: 16, right: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Theme.of(context).dividerColor),
),
child: ContextMenuRegion(
contextMenu: ContextMenu(
entries: [
MenuItem(
label: 'attachmentSetAlt'.tr(),
icon: Symbols.description,
onSelected: () {
_setAlt(context);
},
),
MenuItem(
label: 'attachmentBoost'.tr(),
icon: Symbols.bolt,
onSelected: () {
_createBoost(context);
},
),
MenuItem(
label: 'attachmentSetThumbnail'.tr(),
icon: Symbols.image,
onSelected: () {
_setThumbnail(context);
},
),
MenuItem(
label: 'attachmentCopyRandomId'.tr(),
icon: Symbols.content_copy,
onSelected: () {
Clipboard.setData(ClipboardData(
text: controller.videoAttachment!.rid));
},
),
MenuItem(
label: 'delete'.tr(),
icon: Symbols.delete,
onSelected: () => _deleteAttachment(context),
),
MenuItem(
label: 'unlink'.tr(),
icon: Symbols.link_off,
onSelected: () {
controller.setVideoAttachment(null);
},
),
],
),
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: controller.videoAttachment == null
? () => _selectVideo(context)
: null,
child: AspectRatio(
aspectRatio: 16 / 9,
child: controller.videoAttachment == null
? Center(
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.add),
const Gap(4),
Text('postVideoUpload'.tr()),
],
),
)
: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: AttachmentItem(
data: controller.videoAttachment!,
heroTag: const Uuid().v4(),
),
),
),
),
),
),
],
),
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: controller.videoAttachment == null ? () => _selectVideo(context) : null,
child: AspectRatio(
aspectRatio: 16 / 9,
child: controller.videoAttachment == null
? Center(
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.add),
const Gap(4),
Text('postVideoUpload'.tr()),
],
),
)
: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: AttachmentItem(
data: controller.videoAttachment!,
heroTag: const Uuid().v4(),
),
),
),
),
),
),
],
],
),
);
}
}

View File

@ -229,7 +229,10 @@ class ChatMessage extends StatelessWidget {
bordered: true,
maxHeight: 360,
maxWidth: 480 - 48 - padding.left,
padding: padding.copyWith(top: 8, left: 48 + padding.left),
padding: padding.copyWith(
top: 8,
left: isCompact ? padding.left : 48 + padding.left,
),
),
if (!hasMerged && !isCompact)
const Gap(12)

View File

@ -28,7 +28,8 @@ class ChatMessageInput extends StatefulWidget {
final ChatMessageController controller;
final SnChannelMember? otherMember;
const ChatMessageInput({super.key, required this.controller, this.otherMember});
const ChatMessageInput(
{super.key, required this.controller, this.otherMember});
@override
State<ChatMessageInput> createState() => ChatMessageInputState();
@ -45,12 +46,20 @@ class ChatMessageInputState extends State<ChatMessageInput> {
final HotKey _pasteHotKey = HotKey(
key: PhysicalKeyboardKey.keyV,
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
modifiers: [
(!kIsWeb && Platform.isMacOS)
? HotKeyModifier.meta
: HotKeyModifier.control
],
scope: HotKeyScope.inapp,
);
final HotKey _newLineHotKey = HotKey(
key: PhysicalKeyboardKey.enter,
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
modifiers: [
(!kIsWeb && Platform.isMacOS)
? HotKeyModifier.meta
: HotKeyModifier.control
],
scope: HotKeyScope.inapp,
);
@ -100,7 +109,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
void setEdit(SnChatMessage? value) {
_contentController.text = value?.body['text'] ?? '';
_attachments.clear();
_attachments.addAll(value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []);
_attachments.addAll(
value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []);
setState(() => _editingMessage = value);
}
@ -139,7 +149,9 @@ class ChatMessageInputState extends State<ChatMessageInput> {
media.name,
'messaging',
null,
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
mimetype: media.raw != null && media.type == SnMediaType.image
? 'image/png'
: null,
);
final item = await attach.chunkedUploadParts(
@ -171,7 +183,10 @@ class ChatMessageInputState extends State<ChatMessageInput> {
widget.controller.sendMessage(
_editingMessage != null ? 'messages.edit' : 'messages.new',
_contentController.text,
attachments: _attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(),
attachments: _attachments
.where((e) => e.attachment != null)
.map((e) => e.attachment!.rid)
.toList(),
relatedId: _editingMessage?.id,
quoteId: _replyingMessage?.id,
editingMessage: _editingMessage,
@ -232,12 +247,15 @@ class ChatMessageInputState extends State<ChatMessageInput> {
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: _progress),
duration: Duration(milliseconds: 300),
builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2),
builder: (context, value, _) =>
LinearProgressIndicator(value: value, minHeight: 2),
)
else if (_isBusy)
const LinearProgressIndicator(value: null, minHeight: 2),
Padding(
padding: _attachments.isNotEmpty ? const EdgeInsets.only(top: 8) : EdgeInsets.zero,
padding: _attachments.isNotEmpty
? const EdgeInsets.only(top: 8)
: EdgeInsets.zero,
child: PostMediaPendingList(
attachments: _attachments,
isBusy: _isBusy,
@ -249,9 +267,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
},
onUpdateBusy: (state) => setState(() => _isBusy = state),
),
)
.height(_attachments.isNotEmpty ? 80 + 8 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
).height(_attachments.isNotEmpty ? 80 + 8 : 0, animate: true).animate(
const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: _replyingMessage != null
@ -272,7 +289,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
const Gap(8),
Expanded(
child: Text(
_replyingMessage?.body['text'] ?? '${_replyingMessage?.sender.nick}',
_replyingMessage?.body['text'] ??
'${_replyingMessage?.sender.nick}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
@ -289,9 +307,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
).padding(vertical: 8),
)
: const SizedBox.shrink(),
)
.height(_replyingMessage != null ? 38 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
).height(_replyingMessage != null ? 38 : 0, animate: true).animate(
const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: _editingMessage != null
@ -312,7 +329,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
const Gap(8),
Expanded(
child: Text(
_editingMessage?.body['text'] ?? '${_editingMessage?.sender.nick}',
_editingMessage?.body['text'] ??
'${_editingMessage?.sender.nick}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
@ -330,12 +348,13 @@ class ChatMessageInputState extends State<ChatMessageInput> {
).padding(vertical: 8),
)
: const SizedBox.shrink(),
)
.height(_editingMessage != null ? 38 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
SizedBox(
height: 56,
).height(_editingMessage != null ? 38 : 0, animate: true).animate(
const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
Container(
padding: EdgeInsets.symmetric(horizontal: 16),
constraints: BoxConstraints(minHeight: 56, maxHeight: 240),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: TextField(
@ -347,11 +366,14 @@ class ChatMessageInputState extends State<ChatMessageInput> {
? 'fieldChatMessageDirect'.tr(args: [
'@${ud.getAccountFromCache(widget.otherMember?.accountId)?.name}',
])
: 'fieldChatMessage'.tr(args: [widget.controller.channel?.name ?? 'loading'.tr()]),
: 'fieldChatMessage'.tr(args: [
widget.controller.channel?.name ?? 'loading'.tr()
]),
border: InputBorder.none,
),
textInputAction: TextInputAction.send,
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
onSubmitted: (_) {
if (_isBusy) return;
_sendMessage();
@ -366,7 +388,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
Symbols.mood,
color: Theme.of(context).colorScheme.primary,
),
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
visualDensity:
const VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () {
@ -386,13 +409,14 @@ class ChatMessageInputState extends State<ChatMessageInput> {
Symbols.send,
color: Theme.of(context).colorScheme.primary,
),
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
visualDensity:
const VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
).padding(horizontal: 16),
),
],
);
}
@ -403,7 +427,8 @@ class _StickerPicker extends StatelessWidget {
final Function? onDismiss;
final Function(String)? onInsert;
const _StickerPicker({this.onDismiss, required this.originalText, this.onInsert});
const _StickerPicker(
{this.onDismiss, required this.originalText, this.onInsert});
@override
Widget build(BuildContext context) {
@ -413,7 +438,9 @@ class _StickerPicker extends StatelessWidget {
onDismiss?.call();
},
child: Container(
constraints: BoxConstraints(maxWidth: min(360, MediaQuery.of(context).size.width), maxHeight: 240),
constraints: BoxConstraints(
maxWidth: min(360, MediaQuery.of(context).size.width),
maxHeight: 240),
child: Material(
elevation: 8,
borderRadius: const BorderRadius.all(Radius.circular(8)),
@ -426,8 +453,10 @@ class _StickerPicker extends StatelessWidget {
return <Widget>[
Container(
margin: EdgeInsets.only(bottom: 8),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
color: Theme.of(context).colorScheme.surfaceContainerHigh,
padding:
EdgeInsets.symmetric(horizontal: 8, vertical: 4),
color:
Theme.of(context).colorScheme.surfaceContainerHigh,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@ -439,7 +468,8 @@ class _StickerPicker extends StatelessWidget {
),
GridView.builder(
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
padding:
const EdgeInsets.only(left: 8, right: 8, bottom: 8),
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 48,
@ -462,7 +492,8 @@ class _StickerPicker extends StatelessWidget {
richMessage: TextSpan(
children: [
TextSpan(
text: ':${element.pack.prefix}${element.alias}:\n',
text:
':${element.pack.prefix}${element.alias}:\n',
style: GoogleFonts.robotoMono()),
TextSpan(text: element.name).bold(),
],
@ -471,11 +502,15 @@ class _StickerPicker extends StatelessWidget {
width: 48,
height: 48,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: const BorderRadius.all(
Radius.circular(8)),
color: Theme.of(context)
.colorScheme
.surfaceContainerHigh,
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
borderRadius: const BorderRadius.all(
Radius.circular(8)),
child: UniversalImage(
sn.getAttachmentUrl(element.attachment.rid),
width: 48,

View File

@ -67,7 +67,10 @@ class PostMediaPendingList extends StatelessWidget {
if (result == null) return;
final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
final rawBytes =
(await result.uiImage.toByteData(format: ImageByteFormat.png))!
.buffer
.asUint8List();
if (onUpdate != null) {
final updatedMedia = PostWriteMedia.fromBytes(
@ -133,7 +136,8 @@ class PostMediaPendingList extends StatelessWidget {
final result = await showDialog<SnAttachmentBoost?>(
context: context,
builder: (context) => PendingAttachmentBoostDialog(media: attachments[idx]),
builder: (context) =>
PendingAttachmentBoostDialog(media: attachments[idx]),
);
if (result == null) return;
@ -165,11 +169,15 @@ class PostMediaPendingList extends StatelessWidget {
onUpdate!(idx, PostWriteMedia(result));
}
ContextMenu _createContextMenu(BuildContext context, int idx, PostWriteMedia media) {
final canCompressVideo = !kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS);
ContextMenu _createContextMenu(
BuildContext context, int idx, PostWriteMedia media) {
final canCompressVideo =
!kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS);
return ContextMenu(
entries: [
if (media.attachment == null && media.type == SnMediaType.video && canCompressVideo)
if (media.attachment == null &&
media.type == SnMediaType.video &&
canCompressVideo)
MenuItem(
label: 'attachmentCompressVideo'.tr(),
icon: Symbols.compress,
@ -312,12 +320,15 @@ class _PostMediaPendingItem extends StatelessWidget {
AspectRatio(
aspectRatio: 1,
child: switch (media.type) {
SnMediaType.image => LayoutBuilder(builder: (context, constraints) {
SnMediaType.image =>
LayoutBuilder(builder: (context, constraints) {
return Image(
image: media.getImageProvider(
context,
width: (constraints.maxWidth * devicePixelRatio).round(),
height: (constraints.maxHeight * devicePixelRatio).round(),
width:
(constraints.maxWidth * devicePixelRatio).round(),
height:
(constraints.maxHeight * devicePixelRatio).round(),
)!,
fit: BoxFit.contain,
);
@ -326,28 +337,34 @@ class _PostMediaPendingItem extends StatelessWidget {
fit: StackFit.expand,
children: [
if (media.attachment?.thumbnail != null)
AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment!.thumbnail!.rid)),
const Icon(Symbols.videocam, color: Colors.white, shadows: [
Shadow(
offset: Offset(1, 1),
blurRadius: 8.0,
color: Color.fromARGB(255, 0, 0, 0),
),
]),
AutoResizeUniversalImage(sn.getAttachmentUrl(
media.attachment!.thumbnail!.rid)),
const Icon(Symbols.videocam,
color: Colors.white,
shadows: [
Shadow(
offset: Offset(1, 1),
blurRadius: 8.0,
color: Color.fromARGB(255, 0, 0, 0),
),
]),
],
),
SnMediaType.audio => Stack(
fit: StackFit.expand,
children: [
if (media.attachment?.thumbnail != null)
AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment!.thumbnail!.rid)),
const Icon(Symbols.audio_file, color: Colors.white, shadows: [
Shadow(
offset: Offset(1, 1),
blurRadius: 8.0,
color: Color.fromARGB(255, 0, 0, 0),
),
]),
AutoResizeUniversalImage(sn.getAttachmentUrl(
media.attachment!.thumbnail!.rid)),
const Icon(Symbols.audio_file,
color: Colors.white,
shadows: [
Shadow(
offset: Offset(1, 1),
blurRadius: 8.0,
color: Color.fromARGB(255, 0, 0, 0),
),
]),
],
),
_ => Container(
@ -356,7 +373,8 @@ class _PostMediaPendingItem extends StatelessWidget {
),
},
),
if (media.type != SnMediaType.image) const VerticalDivider(width: 1, thickness: 1),
if (media.type != SnMediaType.image)
const VerticalDivider(width: 1, thickness: 1),
if (media.type != SnMediaType.image)
SizedBox(
width: 160,
@ -374,7 +392,8 @@ class _PostMediaPendingItem extends StatelessWidget {
overflow: TextOverflow.ellipsis,
)
else if (media.file != null)
Text(media.file!.name, maxLines: 1, overflow: TextOverflow.ellipsis)
Text(media.file!.name,
maxLines: 1, overflow: TextOverflow.ellipsis)
else
Text('unknown'.tr()),
if (media.attachment != null)
@ -387,7 +406,8 @@ class _PostMediaPendingItem extends StatelessWidget {
FutureBuilder<int?>(
future: media.length(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const SizedBox.shrink();
if (!snapshot.hasData)
return const SizedBox.shrink();
return Text(
snapshot.data!.formatBytes(),
style: GoogleFonts.robotoMono(fontSize: 13),
@ -398,7 +418,8 @@ class _PostMediaPendingItem extends StatelessWidget {
],
),
),
if (media.attachment != null && media.attachment!.boosts.isNotEmpty)
if (media.attachment != null &&
media.attachment!.boosts.isNotEmpty)
Row(
children: [
Icon(Symbols.bolt, size: 16),
@ -406,7 +427,8 @@ class _PostMediaPendingItem extends StatelessWidget {
Text('attachmentGotBoosted').tr().fontSize(13),
],
),
if (media.attachment != null && media.attachment!.compressedId != null)
if (media.attachment != null &&
media.attachment!.compressedId != null)
Row(
children: [
Icon(Symbols.compress, size: 16),
@ -527,12 +549,20 @@ class AddPostMediaButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return PopupMenuButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
style: ButtonStyle(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
),
icon: Icon(
Symbols.add_photo_alternate,
color: Theme.of(context).colorScheme.primary,
),
itemBuilder: (context) => [
if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows)
if (!kIsWeb &&
!Platform.isLinux &&
!Platform.isMacOS &&
!Platform.isWindows)
PopupMenuItem(
child: Row(
children: [
@ -545,7 +575,10 @@ class AddPostMediaButton extends StatelessWidget {
_takeMedia(false);
},
),
if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows)
if (!kIsWeb &&
!Platform.isLinux &&
!Platform.isMacOS &&
!Platform.isWindows)
PopupMenuItem(
child: Row(
children: [