2024-11-11 13:30:05 +00:00
|
|
|
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';
|
2024-11-11 13:30:05 +00:00
|
|
|
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;
|
|
|
|
|
2024-11-11 13:48:50 +00:00
|
|
|
String? mimetype = file!.mimeType;
|
|
|
|
mimetype ??= lookupMimeType(file!.path);
|
|
|
|
|
|
|
|
switch (mimetype?.split('/').firstOrNull) {
|
2024-11-11 13:30:05 +00:00
|
|
|
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) {
|
2024-11-21 16:28:29 +00:00
|
|
|
return XFile.fromData(
|
|
|
|
raw!,
|
|
|
|
name: name,
|
|
|
|
);
|
2024-11-11 13:30:05 +00:00
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
ImageProvider? getImageProvider(
|
|
|
|
BuildContext context, {
|
|
|
|
int? width,
|
|
|
|
int? height,
|
|
|
|
}) {
|
|
|
|
if (attachment != null) {
|
|
|
|
final sn = context.read<SnNetworkProvider>();
|
2024-11-11 13:48:50 +00:00
|
|
|
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;
|
2024-11-11 13:30:05 +00:00
|
|
|
} 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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-11 14:43:09 +00:00
|
|
|
Future<void> post(BuildContext context) async {
|
2024-11-11 13:30:05 +00:00
|
|
|
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,
|
2024-11-21 16:28:29 +00:00
|
|
|
mimetype: media.raw != null && media.type == PostWriteMediaType.image
|
|
|
|
? 'image/png'
|
|
|
|
: null,
|
2024-11-11 13:30:05 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
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,
|
2024-11-11 14:43:09 +00:00
|
|
|
if (titleController.text.isNotEmpty) 'title': titleController.text,
|
|
|
|
if (descriptionController.text.isNotEmpty)
|
|
|
|
'description': descriptionController.text,
|
2024-11-11 13:30:05 +00:00
|
|
|
'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',
|
|
|
|
),
|
|
|
|
);
|
|
|
|
} 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) {
|
|
|
|
attachments[idx] = item;
|
|
|
|
notifyListeners();
|
|
|
|
}
|
|
|
|
|
2024-11-11 13:30:05 +00:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2024-11-11 13:57:09 +00:00
|
|
|
void setIsBusy(bool value) {
|
|
|
|
isBusy = 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();
|
|
|
|
}
|
|
|
|
|
2024-11-11 13:30:05 +00:00
|
|
|
@override
|
|
|
|
void dispose() {
|
|
|
|
contentController.dispose();
|
|
|
|
titleController.dispose();
|
|
|
|
descriptionController.dispose();
|
|
|
|
super.dispose();
|
|
|
|
}
|
|
|
|
}
|