diff --git a/lib/business_logic/instance/instance.dart b/lib/business_logic/instance/instance.dart index 2b4796b..4fbee9d 100644 --- a/lib/business_logic/instance/instance.dart +++ b/lib/business_logic/instance/instance.dart @@ -29,11 +29,13 @@ class InstanceConfiguration { class StatusConfiguration { final int maxChars; + final int maxMediaAttachments; - StatusConfiguration(this.maxChars); + StatusConfiguration(this.maxChars, this.maxMediaAttachments); static StatusConfiguration fromJson(Map json) { return StatusConfiguration( json["max_characters"], + json["max_media_attachments"], ); } } diff --git a/lib/dialogues/makepost.dart b/lib/dialogues/makepost.dart index 2f74bfe..fa3cee8 100644 --- a/lib/dialogues/makepost.dart +++ b/lib/dialogues/makepost.dart @@ -1,11 +1,14 @@ 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); @@ -18,26 +21,57 @@ class MakePost extends StatefulWidget { class _MakePostState extends State { String replyAts = ""; String accountid = global.settings!.activeIdentity; - int? maxLength; + 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(() { - maxLength = null; + 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) { + if (info.keys.first == 200 && mounted) { setState(() { - maxLength = info.values.first!.configuration.statusconfig.maxChars; + instanceInfo = info.values.first!; }); } } @@ -100,6 +134,7 @@ class _MakePostState extends State { @override Widget build(BuildContext context) { + // make list of all different widgets to be displayed List c = []; if (widget.inReplyTo != null) { c.add( @@ -140,9 +175,6 @@ class _MakePostState extends State { ))); } List actionButtons = [ - maxLength == null - ? const CircularProgressIndicator() - : SelectableText((maxLength! - text.length).toString()), DropdownButtonHideUnderline( child: DropdownButton( alignment: Alignment.center, @@ -156,6 +188,31 @@ class _MakePostState extends State { }, ), ), + // 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, @@ -173,19 +230,24 @@ class _MakePostState extends State { "send-post".i18n(), ), // send the post!!! - onPressed: () async { - final model = MakePostModel( - spoilerText: spoilerText.trim(), - identity: accountid, - status: text, - visibility: visibility, - inReplyToId: widget.inReplyTo == null - ? null - : identitiesAvailable[accountid]!, - ); - model.sendPost(); - Navigator.of(context).pop(); - }, + 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), ), ]; @@ -194,6 +256,12 @@ class _MakePostState extends State { 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, @@ -221,6 +289,10 @@ class _MakePostState extends State { const SizedBox( height: 24, ), + ]); + + // these are the action buttons + c.add( Wrap( runSpacing: 8, spacing: 24, @@ -229,7 +301,52 @@ class _MakePostState extends State { 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, @@ -244,10 +361,7 @@ class _MakePostState extends State { width: (MediaQuery.of(context).size.width * global.settings!.postWidth) - 56, - constraints: BoxConstraints( - maxWidth: global.settings!.maxPostWidth, - minWidth: 375, - ), + constraints: global.getConstraints(context), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -259,3 +373,36 @@ class _MakePostState extends State { ); } } + +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, + ), + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index e95af36..d9a7a00 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -99,6 +99,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.1.4" + file_picker: + dependency: "direct main" + description: + name: file_picker + url: "https://pub.dartlang.org" + source: hosted + version: "5.2.0" flutter: dependency: "direct main" description: flutter @@ -122,7 +129,14 @@ packages: name: flutter_markdown url: "https://pub.dartlang.org" source: hosted - version: "0.6.10+5" + version: "0.6.10+6" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.7" flutter_test: dependency: "direct dev" description: flutter @@ -217,6 +231,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + mime: + dependency: "direct main" + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" path: dependency: transitive description: @@ -244,7 +265,7 @@ packages: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.1.3" platform: dependency: transitive description: @@ -258,7 +279,7 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.1.3" process: dependency: transitive description: @@ -279,7 +300,7 @@ packages: name: shared_preferences_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.12" + version: "2.0.13" shared_preferences_ios: dependency: transitive description: @@ -328,7 +349,7 @@ packages: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "1.3.2" + version: "1.4.0" sky_engine: dependency: transitive description: flutter @@ -410,7 +431,7 @@ packages: name: url_launcher_android url: "https://pub.dartlang.org" source: hosted - version: "6.0.17" + version: "6.0.19" url_launcher_ios: dependency: transitive description: @@ -480,7 +501,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.7.0" + version: "3.0.0" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 45daccb..23c23a6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,6 +51,8 @@ dependencies: url_strategy: ^0.2.0 universal_html: ^2.0.8 universal_io: ^2.0.4 + file_picker: ^5.2.0 + mime: ^1.0.2 dev_dependencies: flutter_test: