Mercurial > libervia-desktop-kivy
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) |