461 lines
14 KiB
Dart
461 lines
14 KiB
Dart
import 'dart:convert';
|
|
import 'dart:ui';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:localization/localization.dart';
|
|
import 'package:loris/business_logic/fileupload/fileupload.dart';
|
|
import 'package:loris/business_logic/instance/instance.dart';
|
|
import 'package:loris/business_logic/network_tools/get_post_from_url.dart';
|
|
import 'package:loris/business_logic/timeline/timeline.dart' as tl;
|
|
import '../business_logic/posting/posting.dart';
|
|
import '../partials/post.dart';
|
|
import 'package:loris/global.dart' as global;
|
|
import 'package:file_picker/file_picker.dart';
|
|
import 'package:mime/mime.dart';
|
|
import 'package:universal_io/io.dart' as io;
|
|
import 'package:loris/themes/themes.dart' as themes;
|
|
|
|
class MakePost extends StatefulWidget {
|
|
const MakePost({Key? key, this.inReplyTo}) : super(key: key);
|
|
final tl.PostModel? inReplyTo;
|
|
|
|
@override
|
|
State<MakePost> createState() => _MakePostState();
|
|
}
|
|
|
|
class _MakePostState extends State<MakePost> {
|
|
String replyAts = "";
|
|
String accountid = global.settings!.activeIdentity;
|
|
InstanceInformation? instanceInfo;
|
|
String text = "";
|
|
String spoilerText = "";
|
|
String? status;
|
|
bool sending = false;
|
|
// tracks fileuploads and their ids on servers
|
|
// if the id is null the file has not been uploaded yet
|
|
List<FileUpload> files = [];
|
|
// stores all identities and if available the post id you are replying to
|
|
Map<String, String?> identitiesAvailable = {};
|
|
tl.Visibility visibility = tl.Visibility.public;
|
|
|
|
void switchAccount(String acct) {
|
|
setState(() {
|
|
instanceInfo = null;
|
|
accountid = acct;
|
|
});
|
|
updateMaxChars();
|
|
}
|
|
|
|
// check if there are more attachments than the instance ur posting to allowss
|
|
bool tooManyAttachments() {
|
|
if (instanceInfo == null) {
|
|
return true;
|
|
}
|
|
if (instanceInfo!.configuration.statusconfig.maxMediaAttachments <
|
|
files.length) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// moves file in list up or down,
|
|
// has to be called with file from items list
|
|
void moveFile(FileUpload file, {up = true}) {
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
|
|
final index = files.indexOf(file);
|
|
final newIndex = (index + (up ? -1 : 1)) % files.length;
|
|
|
|
files.remove(file);
|
|
setState(() {
|
|
files.insert(newIndex, file);
|
|
});
|
|
}
|
|
|
|
void updateMaxChars() async {
|
|
final info = await instanceInformationForIdentity(accountid);
|
|
if (info.keys.first == 200 && mounted) {
|
|
setState(() {
|
|
instanceInfo = info.values.first!;
|
|
});
|
|
}
|
|
}
|
|
|
|
void updateAvailableIds() async {
|
|
if (widget.inReplyTo != null) {
|
|
global.settings!.identities.forEach((key, value) async {
|
|
final post = await getPostFromUrl(key, widget.inReplyTo!.uri);
|
|
if (mounted) {
|
|
if (post.keys.first == 200) {
|
|
setState(() {
|
|
identitiesAvailable.addAll({key: post.values.first!.id});
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void addAt(String at) {
|
|
if (!at.contains("@")) {
|
|
at = "$at${widget.inReplyTo!.getReceiverInstance()}";
|
|
}
|
|
if (global.settings!.identities.keys.contains(at)) {
|
|
return;
|
|
}
|
|
replyAts = "$replyAts@$at ";
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
if (widget.inReplyTo != null) {
|
|
visibility = widget.inReplyTo!.visibility;
|
|
addAt(widget.inReplyTo!.account.acct);
|
|
for (var element in widget.inReplyTo!.mentions) {
|
|
addAt(element.acct);
|
|
}
|
|
|
|
accountid = widget.inReplyTo!.identity;
|
|
identitiesAvailable.addAll({
|
|
widget.inReplyTo!.identity: widget.inReplyTo!.id,
|
|
});
|
|
} else {
|
|
global.settings!.identities.forEach((key, value) {
|
|
if (!identitiesAvailable.containsKey(DropdownMenuItem(
|
|
value: key,
|
|
child: Text(key),
|
|
))) {
|
|
identitiesAvailable.addAll({key: null});
|
|
}
|
|
});
|
|
}
|
|
if (widget.inReplyTo != null) {
|
|
spoilerText = widget.inReplyTo!.spoilerText;
|
|
}
|
|
super.initState();
|
|
updateMaxChars();
|
|
updateAvailableIds();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// make list of all different widgets to be displayed
|
|
List<Widget> c = [];
|
|
if (widget.inReplyTo != null) {
|
|
c.add(
|
|
Post(
|
|
model: widget.inReplyTo!,
|
|
reblogVisible: false,
|
|
hideActionBar: true,
|
|
),
|
|
);
|
|
}
|
|
|
|
List<DropdownMenuItem<String>> idDropdownItems = [];
|
|
identitiesAvailable.forEach((key, value) {
|
|
idDropdownItems.add(DropdownMenuItem(
|
|
value: key,
|
|
alignment: Alignment.center,
|
|
child: Text(
|
|
key,
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
textAlign: TextAlign.center,
|
|
),
|
|
));
|
|
});
|
|
|
|
List<DropdownMenuItem<tl.Visibility>> visibilityDropdowns = [];
|
|
for (var value in tl.Visibility.values) {
|
|
visibilityDropdowns.add(DropdownMenuItem(
|
|
value: value,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(value.icon),
|
|
Text(
|
|
value.name,
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
),
|
|
],
|
|
)));
|
|
}
|
|
List<Widget> actionButtons = [
|
|
DropdownButtonHideUnderline(
|
|
child: DropdownButton<tl.Visibility>(
|
|
alignment: Alignment.center,
|
|
iconEnabledColor: Theme.of(context).colorScheme.onSurface,
|
|
items: visibilityDropdowns,
|
|
value: visibility,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
visibility = value ?? tl.Visibility.private;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
// media attachment button
|
|
TextButton.icon(
|
|
onPressed: () async {
|
|
// open filepicker
|
|
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
|
withData: false,
|
|
allowMultiple: true,
|
|
);
|
|
if (result != null) {
|
|
// if there are any files
|
|
// then parse them
|
|
for (var path in result.paths) {
|
|
if (path != null && mounted) {
|
|
if (await io.Directory(path).exists()) break;
|
|
final FileUpload f = await FileUpload.fromPath(path, "");
|
|
setState(() {
|
|
files.add(f);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
},
|
|
icon: const Icon(Icons.attachment),
|
|
label: Text(
|
|
"${"add-files".i18n()}${instanceInfo == null ? "(${"loading...".i18n()})" : " (${instanceInfo!.configuration.statusconfig.maxMediaAttachments - files.length} ${"remaining".i18n()})"}")),
|
|
DropdownButtonHideUnderline(
|
|
child: DropdownButton<String>(
|
|
alignment: Alignment.center,
|
|
borderRadius: BorderRadius.circular(8),
|
|
iconEnabledColor: Theme.of(context).colorScheme.onSurface,
|
|
items: idDropdownItems,
|
|
value: accountid,
|
|
onChanged: (value) {
|
|
switchAccount(value as String);
|
|
},
|
|
),
|
|
),
|
|
ElevatedButton.icon(
|
|
label: Text(
|
|
"send-post".i18n(),
|
|
),
|
|
// send the post!!!
|
|
onPressed: ((spoilerText.isEmpty && text.isEmpty && files.isEmpty) ||
|
|
tooManyAttachments())
|
|
? null
|
|
: () async {
|
|
setState(() {
|
|
sending = true;
|
|
status = "sending...".i18n();
|
|
});
|
|
|
|
// upload the media attachments
|
|
List<String> mediaIds = [];
|
|
for (var file in files) {
|
|
if (mounted) {
|
|
setState(() {
|
|
status =
|
|
"${"sending-file".i18n()} ${mediaIds.length + 1}/${files.length}";
|
|
});
|
|
}
|
|
final response = await file.upload(accountid);
|
|
if (response.key == 200 || response.key == 202) {
|
|
mediaIds.add(response.value);
|
|
} else if (mounted) {
|
|
setState(() {
|
|
status = response.value;
|
|
sending = false;
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
// send the final post
|
|
final model = MakePostModel(
|
|
mediaIds: mediaIds,
|
|
spoilerText: spoilerText.trim(),
|
|
identity: accountid,
|
|
status: text,
|
|
visibility: visibility,
|
|
inReplyToId: widget.inReplyTo == null
|
|
? null
|
|
: identitiesAvailable[accountid]!,
|
|
);
|
|
final result = await model.sendPost();
|
|
if (mounted) {
|
|
if (result.statusCode == 200) {
|
|
Navigator.of(context).pop();
|
|
}
|
|
setState(() {
|
|
sending = false;
|
|
status =
|
|
"${"failed-to-send".i18n()}: ${jsonDecode(result.body)["error"].toString()}";
|
|
});
|
|
}
|
|
},
|
|
icon: const Icon(Icons.send),
|
|
),
|
|
];
|
|
c.addAll([
|
|
// content warnings
|
|
TextFormField(
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
initialValue: spoilerText,
|
|
decoration: InputDecoration(
|
|
counterText: instanceInfo == null
|
|
? "loading...".i18n()
|
|
: (instanceInfo!.configuration.statusconfig.maxChars -
|
|
text.length -
|
|
spoilerText.length)
|
|
.toString(),
|
|
prefixIcon: Icon(
|
|
Icons.warning,
|
|
color: Theme.of(context).colorScheme.onSurface,
|
|
),
|
|
labelText: "content-warning".i18n(),
|
|
),
|
|
onChanged: ((value) => setState(() {
|
|
spoilerText = value;
|
|
})),
|
|
),
|
|
Container(
|
|
constraints:
|
|
BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.6),
|
|
child: TextFormField(
|
|
autofocus: true,
|
|
initialValue: replyAts,
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
maxLines: null,
|
|
expands: true,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
text = value;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(
|
|
height: 24,
|
|
),
|
|
]);
|
|
|
|
// these are the action buttons
|
|
c.add(
|
|
Wrap(
|
|
runSpacing: 8,
|
|
spacing: 24,
|
|
direction: Axis.horizontal,
|
|
crossAxisAlignment: WrapCrossAlignment.center,
|
|
alignment: WrapAlignment.spaceAround,
|
|
children: actionButtons,
|
|
),
|
|
);
|
|
|
|
// display media attachments
|
|
for (var file in files) {
|
|
c.add(Column(
|
|
children: [
|
|
FileUploadDisplay(file: file),
|
|
TextField(
|
|
onChanged: (value) => file.description = value,
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
minLines: 1,
|
|
maxLines: null,
|
|
decoration: InputDecoration(
|
|
hintText: "file-description".i18n(),
|
|
),
|
|
),
|
|
Wrap(
|
|
children: [
|
|
TextButton.icon(
|
|
onPressed: () {
|
|
moveFile(file, up: false);
|
|
},
|
|
icon: const Icon(Icons.move_down),
|
|
label: Text("move-down".i18n()),
|
|
),
|
|
TextButton.icon(
|
|
onPressed: () => setState(() {
|
|
files.remove(file);
|
|
}),
|
|
icon: const Icon(Icons.delete),
|
|
label: Text(
|
|
"remove-attached-file".i18n(),
|
|
),
|
|
),
|
|
TextButton.icon(
|
|
onPressed: () {
|
|
moveFile(file);
|
|
},
|
|
icon: const Icon(Icons.move_up),
|
|
label: Text("move-up".i18n()),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
));
|
|
}
|
|
|
|
return BackdropFilter(
|
|
filter:
|
|
ImageFilter.blur(sigmaX: 10, sigmaY: 10, tileMode: TileMode.mirror),
|
|
child: SimpleDialog(
|
|
titlePadding: const EdgeInsets.all(0),
|
|
alignment: Alignment.center,
|
|
contentPadding: themes.defaultMargins,
|
|
title: Column(
|
|
children: [
|
|
if (status != null) SelectableText(status!),
|
|
if (sending) const LinearProgressIndicator(),
|
|
],
|
|
),
|
|
children: [
|
|
SingleChildScrollView(
|
|
child: Container(
|
|
width: (MediaQuery.of(context).size.width *
|
|
global.settings!.postWidth) -
|
|
56,
|
|
constraints: global.getConstraints(context),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: c,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class FileUploadDisplay extends StatelessWidget {
|
|
const FileUploadDisplay({super.key, required this.file});
|
|
final FileUpload file;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Divider(
|
|
color: Theme.of(context).hintColor,
|
|
),
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.attachment, size: 32),
|
|
Expanded(
|
|
child: SelectableText(file.path,
|
|
style: Theme.of(context).textTheme.displaySmall),
|
|
),
|
|
],
|
|
),
|
|
if (lookupMimeType(file.path)!.startsWith("image/"))
|
|
Image.asset(
|
|
file.path,
|
|
fit: BoxFit.fitWidth,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|