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 createState() => _MakePostState(); } class _MakePostState extends State { 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 files = []; // stores all identities and if available the post id you are replying to Map 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 c = []; if (widget.inReplyTo != null) { c.add( Post( model: widget.inReplyTo!, reblogVisible: false, hideActionBar: true, ), ); } List> 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> 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 actionButtons = [ DropdownButtonHideUnderline( child: DropdownButton( 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( 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 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/") ?? false) Image.asset( file.path, fit: BoxFit.fitWidth, ), ], ); } }