comparison libervia/desktop_kivy/core/cagou_main.py @ 514:d78728d7fd6a

plugin wid calls, core: implements WebRTC DataChannel file transfer: - Add a new "file" icon in call UI to send a file via WebRTC. - Handle new preflight mechanism, and WebRTC file transfer. - Native file chooser handling has been moved to new `core.file_chooser` module, and now supports "save" and "dir" modes (based on `plyer`). rel 442
author Goffi <goffi@goffi.org>
date Sat, 06 Apr 2024 13:37:27 +0200
parents bbef1a413515
children 2ff26b4273df
comparison
equal deleted inserted replaced
513:0fdf3e59aaad 514:d78728d7fd6a
20 import asyncio 20 import asyncio
21 import os.path 21 import os.path
22 import glob 22 import glob
23 import sys 23 import sys
24 from pathlib import Path 24 from pathlib import Path
25 from typing import Callable
25 from urllib import parse as urlparse 26 from urllib import parse as urlparse
26 from functools import partial 27 from functools import partial
27 from libervia.backend.core.i18n import _ 28 from libervia.backend.core.i18n import _
29
30 # `do_hack` msut be run before any Kivy import!
28 from . import kivy_hack 31 from . import kivy_hack
29 kivy_hack.do_hack() 32 kivy_hack.do_hack()
30 from .constants import Const as C 33 from .constants import Const as C
31 from libervia.backend.core import log as logging 34 from libervia.backend.core import log as logging
32 from libervia.backend.core import exceptions 35 from libervia.backend.core import exceptions
36 from libervia.desktop_kivy.core.file_chooser import FileChooser
33 from libervia.frontends.quick_frontend.quick_app import QuickApp 37 from libervia.frontends.quick_frontend.quick_app import QuickApp
34 from libervia.frontends.quick_frontend import quick_widgets 38 from libervia.frontends.quick_frontend import quick_widgets
35 from libervia.frontends.quick_frontend import quick_chat 39 from libervia.frontends.quick_frontend import quick_chat
36 from libervia.frontends.quick_frontend import quick_utils 40 from libervia.frontends.quick_frontend import quick_utils
37 from libervia.frontends.tools import jid 41 from libervia.frontends.tools import aio, jid
38 from libervia.backend.tools import utils as libervia_utils 42 from libervia.backend.tools import utils as libervia_utils
39 from libervia.backend.tools import config 43 from libervia.backend.tools import config
40 from libervia.backend.tools.common import data_format 44 from libervia.backend.tools.common import data_format
41 from libervia.backend.tools.common import dynamic_import 45 from libervia.backend.tools.common import dynamic_import
42 from libervia.backend.tools.common import files_utils 46 from libervia.backend.tools.common import files_utils
993 997
994 return self.switch_widget(None, wid) 998 return self.switch_widget(None, wid)
995 999
996 ## bridge handlers ## 1000 ## bridge handlers ##
997 1001
1002 async def _on_webrtc_file(
1003 self,
1004 action_data: dict,
1005 action_id: str|None,
1006 profile: str
1007 ) -> None:
1008 assert action_id is not None
1009 try:
1010 xmlui_data = action_data["xmlui"]
1011 except KeyError:
1012 raise exceptions.InternalError("Missing XMLUI in file action.")
1013
1014 try:
1015 from_jid = jid.JID(action_data["from_jid"])
1016 except KeyError:
1017 raise exceptions.InternalError(
1018 f"Missing 'from_jid' key: {action_data!r}"
1019 )
1020 from libervia.frontends.tools.webrtc_file import WebRTCFileReceiver
1021 confirm_msg = WebRTCFileReceiver.format_confirm_msg(action_data, from_jid)
1022
1023 file_accepted = action_data.get("file_accepted", False)
1024 if file_accepted:
1025 accepted = True
1026 else:
1027 accepted = await self.ask_confirmation(confirm_msg, _("File Request"))
1028
1029 xmlui_data = {"answer": C.bool_const(accepted)}
1030 if accepted:
1031 file_data = action_data.get("file_data") or {}
1032 file_name = file_data.get("name", "received_file")
1033 dest_path_s = await FileChooser.a_open(
1034 mode="save",
1035 title=_("Please select the destination for file {file_name!r}.").format(
1036 file_name=file_name
1037 ),
1038 # FIXME: It doesn't seem to be a way to specify destination file name,
1039 # ``path`` doesn't work this way, at least on Linux/KDE.
1040 default_path=f"./{file_name}"
1041 )
1042 if dest_path_s is None:
1043 accepted = False
1044 else:
1045 dest_path = Path(dest_path_s)
1046 try:
1047 session_id = action_data["session_id"]
1048 except KeyError:
1049 raise exceptions.InternalError("'session_id' is missing.")
1050 file_receiver = WebRTCFileReceiver(
1051 self.a_bridge,
1052 profile,
1053 on_close_cb=lambda: self.add_note(
1054 _("File Received"),
1055 _('The file "{file_name}" has been successfuly received.').format(
1056 file_name = file_data.get("name", "")
1057 )
1058 )
1059 )
1060 await file_receiver.receive_file_webrtc(
1061 from_jid,
1062 session_id,
1063 dest_path,
1064 file_data
1065 )
1066
1067 await self.a_bridge.action_launch(
1068 action_id, data_format.serialise(xmlui_data), profile_key=profile
1069 )
1070
1071 def action_manager(
1072 self,
1073 action_data: dict,
1074 callback: Callable|None = None,
1075 ui_show_cb: Callable|None = None,
1076 user_action: bool = True,
1077 action_id: str|None = None,
1078 progress_cb: Callable|None = None,
1079 progress_eb: Callable|None = None,
1080 profile: str = C.PROF_KEY_NONE
1081 ) -> None:
1082 if (
1083 action_data.get("type") == C.META_TYPE_FILE
1084 and action_data.get("webrtc", False)
1085 ):
1086 aio.run_async(self._on_webrtc_file(action_data, action_id, profile))
1087 else:
1088 super().action_manager(
1089 action_data, callback, ui_show_cb, user_action, action_id, progress_cb,
1090 progress_eb, profile
1091 )
1092
998 def otr_state_handler(self, state, dest_jid, profile): 1093 def otr_state_handler(self, state, dest_jid, profile):
999 """OTR state has changed for on destinee""" 1094 """OTR state has changed for on destinee"""
1000 # XXX: this method could be in QuickApp but it's here as 1095 # XXX: this method could be in QuickApp but it's here as
1001 # it's only used by LiberviaDesktopKivy so far 1096 # it's only used by LiberviaDesktopKivy so far
1002 dest_jid = jid.JID(dest_jid) 1097 dest_jid = jid.JID(dest_jid)
1099 def callback(): 1194 def callback():
1100 self.close_ui() 1195 self.close_ui()
1101 cb(*args, **kwargs) 1196 cb(*args, **kwargs)
1102 return callback 1197 return callback
1103 1198
1104 def show_dialog(self, message, title, type="info", answer_cb=None, answer_data=None): 1199 def show_dialog(
1200 self,
1201 message: str,
1202 title: str,
1203 type: str = "info",
1204 answer_cb: Callable|None = None,
1205 answer_data: dict|None = None
1206 ):
1207 """Show a dialog to the user.
1208
1209 @param message: The main text of the dialog.
1210 @param title: Title of the dialog.
1211 @param type: Type of dialog (info, warning, error, yes/no).
1212 @param answer_cb: A callback that will be called when the user answers to the dialog.
1213 You can pass an asynchronous function as well.
1214 @param answer_data: Additional data for the dialog.
1215
1216 """
1105 if type in ('info', 'warning', 'error'): 1217 if type in ('info', 'warning', 'error'):
1106 self.add_note(title, message, type) 1218 self.add_note(title, message, type)
1107 elif type == "yes/no": 1219 elif type == "yes/no":
1108 wid = dialog.ConfirmDialog(title=title, message=message, 1220 wid = dialog.ConfirmDialog(title=title, message=message,
1109 yes_cb=self._dialog_cb(answer_cb, 1221 yes_cb=self._dialog_cb(answer_cb,
1115 ) 1227 )
1116 self.add_notif_widget(wid) 1228 self.add_notif_widget(wid)
1117 return wid 1229 return wid
1118 else: 1230 else:
1119 log.warning(_("unknown dialog type: {dialog_type}").format(dialog_type=type)) 1231 log.warning(_("unknown dialog type: {dialog_type}").format(dialog_type=type))
1232
1233 async def a_show_dialog(
1234 self,
1235 message: str,
1236 title: str,
1237 type: str = "info",
1238 answer_data: dict|None = None
1239 ) -> bool|None:
1240 """Shows a dialog asynchronously and returns the user's response for 'yes/no' dialogs.
1241
1242 This method wraps the synchronous ``show_dialog`` method to work in an
1243 asynchronous context.
1244 It is specifically useful for 'yes/no' type dialogs, returning True for 'yes' and
1245 False for 'no'. For other types, it returns None immediately after showing the
1246 dialog.
1247
1248 See [show_dialog] for params.
1249 @return: True if the user clicked 'yes', False if 'no', and None for other dialog types.
1250 """
1251 future = asyncio.Future()
1252
1253 def answer_cb(answer: bool, data: dict):
1254 if not future.done():
1255 future.set_result(answer)
1256
1257 if type == "yes/no":
1258 self.show_dialog(message, title, type, answer_cb, answer_data)
1259 return await future
1260 else:
1261 self.show_dialog(message, title, type)
1262 return None
1263
1264 async def ask_confirmation(
1265 self,
1266 message: str,
1267 title: str,
1268 answer_data: dict|None = None
1269 ) -> bool:
1270 ret = await self.a_show_dialog(message, title, "yes/no", answer_data)
1271 assert ret is bool
1272 return ret
1120 1273
1121 def share(self, media_type, data): 1274 def share(self, media_type, data):
1122 share_wid = ShareWidget(media_type=media_type, data=data) 1275 share_wid = ShareWidget(media_type=media_type, data=data)
1123 try: 1276 try:
1124 self.show_extra_ui(share_wid) 1277 self.show_extra_ui(share_wid)