ui: refactor pair_window
This commit is contained in:
parent
7d868425e7
commit
bd437b548b
|
@ -249,6 +249,9 @@ class Receiver:
|
||||||
if bool(self):
|
if bool(self):
|
||||||
return _base.request(self.handle, 0xFF, request_id, *params)
|
return _base.request(self.handle, 0xFF, request_id, *params)
|
||||||
|
|
||||||
|
def reset_pairing(self):
|
||||||
|
self.pairing = Pairing()
|
||||||
|
|
||||||
read_register = hidpp10.read_register
|
read_register = hidpp10.read_register
|
||||||
write_register = hidpp10.write_register
|
write_register = hidpp10.write_register
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
## Copyright (C) 2012-2013 Daniel Pavel
|
## Copyright (C) 2012-2013 Daniel Pavel
|
||||||
|
## Copyright (C) 2014-2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/
|
||||||
##
|
##
|
||||||
## This program is free software; you can redistribute it and/or modify
|
## This program is free software; you can redistribute it and/or modify
|
||||||
## it under the terms of the GNU General Public License as published by
|
## it under the terms of the GNU General Public License as published by
|
||||||
|
@ -27,97 +28,125 @@ from . import icons as _icons
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
#
|
|
||||||
#
|
|
||||||
#
|
|
||||||
_PAIRING_TIMEOUT = 30 # seconds
|
_PAIRING_TIMEOUT = 30 # seconds
|
||||||
_STATUS_CHECK = 500 # milliseconds
|
_STATUS_CHECK = 500 # milliseconds
|
||||||
|
|
||||||
address = kind = authentication = name = passcode = None
|
|
||||||
|
def create(receiver):
|
||||||
|
receiver.reset_pairing() # clear out any information on previous pairing
|
||||||
|
title = _("%(receiver_name)s: pair new device") % {"receiver_name": receiver.name}
|
||||||
|
if receiver.receiver_kind == "bolt":
|
||||||
|
text = _("Bolt receivers are only compatible with Bolt devices.")
|
||||||
|
text += "\n\n"
|
||||||
|
text += _("Press a pairing button or key until the pairing light flashes quickly.")
|
||||||
|
else:
|
||||||
|
if receiver.receiver_kind == "unifying":
|
||||||
|
text = _("Unifying receivers are only compatible with Unifying devices.")
|
||||||
|
else:
|
||||||
|
text = _("Other receivers are only compatible with a few devices.")
|
||||||
|
text += "\n\n"
|
||||||
|
text += _("Turn on the device you want to pair.")
|
||||||
|
text += _("The device must not be paired with a nearby powered-on receiver.")
|
||||||
|
text += "\n"
|
||||||
|
text += _("If the device is already turned on, turn it off and on again.")
|
||||||
|
if receiver.remaining_pairings() and receiver.remaining_pairings() >= 0:
|
||||||
|
text += (
|
||||||
|
ngettext(
|
||||||
|
"\n\nThis receiver has %d pairing remaining.",
|
||||||
|
"\n\nThis receiver has %d pairings remaining.",
|
||||||
|
receiver.remaining_pairings(),
|
||||||
|
)
|
||||||
|
% receiver.remaining_pairings()
|
||||||
|
)
|
||||||
|
text += _("\nCancelling at this point will not use up a pairing.")
|
||||||
|
ok = prepare(receiver)
|
||||||
|
assistant = _create_assistant(receiver, ok, _finish, title, text)
|
||||||
|
if ok:
|
||||||
|
GLib.timeout_add(_STATUS_CHECK, check_lock_state, assistant, receiver)
|
||||||
|
return assistant
|
||||||
|
|
||||||
|
|
||||||
def _create_page(assistant, kind, header=None, icon_name=None, text=None):
|
def prepare(receiver):
|
||||||
p = Gtk.VBox(homogeneous=False, spacing=8)
|
if receiver.receiver_kind == "bolt":
|
||||||
assistant.append_page(p)
|
if receiver.discover(timeout=_PAIRING_TIMEOUT):
|
||||||
assistant.set_page_type(p, kind)
|
return True
|
||||||
|
else:
|
||||||
if header:
|
receiver.pairing.error = "discovery did not start"
|
||||||
item = Gtk.HBox(homogeneous=False, spacing=16)
|
return False
|
||||||
p.pack_start(item, False, True, 0)
|
elif receiver.set_lock(False, timeout=_PAIRING_TIMEOUT):
|
||||||
|
return True
|
||||||
label = Gtk.Label(label=header)
|
else:
|
||||||
# deprecated - not needed label.set_alignment(0, 0)
|
receiver.pairing.error = "the pairing lock did not open"
|
||||||
label.set_line_wrap(True)
|
return False
|
||||||
item.pack_start(label, True, True, 0)
|
|
||||||
|
|
||||||
if icon_name:
|
|
||||||
icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.DIALOG)
|
|
||||||
# deprecated - not needed icon.set_alignment(1, 0)
|
|
||||||
item.pack_start(icon, False, False, 0)
|
|
||||||
|
|
||||||
if text:
|
|
||||||
label = Gtk.Label(label=text)
|
|
||||||
# deprecated - not needed label.set_alignment(0, 0)
|
|
||||||
label.set_line_wrap(True)
|
|
||||||
p.pack_start(label, False, False, 0)
|
|
||||||
|
|
||||||
p.show_all()
|
|
||||||
return p
|
|
||||||
|
|
||||||
|
|
||||||
def _check_lock_state(assistant, receiver, count=2):
|
def check_lock_state(assistant, receiver, count=2):
|
||||||
global address, kind, authentication, name, passcode
|
|
||||||
|
|
||||||
if not assistant.is_drawable():
|
if not assistant.is_drawable():
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
logger.debug("assistant %s destroyed, bailing out", assistant)
|
logger.debug("assistant %s destroyed, bailing out", assistant)
|
||||||
return False
|
return False
|
||||||
|
return _check_lock_state(assistant, receiver, count)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_lock_state(assistant, receiver, count):
|
||||||
if receiver.pairing.error:
|
if receiver.pairing.error:
|
||||||
# receiver.pairing.new_device = _fake_device(receiver)
|
|
||||||
_pairing_failed(assistant, receiver, receiver.pairing.error)
|
_pairing_failed(assistant, receiver, receiver.pairing.error)
|
||||||
receiver.pairing.error = None
|
|
||||||
return False
|
return False
|
||||||
|
elif receiver.pairing.new_device:
|
||||||
if receiver.pairing.new_device:
|
|
||||||
receiver.remaining_pairings(False) # Update remaining pairings
|
receiver.remaining_pairings(False) # Update remaining pairings
|
||||||
device, receiver.pairing.new_device = receiver.pairing.new_device, None
|
_pairing_succeeded(assistant, receiver, receiver.pairing.new_device)
|
||||||
_pairing_succeeded(assistant, receiver, device)
|
|
||||||
return False
|
return False
|
||||||
elif receiver.pairing.device_address and receiver.pairing.device_name and not address:
|
elif not receiver.pairing.lock_open and not receiver.pairing.discovering:
|
||||||
address = receiver.pairing.device_address
|
if count > 0:
|
||||||
name = receiver.pairing.device_name
|
# the actual device notification may arrive later so have a little patience
|
||||||
kind = receiver.pairing.device_kind
|
GLib.timeout_add(_STATUS_CHECK, check_lock_state, assistant, receiver, count - 1)
|
||||||
authentication = receiver.pairing.device_authentication
|
else:
|
||||||
name = receiver.pairing.device_name
|
_pairing_failed(assistant, receiver, "failed to open pairing lock")
|
||||||
entropy = 10
|
return False
|
||||||
if kind == _hidpp10_constants.DEVICE_KIND.keyboard:
|
elif receiver.pairing.lock_open and receiver.pairing.device_passkey:
|
||||||
entropy = 20
|
_show_passcode(assistant, receiver, receiver.pairing.device_passkey)
|
||||||
if receiver.pair_device(
|
return True
|
||||||
address=address,
|
elif receiver.pairing.discovering and receiver.pairing.device_address and receiver.pairing.device_name:
|
||||||
authentication=authentication,
|
add = receiver.pairing.device_address
|
||||||
entropy=entropy,
|
ent = 20 if receiver.pairing.device_kind == _hidpp10_constants.DEVICE_KIND.keyboard else 10
|
||||||
):
|
if receiver.pair_device(address=add, authentication=receiver.pairing.device_authentication, entropy=ent):
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
_pairing_failed(assistant, receiver, "failed to open pairing lock")
|
_pairing_failed(assistant, receiver, "failed to open pairing lock")
|
||||||
return False
|
return False
|
||||||
elif address and receiver.pairing.device_passkey and not passcode:
|
|
||||||
passcode = receiver.pairing.device_passkey
|
|
||||||
_show_passcode(assistant, receiver, passcode)
|
|
||||||
return True
|
|
||||||
|
|
||||||
if not receiver.pairing.lock_open and not receiver.pairing.discovering:
|
|
||||||
if count > 0:
|
|
||||||
# the actual device notification may arrive later so have a little patience
|
|
||||||
GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant, receiver, count - 1)
|
|
||||||
else:
|
|
||||||
_pairing_failed(assistant, receiver, "failed to open pairing lock")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _pairing_failed(assistant, receiver, error):
|
||||||
|
assistant.remove_page(0) # needed to reset the window size
|
||||||
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
|
logger.debug("%s fail: %s", receiver, error)
|
||||||
|
_create_failure_page(assistant, error)
|
||||||
|
|
||||||
|
|
||||||
|
def _pairing_succeeded(assistant, receiver, device):
|
||||||
|
assistant.remove_page(0) # needed to reset the window size
|
||||||
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
|
logger.debug("%s success: %s", receiver, device)
|
||||||
|
_create_success_page(assistant, device)
|
||||||
|
|
||||||
|
|
||||||
|
def _finish(assistant, receiver):
|
||||||
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
|
logger.debug("finish %s", assistant)
|
||||||
|
assistant.destroy()
|
||||||
|
receiver.pairing.new_device = None
|
||||||
|
if receiver.pairing.lock_open:
|
||||||
|
if receiver.receiver_kind == "bolt":
|
||||||
|
receiver.pair_device("cancel")
|
||||||
|
else:
|
||||||
|
receiver.set_lock()
|
||||||
|
if receiver.pairing.discovering:
|
||||||
|
receiver.discover(True)
|
||||||
|
if not receiver.pairing.lock_open and not receiver.pairing.discovering:
|
||||||
|
receiver.pairing.error = None
|
||||||
|
|
||||||
|
|
||||||
def _show_passcode(assistant, receiver, passkey):
|
def _show_passcode(assistant, receiver, passkey):
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
logger.debug("%s show passkey: %s", receiver, passkey)
|
logger.debug("%s show passkey: %s", receiver, passkey)
|
||||||
|
@ -138,57 +167,57 @@ def _show_passcode(assistant, receiver, passkey):
|
||||||
assistant.next_page()
|
assistant.next_page()
|
||||||
|
|
||||||
|
|
||||||
def _prepare(assistant, page, receiver):
|
def _create_assistant(receiver, ok, finish, title, text):
|
||||||
index = assistant.get_current_page()
|
assistant = Gtk.Assistant()
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
assistant.set_title(title)
|
||||||
logger.debug("prepare %s %d %s", assistant, index, page)
|
assistant.set_icon_name("list-add")
|
||||||
|
assistant.set_size_request(400, 240)
|
||||||
if index == 0:
|
assistant.set_resizable(False)
|
||||||
if receiver.receiver_kind == "bolt":
|
assistant.set_role("pair-device")
|
||||||
if receiver.discover(timeout=_PAIRING_TIMEOUT):
|
if ok:
|
||||||
assert receiver.pairing.new_device is None
|
page_intro = _create_page(assistant, Gtk.AssistantPageType.PROGRESS, title, "preferences-desktop-peripherals", text)
|
||||||
assert receiver.pairing.error is None
|
spinner = Gtk.Spinner()
|
||||||
spinner = page.get_children()[-1]
|
spinner.set_visible(True)
|
||||||
spinner.start()
|
spinner.start()
|
||||||
GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant, receiver)
|
page_intro.pack_end(spinner, True, True, 24)
|
||||||
assistant.set_page_complete(page, True)
|
assistant.set_page_complete(page_intro, True)
|
||||||
else:
|
|
||||||
GLib.idle_add(_pairing_failed, assistant, receiver, "discovery did not start")
|
|
||||||
elif receiver.set_lock(False, timeout=_PAIRING_TIMEOUT):
|
|
||||||
assert receiver.pairing.new_device is None
|
|
||||||
assert receiver.pairing.error is None
|
|
||||||
spinner = page.get_children()[-1]
|
|
||||||
spinner.start()
|
|
||||||
GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant, receiver)
|
|
||||||
assistant.set_page_complete(page, True)
|
|
||||||
else:
|
|
||||||
GLib.idle_add(_pairing_failed, assistant, receiver, "the pairing lock did not open")
|
|
||||||
else:
|
else:
|
||||||
assistant.remove_page(0)
|
page_intro = _create_failure_page(assistant, receiver.pairing.error)
|
||||||
|
assistant.connect("cancel", finish, receiver)
|
||||||
|
assistant.connect("close", finish, receiver)
|
||||||
|
return assistant
|
||||||
|
|
||||||
|
|
||||||
def _finish(assistant, receiver):
|
def _create_success_page(assistant, device):
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
def _check_encrypted(device, assistant, hbox):
|
||||||
logger.debug("finish %s", assistant)
|
if assistant.is_drawable() and device.link_encrypted is False:
|
||||||
assistant.destroy()
|
hbox.pack_start(Gtk.Image.new_from_icon_name("security-low", Gtk.IconSize.MENU), False, False, 0)
|
||||||
receiver.pairing.new_device = None
|
hbox.pack_start(Gtk.Label(label=_("The wireless link is not encrypted")), False, False, 0)
|
||||||
if receiver.pairing.lock_open:
|
hbox.show_all()
|
||||||
if receiver.receiver_kind == "bolt":
|
return False
|
||||||
receiver.pair_device("cancel")
|
|
||||||
else:
|
|
||||||
receiver.set_lock()
|
|
||||||
if receiver.pairing.discovering:
|
|
||||||
receiver.discover(True)
|
|
||||||
if not receiver.pairing.lock_open and not receiver.pairing.discovering:
|
|
||||||
receiver.pairing.error = None
|
|
||||||
|
|
||||||
|
|
||||||
def _pairing_failed(assistant, receiver, error):
|
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
|
||||||
logger.debug("%s fail: %s", receiver, error)
|
|
||||||
|
|
||||||
|
page = _create_page(assistant, Gtk.AssistantPageType.SUMMARY)
|
||||||
|
header = Gtk.Label(label=_("Found a new device:"))
|
||||||
|
page.pack_start(header, False, False, 0)
|
||||||
|
device_icon = Gtk.Image()
|
||||||
|
icon_name = _icons.device_icon_name(device.name, device.kind)
|
||||||
|
device_icon.set_from_icon_name(icon_name, _icons.LARGE_SIZE)
|
||||||
|
page.pack_start(device_icon, True, True, 0)
|
||||||
|
device_label = Gtk.Label()
|
||||||
|
device_label.set_markup(f"<b>{device.name}</b>")
|
||||||
|
page.pack_start(device_label, True, True, 0)
|
||||||
|
hbox = Gtk.HBox(homogeneous=False, spacing=8)
|
||||||
|
hbox.pack_start(Gtk.Label(label=" "), False, False, 0)
|
||||||
|
hbox.set_property("expand", False)
|
||||||
|
hbox.set_property("halign", Gtk.Align.CENTER)
|
||||||
|
page.pack_start(hbox, False, False, 0)
|
||||||
|
GLib.timeout_add(_STATUS_CHECK, _check_encrypted, device, assistant, hbox) # wait a bit to check link status
|
||||||
|
page.show_all()
|
||||||
|
assistant.next_page()
|
||||||
assistant.commit()
|
assistant.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def _create_failure_page(assistant, error):
|
||||||
header = _("Pairing failed") + ": " + _(str(error)) + "."
|
header = _("Pairing failed") + ": " + _(str(error)) + "."
|
||||||
if "timeout" in str(error):
|
if "timeout" in str(error):
|
||||||
text = _("Make sure your device is within range, and has a decent battery charge.")
|
text = _("Make sure your device is within range, and has a decent battery charge.")
|
||||||
|
@ -199,111 +228,26 @@ def _pairing_failed(assistant, receiver, error):
|
||||||
else:
|
else:
|
||||||
text = _("No further details are available about the error.")
|
text = _("No further details are available about the error.")
|
||||||
_create_page(assistant, Gtk.AssistantPageType.SUMMARY, header, "dialog-error", text)
|
_create_page(assistant, Gtk.AssistantPageType.SUMMARY, header, "dialog-error", text)
|
||||||
|
|
||||||
assistant.next_page()
|
assistant.next_page()
|
||||||
assistant.commit()
|
assistant.commit()
|
||||||
|
|
||||||
|
|
||||||
def _pairing_succeeded(assistant, receiver, device):
|
def _create_page(assistant, kind, header=None, icon_name=None, text=None):
|
||||||
assert device
|
p = Gtk.VBox(homogeneous=False, spacing=8)
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
assistant.append_page(p)
|
||||||
logger.debug("%s success: %s", receiver, device)
|
assistant.set_page_type(p, kind)
|
||||||
|
if header:
|
||||||
page = _create_page(assistant, Gtk.AssistantPageType.SUMMARY)
|
item = Gtk.HBox(homogeneous=False, spacing=16)
|
||||||
|
p.pack_start(item, False, True, 0)
|
||||||
header = Gtk.Label(label=_("Found a new device:"))
|
label = Gtk.Label(label=header)
|
||||||
# deprecated - not needed header.set_alignment(0.5, 0)
|
label.set_line_wrap(True)
|
||||||
page.pack_start(header, False, False, 0)
|
item.pack_start(label, True, True, 0)
|
||||||
|
if icon_name:
|
||||||
device_icon = Gtk.Image()
|
icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.DIALOG)
|
||||||
icon_name = _icons.device_icon_name(device.name, device.kind)
|
item.pack_start(icon, False, False, 0)
|
||||||
device_icon.set_from_icon_name(icon_name, _icons.LARGE_SIZE)
|
if text:
|
||||||
# deprecated - not needed device_icon.set_alignment(0.5, 1)
|
label = Gtk.Label(label=text)
|
||||||
page.pack_start(device_icon, True, True, 0)
|
label.set_line_wrap(True)
|
||||||
|
p.pack_start(label, False, False, 0)
|
||||||
device_label = Gtk.Label()
|
p.show_all()
|
||||||
device_label.set_markup(f"<b>{device.name}</b>")
|
return p
|
||||||
# deprecated - not needed device_label.set_alignment(0.5, 0)
|
|
||||||
page.pack_start(device_label, True, True, 0)
|
|
||||||
|
|
||||||
hbox = Gtk.HBox(homogeneous=False, spacing=8)
|
|
||||||
hbox.pack_start(Gtk.Label(label=" "), False, False, 0)
|
|
||||||
hbox.set_property("expand", False)
|
|
||||||
hbox.set_property("halign", Gtk.Align.CENTER)
|
|
||||||
page.pack_start(hbox, False, False, 0)
|
|
||||||
|
|
||||||
def _check_encrypted(dev):
|
|
||||||
if assistant.is_drawable():
|
|
||||||
if device.link_encrypted is False:
|
|
||||||
hbox.pack_start(Gtk.Image.new_from_icon_name("security-low", Gtk.IconSize.MENU), False, False, 0)
|
|
||||||
hbox.pack_start(Gtk.Label(label=_("The wireless link is not encrypted") + "!"), False, False, 0)
|
|
||||||
hbox.show_all()
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
GLib.timeout_add(_STATUS_CHECK, _check_encrypted, device)
|
|
||||||
|
|
||||||
page.show_all()
|
|
||||||
|
|
||||||
assistant.next_page()
|
|
||||||
assistant.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def create(receiver):
|
|
||||||
assert receiver is not None
|
|
||||||
assert receiver.kind is None
|
|
||||||
|
|
||||||
global address, kind, authentication, name, passcode
|
|
||||||
address = name = kind = authentication = passcode = None
|
|
||||||
|
|
||||||
assistant = Gtk.Assistant()
|
|
||||||
assistant.set_title(_("%(receiver_name)s: pair new device") % {"receiver_name": receiver.name})
|
|
||||||
assistant.set_icon_name("list-add")
|
|
||||||
|
|
||||||
assistant.set_size_request(400, 240)
|
|
||||||
assistant.set_resizable(False)
|
|
||||||
assistant.set_role("pair-device")
|
|
||||||
|
|
||||||
if receiver.receiver_kind == "unifying":
|
|
||||||
page_text = _("Unifying receivers are only compatible with Unifying devices.")
|
|
||||||
elif receiver.receiver_kind == "bolt":
|
|
||||||
page_text = _("Bolt receivers are only compatible with Bolt devices.")
|
|
||||||
else:
|
|
||||||
page_text = _("Other receivers are only compatible with a few devices.")
|
|
||||||
page_text += "\n"
|
|
||||||
page_text += _("The device must not be paired with a nearby powered-on receiver.")
|
|
||||||
page_text += "\n\n"
|
|
||||||
|
|
||||||
if receiver.receiver_kind == "bolt":
|
|
||||||
page_text += _("Press a pairing button or key until the pairing light flashes quickly.")
|
|
||||||
page_text += "\n"
|
|
||||||
page_text += _("You may have to first turn the device off and on again.")
|
|
||||||
else:
|
|
||||||
page_text += _("Turn on the device you want to pair.")
|
|
||||||
page_text += "\n"
|
|
||||||
page_text += _("If the device is already turned on, turn it off and on again.")
|
|
||||||
if receiver.remaining_pairings() and receiver.remaining_pairings() >= 0:
|
|
||||||
page_text += (
|
|
||||||
ngettext(
|
|
||||||
"\n\nThis receiver has %d pairing remaining.",
|
|
||||||
"\n\nThis receiver has %d pairings remaining.",
|
|
||||||
receiver.remaining_pairings(),
|
|
||||||
)
|
|
||||||
% receiver.remaining_pairings()
|
|
||||||
)
|
|
||||||
page_text += _("\nCancelling at this point will not use up a pairing.")
|
|
||||||
|
|
||||||
intro_text = _("%(receiver_name)s: pair new device") % {"receiver_name": receiver.name}
|
|
||||||
|
|
||||||
page_intro = _create_page(
|
|
||||||
assistant, Gtk.AssistantPageType.PROGRESS, intro_text, "preferences-desktop-peripherals", page_text
|
|
||||||
)
|
|
||||||
spinner = Gtk.Spinner()
|
|
||||||
spinner.set_visible(True)
|
|
||||||
page_intro.pack_end(spinner, True, True, 24)
|
|
||||||
|
|
||||||
assistant.connect("prepare", _prepare, receiver)
|
|
||||||
assistant.connect("cancel", _finish, receiver)
|
|
||||||
assistant.connect("close", _finish, receiver)
|
|
||||||
|
|
||||||
return assistant
|
|
||||||
|
|
Loading…
Reference in New Issue