From 7f891d68e0edeb6507dca23ca5dfa6af937f81ac Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Sat, 16 Mar 2024 22:59:19 +0100 Subject: [PATCH] Added image and file attachment sending --- sbapp/main.py | 310 ++++++++++++++++++++++++++++++++++------- sbapp/sideband/core.py | 7 +- 2 files changed, 269 insertions(+), 48 deletions(-) diff --git a/sbapp/main.py b/sbapp/main.py index 7735a0c..f2a109c 100644 --- a/sbapp/main.py +++ b/sbapp/main.py @@ -24,6 +24,8 @@ import RNS.vendor.umsgpack as msgpack if not args.daemon: import plyer from kivy.logger import Logger, LOG_LEVELS + from PIL import Image as PilImage + import io # Squelch excessive method signature logging class redirect_log(): @@ -67,7 +69,7 @@ if args.daemon: StringProperty = DaemonElement; BaseButton = DaemonElement; MDIconButton = DaemonElement; MDFileManager = DaemonElement; toast = DaemonElement; dp = DaemonElement; sp = DaemonElement; MDRectangleFlatButton = DaemonElement; MDDialog = DaemonElement; colors = DaemonElement; Telemeter = DaemonElement; CustomMapMarker = DaemonElement; MBTilesMapSource = DaemonElement; - MapSource = DaemonElement; webbrowser = DaemonElement; Conversations = DaemonElement; MsgSync = DaemonElement; + MapSource = DaemonElement; webbrowser = DaemonElement; Conversations = DaemonElement; MsgSync = DaemonElement; IconLeftWidget = DaemonElement; NewConv = DaemonElement; Telemetry = DaemonElement; ObjectDetails = DaemonElement; Announces = DaemonElement; Messages = DaemonElement; ts_format = DaemonElement; messages_screen_kv = DaemonElement; plyer = DaemonElement; multilingual_markup = DaemonElement; ContentNavigationDrawer = DaemonElement; DrawerList = DaemonElement; IconListItem = DaemonElement; escape_markup = DaemonElement; @@ -83,11 +85,10 @@ else: from kivy.effects.scroll import ScrollEffect from kivy.uix.screenmanager import ScreenManager from kivy.uix.screenmanager import FadeTransition, NoTransition, SlideTransition - from kivymd.uix.list import OneLineIconListItem + from kivymd.uix.list import OneLineIconListItem, IconLeftWidget from kivy.properties import StringProperty from kivymd.uix.button import BaseButton, MDIconButton from kivymd.uix.filemanager import MDFileManager - from kivymd.toast import toast from kivy.metrics import dp, sp from kivymd.uix.button import MDRectangleFlatButton from kivymd.uix.dialog import MDDialog @@ -112,6 +113,7 @@ else: from ui.messages import Messages, ts_format, messages_screen_kv from ui.helpers import ContentNavigationDrawer, DrawerList, IconListItem from ui.helpers import multilingual_markup + from kivymd.toast import toast from jnius import cast from jnius import autoclass @@ -134,6 +136,10 @@ else: from .ui.helpers import ContentNavigationDrawer, DrawerList, IconListItem from .ui.helpers import multilingual_markup + class toast: + def __init__(self, *kwargs): + pass + from kivy.config import Config Config.set('input', 'mouse', 'mouse,disable_multitouch') @@ -209,6 +215,9 @@ class SidebandApp(MDApp): self.final_load_completed = False self.service_last_available = 0 + self.attach_path = None + self.attach_type = None + Window.softinput_mode = "below_target" self.icon = self.sideband.asset_dir+"/icon.png" self.notification_icon = self.sideband.asset_dir+"/notification_icon.png" @@ -1155,6 +1164,67 @@ class SidebandApp(MDApp): else: msg_content = self.messages_view.ids.message_text.text context_dest = self.messages_view.ids.messages_scrollview.active_conversation + + attachment = None + image = None + if not self.outbound_mode_command and not self.outbound_mode_paper: + if self.attach_type != None and self.attach_path != None: + try: + RNS.log("Processing "+str(self.attach_type)+" attachment \""+str(self.attach_path)+"\"", RNS.LOG_DEBUG) + fbn = os.path.basename(self.attach_path) + + if self.attach_type == "file": + with open(self.attach_path, "rb") as af: + attachment = [fbn, af.read()] + + elif self.attach_type == "lbimg": + max_size = 320, 320 + with PilImage.open(self.attach_path) as im: + im.thumbnail(max_size) + buf = io.BytesIO() + im.save(buf, format="webp", quality=22) + image = ["webp", buf.getvalue()] + + elif self.attach_type == "defimg": + max_size = 640, 640 + with PilImage.open(self.attach_path) as im: + im.thumbnail(max_size) + buf = io.BytesIO() + im.save(buf, format="webp", quality=66) + image = ["webp", buf.getvalue()] + + elif self.attach_type == "hqimg": + max_size = 1280, 1280 + with PilImage.open(self.attach_path) as im: + im.thumbnail(max_size) + buf = io.BytesIO() + im.save(buf, format="webp", quality=75) + image = ["webp", buf.getvalue()] + + except Exception as e: + self.messages_view.send_error_dialog = MDDialog( + title="Attachment Error", + text="An error occurred while processing the attachment:\n\n[i]"+str(e)+"[/i]", + buttons=[ + MDRectangleFlatButton( + text="OK", + font_size=dp(18), + on_release=self.messages_view.close_send_error_dialog + ) + ], + ) + self.messages_view.send_error_dialog.open() + self.attach_type = None + self.attach_path = None + self.update_message_widgets() + RNS.log("Trace:") + RNS.trace_exception(e) + return + + self.attach_type = None + self.attach_path = None + self.update_message_widgets() + if self.outbound_mode_command: if self.sideband.send_command(msg_content, context_dest, False): self.messages_view.ids.message_text.text = "" @@ -1180,7 +1250,7 @@ class SidebandApp(MDApp): self.messages_view.ids.messages_scrollview.scroll_y = 0 self.jobs(0) - elif self.sideband.send_message(msg_content, context_dest, self.outbound_mode_propagation): + elif self.sideband.send_message(msg_content, context_dest, self.outbound_mode_propagation, attachment = attachment, image = image): self.messages_view.ids.message_text.text = "" self.messages_view.ids.messages_scrollview.scroll_y = 0 self.jobs(0) @@ -1237,12 +1307,145 @@ class SidebandApp(MDApp): self.update_message_widgets() + def message_fm_got_path(self, path): + self.message_fm_exited() + fbn = os.path.basename(path) + try: + tf = open(path, "rb") + tf.close() + self.attach_path = path + + if RNS.vendor.platformutils.is_android(): + toast("Attached \""+str(fbn)+"\"") + else: + ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18)) + ate_dialog = MDDialog( + title="File Attached", + text="The file \""+str(fbn)+"\" was attached, and will be included with the next message sent.", + buttons=[ ok_button ], + ) + ok_button.bind(on_release=ate_dialog.dismiss) + ate_dialog.open() + + except Exception as e: + RNS.log(f"Error while attaching \"{fbn}\": "+str(e), RNS.LOG_ERROR) + if RNS.vendor.platformutils.get_platform() == "android": + toast("Could not attach \""+str(fbn)+"\"") + else: + ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18)) + ate_dialog = MDDialog( + title="Attachment Error", + text="The specified file could not be attached:\n\n[i]"+str(e)+"[/i]", + buttons=[ ok_button ], + ) + ok_button.bind(on_release=ate_dialog.dismiss) + ate_dialog.open() + + self.update_message_widgets() + + + def message_fm_exited(self, *args): + self.manager_open = False + self.file_manager.close() + + def message_select_file_action(self, sender=None): + perm_ok = False + if RNS.vendor.platformutils.is_android(): + perm_ok = self.check_storage_permission() + path = primary_external_storage_path() + + else: + perm_ok = True + path = os.path.expanduser("~") + + + if perm_ok and path != None: + try: + self.file_manager = MDFileManager( + exit_manager=self.message_fm_exited, + select_path=self.message_fm_got_path, + ) + # self.file_manager.ext = ["*"] + self.file_manager.show(path) + + except Exception as e: + self.sideband.config["map_storage_path"] = None + self.sideband.save_configuration() + toast("Error reading directory, check permissions!") + + else: + self.sideband.config["map_storage_path"] = None + self.sideband.save_configuration() + toast("No file access, check permissions!") + + def message_attach_action(self, attach_type=None): + self.attach_path = None + self.attach_type = attach_type + self.message_select_file_action() + def message_attachment_action(self, sender): - pass + if self.attach_path == None: + attach_dialog = None + def a_img_lb(sender): + attach_dialog.dismiss() + self.message_attach_action(attach_type="lbimg") + def a_img_def(sender): + attach_dialog.dismiss() + self.message_attach_action(attach_type="defimg") + def a_img_hq(sender): + attach_dialog.dismiss() + self.message_attach_action(attach_type="hqimg") + def a_file(sender): + attach_dialog.dismiss() + self.message_attach_action(attach_type="file") + + ss = int(dp(18)) + cancel_button = MDRectangleFlatButton(text="Cancel", font_size=dp(18)) + attach_dialog = MDDialog( + title="Add Attachment", + type="simple", + text="Select the type of attachment you want to send with this message\n", + items=[ + DialogItem(IconLeftWidget(icon="message-image-outline"), text="[size="+str(ss)+"]Low-bandwidth Image[/size]", on_release=a_img_lb), + DialogItem(IconLeftWidget(icon="file-image"), text="[size="+str(ss)+"]Medium Image[/size]", on_release=a_img_def), + DialogItem(IconLeftWidget(icon="image-outline"), text="[size="+str(ss)+"]High-res Image[/size]", on_release=a_img_hq), + DialogItem(IconLeftWidget(icon="file-outline"), text="[size="+str(ss)+"]File Attachment[/size]", on_release=a_file), + ], + buttons=[ cancel_button ], + width_offset=dp(12), + ) + + cancel_button.bind(on_release=attach_dialog.dismiss) + attach_dialog.open() + attach_dialog.update_width() + + else: + self.attach_path = None + self.attach_type = None + self.update_message_widgets() + + if RNS.vendor.platformutils.get_platform() == "android": + toast("Attachment removed") + else: + ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18)) + ate_dialog = MDDialog( + title="Attachment Removed", + text="The attached resource was removed from the message", + buttons=[ ok_button ], + ) + ok_button.bind(on_release=ate_dialog.dismiss) + ate_dialog.open() + def update_message_widgets(self): toolbar_items = self.messages_view.ids.messages_toolbar.ids.right_actions.children mode_item = toolbar_items[1] + attachment_item = toolbar_items[4] + + if self.attach_path != None: + attachment_item.icon = "attachment-check" + else: + attachment_item.icon = "attachment-plus" if self.outbound_mode_paper: mode_item.icon = "qrcode" @@ -3719,7 +3922,17 @@ class SidebandApp(MDApp): toast("Using \""+os.path.basename(path)+"\" as offline map") except Exception as e: RNS.log(f"Error while loading map \"{path}\": "+str(e), RNS.LOG_ERROR) - toast("Could not load map \""+os.path.basename(path)+"\"") + if RNS.vendor.platformutils.get_platform() == "android": + toast("Could not load map \""+os.path.basename(path)+"\"") + else: + ok_button = MDRectangleFlatButton(text="OK",font_size=dp(18)) + map_dialog = MDDialog( + title="Map Error", + text="The specified map file could not be loaded. Make sure the selected file is an MBTiles map in raster format. Vector maps are currently not supported.", + buttons=[ ok_button ], + ) + ok_button.bind(on_release=map_dialog.dismiss) + map_dialog.open() self.sideband.config["map_storage_file"] = None self.sideband.config["map_use_offline"] = False self.sideband.config["map_use_online"] = True @@ -3731,6 +3944,47 @@ class SidebandApp(MDApp): self.file_manager.close() self.map_update_source() + def map_select_file_action(self, sender=None): + perm_ok = False + if self.sideband.config["map_storage_path"] == None: + if RNS.vendor.platformutils.is_android(): + perm_ok = self.check_storage_permission() + + if self.sideband.config["map_storage_external"]: + path = secondary_external_storage_path() + if path == None: path = primary_external_storage_path() + else: + path = primary_external_storage_path() + + else: + perm_ok = True + if self.sideband.config["map_storage_external"]: + path = "/" + else: + path = os.path.expanduser("~") + else: + perm_ok = True + path = self.sideband.config["map_storage_path"] + + if perm_ok and path != None: + try: + self.file_manager = MDFileManager( + exit_manager=self.map_fm_exited, + select_path=self.map_fm_got_path, + ) + self.file_manager.ext = [".mbtiles"] + self.file_manager.show(path) + + except Exception as e: + self.sideband.config["map_storage_path"] = None + self.sideband.save_configuration() + toast("Error reading directory, check permissions!") + + else: + self.sideband.config["map_storage_path"] = None + self.sideband.save_configuration() + toast("No file access, check permissions!") + def map_get_offline_source(self): if self.offline_source != None: return self.offline_source @@ -3819,47 +4073,6 @@ class SidebandApp(MDApp): except Exception as e: RNS.log("Error while switching map layer: "+str(e), RNS.LOG_ERROR) - def map_select_file_action(self, sender=None): - perm_ok = False - if self.sideband.config["map_storage_path"] == None: - if RNS.vendor.platformutils.is_android(): - perm_ok = self.check_storage_permission() - - if self.sideband.config["map_storage_external"]: - path = secondary_external_storage_path() - if path == None: path = primary_external_storage_path() - else: - path = primary_external_storage_path() - - else: - perm_ok = True - if self.sideband.config["map_storage_external"]: - path = "/" - else: - path = os.path.expanduser("~") - else: - perm_ok = True - path = self.sideband.config["map_storage_path"] - - if perm_ok and path != None: - try: - self.file_manager = MDFileManager( - exit_manager=self.map_fm_exited, - select_path=self.map_fm_got_path, - ) - self.file_manager.ext = [".mbtiles"] - self.file_manager.show(path) - - except Exception as e: - self.sideband.config["map_storage_path"] = None - self.sideband.save_configuration() - toast("Error reading directory, check permissions!") - - else: - self.sideband.config["map_storage_path"] = None - self.sideband.save_configuration() - toast("No file access, check permissions!") - map_nav_divisor = 12 map_nav_zoom = 0.25 def map_nav_left(self, sender=None, modifier=1.0): @@ -4519,6 +4732,9 @@ Thank you very much for using Free Communications Systems. class CustomOneLineIconListItem(OneLineIconListItem): icon = StringProperty() +class DialogItem(OneLineIconListItem): + divider = None + class MDMapIconButton(MDIconButton): pass diff --git a/sbapp/sideband/core.py b/sbapp/sideband/core.py index 5325aca..a29f2fa 100644 --- a/sbapp/sideband/core.py +++ b/sbapp/sideband/core.py @@ -3244,7 +3244,7 @@ class SidebandCore(): RNS.log("Error while creating paper message: "+str(e), RNS.LOG_ERROR) return False - def send_message(self, content, destination_hash, propagation, skip_fields=False, no_display=False): + def send_message(self, content, destination_hash, propagation, skip_fields=False, no_display=False, attachment = None, image = None, audio = None): try: if content == "": raise ValueError("Message content cannot be empty") @@ -3263,6 +3263,11 @@ class SidebandCore(): else: fields = self.get_message_fields(destination_hash) + if attachment != None: + fields[LXMF.FIELD_FILE_ATTACHMENTS] = [attachment] + if image != None: + fields[LXMF.FIELD_IMAGE] = image + lxm = LXMF.LXMessage(dest, source, content, title="", desired_method=desired_method, fields = fields) if not no_display: