Surface/lib/controllers/post_write_controller.dart
2024-11-11 21:48:50 +08:00

374 lines
10 KiB
Dart

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:mime/mime.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;
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;
}
}
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>();
final ImageProvider provider =
UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
if (width != null && height != null) {
return ResizeImage(
provider,
width: width,
height: height,
policy: ResizeImagePolicy.fit,
);
}
return provider;
} 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 setAttachmentAt(int idx, PostWriteMedia item) {
attachments[idx] = item;
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();
}
}