Browse Source

qt: qrreader: keep both old and new toolchain; try to abstract it away

patch-4
SomberNight 4 years ago
parent
commit
013cf869f1
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 2
      contrib/build-wine/deterministic.spec
  2. 56
      contrib/make_zbar.sh
  3. 2
      contrib/osx/osx.spec
  4. 22
      electrum/gui/qt/__init__.py
  5. 67
      electrum/gui/qt/main_window.py
  6. 163
      electrum/gui/qt/qrreader/__init__.py
  7. 39
      electrum/gui/qt/qrreader/qtmultimedia/__init__.py
  8. 4
      electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py
  9. 0
      electrum/gui/qt/qrreader/qtmultimedia/crop_blur_effect.py
  10. 0
      electrum/gui/qt/qrreader/qtmultimedia/validator.py
  11. 0
      electrum/gui/qt/qrreader/qtmultimedia/video_overlay.py
  12. 0
      electrum/gui/qt/qrreader/qtmultimedia/video_surface.py
  13. 0
      electrum/gui/qt/qrreader/qtmultimedia/video_widget.py
  14. 59
      electrum/gui/qt/qrtextedit.py
  15. 16
      electrum/gui/qt/settings_dialog.py
  16. 4
      electrum/qrscanner.py
  17. 1
      setup.py

2
contrib/build-wine/deterministic.spec

