parent
27137c488d
commit
36f61a62d8
12 changed files with 154 additions and 15 deletions
|
|
@ -51,6 +51,16 @@
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
|
<!-- Needed by pasteboard, for pasting files from the clipboard -->
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.provider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/provider_paths" />
|
||||||
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
<!-- Required to query activities that can process text, see:
|
<!-- Required to query activities that can process text, see:
|
||||||
https://developer.android.com/training/package-visibility and
|
https://developer.android.com/training/package-visibility and
|
||||||
|
|
|
||||||
6
android/app/src/main/res/xml/provider_paths.xml
Normal file
6
android/app/src/main/res/xml/provider_paths.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<external-path
|
||||||
|
name="external_files"
|
||||||
|
path="." />
|
||||||
|
</paths>
|
||||||
|
|
@ -18,6 +18,10 @@ import "package:nexus/controllers/power_level.dart";
|
||||||
import "package:nexus/models/content/message.dart";
|
import "package:nexus/models/content/message.dart";
|
||||||
import "package:nexus/models/relation_type.dart";
|
import "package:nexus/models/relation_type.dart";
|
||||||
import "package:nexus/widgets/composer/overlays/tagger_overlay.dart";
|
import "package:nexus/widgets/composer/overlays/tagger_overlay.dart";
|
||||||
|
import "package:nexus/widgets/composer/paste_files_action.dart";
|
||||||
|
import "package:pasteboard/pasteboard.dart";
|
||||||
|
import "package:path/path.dart";
|
||||||
|
import "package:path_provider/path_provider.dart";
|
||||||
import "package:nexus/widgets/composer/relation_preview.dart";
|
import "package:nexus/widgets/composer/relation_preview.dart";
|
||||||
import "package:nexus/main.dart";
|
import "package:nexus/main.dart";
|
||||||
|
|
||||||
|
|
@ -100,6 +104,46 @@ class const Composer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attaches [bytes] as a file called [filename], e.g. for pasted images.
|
||||||
|
Future<void> attachBytes(Uint8List bytes, String filename) async {
|
||||||
|
// A new directory each time, so files with the same name don't clash
|
||||||
|
final directory = await (await getTemporaryDirectory()).createTemp(
|
||||||
|
"attachment",
|
||||||
|
);
|
||||||
|
final file = await File(join(directory.path, filename))
|
||||||
|
.writeAsBytes(bytes);
|
||||||
|
|
||||||
|
await ref
|
||||||
|
.read(AttachmentController.provider(roomId).notifier)
|
||||||
|
.add(.new(file.path));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attaches the first file on the clipboard, or the image on it if there
|
||||||
|
/// are no files. Returns whether there was either, so that text can be
|
||||||
|
/// pasted otherwise.
|
||||||
|
Future<bool> pasteFiles() async {
|
||||||
|
// Only one attachment is supported at a time
|
||||||
|
if (attachment != null) return false;
|
||||||
|
|
||||||
|
// Failing to read the clipboard counts as it being empty, so that text
|
||||||
|
// can still be pasted
|
||||||
|
final files = await Pasteboard.files().onError((_, _) => const []);
|
||||||
|
if (files.firstOrNull case final path?) {
|
||||||
|
ref
|
||||||
|
.read(AttachmentController.provider(roomId).notifier)
|
||||||
|
.add(XFile(path))
|
||||||
|
.onError(showError);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await Pasteboard.image.onError((_, _) => null) case final image?) {
|
||||||
|
attachBytes(image, "image.png").onError(showError);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void send() {
|
void send() {
|
||||||
if (controller.value.text.isEmpty && attachment == null ||
|
if (controller.value.text.isEmpty && attachment == null ||
|
||||||
attachment != null && attachment.$2 == null) {
|
attachment != null && attachment.$2 == null) {
|
||||||
|
|
@ -257,6 +301,13 @@ class const Composer(
|
||||||
|
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
},
|
},
|
||||||
|
// Overrides the keyboard shortcut for pasting
|
||||||
|
child: Actions(
|
||||||
|
actions: {
|
||||||
|
PasteTextIntent: PasteFilesAction(
|
||||||
|
pasteFiles,
|
||||||
|
),
|
||||||
|
},
|
||||||
child: TextField(
|
child: TextField(
|
||||||
maxLines: 12,
|
maxLines: 12,
|
||||||
minLines: 1,
|
minLines: 1,
|
||||||
|
|
@ -272,6 +323,43 @@ class const Composer(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
key: key,
|
key: key,
|
||||||
focusNode: node,
|
focusNode: node,
|
||||||
|
// The toolbar's paste button doesn't use the shortcut's action, so replace it too
|
||||||
|
contextMenuBuilder: (_, editableText) =>
|
||||||
|
AdaptiveTextSelectionToolbar.buttonItems(
|
||||||
|
anchors:
|
||||||
|
editableText.contextMenuAnchors,
|
||||||
|
buttonItems: editableText
|
||||||
|
.contextMenuButtonItems
|
||||||
|
.map(
|
||||||
|
(item) => item.type == .paste
|
||||||
|
? item.copyWith(
|
||||||
|
onPressed: () async {
|
||||||
|
editableText
|
||||||
|
.hideToolbar();
|
||||||
|
if (!await pasteFiles()) {
|
||||||
|
item.onPressed
|
||||||
|
?.call();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: item,
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
// Images pasted from Android keyboards' clipboards are inserted as content
|
||||||
|
contentInsertionConfiguration: .new(
|
||||||
|
onContentInserted: (content) {
|
||||||
|
if (attachment != null) return;
|
||||||
|
|
||||||
|
if (content.data case final data?) {
|
||||||
|
attachBytes(
|
||||||
|
data,
|
||||||
|
"image.${content.mimeType.split("/").last}",
|
||||||
|
).onError(showError);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
12
lib/widgets/composer/paste_files_action.dart
Normal file
12
lib/widgets/composer/paste_files_action.dart
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import "package:material_ui/material_ui.dart";
|
||||||
|
|
||||||
|
/// Overrides a text field's paste, to paste files and images with
|
||||||
|
/// [pasteFiles], falling back to pasting text if it returns false.
|
||||||
|
class PasteFilesAction(final Future<bool> Function() pasteFiles)
|
||||||
|
extends Action<PasteTextIntent> {
|
||||||
|
@override
|
||||||
|
Future<void> invoke(PasteTextIntent intent) async {
|
||||||
|
// callingAction is the text field's own paste action, which this overrides
|
||||||
|
if (!await pasteFiles()) callingAction?.invoke(intent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
#include <file_selector_linux/file_selector_plugin.h>
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||||
#include <media_kit_video/media_kit_video_plugin.h>
|
#include <media_kit_video/media_kit_video_plugin.h>
|
||||||
|
#include <pasteboard/pasteboard_plugin.h>
|
||||||
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
|
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
#include <webcrypto/webcrypto_plugin.h>
|
#include <webcrypto/webcrypto_plugin.h>
|
||||||
|
|
@ -32,6 +33,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
g_autoptr(FlPluginRegistrar) media_kit_video_registrar =
|
g_autoptr(FlPluginRegistrar) media_kit_video_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin");
|
||||||
media_kit_video_plugin_register_with_registrar(media_kit_video_registrar);
|
media_kit_video_plugin_register_with_registrar(media_kit_video_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) pasteboard_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin");
|
||||||
|
pasteboard_plugin_register_with_registrar(pasteboard_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
|
g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
|
||||||
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
|
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
file_selector_linux
|
file_selector_linux
|
||||||
media_kit_libs_linux
|
media_kit_libs_linux
|
||||||
media_kit_video
|
media_kit_video
|
||||||
|
pasteboard
|
||||||
screen_retriever_linux
|
screen_retriever_linux
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
webcrypto
|
webcrypto
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,10 @@ let
|
||||||
"36.0.0"
|
"36.0.0"
|
||||||
];
|
];
|
||||||
cmakeVersions = [ "3.22.1" ];
|
cmakeVersions = [ "3.22.1" ];
|
||||||
platformVersions = [ "36" ];
|
platformVersions = [
|
||||||
|
"34"
|
||||||
|
"36"
|
||||||
|
];
|
||||||
abiVersions = [
|
abiVersions = [
|
||||||
"armeabi-v7a"
|
"armeabi-v7a"
|
||||||
"arm64-v8a"
|
"arm64-v8a"
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import flutter_local_notifications
|
||||||
import media_kit_libs_macos_video
|
import media_kit_libs_macos_video
|
||||||
import media_kit_video
|
import media_kit_video
|
||||||
import package_info_plus
|
import package_info_plus
|
||||||
|
import pasteboard
|
||||||
import screen_retriever_macos
|
import screen_retriever_macos
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
|
@ -27,6 +28,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
|
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
|
||||||
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
|
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
|
||||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
|
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
|
||||||
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
|
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
|
|
|
||||||
|
|
@ -1047,6 +1047,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.0"
|
version: "4.1.0"
|
||||||
|
pasteboard:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: pasteboard
|
||||||
|
sha256: fedbe8da188d2f713aa8b01260737342e6e1087534a3ab26e1a719f8d3e8f32f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.0"
|
||||||
path:
|
path:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,7 @@ dependencies:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Henry-Hiles/xdg_desktop_portal.dart
|
url: https://github.com/Henry-Hiles/xdg_desktop_portal.dart
|
||||||
ref: quad/nexus
|
ref: quad/nexus
|
||||||
|
pasteboard: 0.5.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: 2.16.2
|
build_runner: 2.16.2
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
#include <file_selector_windows/file_selector_windows.h>
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
|
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
|
||||||
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
||||||
|
#include <pasteboard/pasteboard_plugin.h>
|
||||||
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
|
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
#include <webcrypto/webcrypto_plugin.h>
|
#include <webcrypto/webcrypto_plugin.h>
|
||||||
|
|
@ -27,6 +28,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi"));
|
registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi"));
|
||||||
MediaKitVideoPluginCApiRegisterWithRegistrar(
|
MediaKitVideoPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
|
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
|
||||||
|
PasteboardPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("PasteboardPlugin"));
|
||||||
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
|
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
|
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
file_selector_windows
|
file_selector_windows
|
||||||
media_kit_libs_windows_video
|
media_kit_libs_windows_video
|
||||||
media_kit_video
|
media_kit_video
|
||||||
|
pasteboard
|
||||||
screen_retriever_windows
|
screen_retriever_windows
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
webcrypto
|
webcrypto
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue