Surface/lib/controllers/post_write_controller.dart

480 lines
13 KiB
Dart
Raw Normal View History

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';
2024-11-11 13:48:50 +00:00
import 'package:mime/mime.dart';
import 'package:provider/provider.dart';
2024-11-25 16:00:09 +00:00
import 'package:surface/providers/post.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;
2024-12-07 09:43:44 +00:00
PostWriteMedia? thumbnail;
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;
2024-11-11 13:48:50 +00:00
String? mimetype = file!.mimeType;
mimetype ??= lookupMimeType(file!.path);
switch (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;
}
}
2024-12-07 09:43:44 +00:00
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) {
2024-11-21 16:28:29 +00:00
return XFile.fromData(
raw!,
name: name,
);
}
return null;
}
ImageProvider? getImageProvider(
BuildContext context, {
int? width,
int? height,
}) {
if (attachment != null) {
final sn = context.read<SnNetworkProvider>();
2024-12-07 09:43:44 +00:00
final ImageProvider provider = UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
2024-11-11 13:48:50 +00:00
if (width != null && height != null) {
return ResizeImage(
provider,
width: width,
height: height,
policy: ResizeImagePolicy.fit,
);
}
return provider;
} else if (file != null) {
2024-12-07 09:43:44 +00:00
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;
2024-12-07 09:43:44 +00:00
String get description => descriptionController.text;
2024-12-07 09:43:44 +00:00
bool get isRelatedNull => ![editingPost, repostingPost, replyingPost].any((ele) => ele != null);
bool isLoading = false, isBusy = false;
double? progress;
SnPublisher? publisher;
SnPost? editingPost, repostingPost, replyingPost;
2024-12-04 15:54:47 +00:00
int visibility = 0;
List<int> visibleUsers = List.empty();
List<int> invisibleUsers = List.empty();
2024-11-26 16:06:11 +00:00
List<String> tags = List.empty();
2024-12-07 09:43:44 +00:00
PostWriteMedia? thumbnail;
List<PostWriteMedia> attachments = List.empty(growable: true);
DateTime? publishedAt, publishedUntil;
Future<void> fetchRelatedPost(
BuildContext context, {
int? editing,
int? reposting,
int? replying,
}) async {
2024-11-25 16:00:09 +00:00
final pt = context.read<SnPostContentProvider>();
isLoading = true;
notifyListeners();
try {
if (editing != null) {
2024-11-25 16:00:09 +00:00
final post = await pt.getPost(editing);
publisher = post.publisher;
titleController.text = post.body['title'] ?? '';
descriptionController.text = post.body['description'] ?? '';
contentController.text = post.body['content'] ?? '';
publishedAt = post.publishedAt;
publishedUntil = post.publishedUntil;
visibleUsers = List.from(post.visibleUsersList ?? []);
invisibleUsers = List.from(post.invisibleUsersList ?? []);
2024-12-04 15:54:47 +00:00
visibility = post.visibility;
2024-11-26 16:06:11 +00:00
tags = List.from(post.tags.map((ele) => ele.alias));
2024-12-07 09:43:44 +00:00
attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
2024-12-07 14:27:07 +00:00
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
2024-12-07 09:43:44 +00:00
thumbnail = PostWriteMedia(post.preload!.thumbnail);
}
2024-11-25 16:00:09 +00:00
editingPost = post;
}
if (replying != null) {
2024-11-25 16:00:09 +00:00
final post = await pt.getPost(replying);
replyingPost = post;
}
if (reposting != null) {
2024-11-25 16:00:09 +00:00
final post = await pt.getPost(reposting);
2024-12-07 15:40:26 +00:00
repostingPost = post;
}
} catch (err) {
if (!context.mounted) return;
context.showErrorDialog(err);
} finally {
isLoading = false;
notifyListeners();
}
}
2024-12-07 09:43:44 +00:00
Future<SnAttachment> _uploadAttachment(BuildContext context, PostWriteMedia media) async {
final attach = context.read<SnAttachmentProvider>();
final place = await attach.chunkedUploadInitialize(
(await media.length())!,
media.name,
'interactive',
null,
mimetype: media.raw != null && media.type == PostWriteMediaType.image ? 'image/png' : null,
);
final item = await attach.chunkedUploadParts(
media.toFile()!,
place.$1,
place.$2,
onProgress: (progress) {
progress = progress;
notifyListeners();
},
);
return item;
}
Future<void> uploadSingleAttachment(BuildContext context, int idx) async {
if (isBusy) return;
final media = idx == -1 ? thumbnail! : attachments[idx];
isBusy = true;
notifyListeners();
final item = await _uploadAttachment(context, media);
attachments[idx] = PostWriteMedia(item);
isBusy = false;
2024-12-07 09:43:44 +00:00
notifyListeners();
}
2024-11-11 14:43:09 +00:00
Future<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 {
2024-12-07 09:43:44 +00:00
if (thumbnail != null && thumbnail!.attachment == null) {
final thumb = await _uploadAttachment(context, thumbnail!);
thumbnail = PostWriteMedia(thumb);
}
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,
2024-12-07 09:43:44 +00:00
mimetype: media.raw != null && media.type == PostWriteMediaType.image ? 'image/png' : null,
);
final item = await attach.chunkedUploadParts(
media.toFile()!,
place.$1,
place.$2,
onProgress: (progress) {
// Calculate overall progress for attachments
2024-12-07 09:43:44 +00:00
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,
2024-11-11 14:43:09 +00:00
if (titleController.text.isNotEmpty) 'title': titleController.text,
2024-12-07 09:43:44 +00:00
if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.rid,
'attachments': attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(),
2024-11-26 16:06:11 +00:00
'tags': tags.map((ele) => {'alias': ele}).toList(),
2024-12-04 15:54:47 +00:00
'visibility': visibility,
'visible_users_list': visibleUsers,
'invisible_users_list': invisibleUsers,
2024-12-07 09:43:44 +00:00
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) {
2024-12-07 09:43:44 +00:00
progress = baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
notifyListeners();
},
onReceiveProgress: (count, total) {
2024-12-07 09:43:44 +00:00
progress = baseProgressVal + (kPostingProgressWeight / 2) + (count / total) * (kPostingProgressWeight / 2);
notifyListeners();
},
options: Options(
method: editingPost != null ? 'PUT' : 'POST',
),
);
} catch (err) {
if (!context.mounted) return;
context.showErrorDialog(err);
} finally {
isBusy = false;
notifyListeners();
}
}
void addAttachments(Iterable<PostWriteMedia> items) {
attachments.addAll(items);
notifyListeners();
}
2024-11-11 13:48:50 +00:00
void setAttachmentAt(int idx, PostWriteMedia item) {
2024-12-07 09:43:44 +00:00
if (idx == -1) {
thumbnail = item;
} else {
attachments[idx] = item;
}
2024-11-11 13:48:50 +00:00
notifyListeners();
}
void removeAttachmentAt(int idx) {
2024-12-07 09:43:44 +00:00
if (idx == -1) {
thumbnail = null;
} else {
attachments.removeAt(idx);
}
notifyListeners();
}
void setThumbnail(int? idx) {
if (idx == null) {
attachments.add(thumbnail!);
thumbnail = null;
} else {
if (thumbnail != null) {
attachments.add(thumbnail!);
}
2024-12-07 09:43:44 +00:00
thumbnail = attachments[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();
}
2024-11-26 16:06:11 +00:00
void setTags(List<String> value) {
tags = value;
notifyListeners();
}
2024-12-04 15:54:47 +00:00
void setVisibility(int value) {
visibility = value;
notifyListeners();
}
void setVisibleUsers(List<int> value) {
visibleUsers = value;
notifyListeners();
}
void setInvisibleUsers(List<int> value) {
invisibleUsers = value;
notifyListeners();
}
2024-12-07 09:43:44 +00:00
void setProgress(double? value) {
progress = value;
notifyListeners();
}
void setIsBusy(bool value) {
isBusy = value;
notifyListeners();
}
2024-12-07 09:43:44 +00:00
void setMode(String value) {
mode = value;
notifyListeners();
}
2024-11-11 14:43:09 +00:00
void reset() {
publishedAt = null;
publishedUntil = null;
titleController.clear();
descriptionController.clear();
contentController.clear();
attachments.clear();
editingPost = null;
replyingPost = null;
repostingPost = null;
mode = kTitleMap.keys.first;
notifyListeners();
}
@override
void dispose() {
contentController.dispose();
titleController.dispose();
descriptionController.dispose();
super.dispose();
}
}