@ -53,7 +53,7 @@ datas += collect_data_files('bitbox02')
# We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports # We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports
a = Analysis([home+'run_electrum', a = Analysis([home+'run_electrum',
home+'electrum/gui/qt/main_window.py', home+'electrum/gui/qt/main_window.py',
home+'electrum/gui/qt/qrreader/camera_dialog.py', home+'electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py',
home+'electrum/gui/text.py', home+'electrum/gui/text.py',
home+'electrum/util.py', home+'electrum/util.py',
home+'electrum/wallet.py', home+'electrum/wallet.py',

56
contrib/make_zbar.sh

@ -47,45 +47,39 @@ info "Building $pkgname..."
if ! [ -r config.status ] ; then if ! [ -r config.status ] ; then
if [ "$BUILD_TYPE" = "wine" ] ; then if [ "$BUILD_TYPE" = "wine" ] ; then
# windows target # windows target
./configure \ AUTOCONF_FLAGS="$AUTOCONF_FLAGS \
$AUTOCONF_FLAGS \
--prefix="$here/$pkgname/dist" \
--with-x=no \ --with-x=no \
--enable-pthread=no \
--enable-doc=no \
--enable-video=yes \ --enable-video=yes \
--with-directshow=yes \
--with-jpeg=no \ --with-jpeg=no \
--with-python=no \ --with-directshow=yes \
--with-gtk=no \ --disable-dependency-tracking"
--with-qt=no \ elif [ $(uname) == "Darwin" ]; then
--with-java=no \ # macos target
--with-imagemagick=no \ AUTOCONF_FLAGS="$AUTOCONF_FLAGS \
--with-dbus=no \ --with-x=no \
--enable-codes=qrcode \ --enable-video=no \
--disable-dependency-tracking \ --with-jpeg=no"
--disable-static \
--enable-shared || fail "Could not configure $pkgname. Please make sure you have a C compiler installed and try again."
else else
# linux target # linux target
./configure \ AUTOCONF_FLAGS="$AUTOCONF_FLAGS \
$AUTOCONF_FLAGS \
--prefix="$here/$pkgname/dist" \
--with-x=yes \ --with-x=yes \
--enable-pthread=no \
--enable-doc=no \
--enable-video=yes \ --enable-video=yes \
--with-jpeg=yes \ --with-jpeg=yes"
--with-python=no \
--with-gtk=no \
--with-qt=no \
--with-java=no \
--with-imagemagick=no \
--with-dbus=no \
--enable-codes=qrcode \
--disable-static \
--enable-shared || fail "Could not configure $pkgname. Please make sure you have a C compiler installed and try again."
fi fi
./configure \
$AUTOCONF_FLAGS \
--prefix="$here/$pkgname/dist" \
--enable-pthread=no \
--enable-doc=no \
--with-python=no \
--with-gtk=no \
--with-qt=no \
--with-java=no \
--with-imagemagick=no \
--with-dbus=no \
--enable-codes=qrcode \
--disable-static \
--enable-shared || fail "Could not configure $pkgname. Please make sure you have a C compiler installed and try again."
fi fi
make -j4 || fail "Could not build $pkgname" make -j4 || fail "Could not build $pkgname"
make install || fail "Could not install $pkgname" make install || fail "Could not install $pkgname"

2
contrib/osx/osx.spec

@ -96,7 +96,7 @@ binaries += [b for b in collect_dynamic_libs('PyQt5') if 'macstyle' in b[0]]
# We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports # We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports
a = Analysis([electrum+ MAIN_SCRIPT, a = Analysis([electrum+ MAIN_SCRIPT,
electrum+'electrum/gui/qt/main_window.py', electrum+'electrum/gui/qt/main_window.py',
electrum+'electrum/gui/qt/qrreader/camera_dialog.py', electrum+'electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py',
electrum+'electrum/gui/text.py', electrum+'electrum/gui/text.py',
electrum+'electrum/util.py', electrum+'electrum/util.py',
electrum+'electrum/wallet.py', electrum+'electrum/wallet.py',

22
electrum/gui/qt/__init__.py

@ -275,28 +275,6 @@ class ElectrumGui(Logger):
network_updated_signal_obj=self.network_updated_signal_obj) network_updated_signal_obj=self.network_updated_signal_obj)
self.network_dialog.show() self.network_dialog.show()
@staticmethod
def warn_if_cant_import_qrreader(parent, *, show_warning=True) -> bool:
"""Checks it QR reading from camera is possible. It can fail on a
system lacking QtMultimedia. This can be removed in the future when
we are unlikely to encounter Qt5 installations that are missing
QtMultimedia
"""
try:
from .qrreader import QrReaderCameraDialog
except ImportError as e:
if show_warning:
icon = QMessageBox.Warning
title = _("QR Reader Error")
message = _("QR reader failed to load. This may happen if "
"you are using an older version of PyQt5.") + "\n\n" + str(e)
if isinstance(parent, MessageBoxMixin):
parent.msg_box(title=title, text=message, icon=icon, parent=None)
else:
custom_message_box(title=title, text=message, icon=icon, parent=parent)
return True
return False
def _create_window_for_wallet(self, wallet): def _create_window_for_wallet(self, wallet):
w = ElectrumWindow(self, wallet) w = ElectrumWindow(self, wallet)
self.windows.append(w) self.windows.append(w)

67
electrum/gui/qt/main_window.py

@ -103,6 +103,7 @@ from .channels_list import ChannelsList
from .confirm_tx_dialog import ConfirmTxDialog from .confirm_tx_dialog import ConfirmTxDialog
from .transaction_dialog import PreviewTxDialog from .transaction_dialog import PreviewTxDialog
from .rbf_dialog import BumpFeeDialog, DSCancelDialog from .rbf_dialog import BumpFeeDialog, DSCancelDialog
from .qrreader import scan_qrcode
if TYPE_CHECKING: if TYPE_CHECKING:
from . import ElectrumGui from . import ElectrumGui
@ -2820,54 +2821,28 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.show_error("failed to import backup" + '\n' + str(e)) self.show_error("failed to import backup" + '\n' + str(e))
return return
# Due to the asynchronous nature of the qr reader we need to keep the
# dialog instance as member variable to prevent reentrancy/multiple ones
# from being presented at once.
_qr_dialog = None
def read_tx_from_qrcode(self): def read_tx_from_qrcode(self):
if self._qr_dialog: def cb(success: bool, error: str, data):
self.logger.warning("QR dialog is already presented, ignoring.") if not success:
return if error:
if self.gui_object.warn_if_cant_import_qrreader(self): self.show_error(error)
return return
from .qrreader import QrReaderCameraDialog, CameraError, MissingQrDetectionLib if not data:
self._qr_dialog = None return
try: # if the user scanned a bitcoin URI
self._qr_dialog = QrReaderCameraDialog(parent=self.top_level_window(), config=self.config) if data.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'):
self.pay_to_URI(data)
def _on_qr_reader_finished(success: bool, error: str, data): return
if self._qr_dialog: if data.lower().startswith('channel_backup:'):
self._qr_dialog.deleteLater() self.import_channel_backup(data)
self._qr_dialog = None return
if not success: # else if the user scanned an offline signed tx
if error: tx = self.tx_from_text(data)
self.show_error(error) if not tx:
return return
if not data: self.show_transaction(tx)
return
# if the user scanned a bitcoin URI
if data.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'):
self.pay_to_URI(data)
return
if data.lower().startswith('channel_backup:'):
self.import_channel_backup(data)
return
# else if the user scanned an offline signed tx
tx = self.tx_from_text(data)
if not tx:
return
self.show_transaction(tx)
self._qr_dialog.qr_finished.connect(_on_qr_reader_finished) scan_qrcode(parent=self.top_level_window(), config=self.config, callback=cb)
self._qr_dialog.start_scan(self.config.get_video_device())
except (MissingQrDetectionLib, CameraError) as e:
self._qr_dialog = None
self.show_error(str(e))
except Exception as e:
self.logger.exception('camera error')
self._qr_dialog = None
self.show_error(repr(e))
def read_tx_from_file(self) -> Optional[Transaction]: def read_tx_from_file(self) -> Optional[Transaction]:
fileName = getOpenFileName( fileName = getOpenFileName(

163
electrum/gui/qt/qrreader/__init__.py

@ -1,30 +1,141 @@
#!/usr/bin/env python3 # Copyright (C) 2021 The Electrum developers
# Distributed under the MIT software license, see the accompanying
# file LICENCE or http://www.opensource.org/licenses/mit-license.php
# #
# Electron Cash - lightweight Bitcoin client # We have two toolchains to scan qr codes:
# Copyright (C) 2019 Axel Gembe <derago@gmail.com> # 1. access camera via QtMultimedia, take picture, feed picture to zbar
# 2. let zbar handle whole flow (including accessing the camera)
# #
# Permission is hereby granted, free of charge, to any person # notes:
# obtaining a copy of this software and associated documentation files # - zbar needs to be compiled with platform-dependent extra config options to be able
# (the "Software"), to deal in the Software without restriction, # to access the camera
# including without limitation the rights to use, copy, modify, merge, # - zbar fails to access the camera on macOS
# publish, distribute, sublicense, and/or sell copies of the Software, # - qtmultimedia seems to support more cameras on Windows than zbar
# and to permit persons to whom the Software is furnished to do so, # - qtmultimedia is often not packaged with PyQt5
# subject to the following conditions: # in particular, on debian, you need both "python3-pyqt5" and "python3-pyqt5.qtmultimedia"
# - older versions of qtmultimedia don't seem to work reliably
# #
# The above copyright notice and this permission notice shall be # Considering the above, we use QtMultimedia for Windows and macOS, as there
# included in all copies or substantial portions of the Software. # most users run our binaries where we can make sure the packaged versions work well.
# On Linux where many people run from source, we use zbar.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # Note: this module is safe to import on all platforms.
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND import sys
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS from typing import Callable, Optional, TYPE_CHECKING, Mapping
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN from PyQt5.QtWidgets import QMessageBox, QWidget
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. from electrum.i18n import _
from electrum.util import UserFacingException
from .camera_dialog import (QrReaderCameraDialog, CameraError, NoCamerasFound, from electrum.logging import get_logger
NoCameraResolutionsFound, MissingQrDetectionLib)
from .validator import (QrReaderValidatorResult, AbstractQrReaderValidator, from electrum.gui.qt.util import MessageBoxMixin, custom_message_box
QrReaderValidatorCounting, QrReaderValidatorColorizing,
QrReaderValidatorStrong, QrReaderValidatorCounted)
if TYPE_CHECKING:
from electrum.simple_config import SimpleConfig
_logger = get_logger(__name__)
def scan_qrcode(
*,
parent: Optional[QWidget],
config: 'SimpleConfig',
callback: Callable[[bool, str, Optional[str]], None],
) -> None:
if sys.platform == 'darwin' or sys.platform in ('windows', 'win32'):
_scan_qrcode_using_qtmultimedia(parent=parent, config=config, callback=callback)
else: # desktop Linux and similar
_scan_qrcode_using_zbar(parent=parent, config=config, callback=callback)
def find_system_cameras() -> Mapping[str, str]:
"""Returns a camera_description -> camera_path map."""
if sys.platform == 'darwin' or sys.platform in ('windows', 'win32'):
try:
from .qtmultimedia import find_system_cameras
except ImportError as e:
return {}
else:
return find_system_cameras()
else: # desktop Linux and similar
from electrum import qrscanner
return qrscanner.find_system_cameras()
# --- Internals below (not part of external API)
def _scan_qrcode_using_zbar(
*,
parent: Optional[QWidget],
config: 'SimpleConfig',
callback: Callable[[bool, str, Optional[str]], None],
) -> None:
from electrum import qrscanner
data = None
try:
data = qrscanner.scan_barcode(config.get_video_device())
except UserFacingException as e:
success = False
error = str(e)
except BaseException as e:
_logger.exception('camera error')
success = False
error = repr(e)
else:
success = True
error = ""
callback(success, error, data)
# Use a global to prevent multiple QR dialogs created simultaneously
_qr_dialog = None
def _scan_qrcode_using_qtmultimedia(
*,
parent: Optional[QWidget],
config: 'SimpleConfig',
callback: Callable[[bool, str, Optional[str]], None],
) -> None:
try:
from .qtmultimedia import QrReaderCameraDialog, CameraError, MissingQrDetectionLib
except ImportError as e:
icon = QMessageBox.Warning
title = _("QR Reader Error")
message = _("QR reader failed to load. This may happen if "
"you are using an older version of PyQt5.") + "\n\n" + str(e)
if isinstance(parent, MessageBoxMixin):
parent.msg_box(title=title, text=message, icon=icon, parent=None)
else:
custom_message_box(title=title, text=message, icon=icon, parent=parent)
return
global _qr_dialog
if _qr_dialog:
_logger.warning("QR dialog is already presented, ignoring.")
return
_qr_dialog = None
try:
_qr_dialog = QrReaderCameraDialog(parent=parent, config=config)
def _on_qr_reader_finished(success: bool, error: str, data):
global _qr_dialog
if _qr_dialog:
_qr_dialog.deleteLater()
_qr_dialog = None
callback(success, error, data)
_qr_dialog.qr_finished.connect(_on_qr_reader_finished)
_qr_dialog.start_scan(config.get_video_device())
except (MissingQrDetectionLib, CameraError) as e:
_qr_dialog = None
callback(False, str(e), None)
except Exception as e:
_logger.exception('camera error')
_qr_dialog = None
callback(False, repr(e), None)

39
electrum/gui/qt/qrreader/qtmultimedia/__init__.py

@ -0,0 +1,39 @@
#!/usr/bin/env python3
#
# Electron Cash - lightweight Bitcoin client
# Copyright (C) 2019 Axel Gembe <derago@gmail.com>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from typing import Mapping
from .camera_dialog import (QrReaderCameraDialog, CameraError, NoCamerasFound,
NoCameraResolutionsFound, MissingQrDetectionLib)
from .validator import (QrReaderValidatorResult, AbstractQrReaderValidator,
QrReaderValidatorCounting, QrReaderValidatorColorizing,
QrReaderValidatorStrong, QrReaderValidatorCounted)
def find_system_cameras() -> Mapping[str, str]:
"""Returns a camera_description -> camera_path map."""
from PyQt5.QtMultimedia import QCameraInfo
system_cameras = QCameraInfo.availableCameras()
return {cam.description(): cam.deviceName() for cam in system_cameras}

4
electrum/gui/qt/qrreader/camera_dialog.py → electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py

@ -39,12 +39,14 @@ from electrum.i18n import _
from electrum.qrreader import get_qr_reader, QrCodeResult from electrum.qrreader import get_qr_reader, QrCodeResult
from electrum.logging import Logger from electrum.logging import Logger
from electrum.gui.qt.util import MessageBoxMixin, FixedAspectRatioLayout, ImageGraphicsEffect
from .video_widget import QrReaderVideoWidget from .video_widget import QrReaderVideoWidget
from .video_overlay import QrReaderVideoOverlay from .video_overlay import QrReaderVideoOverlay
from .video_surface import QrReaderVideoSurface from .video_surface import QrReaderVideoSurface
from .crop_blur_effect import QrReaderCropBlurEffect from .crop_blur_effect import QrReaderCropBlurEffect
from .validator import AbstractQrReaderValidator, QrReaderValidatorCounted, QrReaderValidatorResult from .validator import AbstractQrReaderValidator, QrReaderValidatorCounted, QrReaderValidatorResult
from ..util import MessageBoxMixin, FixedAspectRatioLayout, ImageGraphicsEffect
class CameraError(RuntimeError): class CameraError(RuntimeError):
''' Base class of the camera-related error conditions. ''' ''' Base class of the camera-related error conditions. '''

0
electrum/gui/qt/qrreader/crop_blur_effect.py → electrum/gui/qt/qrreader/qtmultimedia/crop_blur_effect.py

0
electrum/gui/qt/qrreader/validator.py → electrum/gui/qt/qrreader/qtmultimedia/validator.py

0
electrum/gui/qt/qrreader/video_overlay.py → electrum/gui/qt/qrreader/qtmultimedia/video_overlay.py

0
electrum/gui/qt/qrreader/video_surface.py → electrum/gui/qt/qrreader/qtmultimedia/video_surface.py

0
electrum/gui/qt/qrreader/video_widget.py → electrum/gui/qt/qrreader/qtmultimedia/video_widget.py

59
electrum/gui/qt/qrtextedit.py

@ -7,6 +7,7 @@ from electrum.util import UserFacingException
from electrum.logging import Logger from electrum.logging import Logger
from .util import ButtonsTextEdit, MessageBoxMixin, ColorScheme, getOpenFileName from .util import ButtonsTextEdit, MessageBoxMixin, ColorScheme, getOpenFileName
from .qrreader import scan_qrcode
class ShowQRTextEdit(ButtonsTextEdit): class ShowQRTextEdit(ButtonsTextEdit):
@ -72,49 +73,23 @@ class ScanQRTextEdit(ButtonsTextEdit, MessageBoxMixin, Logger):
else: else:
self.setText(data) self.setText(data)
# Due to the asynchronous nature of the qr reader we need to keep the
# dialog instance as member variable to prevent reentrancy/multiple ones
# from being presented at once.
qr_dialog = None
def qr_input(self, *, callback=None) -> None: def qr_input(self, *, callback=None) -> None:
if self.qr_dialog: def cb(success: bool, error: str, data):
self.logger.warning("QR dialog is already presented, ignoring.") if not success:
return if error:
from . import ElectrumGui self.show_error(error)
if ElectrumGui.warn_if_cant_import_qrreader(self): return
return if not data:
from .qrreader import QrReaderCameraDialog, CameraError, MissingQrDetectionLib data = ''
try: if self.allow_multi:
self.qr_dialog = QrReaderCameraDialog(parent=self.top_level_window(), config=self.config) new_text = self.text() + data + '\n'
else:
def _on_qr_reader_finished(success: bool, error: str, data): new_text = data
if self.qr_dialog: self.setText(new_text)
self.qr_dialog.deleteLater() if callback and success:
self.qr_dialog = None callback(data)
if not success:
if error: scan_qrcode(parent=self.top_level_window(), config=self.config, callback=cb)
self.show_error(error)
return
if not data:
data = ''
if self.allow_multi:
new_text = self.text() + data + '\n'
else:
new_text = data
self.setText(new_text)
if callback and success:
callback(data)
self.qr_dialog.qr_finished.connect(_on_qr_reader_finished)
self.qr_dialog.start_scan(self.config.get_video_device())
except (MissingQrDetectionLib, CameraError) as e:
self.qr_dialog = None
self.show_error(str(e))
except Exception as e:
self.logger.exception('camera error')
self.qr_dialog = None
self.show_error(repr(e))
def contextMenuEvent(self, e): def contextMenuEvent(self, e):
m = self.createStandardContextMenu() m = self.createStandardContextMenu()

16
electrum/gui/qt/settings_dialog.py

@ -220,18 +220,10 @@ class SettingsDialog(WindowModalDialog):
msg = (_("For scanning QR codes.") + "\n" msg = (_("For scanning QR codes.") + "\n"
+ _("Install the zbar package to enable this.")) + _("Install the zbar package to enable this."))
qr_label = HelpLabel(_('Video Device') + ':', msg) qr_label = HelpLabel(_('Video Device') + ':', msg)
system_cameras = [] from .qrreader import find_system_cameras
try: system_cameras = find_system_cameras()
from PyQt5.QtMultimedia import QCameraInfo for cam_desc, cam_path in system_cameras.items():
system_cameras = QCameraInfo.availableCameras() qr_combo.addItem(cam_desc, cam_path)
except ImportError as e:
# Older Qt or missing libs -- disable GUI control and inform user why
qr_combo.setEnabled(False)
qr_label.setEnabled(False)
qr_combo.setToolTip(_("Unable to probe for cameras on this system. QtMultimedia is likely missing."))
qr_label.setToolTip(qr_combo.toolTip())
for cam in system_cameras:
qr_combo.addItem(cam.description(), cam.deviceName())
index = qr_combo.findData(self.config.get("video_device")) index = qr_combo.findData(self.config.get("video_device"))
qr_combo.setCurrentIndex(index) qr_combo.setCurrentIndex(index)
on_video_device = lambda x: self.config.set_key("video_device", qr_combo.itemData(x), True) on_video_device = lambda x: self.config.set_key("video_device", qr_combo.itemData(x), True)

4
electrum/qrscanner.py

@ -26,7 +26,7 @@
import os import os
import sys import sys
import ctypes import ctypes
from typing import Optional from typing import Optional, Mapping
from .util import UserFacingException from .util import UserFacingException
from .i18n import _ from .i18n import _
@ -82,7 +82,7 @@ def scan_barcode(device='', timeout=-1, display=True, threaded=False) -> Optiona
return data.decode('utf8') return data.decode('utf8')
def _find_system_cameras(): def find_system_cameras() -> Mapping[str, str]:
device_root = "/sys/class/video4linux" device_root = "/sys/class/video4linux"
devices = {} # Name -> device devices = {} # Name -> device
if os.path.exists(device_root): if os.path.exists(device_root):

1
setup.py

@ -76,6 +76,7 @@ setup(
'electrum.gui', 'electrum.gui',
'electrum.gui.qt', 'electrum.gui.qt',
'electrum.gui.qt.qrreader', 'electrum.gui.qt.qrreader',
'electrum.gui.qt.qrreader.qtmultimedia',
'electrum.plugins', 'electrum.plugins',
] + [('electrum.plugins.'+pkg) for pkg in find_packages('electrum/plugins')], ] + [('electrum.plugins.'+pkg) for pkg in find_packages('electrum/plugins')],
package_dir={ package_dir={

Loading…
Cancel
Save