import 'package:flutter/material.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'; 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 = ""; // 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(); } 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; print(newIndex); 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) { print(path); final FileUpload f = await FileUpload.fromPath(path, ""); setState(() { files.add(f); }); } } } }, icon: const Icon(Icons.attachment), label: Text( "${"add-file".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 { final model = MakePostModel( spoilerText: spoilerText.trim(), identity: accountid, status: text, visibility: visibility, inReplyToId: widget.inReplyTo == null ? null : identitiesAvailable[accountid]!, ); final result = await model.sendPost(); if (result == 200) { Navigator.of(context).pop(); } }, icon: const Icon(Icons.send), ), ]; c.addAll([ 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; })), ), SizedBox( height: MediaQuery.of(context).size.height * 0.6, child: TextFormField( 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 SimpleDialog( alignment: Alignment.center, contentPadding: const EdgeInsets.all(24), title: SelectableText( textAlign: TextAlign.center, widget.inReplyTo == null ? "make-post".i18n() : "make-reply".i18n(), style: Theme.of(context).textTheme.displayMedium), 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} (${(file.data.length / 1024).toStringAsFixed(2)}kb)", style: Theme.of(context).textTheme.displaySmall), ), ], ), if (lookupMimeType(file.path)!.startsWith("image/")) Image.asset( file.path, fit: BoxFit.fitWidth, ), ], ); } }