Mercurial > libervia-desktop-kivy
diff 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 |
line wrap: on
line diff
--- a/libervia/desktop_kivy/core/cagou_main.py Thu Jan 18 23:46:31 2024 +0100 +++ b/libervia/desktop_kivy/core/cagou_main.py Sat Apr 06 13:37:27 2024 +0200 @@ -22,19 +22,23 @@ import glob import sys from pathlib import Path +from typing import Callable from urllib import parse as urlparse from functools import partial from libervia.backend.core.i18n import _ + +# `do_hack` msut be run before any Kivy import! from . import kivy_hack kivy_hack.do_hack() from .constants import Const as C from libervia.backend.core import log as logging from libervia.backend.core import exceptions +from libervia.desktop_kivy.core.file_chooser import FileChooser from libervia.frontends.quick_frontend.quick_app import QuickApp from libervia.frontends.quick_frontend import quick_widgets from libervia.frontends.quick_frontend import quick_chat from libervia.frontends.quick_frontend import quick_utils -from libervia.frontends.tools import jid +from libervia.frontends.tools import aio, jid from libervia.backend.tools import utils as libervia_utils from libervia.backend.tools import config from libervia.backend.tools.common import data_format @@ -995,6 +999,97 @@ ## bridge handlers ## + async def _on_webrtc_file( + self, + action_data: dict, + action_id: str|None, + profile: str + ) -> None: + assert action_id is not None + try: + xmlui_data = action_data["xmlui"] + except KeyError: + raise exceptions.InternalError("Missing XMLUI in file action.") + + try: + from_jid = jid.JID(action_data["from_jid"]) + except KeyError: + raise exceptions.InternalError( + f"Missing 'from_jid' key: {action_data!r}" + ) + from libervia.frontends.tools.webrtc_file import WebRTCFileReceiver + confirm_msg = WebRTCFileReceiver.format_confirm_msg(action_data, from_jid) + + file_accepted = action_data.get("file_accepted", False) + if file_accepted: + accepted = True + else: + accepted = await self.ask_confirmation(confirm_msg, _("File Request")) + + xmlui_data = {"answer": C.bool_const(accepted)} + if accepted: + file_data = action_data.get("file_data") or {} + file_name = file_data.get("name", "received_file") + dest_path_s = await FileChooser.a_open( + mode="save", + title=_("Please select the destination for file {file_name!r}.").format( + file_name=file_name + ), + # FIXME: It doesn't seem to be a way to specify destination file name, + # ``path`` doesn't work this way, at least on Linux/KDE. + default_path=f"./{file_name}" + ) + if dest_path_s is None: + accepted = False + else: + dest_path = Path(dest_path_s) + try: + session_id = action_data["session_id"] + except KeyError: + raise exceptions.InternalError("'session_id' is missing.") + file_receiver = WebRTCFileReceiver( + self.a_bridge, + profile, + on_close_cb=lambda: self.add_note( + _("File Received"), + _('The file "{file_name}" has been successfuly received.').format( + file_name = file_data.get("name", "") + ) + ) + ) + await file_receiver.receive_file_webrtc( + from_jid, + session_id, + dest_path, + file_data + ) + + await self.a_bridge.action_launch( + action_id, data_format.serialise(xmlui_data), profile_key=profile + ) + + def action_manager( + self, + action_data: dict, + callback: Callable|None = None, + ui_show_cb: Callable|None = None, + user_action: bool = True, + action_id: str|None = None, + progress_cb: Callable|None = None, + progress_eb: Callable|None = None, + profile: str = C.PROF_KEY_NONE + ) -> None: + if ( + action_data.get("type") == C.META_TYPE_FILE + and action_data.get("webrtc", False) + ): + aio.run_async(self._on_webrtc_file(action_data, action_id, profile)) + else: + super().action_manager( + action_data, callback, ui_show_cb, user_action, action_id, progress_cb, + progress_eb, profile + ) + def otr_state_handler(self, state, dest_jid, profile): """OTR state has changed for on destinee""" # XXX: this method could be in QuickApp but it's here as @@ -1101,7 +1196,24 @@ cb(*args, **kwargs) return callback - def show_dialog(self, message, title, type="info", answer_cb=None, answer_data=None): + def show_dialog( + self, + message: str, + title: str, + type: str = "info", + answer_cb: Callable|None = None, + answer_data: dict|None = None + ): + """Show a dialog to the user. + + @param message: The main text of the dialog. + @param title: Title of the dialog. + @param type: Type of dialog (info, warning, error, yes/no). + @param answer_cb: A callback that will be called when the user answers to the dialog. + You can pass an asynchronous function as well. + @param answer_data: Additional data for the dialog. + + """ if type in ('info', 'warning', 'error'): self.add_note(title, message, type) elif type == "yes/no": @@ -1118,6 +1230,47 @@ else: log.warning(_("unknown dialog type: {dialog_type}").format(dialog_type=type)) + async def a_show_dialog( + self, + message: str, + title: str, + type: str = "info", + answer_data: dict|None = None + ) -> bool|None: + """Shows a dialog asynchronously and returns the user's response for 'yes/no' dialogs. + + This method wraps the synchronous ``show_dialog`` method to work in an + asynchronous context. + It is specifically useful for 'yes/no' type dialogs, returning True for 'yes' and + False for 'no'. For other types, it returns None immediately after showing the + dialog. + + See [show_dialog] for params. + @return: True if the user clicked 'yes', False if 'no', and None for other dialog types. + """ + future = asyncio.Future() + + def answer_cb(answer: bool, data: dict): + if not future.done(): + future.set_result(answer) + + if type == "yes/no": + self.show_dialog(message, title, type, answer_cb, answer_data) + return await future + else: + self.show_dialog(message, title, type) + return None + + async def ask_confirmation( + self, + message: str, + title: str, + answer_data: dict|None = None + ) -> bool: + ret = await self.a_show_dialog(message, title, "yes/no", answer_data) + assert ret is bool + return ret + def share(self, media_type, data): share_wid = ShareWidget(media_type=media_type, data=data) try: