♻️ Refactored using controller as post editing
This commit is contained in:
parent
1ff4dc2a4b
commit
f23ffe61f5
@ -37,6 +37,7 @@
|
||||
"report": "Report",
|
||||
"repost": "Repost",
|
||||
"reply": "Reply",
|
||||
"unset": "Unset",
|
||||
"untitled": "Untitled",
|
||||
"postDetail": "Post detail",
|
||||
"postNoun": "Post",
|
||||
@ -82,6 +83,8 @@
|
||||
"fieldPostTitle": "Title",
|
||||
"fieldPostDescription": "Description",
|
||||
"postPublish": "Publish",
|
||||
"postPublishedAt": "Published At",
|
||||
"postPublishedUntil": "Published Until",
|
||||
"postEditingNotice": "You're about to editing a post that posted {}.",
|
||||
"postReplyingNotice": "You're about to reply to a post that posted {}.",
|
||||
"postRepostingNotice": "You're about to repost a post that posted {}.",
|
||||
|
@ -37,6 +37,7 @@
|
||||
"report": "检举",
|
||||
"repost": "转帖",
|
||||
"reply": "回贴",
|
||||
"unset": "未设置",
|
||||
"untitled": "无题",
|
||||
"postDetail": "帖子详情",
|
||||
"postNoun": "帖子",
|
||||
@ -82,6 +83,8 @@
|
||||
"fieldPostTitle": "标题",
|
||||
"fieldPostDescription": "描述",
|
||||
"postPublish": "发布",
|
||||
"postPublishedAt": "发布于",
|
||||
"postPublishedUntil": "取消发布于",
|
||||
"postEditingNotice": "你正在修改由 {} 发布的帖子。",
|
||||
"postReplyingNotice": "你正在回复由 {} 发布的帖子。",
|
||||
"postRepostingNotice": "你正在转发由 {} 发布的帖子。",
|
||||
|
354
lib/controllers/post_write_controller.dart
Normal file
354
lib/controllers/post_write_controller.dart
Normal file
@ -0,0 +1,354 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
enum PostWriteMediaType {
|
||||
image,
|
||||
video,
|
||||
audio,
|
||||
file,
|
||||
}
|
||||
|
||||
class PostWriteMedia {
|
||||
late String name;
|
||||
late PostWriteMediaType type;
|
||||
final SnAttachment? attachment;
|
||||
final XFile? file;
|
||||
final Uint8List? raw;
|
||||
|
||||
PostWriteMedia(this.attachment, {this.file, this.raw}) {
|
||||
name = attachment!.name;
|
||||
|
||||
switch (attachment?.mimetype.split('/').firstOrNull) {
|
||||
case 'image':
|
||||
type = PostWriteMediaType.image;
|
||||
break;
|
||||
case 'video':
|
||||
type = PostWriteMediaType.video;
|
||||
break;
|
||||
case 'audio':
|
||||
type = PostWriteMediaType.audio;
|
||||
break;
|
||||
default:
|
||||
type = PostWriteMediaType.file;
|
||||
}
|
||||
}
|
||||
|
||||
PostWriteMedia.fromFile(this.file, {this.attachment, this.raw}) {
|
||||
name = file!.name;
|
||||
|
||||
switch (file?.mimeType?.split('/').firstOrNull) {
|
||||
case 'image':
|
||||
type = PostWriteMediaType.image;
|
||||
break;
|
||||
case 'video':
|
||||
type = PostWriteMediaType.video;
|
||||
break;
|
||||
case 'audio':
|
||||
type = PostWriteMediaType.audio;
|
||||
break;
|
||||
default:
|
||||
type = PostWriteMediaType.file;
|
||||
}
|
||||
}
|
||||
|
||||
PostWriteMedia.fromBytes(this.raw, this.name, this.type,
|
||||
{this.attachment, this.file});
|
||||
|
||||
bool get isEmpty => attachment == null && file == null && raw == null;
|
||||
|
||||
Future<int?> length() async {
|
||||
if (attachment != null) {
|
||||
return attachment!.size;
|
||||
} else if (file != null) {
|
||||
return await file!.length();
|
||||
} else if (raw != null) {
|
||||
return raw!.length;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
XFile? toFile() {
|
||||
if (file != null) {
|
||||
return file!;
|
||||
} else if (raw != null) {
|
||||
return XFile.fromData(raw!, name: name);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
ImageProvider? getImageProvider(
|
||||
BuildContext context, {
|
||||
int? width,
|
||||
int? height,
|
||||
}) {
|
||||
if (attachment != null) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
return UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
|
||||
} else if (file != null) {
|
||||
final ImageProvider provider =
|
||||
kIsWeb ? NetworkImage(file!.path) : FileImage(File(file!.path));
|
||||
if (width != null && height != null) {
|
||||
return ResizeImage(
|
||||
provider,
|
||||
width: width,
|
||||
height: height,
|
||||
policy: ResizeImagePolicy.fit,
|
||||
);
|
||||
}
|
||||
return provider;
|
||||
} else if (raw != null) {
|
||||
final provider = MemoryImage(raw!);
|
||||
if (width != null && height != null) {
|
||||
return ResizeImage(
|
||||
provider,
|
||||
width: width,
|
||||
height: height,
|
||||
policy: ResizeImagePolicy.fit,
|
||||
);
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class PostWriteController extends ChangeNotifier {
|
||||
static const Map<String, String> kTitleMap = {
|
||||
'stories': 'writePostTypeStory',
|
||||
'articles': 'writePostTypeArticle',
|
||||
};
|
||||
|
||||
static const kAttachmentProgressWeight = 0.9;
|
||||
static const kPostingProgressWeight = 0.1;
|
||||
|
||||
final TextEditingController contentController = TextEditingController();
|
||||
final TextEditingController titleController = TextEditingController();
|
||||
final TextEditingController descriptionController = TextEditingController();
|
||||
|
||||
PostWriteController() {
|
||||
titleController.addListener(() => notifyListeners());
|
||||
descriptionController.addListener(() => notifyListeners());
|
||||
}
|
||||
|
||||
String mode = kTitleMap.keys.first;
|
||||
|
||||
String get title => titleController.text;
|
||||
String get description => descriptionController.text;
|
||||
bool get isRelatedNull =>
|
||||
![editingPost, repostingPost, replyingPost].any((ele) => ele != null);
|
||||
|
||||
bool isLoading = false, isBusy = false;
|
||||
double? progress;
|
||||
|
||||
SnPublisher? publisher;
|
||||
SnPost? editingPost, repostingPost, replyingPost;
|
||||
|
||||
List<PostWriteMedia> attachments = List.empty(growable: true);
|
||||
DateTime? publishedAt, publishedUntil;
|
||||
|
||||
Future<void> fetchRelatedPost(
|
||||
BuildContext context, {
|
||||
int? editing,
|
||||
int? reposting,
|
||||
int? replying,
|
||||
}) async {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final attach = context.read<SnAttachmentProvider>();
|
||||
|
||||
isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
if (editing != null) {
|
||||
final resp = await sn.client.get('/cgi/co/posts/$editing');
|
||||
final post = SnPost.fromJson(resp.data);
|
||||
final alts = await attach
|
||||
.getMultiple(post.body['attachments']?.cast<String>() ?? []);
|
||||
publisher = post.publisher;
|
||||
titleController.text = post.body['title'] ?? '';
|
||||
descriptionController.text = post.body['description'] ?? '';
|
||||
contentController.text = post.body['content'] ?? '';
|
||||
publishedAt = post.publishedAt;
|
||||
publishedUntil = post.publishedUntil;
|
||||
attachments.addAll(alts.map((ele) => PostWriteMedia(ele)));
|
||||
|
||||
editingPost = post.copyWith(
|
||||
preload: SnPostPreload(
|
||||
attachments: alts,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (replying != null) {
|
||||
final resp = await sn.client.get('/cgi/co/posts/$replying');
|
||||
final post = SnPost.fromJson(resp.data);
|
||||
replyingPost = post.copyWith(
|
||||
preload: SnPostPreload(
|
||||
attachments: await attach
|
||||
.getMultiple(post.body['attachments']?.cast<String>() ?? []),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (reposting != null) {
|
||||
final resp = await sn.client.get('/cgi/co/posts/$reposting');
|
||||
final post = SnPost.fromJson(resp.data);
|
||||
repostingPost = post.copyWith(
|
||||
preload: SnPostPreload(
|
||||
attachments: await attach
|
||||
.getMultiple(post.body['attachments']?.cast<String>() ?? []),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void post(BuildContext context) async {
|
||||
if (isBusy || publisher == null) return;
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final attach = context.read<SnAttachmentProvider>();
|
||||
|
||||
progress = 0;
|
||||
isBusy = true;
|
||||
notifyListeners();
|
||||
|
||||
// Uploading attachments
|
||||
try {
|
||||
for (int i = 0; i < attachments.length; i++) {
|
||||
final media = attachments[i];
|
||||
if (media.attachment != null) continue; // Already uploaded, skip
|
||||
if (media.isEmpty) continue; // Nothing to do, skip
|
||||
|
||||
final place = await attach.chunkedUploadInitialize(
|
||||
(await media.length())!,
|
||||
media.name,
|
||||
'interactive',
|
||||
null,
|
||||
);
|
||||
|
||||
final item = await attach.chunkedUploadParts(
|
||||
media.toFile()!,
|
||||
place.$1,
|
||||
place.$2,
|
||||
onProgress: (progress) {
|
||||
// Calculate overall progress for attachments
|
||||
progress = ((i + progress) / attachments.length) *
|
||||
kAttachmentProgressWeight;
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
|
||||
attachments[i] = PostWriteMedia(item);
|
||||
}
|
||||
} catch (err) {
|
||||
isBusy = false;
|
||||
notifyListeners();
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
return;
|
||||
}
|
||||
|
||||
progress = kAttachmentProgressWeight;
|
||||
notifyListeners();
|
||||
|
||||
// Posting the content
|
||||
try {
|
||||
final baseProgressVal = progress!;
|
||||
await sn.client.request(
|
||||
[
|
||||
'/cgi/co/$mode',
|
||||
if (editingPost != null) '${editingPost!.id}',
|
||||
].join('/'),
|
||||
data: {
|
||||
'publisher': publisher!.id,
|
||||
'content': contentController.text,
|
||||
'title': titleController.text,
|
||||
'description': descriptionController.text,
|
||||
'attachments': attachments
|
||||
.where((e) => e.attachment != null)
|
||||
.map((e) => e.attachment!.rid)
|
||||
.toList(),
|
||||
if (publishedAt != null)
|
||||
'published_at': publishedAt!.toUtc().toIso8601String(),
|
||||
if (publishedUntil != null)
|
||||
'published_until': publishedAt!.toUtc().toIso8601String(),
|
||||
if (replyingPost != null) 'reply_to': replyingPost!.id,
|
||||
if (repostingPost != null) 'repost_to': repostingPost!.id,
|
||||
},
|
||||
onSendProgress: (count, total) {
|
||||
progress =
|
||||
baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
|
||||
notifyListeners();
|
||||
},
|
||||
onReceiveProgress: (count, total) {
|
||||
progress = baseProgressVal +
|
||||
(kPostingProgressWeight / 2) +
|
||||
(count / total) * (kPostingProgressWeight / 2);
|
||||
notifyListeners();
|
||||
},
|
||||
options: Options(
|
||||
method: editingPost != null ? 'PUT' : 'POST',
|
||||
),
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
Navigator.pop(context, true);
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
isBusy = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void addAttachments(Iterable<PostWriteMedia> items) {
|
||||
attachments.addAll(items);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void removeAttachmentAt(int idx) {
|
||||
attachments.removeAt(idx);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setPublisher(SnPublisher? item) {
|
||||
publisher = item;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setPublishedAt(DateTime? value) {
|
||||
publishedAt = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setPublishedUntil(DateTime? value) {
|
||||
publishedUntil = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
contentController.dispose();
|
||||
titleController.dispose();
|
||||
descriptionController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
@ -11,9 +10,8 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
@ -42,203 +40,37 @@ class PostEditorScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
static const Map<String, String> _kTitleMap = {
|
||||
'stories': 'writePostTypeStory',
|
||||
'articles': 'writePostTypeArticle',
|
||||
};
|
||||
final PostWriteController _writeController = PostWriteController();
|
||||
|
||||
bool _isBusy = false;
|
||||
bool _isLoading = false;
|
||||
bool _isFetching = false;
|
||||
bool get _isLoading => _isFetching || _writeController.isLoading;
|
||||
|
||||
SnPublisher? _publisher;
|
||||
List<SnPublisher>? _publishers;
|
||||
|
||||
final List<XFile> _selectedMedia = List.empty(growable: true);
|
||||
final List<SnAttachment> _attachments = List.empty(growable: true);
|
||||
|
||||
Future<void> _fetchPublishers() async {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/co/publishers');
|
||||
_publishers = List<SnPublisher>.from(
|
||||
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
|
||||
);
|
||||
setState(() {
|
||||
_publisher = _publishers?.first;
|
||||
});
|
||||
}
|
||||
|
||||
SnPost? _editingOg;
|
||||
SnPost? _replyingTo;
|
||||
SnPost? _repostingTo;
|
||||
|
||||
Future<void> _fetchRelatedPost() async {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final attach = context.read<SnAttachmentProvider>();
|
||||
setState(() => _isFetching = true);
|
||||
|
||||
try {
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
if (widget.postEditId != null) {
|
||||
final resp = await sn.client.get('/cgi/co/posts/${widget.postEditId}');
|
||||
final post = SnPost.fromJson(resp.data);
|
||||
final attachments = await attach
|
||||
.getMultiple(post.body['attachments']?.cast<String>() ?? []);
|
||||
_title = post.body['title'];
|
||||
_description = post.body['description'];
|
||||
_contentController.text = post.body['content'] ?? '';
|
||||
_attachments.addAll(attachments);
|
||||
|
||||
_editingOg = post.copyWith(
|
||||
preload: SnPostPreload(
|
||||
attachments: attachments,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.postReplyId != null) {
|
||||
final resp = await sn.client.get('/cgi/co/posts/${widget.postReplyId}');
|
||||
final post = SnPost.fromJson(resp.data);
|
||||
_replyingTo = post.copyWith(
|
||||
preload: SnPostPreload(
|
||||
attachments: await attach
|
||||
.getMultiple(post.body['attachments']?.cast<String>() ?? []),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.postRepostId != null) {
|
||||
final resp =
|
||||
await sn.client.get('/cgi/co/posts/${widget.postRepostId}');
|
||||
final post = SnPost.fromJson(resp.data);
|
||||
_repostingTo = post.copyWith(
|
||||
preload: SnPostPreload(
|
||||
attachments: await attach
|
||||
.getMultiple(post.body['attachments']?.cast<String>() ?? []),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
String? _title;
|
||||
String? _description;
|
||||
|
||||
final TextEditingController _contentController = TextEditingController();
|
||||
|
||||
double? _progress;
|
||||
|
||||
static const kAttachmentProgressWeight = 0.9;
|
||||
static const kPostingProgressWeight = 0.1;
|
||||
|
||||
void _performAction() async {
|
||||
if (_isBusy || _publisher == null) return;
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final attach = context.read<SnAttachmentProvider>();
|
||||
|
||||
setState(() {
|
||||
_progress = 0;
|
||||
_isBusy = true;
|
||||
});
|
||||
|
||||
// Uploading attachments
|
||||
try {
|
||||
for (int i = 0; i < _selectedMedia.length; i++) {
|
||||
final media = _selectedMedia[i];
|
||||
final place = await attach.chunkedUploadInitialize(
|
||||
await media.length(),
|
||||
media.name,
|
||||
'interactive',
|
||||
null,
|
||||
);
|
||||
|
||||
final item = await attach.chunkedUploadParts(
|
||||
media,
|
||||
place.$1,
|
||||
place.$2,
|
||||
onProgress: (progress) {
|
||||
// Calculate overall progress for attachments
|
||||
setState(() {
|
||||
_progress = ((i + progress) / _selectedMedia.length) *
|
||||
kAttachmentProgressWeight;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
_attachments.add(item);
|
||||
}
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
setState(() => _isBusy = false);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _progress = kAttachmentProgressWeight);
|
||||
|
||||
// Posting the content
|
||||
try {
|
||||
final baseProgressVal = _progress!;
|
||||
await sn.client.request(
|
||||
[
|
||||
'/cgi/co/${widget.mode}',
|
||||
if (widget.postEditId != null) '${widget.postEditId}',
|
||||
].join('/'),
|
||||
data: {
|
||||
'publisher': _publisher!.id,
|
||||
'content': _contentController.value.text,
|
||||
'title': _title,
|
||||
'description': _description,
|
||||
'attachments': _attachments.map((e) => e.rid).toList(),
|
||||
if (_replyingTo != null) 'reply_to': _replyingTo!.id,
|
||||
if (_repostingTo != null) 'repost_to': _repostingTo!.id,
|
||||
},
|
||||
onSendProgress: (count, total) {
|
||||
setState(() {
|
||||
_progress = baseProgressVal +
|
||||
(count / total) * (kPostingProgressWeight / 2);
|
||||
});
|
||||
},
|
||||
onReceiveProgress: (count, total) {
|
||||
setState(() {
|
||||
_progress = baseProgressVal +
|
||||
(kPostingProgressWeight / 2) +
|
||||
(count / total) * (kPostingProgressWeight / 2);
|
||||
});
|
||||
},
|
||||
options: Options(
|
||||
method: widget.postEditId != null ? 'PUT' : 'POST',
|
||||
),
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/co/publishers');
|
||||
_publishers = List<SnPublisher>.from(
|
||||
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
|
||||
);
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, true);
|
||||
_writeController.setPublisher(_publishers?.firstOrNull);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
setState(() => _isFetching = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _updateMeta() {
|
||||
showModalBottomSheet<PostMetaResult?>(
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => PostMetaEditor(
|
||||
initialTitle: _title,
|
||||
initialDescription: _description,
|
||||
),
|
||||
builder: (context) => PostMetaEditor(controller: _writeController),
|
||||
useRootNavigator: true,
|
||||
).then((value) {
|
||||
if (value is PostMetaResult) {
|
||||
_title = value.title;
|
||||
_description = value.description;
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
final _imagePicker = ImagePicker();
|
||||
@ -246,308 +78,337 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
void _selectMedia() async {
|
||||
final result = await _imagePicker.pickMultipleMedia();
|
||||
if (result.isEmpty) return;
|
||||
_selectedMedia.addAll(result);
|
||||
_writeController.addAttachments(
|
||||
result.map((e) => PostWriteMedia.fromFile(e)),
|
||||
);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_contentController.dispose();
|
||||
_writeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (!_kTitleMap.keys.contains(widget.mode)) {
|
||||
if (!PostWriteController.kTitleMap.keys.contains(widget.mode)) {
|
||||
context.showErrorDialog('Unknown post type');
|
||||
Navigator.pop(context);
|
||||
}
|
||||
_fetchRelatedPost();
|
||||
_fetchPublishers();
|
||||
_writeController.fetchRelatedPost(
|
||||
context,
|
||||
editing: widget.postEditId,
|
||||
replying: widget.postReplyId,
|
||||
reposting: widget.postRepostId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
flexibleSpace: Column(
|
||||
children: [
|
||||
Text(_title ?? 'untitled'.tr())
|
||||
.textStyle(Theme.of(context).textTheme.titleLarge!)
|
||||
.textColor(Colors.white),
|
||||
Text(_kTitleMap[widget.mode]!)
|
||||
.tr()
|
||||
.textColor(Colors.white.withAlpha((255 * 0.9).round())),
|
||||
],
|
||||
).padding(top: math.max(MediaQuery.of(context).padding.top, 8)),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.tune),
|
||||
onPressed: _isBusy ? null : _updateMeta,
|
||||
return ListenableBuilder(
|
||||
listenable: _writeController,
|
||||
builder: (context, _) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
flexibleSpace: Column(
|
||||
children: [
|
||||
Text(_writeController.title.isNotEmpty
|
||||
? _writeController.title
|
||||
: 'untitled'.tr())
|
||||
.textStyle(Theme.of(context).textTheme.titleLarge!)
|
||||
.textColor(Colors.white),
|
||||
Text(PostWriteController.kTitleMap[widget.mode]!)
|
||||
.tr()
|
||||
.textColor(Colors.white.withAlpha((255 * 0.9).round())),
|
||||
],
|
||||
).padding(top: math.max(MediaQuery.of(context).padding.top, 8)),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.tune),
|
||||
onPressed: _writeController.isBusy ? null : _updateMeta,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<SnPublisher>(
|
||||
isExpanded: true,
|
||||
hint: Text(
|
||||
'fieldPostPublisher',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).hintColor,
|
||||
body: Column(
|
||||
children: [
|
||||
DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<SnPublisher>(
|
||||
isExpanded: true,
|
||||
hint: Text(
|
||||
'fieldPostPublisher',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
).tr(),
|
||||
items: <DropdownMenuItem<SnPublisher>>[
|
||||
...(_publishers?.map(
|
||||
(item) => DropdownMenuItem<SnPublisher>(
|
||||
enabled: _writeController.editingPost == null,
|
||||
value: item,
|
||||
child: Row(
|
||||
children: [
|
||||
AccountImage(content: item.avatar, radius: 16),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(item.nick).textStyle(
|
||||
Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!),
|
||||
Text('@${item.name}')
|
||||
.textStyle(Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!)
|
||||
.fontSize(12),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
) ??
|
||||
[]),
|
||||
DropdownMenuItem<SnPublisher>(
|
||||
value: null,
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onSurface,
|
||||
child: const Icon(Symbols.add),
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('publishersNew').tr().textStyle(
|
||||
Theme.of(context).textTheme.bodyMedium!),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
value: _writeController.publisher,
|
||||
onChanged: (SnPublisher? value) {
|
||||
if (value == null) {
|
||||
GoRouter.of(context)
|
||||
.pushNamed('accountPublisherNew')
|
||||
.then((value) {
|
||||
if (value == true) {
|
||||
_publishers = null;
|
||||
_fetchPublishers();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_writeController.setPublisher(value);
|
||||
}
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.only(right: 16),
|
||||
height: 48,
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 48,
|
||||
),
|
||||
),
|
||||
).tr(),
|
||||
items: <DropdownMenuItem<SnPublisher>>[
|
||||
...(_publishers?.map(
|
||||
(item) => DropdownMenuItem<SnPublisher>(
|
||||
enabled: _editingOg == null,
|
||||
value: item,
|
||||
child: Row(
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.only(bottom: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
// Replying Notice
|
||||
if (_writeController.replyingPost != null)
|
||||
Column(
|
||||
children: [
|
||||
AccountImage(content: item.avatar, radius: 16),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(item.nick).textStyle(
|
||||
Theme.of(context).textTheme.bodyMedium!),
|
||||
Text('@${item.name}')
|
||||
.textStyle(Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!)
|
||||
.fontSize(12),
|
||||
Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
dividerColor: Colors.transparent,
|
||||
),
|
||||
child: ExpansionTile(
|
||||
minTileHeight: 48,
|
||||
leading:
|
||||
const Icon(Symbols.reply).padding(left: 4),
|
||||
title: Text('postReplyingNotice')
|
||||
.fontSize(15)
|
||||
.tr(args: [
|
||||
'@${_writeController.replyingPost!.publisher.name}'
|
||||
]),
|
||||
children: <Widget>[
|
||||
PostItem(data: _writeController.replyingPost!)
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
) ??
|
||||
[]),
|
||||
DropdownMenuItem<SnPublisher>(
|
||||
value: null,
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onSurface,
|
||||
child: const Icon(Symbols.add),
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// Reposting Notice
|
||||
if (_writeController.repostingPost != null)
|
||||
Column(
|
||||
children: [
|
||||
Text('publishersNew').tr().textStyle(
|
||||
Theme.of(context).textTheme.bodyMedium!),
|
||||
Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
dividerColor: Colors.transparent,
|
||||
),
|
||||
child: ExpansionTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.forward)
|
||||
.padding(left: 4),
|
||||
title: Text('postRepostingNotice')
|
||||
.fontSize(15)
|
||||
.tr(args: [
|
||||
'@${_writeController.repostingPost!.publisher.name}'
|
||||
]),
|
||||
children: <Widget>[
|
||||
PostItem(
|
||||
data: _writeController.repostingPost!)
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
// Editing Notice
|
||||
if (_writeController.editingPost != null)
|
||||
Column(
|
||||
children: [
|
||||
Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
dividerColor: Colors.transparent,
|
||||
),
|
||||
child: ExpansionTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.edit_note)
|
||||
.padding(left: 4),
|
||||
title: Text('postEditingNotice')
|
||||
.fontSize(15)
|
||||
.tr(args: [
|
||||
'@${_writeController.editingPost!.publisher.name}'
|
||||
]),
|
||||
children: <Widget>[
|
||||
PostItem(data: _writeController.editingPost!)
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
],
|
||||
),
|
||||
// Content Input Area
|
||||
TextField(
|
||||
controller: _writeController.contentController,
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'fieldPostContent'.tr(),
|
||||
hintStyle: TextStyle(fontSize: 14),
|
||||
isCollapsed: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
)
|
||||
]
|
||||
.expandIndexed(
|
||||
(idx, ele) => [
|
||||
if (idx != 0 || _writeController.isRelatedNull)
|
||||
const Gap(8),
|
||||
ele,
|
||||
],
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
value: _publisher,
|
||||
onChanged: (SnPublisher? value) {
|
||||
if (value == null) {
|
||||
GoRouter.of(context)
|
||||
.pushNamed('accountPublisherNew')
|
||||
.then((value) {
|
||||
if (value == true) {
|
||||
_publisher = null;
|
||||
_publishers = null;
|
||||
_fetchPublishers();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_publisher = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.only(right: 16),
|
||||
height: 48,
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 48,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.only(bottom: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
// Replying Notice
|
||||
if (_replyingTo != null)
|
||||
Column(
|
||||
children: [
|
||||
Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
dividerColor: Colors.transparent,
|
||||
),
|
||||
child: ExpansionTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.reply).padding(left: 4),
|
||||
title: Text('postReplyingNotice')
|
||||
.fontSize(15)
|
||||
.tr(args: ['@${_replyingTo!.publisher.name}']),
|
||||
children: <Widget>[PostItem(data: _replyingTo!)],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
],
|
||||
),
|
||||
// Reposting Notice
|
||||
if (_repostingTo != null)
|
||||
Column(
|
||||
children: [
|
||||
Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
dividerColor: Colors.transparent,
|
||||
),
|
||||
child: ExpansionTile(
|
||||
minTileHeight: 48,
|
||||
leading:
|
||||
const Icon(Symbols.forward).padding(left: 4),
|
||||
title: Text('postRepostingNotice')
|
||||
.fontSize(15)
|
||||
.tr(args: ['@${_repostingTo!.publisher.name}']),
|
||||
children: <Widget>[PostItem(data: _repostingTo!)],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
],
|
||||
),
|
||||
// Editing Notice
|
||||
if (_editingOg != null)
|
||||
Column(
|
||||
children: [
|
||||
Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
dividerColor: Colors.transparent,
|
||||
),
|
||||
child: ExpansionTile(
|
||||
minTileHeight: 48,
|
||||
leading:
|
||||
const Icon(Symbols.edit_note).padding(left: 4),
|
||||
title: Text('postEditingNotice')
|
||||
.fontSize(15)
|
||||
.tr(args: ['@${_editingOg!.publisher.name}']),
|
||||
children: <Widget>[PostItem(data: _editingOg!)],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
],
|
||||
),
|
||||
// Content Input Area
|
||||
TextField(
|
||||
controller: _contentController,
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'fieldPostContent'.tr(),
|
||||
hintStyle: TextStyle(fontSize: 14),
|
||||
isCollapsed: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
)
|
||||
]
|
||||
.expandIndexed(
|
||||
(idx, ele) => [
|
||||
if (idx != 0 ||
|
||||
![_editingOg, _replyingTo, _repostingTo]
|
||||
.any((x) => x != null))
|
||||
const Gap(8),
|
||||
ele,
|
||||
],
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_selectedMedia.isNotEmpty)
|
||||
PostMediaPendingList(
|
||||
data: _selectedMedia,
|
||||
onRemove: (idx) {
|
||||
setState(() {
|
||||
_selectedMedia.removeAt(idx);
|
||||
});
|
||||
},
|
||||
).padding(bottom: 8),
|
||||
Material(
|
||||
elevation: 2,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isLoading),
|
||||
if (_isBusy && _progress != null)
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: 1),
|
||||
duration: Duration(milliseconds: 300),
|
||||
builder: (context, value, _) =>
|
||||
LinearProgressIndicator(value: value, minHeight: 2),
|
||||
)
|
||||
else if (_isBusy)
|
||||
const LinearProgressIndicator(value: null, minHeight: 2),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
if (_writeController.attachments.isNotEmpty)
|
||||
PostMediaPendingList(
|
||||
data: _writeController.attachments,
|
||||
onRemove: (idx) {
|
||||
_writeController.removeAttachmentAt(idx);
|
||||
},
|
||||
).padding(bottom: 8),
|
||||
Material(
|
||||
elevation: 2,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ScrollConfiguration(
|
||||
behavior: _PostEditorActionScrollBehavior(),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: _isBusy ? null : _selectMedia,
|
||||
icon: Icon(
|
||||
Symbols.add_photo_alternate,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
LoadingIndicator(isActive: _isLoading),
|
||||
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),
|
||||
)
|
||||
else if (_writeController.isBusy)
|
||||
const LinearProgressIndicator(value: null, minHeight: 2),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ScrollConfiguration(
|
||||
behavior: _PostEditorActionScrollBehavior(),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: _writeController.isBusy
|
||||
? null
|
||||
: _selectMedia,
|
||||
icon: Icon(
|
||||
Symbols.add_photo_alternate,
|
||||
color:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: (_isBusy || _publisher == null)
|
||||
? null
|
||||
: _performAction,
|
||||
icon: const Icon(Symbols.send),
|
||||
label: Text('postPublish').tr(),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: (_writeController.isBusy ||
|
||||
_writeController.publisher == null)
|
||||
? null
|
||||
: () => _writeController.post(context),
|
||||
icon: const Icon(Symbols.send),
|
||||
label: Text('postPublish').tr(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16),
|
||||
],
|
||||
).padding(horizontal: 16),
|
||||
],
|
||||
).padding(
|
||||
bottom: MediaQuery.of(context).padding.bottom,
|
||||
top: 4,
|
||||
),
|
||||
).padding(
|
||||
bottom: MediaQuery.of(context).padding.bottom,
|
||||
top: 4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ class SnPost with _$SnPost {
|
||||
required DateTime? lockedAt,
|
||||
required bool isDraft,
|
||||
required DateTime? publishedAt,
|
||||
required dynamic publishedUntil,
|
||||
required DateTime? publishedUntil,
|
||||
required int totalUpvote,
|
||||
required int totalDownvote,
|
||||
required int? realmId,
|
||||
|
@ -45,7 +45,7 @@ mixin _$SnPost {
|
||||
DateTime? get lockedAt => throw _privateConstructorUsedError;
|
||||
bool get isDraft => throw _privateConstructorUsedError;
|
||||
DateTime? get publishedAt => throw _privateConstructorUsedError;
|
||||
dynamic get publishedUntil => throw _privateConstructorUsedError;
|
||||
DateTime? get publishedUntil => throw _privateConstructorUsedError;
|
||||
int get totalUpvote => throw _privateConstructorUsedError;
|
||||
int get totalDownvote => throw _privateConstructorUsedError;
|
||||
int? get realmId => throw _privateConstructorUsedError;
|
||||
@ -95,7 +95,7 @@ abstract class $SnPostCopyWith<$Res> {
|
||||
DateTime? lockedAt,
|
||||
bool isDraft,
|
||||
DateTime? publishedAt,
|
||||
dynamic publishedUntil,
|
||||
DateTime? publishedUntil,
|
||||
int totalUpvote,
|
||||
int totalDownvote,
|
||||
int? realmId,
|
||||
@ -264,7 +264,7 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
|
||||
publishedUntil: freezed == publishedUntil
|
||||
? _value.publishedUntil
|
||||
: publishedUntil // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
as DateTime?,
|
||||
totalUpvote: null == totalUpvote
|
||||
? _value.totalUpvote
|
||||
: totalUpvote // ignore: cast_nullable_to_non_nullable
|
||||
@ -368,7 +368,7 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> {
|
||||
DateTime? lockedAt,
|
||||
bool isDraft,
|
||||
DateTime? publishedAt,
|
||||
dynamic publishedUntil,
|
||||
DateTime? publishedUntil,
|
||||
int totalUpvote,
|
||||
int totalDownvote,
|
||||
int? realmId,
|
||||
@ -538,7 +538,7 @@ class __$$SnPostImplCopyWithImpl<$Res>
|
||||
publishedUntil: freezed == publishedUntil
|
||||
? _value.publishedUntil
|
||||
: publishedUntil // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
as DateTime?,
|
||||
totalUpvote: null == totalUpvote
|
||||
? _value.totalUpvote
|
||||
: totalUpvote // ignore: cast_nullable_to_non_nullable
|
||||
@ -690,7 +690,7 @@ class _$SnPostImpl extends _SnPost {
|
||||
@override
|
||||
final DateTime? publishedAt;
|
||||
@override
|
||||
final dynamic publishedUntil;
|
||||
final DateTime? publishedUntil;
|
||||
@override
|
||||
final int totalUpvote;
|
||||
@override
|
||||
@ -756,8 +756,8 @@ class _$SnPostImpl extends _SnPost {
|
||||
(identical(other.isDraft, isDraft) || other.isDraft == isDraft) &&
|
||||
(identical(other.publishedAt, publishedAt) ||
|
||||
other.publishedAt == publishedAt) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.publishedUntil, publishedUntil) &&
|
||||
(identical(other.publishedUntil, publishedUntil) ||
|
||||
other.publishedUntil == publishedUntil) &&
|
||||
(identical(other.totalUpvote, totalUpvote) ||
|
||||
other.totalUpvote == totalUpvote) &&
|
||||
(identical(other.totalDownvote, totalDownvote) ||
|
||||
@ -801,7 +801,7 @@ class _$SnPostImpl extends _SnPost {
|
||||
lockedAt,
|
||||
isDraft,
|
||||
publishedAt,
|
||||
const DeepCollectionEquality().hash(publishedUntil),
|
||||
publishedUntil,
|
||||
totalUpvote,
|
||||
totalDownvote,
|
||||
realmId,
|
||||
@ -855,7 +855,7 @@ abstract class _SnPost extends SnPost {
|
||||
required final DateTime? lockedAt,
|
||||
required final bool isDraft,
|
||||
required final DateTime? publishedAt,
|
||||
required final dynamic publishedUntil,
|
||||
required final DateTime? publishedUntil,
|
||||
required final int totalUpvote,
|
||||
required final int totalDownvote,
|
||||
required final int? realmId,
|
||||
@ -919,7 +919,7 @@ abstract class _SnPost extends SnPost {
|
||||
@override
|
||||
DateTime? get publishedAt;
|
||||
@override
|
||||
dynamic get publishedUntil;
|
||||
DateTime? get publishedUntil;
|
||||
@override
|
||||
int get totalUpvote;
|
||||
@override
|
||||
|
@ -42,7 +42,9 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl(
|
||||
publishedAt: json['published_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['published_at'] as String),
|
||||
publishedUntil: json['published_until'],
|
||||
publishedUntil: json['published_until'] == null
|
||||
? null
|
||||
: DateTime.parse(json['published_until'] as String),
|
||||
totalUpvote: (json['total_upvote'] as num).toInt(),
|
||||
totalDownvote: (json['total_downvote'] as num).toInt(),
|
||||
realmId: (json['realm_id'] as num?)?.toInt(),
|
||||
@ -83,7 +85,7 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) =>
|
||||
'locked_at': instance.lockedAt?.toIso8601String(),
|
||||
'is_draft': instance.isDraft,
|
||||
'published_at': instance.publishedAt?.toIso8601String(),
|
||||
'published_until': instance.publishedUntil,
|
||||
'published_until': instance.publishedUntil?.toIso8601String(),
|
||||
'total_upvote': instance.totalUpvote,
|
||||
'total_downvote': instance.totalDownvote,
|
||||
'realm_id': instance.realmId,
|
||||
|
@ -35,6 +35,7 @@ class AccountImage extends StatelessWidget {
|
||||
UniversalImage.provider(url),
|
||||
width: ((radius ?? 20) * devicePixelRatio * 2).round(),
|
||||
height: ((radius ?? 20) * devicePixelRatio * 2).round(),
|
||||
policy: ResizeImagePolicy.fit,
|
||||
)
|
||||
: null,
|
||||
child: (content?.isEmpty ?? true)
|
||||
|
@ -1,15 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cross_file/cross_file.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
|
||||
class PostMediaPendingList extends StatelessWidget {
|
||||
final List<XFile> data;
|
||||
final List<PostWriteMedia> data;
|
||||
final Function(int idx)? onRemove;
|
||||
const PostMediaPendingList({
|
||||
super.key,
|
||||
@ -19,6 +17,8 @@ class PostMediaPendingList extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxHeight: 120),
|
||||
child: ListView.separated(
|
||||
@ -53,9 +53,25 @@ class PostMediaPendingList extends StatelessWidget {
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: kIsWeb
|
||||
? Image.network(file.path, fit: BoxFit.cover)
|
||||
: Image.file(File(file.path), fit: BoxFit.cover),
|
||||
child: switch (file.type) {
|
||||
PostWriteMediaType.image =>
|
||||
LayoutBuilder(builder: (context, constraints) {
|
||||
return Image(
|
||||
image: file.getImageProvider(
|
||||
context,
|
||||
width: (constraints.maxWidth * devicePixelRatio)
|
||||
.round(),
|
||||
height: (constraints.maxHeight * devicePixelRatio)
|
||||
.round(),
|
||||
)!,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
}),
|
||||
_ => Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: const Icon(Symbols.docs).center(),
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -1,87 +1,130 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
|
||||
class PostMetaResult {
|
||||
final String title;
|
||||
final String description;
|
||||
class PostMetaEditor extends StatelessWidget {
|
||||
final PostWriteController controller;
|
||||
const PostMetaEditor({super.key, required this.controller});
|
||||
|
||||
PostMetaResult({required this.title, required this.description});
|
||||
}
|
||||
|
||||
class PostMetaEditor extends StatefulWidget {
|
||||
final String? initialTitle;
|
||||
final String? initialDescription;
|
||||
const PostMetaEditor({super.key, this.initialTitle, this.initialDescription});
|
||||
|
||||
@override
|
||||
State<PostMetaEditor> createState() => _PostMetaEditorState();
|
||||
}
|
||||
|
||||
class _PostMetaEditorState extends State<PostMetaEditor> {
|
||||
final TextEditingController _titleController = TextEditingController();
|
||||
final TextEditingController _descriptionController = TextEditingController();
|
||||
|
||||
void _applyChanges() {
|
||||
Navigator.pop(
|
||||
context,
|
||||
PostMetaResult(
|
||||
title: _titleController.text,
|
||||
description: _descriptionController.text,
|
||||
Future<DateTime?> _selectDate(
|
||||
BuildContext context, {
|
||||
DateTime? initialDateTime,
|
||||
}) async {
|
||||
DateTime? picked;
|
||||
await showCupertinoModalPopup(
|
||||
context: context,
|
||||
builder: (BuildContext context) => Container(
|
||||
height: 216,
|
||||
padding: const EdgeInsets.only(top: 6.0),
|
||||
margin: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: CupertinoDatePicker(
|
||||
initialDateTime: initialDateTime,
|
||||
mode: CupertinoDatePickerMode.dateAndTime,
|
||||
use24hFormat: true,
|
||||
onDateTimeChanged: (DateTime newDate) {
|
||||
picked = newDate;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_titleController.text = widget.initialTitle ?? '';
|
||||
_descriptionController.text = widget.initialDescription ?? '';
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_titleController.dispose();
|
||||
_descriptionController.dispose();
|
||||
super.dispose();
|
||||
return picked;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: _titleController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldPostTitle'.tr(),
|
||||
border: UnderlineInputBorder(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(4),
|
||||
TextField(
|
||||
controller: _descriptionController,
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldPostDescription'.tr(),
|
||||
border: UnderlineInputBorder(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
final dateFormatter = DateFormat('y/M/d HH:mm:ss');
|
||||
return ListenableBuilder(
|
||||
listenable: controller,
|
||||
builder: (context, _) {
|
||||
return Column(
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: _applyChanges,
|
||||
icon: const Icon(Symbols.save),
|
||||
label: Text('apply').tr(),
|
||||
TextField(
|
||||
controller: controller.titleController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldPostTitle'.tr(),
|
||||
border: UnderlineInputBorder(),
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 24),
|
||||
if (controller.mode == 'article') const Gap(4),
|
||||
if (controller.mode == 'article')
|
||||
TextField(
|
||||
controller: controller.descriptionController,
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldPostDescription'.tr(),
|
||||
border: UnderlineInputBorder(),
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 24),
|
||||
const Gap(12),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.event_available),
|
||||
title: Text('postPublishedAt').tr(),
|
||||
subtitle: Text(
|
||||
controller.publishedAt != null
|
||||
? dateFormatter.format(controller.publishedAt!)
|
||||
: 'unset'.tr(),
|
||||
),
|
||||
trailing: controller.publishedAt != null
|
||||
? IconButton(
|
||||
icon: const Icon(Symbols.cancel),
|
||||
onPressed: () {
|
||||
controller.setPublishedAt(null);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 18),
|
||||
onTap: () {
|
||||
_selectDate(
|
||||
context,
|
||||
initialDateTime: controller.publishedAt,
|
||||
).then((value) {
|
||||
controller.setPublishedAt(value);
|
||||
});
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.event_busy),
|
||||
title: Text('postPublishedUntil').tr(),
|
||||
subtitle: Text(
|
||||
controller.publishedUntil != null
|
||||
? dateFormatter.format(controller.publishedUntil!)
|
||||
: 'unset'.tr(),
|
||||
),
|
||||
trailing: controller.publishedUntil != null
|
||||
? IconButton(
|
||||
icon: const Icon(Symbols.cancel),
|
||||
onPressed: () {
|
||||
controller.setPublishedUntil(null);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 18),
|
||||
onTap: () {
|
||||
_selectDate(
|
||||
context,
|
||||
initialDateTime: controller.publishedUntil,
|
||||
).then((value) {
|
||||
controller.setPublishedUntil(value);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 8);
|
||||
).padding(vertical: 8);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
10
lib/widgets/post/post_mini_editor.dart
Normal file
10
lib/widgets/post/post_mini_editor.dart
Normal file
@ -0,0 +1,10 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class PostMiniEditor extends StatelessWidget {
|
||||
const PostMiniEditor({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Placeholder();
|
||||
}
|
||||
}
|
44
pubspec.lock
44
pubspec.lock
@ -5,23 +5,23 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
|
||||
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "76.0.0"
|
||||
version: "72.0.0"
|
||||
_macros:
|
||||
dependency: transitive
|
||||
description: dart
|
||||
source: sdk
|
||||
version: "0.3.3"
|
||||
version: "0.3.2"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
|
||||
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.11.0"
|
||||
version: "6.7.0"
|
||||
animations:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -202,10 +202,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.0"
|
||||
version: "1.18.0"
|
||||
connectivity_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -790,18 +790,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.8"
|
||||
version: "10.0.5"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.9"
|
||||
version: "3.0.5"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -838,10 +838,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: macros
|
||||
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
|
||||
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3-main.0"
|
||||
version: "0.1.2-main.4"
|
||||
markdown:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1150,7 +1150,7 @@ packages:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
version: "0.0.99"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1227,10 +1227,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.0"
|
||||
version: "1.11.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1251,10 +1251,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.2.0"
|
||||
styled_widget:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1291,10 +1291,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
||||
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.3"
|
||||
version: "0.7.2"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1411,10 +1411,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
|
||||
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.3.0"
|
||||
version: "14.2.5"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
Loading…
Reference in New Issue
Block a user