Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
f26edce071 | |||
603799ea32 | |||
a32baf7798 | |||
498c9af663 | |||
202dbff6d3 | |||
96fd64d85d | |||
e236b7f98b | |||
5c7929e618 | |||
7ba5260246 | |||
a6d4947a23 |
@ -282,6 +282,7 @@
|
||||
"other": "{} attachments"
|
||||
},
|
||||
"fieldAttachmentRandomId": "Random ID",
|
||||
"fieldAttachmentAlt": "Alternative text",
|
||||
"addAttachmentFromAlbum": "Add from album",
|
||||
"addAttachmentFromClipboard": "Paste file",
|
||||
"addAttachmentFromCameraPhoto": "Take photo",
|
||||
@ -293,6 +294,7 @@
|
||||
"attachmentUnsetAsPostThumbnail": "Unset as post thumbnail",
|
||||
"attachmentCompressVideo": "Re-encode video",
|
||||
"attachmentSetThumbnail": "Set thumbnail",
|
||||
"attachmentSetAlt": "Set alternative text",
|
||||
"attachmentCopyRandomId": "Copy RID",
|
||||
"attachmentUpload": "Upload",
|
||||
"attachmentInputDialog": "Upload attachments",
|
||||
@ -467,6 +469,7 @@
|
||||
"accountStatusLastSeen": "Last seen at {}",
|
||||
"postArticle": "Article on the Solar Network",
|
||||
"postStory": "Story on the Solar Network",
|
||||
"postLocalDraftRestored": "Restored from device",
|
||||
"articleWrittenAt": "Written at {}",
|
||||
"articleEditedAt": "Edited at {}",
|
||||
"attachmentSaved": "Saved to album",
|
||||
|
@ -280,6 +280,7 @@
|
||||
"other": "{} 个附件"
|
||||
},
|
||||
"fieldAttachmentRandomId": "访问 ID",
|
||||
"fieldAttachmentAlt": "概述文字",
|
||||
"addAttachmentFromAlbum": "从相册中添加附件",
|
||||
"addAttachmentFromClipboard": "粘贴附件",
|
||||
"addAttachmentFromCameraPhoto": "拍摄照片",
|
||||
@ -291,6 +292,7 @@
|
||||
"attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
|
||||
"attachmentCompressVideo": "重新编码视频",
|
||||
"attachmentSetThumbnail": "设置缩略图",
|
||||
"attachmentSetAlt": "设置概述文字",
|
||||
"attachmentCopyRandomId": "复制访问 ID",
|
||||
"attachmentUpload": "上传",
|
||||
"attachmentInputDialog": "上传附件",
|
||||
@ -465,6 +467,7 @@
|
||||
"accountStatusLastSeen": "最后一次上线于 {}",
|
||||
"postArticle": "Solar Network 上的文章",
|
||||
"postStory": "Solar Network 上的故事",
|
||||
"postLocalDraftRestored": "从本地恢复草稿",
|
||||
"articleWrittenAt": "发表于 {}",
|
||||
"articleEditedAt": "编辑于 {}",
|
||||
"attachmentSaved": "已保存到相册",
|
||||
|
@ -280,6 +280,7 @@
|
||||
"other": "{} 個附件"
|
||||
},
|
||||
"fieldAttachmentRandomId": "訪問 ID",
|
||||
"fieldAttachmentAlt": "概述文字",
|
||||
"addAttachmentFromAlbum": "從相冊中添加附件",
|
||||
"addAttachmentFromClipboard": "粘貼附件",
|
||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||
@ -291,6 +292,7 @@
|
||||
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
||||
"attachmentCompressVideo": "重新編碼視頻",
|
||||
"attachmentSetThumbnail": "設置縮略圖",
|
||||
"attachmentSetAlt": "設置概述文字",
|
||||
"attachmentCopyRandomId": "複製訪問 ID",
|
||||
"attachmentUpload": "上傳",
|
||||
"attachmentInputDialog": "上傳附件",
|
||||
@ -456,6 +458,7 @@
|
||||
"accountJoinedAt": "加入於 {}",
|
||||
"accountBirthday": "出生於 {}",
|
||||
"accountBadge": "徽章",
|
||||
"accountCheckInNoRecords": "暫無運勢記錄",
|
||||
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
|
||||
"badgeSiteMigration": "Solar Network 原住民",
|
||||
"accountStatus": "狀態",
|
||||
@ -464,6 +467,7 @@
|
||||
"accountStatusLastSeen": "最後一次上線於 {}",
|
||||
"postArticle": "Solar Network 上的文章",
|
||||
"postStory": "Solar Network 上的故事",
|
||||
"postLocalDraftRestored": "從本地恢復草稿",
|
||||
"articleWrittenAt": "發表於 {}",
|
||||
"articleEditedAt": "編輯於 {}",
|
||||
"attachmentSaved": "已保存到相冊",
|
||||
|
@ -280,6 +280,7 @@
|
||||
"other": "{} 個附件"
|
||||
},
|
||||
"fieldAttachmentRandomId": "訪問 ID",
|
||||
"fieldAttachmentAlt": "概述文字",
|
||||
"addAttachmentFromAlbum": "從相冊中添加附件",
|
||||
"addAttachmentFromClipboard": "粘貼附件",
|
||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||
@ -291,6 +292,7 @@
|
||||
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
||||
"attachmentCompressVideo": "重新編碼視頻",
|
||||
"attachmentSetThumbnail": "設置縮略圖",
|
||||
"attachmentSetAlt": "設置概述文字",
|
||||
"attachmentCopyRandomId": "複製訪問 ID",
|
||||
"attachmentUpload": "上傳",
|
||||
"attachmentInputDialog": "上傳附件",
|
||||
@ -456,6 +458,7 @@
|
||||
"accountJoinedAt": "加入於 {}",
|
||||
"accountBirthday": "出生於 {}",
|
||||
"accountBadge": "徽章",
|
||||
"accountCheckInNoRecords": "暫無運勢記錄",
|
||||
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
|
||||
"badgeSiteMigration": "Solar Network 原住民",
|
||||
"accountStatus": "狀態",
|
||||
@ -464,6 +467,7 @@
|
||||
"accountStatusLastSeen": "最後一次上線於 {}",
|
||||
"postArticle": "Solar Network 上的文章",
|
||||
"postStory": "Solar Network 上的故事",
|
||||
"postLocalDraftRestored": "從本地恢復草稿",
|
||||
"articleWrittenAt": "發表於 {}",
|
||||
"articleEditedAt": "編輯於 {}",
|
||||
"attachmentSaved": "已保存到相冊",
|
||||
|
@ -1,3 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
@ -8,6 +11,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
@ -151,8 +155,18 @@ class PostWriteController extends ChangeNotifier {
|
||||
final TextEditingController aliasController = TextEditingController();
|
||||
|
||||
PostWriteController() {
|
||||
titleController.addListener(() => notifyListeners());
|
||||
descriptionController.addListener(() => notifyListeners());
|
||||
titleController.addListener(() {
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
});
|
||||
descriptionController.addListener(() {
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
});
|
||||
contentController.addListener(() {
|
||||
_temporaryPlanSave();
|
||||
});
|
||||
_temporaryLoad();
|
||||
}
|
||||
|
||||
String mode = kTitleMap.keys.first;
|
||||
@ -199,11 +213,11 @@ class PostWriteController extends ChangeNotifier {
|
||||
aliasController.text = post.alias ?? '';
|
||||
publishedAt = post.publishedAt;
|
||||
publishedUntil = post.publishedUntil;
|
||||
visibleUsers = List.from(post.visibleUsersList ?? []);
|
||||
invisibleUsers = List.from(post.invisibleUsersList ?? []);
|
||||
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
|
||||
invisibleUsers = List.from(post.invisibleUsersList ?? [], growable: true);
|
||||
visibility = post.visibility;
|
||||
tags = List.from(post.tags.map((ele) => ele.alias));
|
||||
categories = List.from(post.categories.map((ele) => ele.alias));
|
||||
tags = List.from(post.tags.map((ele) => ele.alias), growable: true);
|
||||
categories = List.from(post.categories.map((ele) => ele.alias), growable: true);
|
||||
attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
|
||||
|
||||
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
|
||||
@ -298,6 +312,82 @@ class PostWriteController extends ChangeNotifier {
|
||||
return compressedAttachment;
|
||||
}
|
||||
|
||||
static const kTemporaryStorageKey = 'int_draft_post';
|
||||
|
||||
Timer? _temporarySaveTimer;
|
||||
|
||||
void _temporaryPlanSave() {
|
||||
_temporarySaveTimer?.cancel();
|
||||
_temporarySaveTimer = Timer(const Duration(seconds: 1), () {
|
||||
_temporarySave();
|
||||
log("[PostWriter] Temporary save saved.");
|
||||
});
|
||||
}
|
||||
|
||||
void _temporarySave() {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
if (titleController.text.isEmpty &&
|
||||
descriptionController.text.isEmpty &&
|
||||
contentController.text.isEmpty &&
|
||||
thumbnail == null &&
|
||||
attachments.isEmpty) {
|
||||
prefs.remove(kTemporaryStorageKey);
|
||||
return;
|
||||
}
|
||||
|
||||
prefs.setString(
|
||||
kTemporaryStorageKey,
|
||||
jsonEncode({
|
||||
'publisher': publisher,
|
||||
'content': contentController.text,
|
||||
if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
|
||||
if (titleController.text.isNotEmpty) 'title': titleController.text,
|
||||
if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
|
||||
if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.toJson(),
|
||||
'attachments':
|
||||
attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(growable: true),
|
||||
'tags': tags.map((ele) => {'alias': ele}).toList(growable: true),
|
||||
'categories': categories.map((ele) => {'alias': ele}).toList(growable: true),
|
||||
'visibility': visibility,
|
||||
'visible_users_list': visibleUsers,
|
||||
'invisible_users_list': invisibleUsers,
|
||||
if (publishedAt != null) 'published_at': publishedAt!.toUtc().toIso8601String(),
|
||||
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
|
||||
if (replyingPost != null) 'reply_to': replyingPost!.toJson(),
|
||||
if (repostingPost != null) 'repost_to': repostingPost!.toJson(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
bool temporaryRestored = false;
|
||||
|
||||
void _temporaryLoad() {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
final raw = prefs.getString(kTemporaryStorageKey);
|
||||
if (raw == null) return;
|
||||
final data = jsonDecode(raw);
|
||||
contentController.text = data['content'];
|
||||
aliasController.text = data['alias'] ?? '';
|
||||
titleController.text = data['title'] ?? '';
|
||||
descriptionController.text = data['description'] ?? '';
|
||||
if (data['thumbnail'] != null) thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
|
||||
attachments
|
||||
.addAll(data['attachments'].map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))).cast<PostWriteMedia>());
|
||||
tags = List.from(data['tags'].map((ele) => ele['alias']));
|
||||
categories = List.from(data['categories'].map((ele) => ele['alias']));
|
||||
visibility = data['visibility'];
|
||||
visibleUsers = List.from(data['visible_users_list'] ?? []);
|
||||
invisibleUsers = List.from(data['invisible_users_list'] ?? []);
|
||||
if (data['published_at'] != null) publishedAt = DateTime.tryParse(data['published_at'])?.toLocal();
|
||||
if (data['published_until'] != null) publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
|
||||
replyingPost = data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
|
||||
repostingPost = data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null;
|
||||
temporaryRestored = true;
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> uploadSingleAttachment(BuildContext context, int idx) async {
|
||||
if (isBusy) return;
|
||||
|
||||
@ -354,10 +444,12 @@ class PostWriteController extends ChangeNotifier {
|
||||
);
|
||||
|
||||
try {
|
||||
if (context.mounted) {
|
||||
final compressedAttachment = await _tryCompressVideoCopy(context, media);
|
||||
if (compressedAttachment != null) {
|
||||
item = await attach.updateOne(item, compressedId: compressedAttachment.id);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (context.mounted) context.showErrorDialog(err);
|
||||
}
|
||||
@ -415,6 +507,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
method: editingPost != null ? 'PUT' : 'POST',
|
||||
),
|
||||
);
|
||||
reset();
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -463,56 +556,67 @@ class PostWriteController extends ChangeNotifier {
|
||||
|
||||
void setPublisher(SnPublisher? item) {
|
||||
publisher = item;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setPublishedAt(DateTime? value) {
|
||||
publishedAt = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setPublishedUntil(DateTime? value) {
|
||||
publishedUntil = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setTags(List<String> value) {
|
||||
tags = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setCategories(List<String> value) {
|
||||
categories = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setVisibility(int value) {
|
||||
visibility = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setVisibleUsers(List<int> value) {
|
||||
visibleUsers = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setInvisibleUsers(List<int> value) {
|
||||
invisibleUsers = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setProgress(double? value) {
|
||||
progress = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setIsBusy(bool value) {
|
||||
isBusy = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setMode(String value) {
|
||||
mode = value;
|
||||
_temporaryPlanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@ -530,6 +634,8 @@ class PostWriteController extends ChangeNotifier {
|
||||
replyingPost = null;
|
||||
repostingPost = null;
|
||||
mode = kTitleMap.keys.first;
|
||||
temporaryRestored = false;
|
||||
SharedPreferences.getInstance().then((prefs) => prefs.remove(kTemporaryStorageKey));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
@ -364,6 +364,36 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
child: _writeController.temporaryRestored
|
||||
? Container(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.restore, size: 20),
|
||||
const Gap(8),
|
||||
Expanded(child: Text('postLocalDraftRestored').tr()),
|
||||
InkWell(
|
||||
child: Text('dialogDismiss').tr(),
|
||||
onTap: () {
|
||||
_writeController.reset();
|
||||
},
|
||||
),
|
||||
],
|
||||
))
|
||||
: const SizedBox.shrink(),
|
||||
)
|
||||
.height(_writeController.temporaryRestored ? 32 : 0, animate: true)
|
||||
.animate(const Duration(milliseconds: 300), Curves.fastLinearToSlowEaseIn),
|
||||
LoadingIndicator(isActive: _isLoading),
|
||||
if (_writeController.isBusy && _writeController.progress != null)
|
||||
TweenAnimationBuilder<double>(
|
||||
|
@ -315,6 +315,7 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
|
||||
}
|
||||
|
||||
return MaterialDesktopVideoControlsTheme(
|
||||
key: Key('material-desktop-video-controls-theme-$_showOriginal'),
|
||||
normal: MaterialDesktopVideoControlsThemeData(
|
||||
buttonBarButtonSize: 24,
|
||||
buttonBarButtonColor: Colors.white,
|
||||
@ -324,14 +325,16 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
|
||||
MaterialDesktopCustomButton(
|
||||
iconSize: 24,
|
||||
onPressed: _toggleOriginal,
|
||||
icon: Builder(builder: (context) {
|
||||
return _showOriginal ? const Icon(Symbols.high_quality, size: 24) : const Icon(Symbols.sd, size: 24);
|
||||
}),
|
||||
icon: Icon(
|
||||
_showOriginal ? Symbols.high_quality : Symbols.sd,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
fullscreen: const MaterialDesktopVideoControlsThemeData(),
|
||||
child: MaterialVideoControlsTheme(
|
||||
key: Key('material-video-controls-theme-$_showOriginal'),
|
||||
normal: MaterialVideoControlsThemeData(
|
||||
buttonBarButtonSize: 24,
|
||||
buttonBarButtonColor: Colors.white,
|
||||
|
@ -18,7 +18,8 @@ class AttachmentList extends StatefulWidget {
|
||||
final bool noGrow;
|
||||
final BoxFit fit;
|
||||
final double? maxHeight;
|
||||
final EdgeInsets? listPadding;
|
||||
final double? minWidth;
|
||||
final EdgeInsets? padding;
|
||||
|
||||
const AttachmentList({
|
||||
super.key,
|
||||
@ -28,7 +29,8 @@ class AttachmentList extends StatefulWidget {
|
||||
this.noGrow = false,
|
||||
this.fit = BoxFit.cover,
|
||||
this.maxHeight,
|
||||
this.listPadding,
|
||||
this.minWidth,
|
||||
this.padding,
|
||||
});
|
||||
|
||||
static const BorderRadius kDefaultRadius = BorderRadius.all(Radius.circular(8));
|
||||
@ -43,8 +45,6 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
(_) => const Uuid().v4(),
|
||||
);
|
||||
|
||||
static const double kAttachmentMaxWidth = 640;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
@ -53,8 +53,8 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
widget.bordered ? BorderSide(width: 1, color: Theme.of(context).dividerColor) : BorderSide.none;
|
||||
final backgroundColor = Theme.of(context).colorScheme.surfaceContainer;
|
||||
final constraints = BoxConstraints(
|
||||
minWidth: 80,
|
||||
maxHeight: widget.maxHeight ?? double.infinity,
|
||||
minWidth: widget.minWidth ?? 80,
|
||||
maxHeight: widget.maxHeight ?? MediaQuery.of(context).size.height,
|
||||
);
|
||||
|
||||
if (widget.data.isEmpty) return const SizedBox.shrink();
|
||||
@ -67,11 +67,9 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
}
|
||||
.toDouble();
|
||||
|
||||
return Padding(
|
||||
padding: widget.listPadding ?? EdgeInsets.zero,
|
||||
child: Container(
|
||||
return Container(
|
||||
padding: widget.padding ?? EdgeInsets.zero,
|
||||
constraints: constraints,
|
||||
width: double.infinity,
|
||||
child: GestureDetector(
|
||||
child: AspectRatio(
|
||||
aspectRatio: singleAspectRatio,
|
||||
@ -104,13 +102,12 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.gridded) {
|
||||
return Padding(
|
||||
padding: widget.listPadding ?? EdgeInsets.zero,
|
||||
padding: widget.padding ?? EdgeInsets.zero,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
@ -134,7 +131,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
child: AttachmentItem(
|
||||
data: ele,
|
||||
heroTag: heroTags[idx],
|
||||
fit: widget.fit,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
@ -220,7 +217,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const Gap(8),
|
||||
padding: widget.listPadding,
|
||||
padding: widget.padding,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
scrollDirection: Axis.horizontal,
|
||||
),
|
||||
|
87
lib/widgets/attachment/pending_attachment_alt.dart
Normal file
87
lib/widgets/attachment/pending_attachment_alt.dart
Normal file
@ -0,0 +1,87 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
|
||||
class PendingAttachmentAltDialog extends StatefulWidget {
|
||||
final PostWriteMedia media;
|
||||
const PendingAttachmentAltDialog({super.key, required this.media});
|
||||
|
||||
@override
|
||||
State<PendingAttachmentAltDialog> createState() => _PendingAttachmentAltDialogState();
|
||||
}
|
||||
|
||||
class _PendingAttachmentAltDialogState extends State<PendingAttachmentAltDialog> {
|
||||
final _contentController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_contentController.text = widget.media.attachment!.alt;
|
||||
}
|
||||
|
||||
bool _isBusy = false;
|
||||
|
||||
Future<void> _performAction() async {
|
||||
if (_isBusy) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
final attach = context.read<SnAttachmentProvider>();
|
||||
final result = await attach.updateOne(
|
||||
widget.media.attachment!,
|
||||
alt: _contentController.text,
|
||||
);
|
||||
if (!mounted) return;
|
||||
attach.putCache([result]);
|
||||
Navigator.pop(context, result);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_contentController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('attachmentSetAlt').tr(),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _contentController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldAttachmentAlt'.tr(),
|
||||
border: const UnderlineInputBorder(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text('dialogDismiss'.tr()),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => _performAction(),
|
||||
child: Text('dialogConfirm'.tr()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -53,7 +53,7 @@ class ChatMessage extends StatelessWidget {
|
||||
iconOnRightSwipe: Symbols.edit,
|
||||
swipeSensitivity: 20,
|
||||
onLeftSwipe: onReply != null ? (_) => onReply!(data) : null,
|
||||
onRightSwipe: onEdit != null ? (_) => onEdit!(data) : null,
|
||||
onRightSwipe: (onEdit != null && isOwner) ? (_) => onEdit!(data) : null,
|
||||
child: ContextMenuArea(
|
||||
contextMenu: ContextMenu(
|
||||
entries: [
|
||||
@ -103,18 +103,17 @@ class ChatMessage extends StatelessWidget {
|
||||
children: [
|
||||
if (!isMerged)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (isCompact)
|
||||
AccountImage(
|
||||
content: user?.avatar,
|
||||
radius: 12,
|
||||
).padding(right: 6),
|
||||
).padding(right: 8),
|
||||
Text(
|
||||
(data.sender.nick?.isNotEmpty ?? false) ? data.sender.nick! : user?.nick ?? 'unknown',
|
||||
).bold(),
|
||||
const Gap(6),
|
||||
const Gap(8),
|
||||
Text(
|
||||
dateFormatter.format(data.createdAt.toLocal()),
|
||||
).fontSize(13),
|
||||
@ -153,7 +152,7 @@ class ChatMessage extends StatelessWidget {
|
||||
)
|
||||
],
|
||||
).opacity(isPending ? 0.5 : 1),
|
||||
if (data.body['text'] != null && (data.body['text']?.isNotEmpty ?? false))
|
||||
if (data.body['text'] != null && data.type == 'messages.new' && (data.body['text']?.isNotEmpty ?? false))
|
||||
LinkPreviewWidget(text: data.body['text']!),
|
||||
if (data.preload?.attachments?.isNotEmpty ?? false)
|
||||
AttachmentList(
|
||||
@ -161,8 +160,9 @@ class ChatMessage extends StatelessWidget {
|
||||
bordered: true,
|
||||
gridded: true,
|
||||
noGrow: true,
|
||||
maxHeight: 520,
|
||||
listPadding: const EdgeInsets.only(top: 8),
|
||||
maxHeight: 560,
|
||||
minWidth: 480,
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
),
|
||||
if (!hasMerged && !isCompact) const Gap(12) else if (!isCompact) const Gap(6),
|
||||
],
|
||||
|
@ -161,75 +161,84 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||
SingleChildScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: _replyingMessage != null ? const EdgeInsets.only(top: 8) : EdgeInsets.zero,
|
||||
child: _replyingMessage != null
|
||||
? MaterialBanner(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
leading: const Icon(Symbols.reply),
|
||||
backgroundColor: Colors.transparent,
|
||||
content: SingleChildScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
? Container(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (_replyingMessage?.body['text'] != null)
|
||||
MarkdownTextContent(
|
||||
content: _replyingMessage?.body['text'],
|
||||
),
|
||||
],
|
||||
const Icon(Symbols.reply, size: 20),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_replyingMessage?.body['text'] ?? '${_replyingMessage?.sender.nick}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
const Gap(16),
|
||||
InkWell(
|
||||
child: Text('cancel'.tr()),
|
||||
onPressed: () {
|
||||
onTap: () {
|
||||
setState(() => _replyingMessage = null);
|
||||
},
|
||||
),
|
||||
],
|
||||
).padding(vertical: 8),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
)
|
||||
.height(_replyingMessage != null ? 54 + 8 : 0, animate: true)
|
||||
.height(_replyingMessage != null ? 38 : 0, animate: true)
|
||||
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||
SingleChildScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: _editingMessage != null ? const EdgeInsets.only(top: 8) : EdgeInsets.zero,
|
||||
child: _editingMessage != null
|
||||
? MaterialBanner(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
leading: const Icon(Symbols.edit),
|
||||
backgroundColor: Colors.transparent,
|
||||
content: SingleChildScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
? Container(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (_editingMessage?.body['text'] != null)
|
||||
MarkdownTextContent(
|
||||
content: _editingMessage?.body['text'],
|
||||
),
|
||||
],
|
||||
const Icon(Symbols.edit, size: 20),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_editingMessage?.body['text'] ?? '${_editingMessage?.sender.nick}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
const Gap(16),
|
||||
InkWell(
|
||||
child: Text('cancel'.tr()),
|
||||
onPressed: () {
|
||||
onTap: () {
|
||||
_contentController.clear();
|
||||
setState(() => _editingMessage = null);
|
||||
},
|
||||
),
|
||||
],
|
||||
).padding(vertical: 8),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
)
|
||||
.height(_editingMessage != null ? 54 + 8 : 0, animate: true)
|
||||
.height(_editingMessage != null ? 38 : 0, animate: true)
|
||||
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
|
||||
SizedBox(
|
||||
height: 56,
|
||||
|
@ -253,8 +253,9 @@ class PostItem extends StatelessWidget {
|
||||
bordered: true,
|
||||
gridded: true,
|
||||
maxHeight: showFullPost ? null : 480,
|
||||
minWidth: 640,
|
||||
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
|
||||
listPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
),
|
||||
if (data.body['content'] != null)
|
||||
LinkPreviewWidget(
|
||||
@ -336,10 +337,10 @@ class PostShareImageWidget extends StatelessWidget {
|
||||
isRelativeDate: false,
|
||||
).padding(horizontal: 16, bottom: 8),
|
||||
if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false))
|
||||
AttachmentList(
|
||||
StyledWidget(AttachmentList(
|
||||
data: data.preload!.attachments!,
|
||||
gridded: true,
|
||||
).padding(horizontal: 16, bottom: 8),
|
||||
)).padding(horizontal: 16, bottom: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -930,9 +931,10 @@ class _PostQuoteContent extends StatelessWidget {
|
||||
child: AttachmentList(
|
||||
data: child.preload!.attachments!,
|
||||
maxHeight: 360,
|
||||
minWidth: 640,
|
||||
fit: BoxFit.contain,
|
||||
gridded: true,
|
||||
listPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
),
|
||||
).padding(
|
||||
top: 8,
|
||||
|
@ -21,6 +21,7 @@ import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_input.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_zoom.dart';
|
||||
import 'package:surface/widgets/attachment/pending_attachment_alt.dart';
|
||||
import 'package:surface/widgets/attachment/pending_attachment_boost.dart';
|
||||
import 'package:surface/widgets/context_menu.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
@ -157,6 +158,16 @@ class PostMediaPendingList extends StatelessWidget {
|
||||
onUpdate!(idx, result);
|
||||
}
|
||||
|
||||
Future<void> _setAlt(BuildContext context, int idx) async {
|
||||
final result = await showDialog<SnAttachment?>(
|
||||
context: context,
|
||||
builder: (context) => PendingAttachmentAltDialog(media: attachments[idx]),
|
||||
);
|
||||
if (result == null) return;
|
||||
|
||||
onUpdate!(idx, PostWriteMedia(result));
|
||||
}
|
||||
|
||||
ContextMenu _createContextMenu(BuildContext context, int idx, PostWriteMedia media) {
|
||||
final canCompressVideo = !kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS);
|
||||
return ContextMenu(
|
||||
@ -169,6 +180,14 @@ class PostMediaPendingList extends StatelessWidget {
|
||||
_compressVideo(context, idx);
|
||||
},
|
||||
),
|
||||
if (media.attachment != null)
|
||||
MenuItem(
|
||||
label: 'attachmentSetAlt'.tr(),
|
||||
icon: Symbols.description,
|
||||
onSelected: () {
|
||||
_setAlt(context, idx);
|
||||
},
|
||||
),
|
||||
if (media.attachment != null)
|
||||
MenuItem(
|
||||
label: 'attachmentBoost'.tr(),
|
||||
|
@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 2.2.1+41
|
||||
version: 2.2.1+43
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.4
|
||||
@ -53,7 +53,6 @@ dependencies:
|
||||
markdown: ^7.2.2
|
||||
flutter_markdown: ^0.7.4+1
|
||||
url_launcher: ^6.3.1
|
||||
cached_network_image: ^3.4.1
|
||||
flutter_animate: ^4.5.0
|
||||
syntax_highlight: ^0.4.0
|
||||
google_fonts: ^6.2.1
|
||||
@ -116,6 +115,7 @@ dependencies:
|
||||
flutter_webrtc: ^0.12.5+hotfix.1
|
||||
slide_countdown: ^2.0.2
|
||||
video_compress: ^3.1.3
|
||||
cached_network_image: ^3.4.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Reference in New Issue
Block a user