Merge branch 'main' into fix/android-nfc

This commit is contained in:
Adam Velebil 2022-07-21 17:24:35 +02:00
commit 902eb6718d
No known key found for this signature in database
GPG Key ID: AC6D6B9D715FC084
61 changed files with 1523 additions and 1070 deletions

View File

@ -48,10 +48,18 @@ jobs:
- name: Check generated files
run: git diff --exit-code
- name: Create dmg
run: |
brew install create-dmg
mkdir source_folder
cp -R build/macos/Build/Products/Release/"Yubico Authenticator.app" source_folder
sh create-dmg.sh
- name: Rename and archive app bundle
run: |
export REF=$(echo ${GITHUB_REF} | cut -d '/' -f 3)
mkdir deploy
mv yubioath-desktop.dmg deploy
tar -czf deploy/yubioath-desktop-${REF}.app.tar.gz -C build/macos/Build/Products/Release "Yubico Authenticator.app"
- name: Upload artifact

View File

@ -58,6 +58,7 @@ jobs:
run: |
$env:PATH += ";$env:WIX\bin"
$env:SRCDIR = "build\windows\runner\Release\"
cp resources\win\license.rtf .\
heat dir .\build\windows\runner\Release\ -out fragment.wxs -gg -scom -srd -sfrag -dr INSTALLDIR -cg ApplicationFiles -var env.SRCDIR
candle .\fragment.wxs .\resources\win\yubioath-desktop.wxs -ext WixUtilExtension -arch x64
light fragment.wixobj yubioath-desktop.wixobj -ext WixUIExtension -ext WixUtilExtension -o yubioath-desktop.msi
@ -70,6 +71,7 @@ jobs:
mkdir $dest
mv build\windows\runner\Release\* $dest\
mv yubioath-desktop.msi deploy
mv resources deploy
- name: Upload artifact
uses: actions/upload-artifact@v1

View File

@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v3.0.3), do not edit directly.
// Autogenerated from Pigeon (v3.1.6), do not edit directly.
// See also: https://pub.dev/packages/pigeon
package com.yubico.authenticator.api;

10
create-dmg.sh Normal file
View File

@ -0,0 +1,10 @@
create-dmg \
--volname "Yubico Authenticator" \
--background "resources/icons/yubico-msi-background.png" \
--window-pos 200 120 \
--window-size 800 400 \
--icon-size 100 \
--icon "Yubico Authenticator.app" 200 190 \
--app-drop-link 600 185 \
"yubioath-desktop.dmg" \
"source_folder/"

View File

@ -280,7 +280,7 @@ class UsbDeviceNode(AbstractDeviceNode):
super().__init__(device, info)
def _supports_connection(self, conn_type):
return self._device.supports_connection(conn_type)
return self._device.pid.supports_connection(conn_type)
def _create_connection(self, conn_type):
connection = self._device.open_connection(conn_type)
@ -327,7 +327,9 @@ class ReaderDeviceNode(AbstractDeviceNode):
with self._device.open_connection(SmartCardConnection) as conn:
return dict(self._read_data(conn), present=True)
except NoCardException:
return dict(present=False)
return dict(present=False, status="no-card")
except ValueError:
return dict(present=False, status="unknown-device")
@child
def ccid(self):

View File

@ -171,7 +171,7 @@ class Ctap2Node(RpcNode):
self.ctap = Ctap2(connection)
if target != _ctap_id(self.ctap):
raise ValueError("Re-inserted YubiKey does not match initial device")
self.ctap.reset(event)
self.ctap.reset(event=event)
self._info = self.ctap.get_info()
self._auth_blocked = False
self._token = None
@ -343,7 +343,7 @@ class FingerprintsNode(RpcNode):
template_id = None
while template_id is None:
try:
template_id = enroller.capture(event)
template_id = enroller.capture(event=event)
signal("capture", dict(remaining=enroller.remaining))
except CaptureError as e:
signal("capture-error", dict(code=e.code))

View File

@ -32,7 +32,7 @@ from yubikit.core.smartcard import SmartCardConnection
from yubikit.core.otp import OtpConnection
from yubikit.core.fido import FidoConnection
from yubikit.management import ManagementSession, DeviceConfig, Mode
from ykman.device import connect_to_device
from ykman.device import list_all_devices
from dataclasses import asdict
from time import sleep
import logging
@ -78,12 +78,10 @@ class ManagementNode(RpcNode):
logger.debug("Waiting for device to re-appear...")
for _ in range(10):
sleep(0.2) # Always sleep initially
try:
conn = connect_to_device(serial, connection_types)[0]
conn.close()
break
except ValueError:
logger.debug("Not found, sleep...")
for dev, info in list_all_devices(connection_types):
if info.serial == serial:
return
logger.debug("Not found, sleep...")
else:
logger.warning("Timed out waiting for device")

View File

@ -8,6 +8,7 @@ import subprocess
import tempfile
from mss.exception import ScreenShotError
from PIL import Image
import numpy.core.multiarray # noqa
def _capture_screen():
@ -41,6 +42,6 @@ def scan_qr(image_data=None):
img = _capture_screen()
result = zxingcpp.read_barcode(img)
if result.valid:
if result and result.valid:
return result.text
return None

350
helper/poetry.lock generated
View File

@ -30,7 +30,7 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>
[[package]]
name = "cffi"
version = "1.15.0"
version = "1.15.1"
description = "Foreign Function Interface for Python calling C code."
category = "main"
optional = false
@ -60,7 +60,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "cryptography"
version = "37.0.2"
version = "37.0.4"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
category = "main"
optional = false
@ -101,7 +101,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "importlib-metadata"
version = "4.11.4"
version = "4.12.0"
description = "Read metadata from Python packages"
category = "main"
optional = false
@ -113,7 +113,7 @@ zipp = ">=0.5"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"]
perf = ["ipython"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"]
[[package]]
name = "iniconfig"
@ -174,7 +174,7 @@ python-versions = ">=3.5"
[[package]]
name = "numpy"
version = "1.22.4"
version = "1.23.0"
description = "NumPy is the fundamental package for array computing with Python."
category = "main"
optional = false
@ -204,14 +204,14 @@ future = "*"
[[package]]
name = "pillow"
version = "9.1.1"
version = "9.2.0"
description = "Python Imaging Library (Fork)"
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinx-rtd-theme (>=1.0)", "sphinxext-opengraph"]
docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"]
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
[[package]]
@ -370,7 +370,7 @@ pywin32 = {version = ">=223", markers = "sys_platform == \"win32\""}
type = "git"
url = "https://github.com/Yubico/yubikey-manager.git"
reference = "next"
resolved_reference = "7e0c9e586e507f0dd021a9b7beacc621bfee7baa"
resolved_reference = "32612d177db0d8dd768679ce26c4e509d10f2a97"
[[package]]
name = "zipp"
@ -386,7 +386,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-
[[package]]
name = "zxing-cpp"
version = "1.3.0"
version = "1.4.0"
description = "Python bindings for the zxing-cpp barcode library"
category = "main"
optional = false
@ -414,56 +414,70 @@ attrs = [
{file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
]
cffi = [
{file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"},
{file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"},
{file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"},
{file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"},
{file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"},
{file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"},
{file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"},
{file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"},
{file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"},
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"},
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"},
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"},
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"},
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"},
{file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"},
{file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"},
{file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"},
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"},
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"},
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"},
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"},
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"},
{file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"},
{file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"},
{file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"},
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"},
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"},
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"},
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"},
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"},
{file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"},
{file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"},
{file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"},
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"},
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"},
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"},
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"},
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"},
{file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"},
{file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"},
{file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"},
{file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"},
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"},
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"},
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"},
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"},
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"},
{file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"},
{file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"},
{file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"},
{file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
{file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"},
{file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"},
{file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"},
{file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"},
{file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"},
{file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"},
{file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"},
{file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"},
{file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"},
{file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"},
{file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"},
{file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"},
{file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"},
{file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"},
{file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"},
{file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"},
{file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"},
{file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"},
{file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"},
{file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"},
{file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"},
{file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"},
{file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"},
{file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"},
{file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"},
{file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"},
{file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"},
{file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"},
{file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"},
{file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"},
{file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"},
{file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"},
{file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"},
{file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"},
{file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"},
{file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"},
{file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"},
{file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"},
{file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"},
{file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"},
{file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"},
{file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"},
{file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"},
{file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"},
{file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"},
{file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"},
{file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"},
{file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"},
{file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"},
{file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"},
{file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"},
{file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"},
{file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"},
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"},
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"},
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"},
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"},
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"},
{file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"},
{file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"},
{file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"},
{file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"},
{file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"},
]
click = [
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
@ -474,28 +488,28 @@ colorama = [
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
]
cryptography = [
{file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:ef15c2df7656763b4ff20a9bc4381d8352e6640cfeb95c2972c38ef508e75181"},
{file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3c81599befb4d4f3d7648ed3217e00d21a9341a9a688ecdd615ff72ffbed7336"},
{file = "cryptography-37.0.2-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2bd1096476aaac820426239ab534b636c77d71af66c547b9ddcd76eb9c79e004"},
{file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:31fe38d14d2e5f787e0aecef831457da6cec68e0bb09a35835b0b44ae8b988fe"},
{file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:093cb351031656d3ee2f4fa1be579a8c69c754cf874206be1d4cf3b542042804"},
{file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59b281eab51e1b6b6afa525af2bd93c16d49358404f814fe2c2410058623928c"},
{file = "cryptography-37.0.2-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:0cc20f655157d4cfc7bada909dc5cc228211b075ba8407c46467f63597c78178"},
{file = "cryptography-37.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f8ec91983e638a9bcd75b39f1396e5c0dc2330cbd9ce4accefe68717e6779e0a"},
{file = "cryptography-37.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:46f4c544f6557a2fefa7ac8ac7d1b17bf9b647bd20b16decc8fbcab7117fbc15"},
{file = "cryptography-37.0.2-cp36-abi3-win32.whl", hash = "sha256:731c8abd27693323b348518ed0e0705713a36d79fdbd969ad968fbef0979a7e0"},
{file = "cryptography-37.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:471e0d70201c069f74c837983189949aa0d24bb2d751b57e26e3761f2f782b8d"},
{file = "cryptography-37.0.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a68254dd88021f24a68b613d8c51d5c5e74d735878b9e32cc0adf19d1f10aaf9"},
{file = "cryptography-37.0.2-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:a7d5137e556cc0ea418dca6186deabe9129cee318618eb1ffecbd35bee55ddc1"},
{file = "cryptography-37.0.2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aeaba7b5e756ea52c8861c133c596afe93dd716cbcacae23b80bc238202dc023"},
{file = "cryptography-37.0.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95e590dd70642eb2079d280420a888190aa040ad20f19ec8c6e097e38aa29e06"},
{file = "cryptography-37.0.2-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:1b9362d34363f2c71b7853f6251219298124aa4cc2075ae2932e64c91a3e2717"},
{file = "cryptography-37.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e53258e69874a306fcecb88b7534d61820db8a98655662a3dd2ec7f1afd9132f"},
{file = "cryptography-37.0.2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:1f3bfbd611db5cb58ca82f3deb35e83af34bb8cf06043fa61500157d50a70982"},
{file = "cryptography-37.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:419c57d7b63f5ec38b1199a9521d77d7d1754eb97827bbb773162073ccd8c8d4"},
{file = "cryptography-37.0.2-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:dc26bb134452081859aa21d4990474ddb7e863aa39e60d1592800a8865a702de"},
{file = "cryptography-37.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3b8398b3d0efc420e777c40c16764d6870bcef2eb383df9c6dbb9ffe12c64452"},
{file = "cryptography-37.0.2.tar.gz", hash = "sha256:f224ad253cc9cea7568f49077007d2263efa57396a2f2f78114066fd54b5c68e"},
{file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884"},
{file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6"},
{file = "cryptography-37.0.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046"},
{file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5"},
{file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b"},
{file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8"},
{file = "cryptography-37.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280"},
{file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3"},
{file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59"},
{file = "cryptography-37.0.4-cp36-abi3-win32.whl", hash = "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157"},
{file = "cryptography-37.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327"},
{file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b"},
{file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9"},
{file = "cryptography-37.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67"},
{file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d"},
{file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282"},
{file = "cryptography-37.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa"},
{file = "cryptography-37.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441"},
{file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596"},
{file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a"},
{file = "cryptography-37.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab"},
{file = "cryptography-37.0.4.tar.gz", hash = "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82"},
]
fido2 = [
{file = "fido2-1.0.0-py3-none-any.whl", hash = "sha256:dce13d739b8e0df30505b33f5fd2868fad20f3b309acacce72e5f2d1b0c58761"},
@ -505,8 +519,8 @@ future = [
{file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
]
importlib-metadata = [
{file = "importlib_metadata-4.11.4-py3-none-any.whl", hash = "sha256:c58c8eb8a762858f49e18436ff552e83914778e50e9d2f1660535ffb364552ec"},
{file = "importlib_metadata-4.11.4.tar.gz", hash = "sha256:5d26852efe48c0a32b0509ffbc583fda1a2266545a78d104a6f4aff3db17d700"},
{file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"},
{file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"},
]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
@ -529,28 +543,28 @@ mss = [
{file = "mss-6.1.0.tar.gz", hash = "sha256:aebd069f3e05667fe9c7b9fa4b1771fe42a4710ce1058ce0236936ce06fa5394"},
]
numpy = [
{file = "numpy-1.22.4-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:ba9ead61dfb5d971d77b6c131a9dbee62294a932bf6a356e48c75ae684e635b3"},
{file = "numpy-1.22.4-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:1ce7ab2053e36c0a71e7a13a7475bd3b1f54750b4b433adc96313e127b870887"},
{file = "numpy-1.22.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7228ad13744f63575b3a972d7ee4fd61815b2879998e70930d4ccf9ec721dce0"},
{file = "numpy-1.22.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43a8ca7391b626b4c4fe20aefe79fec683279e31e7c79716863b4b25021e0e74"},
{file = "numpy-1.22.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a911e317e8c826ea632205e63ed8507e0dc877dcdc49744584dfc363df9ca08c"},
{file = "numpy-1.22.4-cp310-cp310-win32.whl", hash = "sha256:9ce7df0abeabe7fbd8ccbf343dc0db72f68549856b863ae3dd580255d009648e"},
{file = "numpy-1.22.4-cp310-cp310-win_amd64.whl", hash = "sha256:3e1ffa4748168e1cc8d3cde93f006fe92b5421396221a02f2274aab6ac83b077"},
{file = "numpy-1.22.4-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:59d55e634968b8f77d3fd674a3cf0b96e85147cd6556ec64ade018f27e9479e1"},
{file = "numpy-1.22.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c1d937820db6e43bec43e8d016b9b3165dcb42892ea9f106c70fb13d430ffe72"},
{file = "numpy-1.22.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4c5d5eb2ec8da0b4f50c9a843393971f31f1d60be87e0fb0917a49133d257d6"},
{file = "numpy-1.22.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64f56fc53a2d18b1924abd15745e30d82a5782b2cab3429aceecc6875bd5add0"},
{file = "numpy-1.22.4-cp38-cp38-win32.whl", hash = "sha256:fb7a980c81dd932381f8228a426df8aeb70d59bbcda2af075b627bbc50207cba"},
{file = "numpy-1.22.4-cp38-cp38-win_amd64.whl", hash = "sha256:e96d7f3096a36c8754207ab89d4b3282ba7b49ea140e4973591852c77d09eb76"},
{file = "numpy-1.22.4-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:4c6036521f11a731ce0648f10c18ae66d7143865f19f7299943c985cdc95afb5"},
{file = "numpy-1.22.4-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:b89bf9b94b3d624e7bb480344e91f68c1c6c75f026ed6755955117de00917a7c"},
{file = "numpy-1.22.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2d487e06ecbf1dc2f18e7efce82ded4f705f4bd0cd02677ffccfb39e5c284c7e"},
{file = "numpy-1.22.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3eb268dbd5cfaffd9448113539e44e2dd1c5ca9ce25576f7c04a5453edc26fa"},
{file = "numpy-1.22.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37431a77ceb9307c28382c9773da9f306435135fae6b80b62a11c53cfedd8802"},
{file = "numpy-1.22.4-cp39-cp39-win32.whl", hash = "sha256:cc7f00008eb7d3f2489fca6f334ec19ca63e31371be28fd5dad955b16ec285bd"},
{file = "numpy-1.22.4-cp39-cp39-win_amd64.whl", hash = "sha256:f0725df166cf4785c0bc4cbfb320203182b1ecd30fee6e541c8752a92df6aa32"},
{file = "numpy-1.22.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0791fbd1e43bf74b3502133207e378901272f3c156c4df4954cad833b1380207"},
{file = "numpy-1.22.4.zip", hash = "sha256:425b390e4619f58d8526b3dcf656dde069133ae5c240229821f01b5f44ea07af"},
{file = "numpy-1.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58bfd40eb478f54ff7a5710dd61c8097e169bc36cc68333d00a9bcd8def53b38"},
{file = "numpy-1.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:196cd074c3f97c4121601790955f915187736f9cf458d3ee1f1b46aff2b1ade0"},
{file = "numpy-1.23.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1d88ef79e0a7fa631bb2c3dda1ea46b32b1fe614e10fedd611d3d5398447f2f"},
{file = "numpy-1.23.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d54b3b828d618a19779a84c3ad952e96e2c2311b16384e973e671aa5be1f6187"},
{file = "numpy-1.23.0-cp310-cp310-win32.whl", hash = "sha256:2b2da66582f3a69c8ce25ed7921dcd8010d05e59ac8d89d126a299be60421171"},
{file = "numpy-1.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:97a76604d9b0e79f59baeca16593c711fddb44936e40310f78bfef79ee9a835f"},
{file = "numpy-1.23.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d8cc87bed09de55477dba9da370c1679bd534df9baa171dd01accbb09687dac3"},
{file = "numpy-1.23.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f0f18804df7370571fb65db9b98bf1378172bd4e962482b857e612d1fec0f53e"},
{file = "numpy-1.23.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac86f407873b952679f5f9e6c0612687e51547af0e14ddea1eedfcb22466babd"},
{file = "numpy-1.23.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae8adff4172692ce56233db04b7ce5792186f179c415c37d539c25de7298d25d"},
{file = "numpy-1.23.0-cp38-cp38-win32.whl", hash = "sha256:fe8b9683eb26d2c4d5db32cd29b38fdcf8381324ab48313b5b69088e0e355379"},
{file = "numpy-1.23.0-cp38-cp38-win_amd64.whl", hash = "sha256:5043bcd71fcc458dfb8a0fc5509bbc979da0131b9d08e3d5f50fb0bbb36f169a"},
{file = "numpy-1.23.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1c29b44905af288b3919803aceb6ec7fec77406d8b08aaa2e8b9e63d0fe2f160"},
{file = "numpy-1.23.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:98e8e0d8d69ff4d3fa63e6c61e8cfe2d03c29b16b58dbef1f9baa175bbed7860"},
{file = "numpy-1.23.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a506cacf2be3a74ead5467aee97b81fca00c9c4c8b3ba16dbab488cd99ba10"},
{file = "numpy-1.23.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:092f5e6025813e64ad6d1b52b519165d08c730d099c114a9247c9bb635a2a450"},
{file = "numpy-1.23.0-cp39-cp39-win32.whl", hash = "sha256:d6ca8dabe696c2785d0c8c9b0d8a9b6e5fdbe4f922bde70d57fa1a2848134f95"},
{file = "numpy-1.23.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc431493df245f3c627c0c05c2bd134535e7929dbe2e602b80e42bf52ff760bc"},
{file = "numpy-1.23.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f9c3fc2adf67762c9fe1849c859942d23f8d3e0bee7b5ed3d4a9c3eeb50a2f07"},
{file = "numpy-1.23.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0d2094e8f4d760500394d77b383a1b06d3663e8892cdf5df3c592f55f3bff66"},
{file = "numpy-1.23.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:94b170b4fa0168cd6be4becf37cb5b127bd12a795123984385b8cd4aca9857e5"},
{file = "numpy-1.23.0.tar.gz", hash = "sha256:bd3fa4fe2e38533d5336e1272fc4e765cabbbde144309ccee8675509d5cd7b05"},
]
packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
@ -560,44 +574,64 @@ pefile = [
{file = "pefile-2022.5.30.tar.gz", hash = "sha256:a5488a3dd1fd021ce33f969780b88fe0f7eebb76eb20996d7318f307612a045b"},
]
pillow = [
{file = "Pillow-9.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:42dfefbef90eb67c10c45a73a9bc1599d4dac920f7dfcbf4ec6b80cb620757fe"},
{file = "Pillow-9.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffde4c6fabb52891d81606411cbfaf77756e3b561b566efd270b3ed3791fde4e"},
{file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c857532c719fb30fafabd2371ce9b7031812ff3889d75273827633bca0c4602"},
{file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59789a7d06c742e9d13b883d5e3569188c16acb02eeed2510fd3bfdbc1bd1530"},
{file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d45dbe4b21a9679c3e8b3f7f4f42a45a7d3ddff8a4a16109dff0e1da30a35b2"},
{file = "Pillow-9.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e9ed59d1b6ee837f4515b9584f3d26cf0388b742a11ecdae0d9237a94505d03a"},
{file = "Pillow-9.1.1-cp310-cp310-win32.whl", hash = "sha256:b3fe2ff1e1715d4475d7e2c3e8dabd7c025f4410f79513b4ff2de3d51ce0fa9c"},
{file = "Pillow-9.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5b650dbbc0969a4e226d98a0b440c2f07a850896aed9266b6fedc0f7e7834108"},
{file = "Pillow-9.1.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:0b4d5ad2cd3a1f0d1df882d926b37dbb2ab6c823ae21d041b46910c8f8cd844b"},
{file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9370d6744d379f2de5d7fa95cdbd3a4d92f0b0ef29609b4b1687f16bc197063d"},
{file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b761727ed7d593e49671d1827044b942dd2f4caae6e51bab144d4accf8244a84"},
{file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a66fe50386162df2da701b3722781cbe90ce043e7d53c1fd6bd801bca6b48d4"},
{file = "Pillow-9.1.1-cp37-cp37m-win32.whl", hash = "sha256:2b291cab8a888658d72b575a03e340509b6b050b62db1f5539dd5cd18fd50578"},
{file = "Pillow-9.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1d4331aeb12f6b3791911a6da82de72257a99ad99726ed6b63f481c0184b6fb9"},
{file = "Pillow-9.1.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8844217cdf66eabe39567118f229e275f0727e9195635a15e0e4b9227458daaf"},
{file = "Pillow-9.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b6617221ff08fbd3b7a811950b5c3f9367f6e941b86259843eab77c8e3d2b56b"},
{file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20d514c989fa28e73a5adbddd7a171afa5824710d0ab06d4e1234195d2a2e546"},
{file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:088df396b047477dd1bbc7de6e22f58400dae2f21310d9e2ec2933b2ef7dfa4f"},
{file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53c27bd452e0f1bc4bfed07ceb235663a1df7c74df08e37fd6b03eb89454946a"},
{file = "Pillow-9.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3f6c1716c473ebd1649663bf3b42702d0d53e27af8b64642be0dd3598c761fb1"},
{file = "Pillow-9.1.1-cp38-cp38-win32.whl", hash = "sha256:c67db410508b9de9c4694c57ed754b65a460e4812126e87f5052ecf23a011a54"},
{file = "Pillow-9.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:f054b020c4d7e9786ae0404278ea318768eb123403b18453e28e47cdb7a0a4bf"},
{file = "Pillow-9.1.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:c17770a62a71718a74b7548098a74cd6880be16bcfff5f937f900ead90ca8e92"},
{file = "Pillow-9.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3f6a6034140e9e17e9abc175fc7a266a6e63652028e157750bd98e804a8ed9a"},
{file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f372d0f08eff1475ef426344efe42493f71f377ec52237bf153c5713de987251"},
{file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09e67ef6e430f90caa093528bd758b0616f8165e57ed8d8ce014ae32df6a831d"},
{file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66daa16952d5bf0c9d5389c5e9df562922a59bd16d77e2a276e575d32e38afd1"},
{file = "Pillow-9.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d78ca526a559fb84faaaf84da2dd4addef5edb109db8b81677c0bb1aad342601"},
{file = "Pillow-9.1.1-cp39-cp39-win32.whl", hash = "sha256:55e74faf8359ddda43fee01bffbc5bd99d96ea508d8a08c527099e84eb708f45"},
{file = "Pillow-9.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:7c150dbbb4a94ea4825d1e5f2c5501af7141ea95825fadd7829f9b11c97aaf6c"},
{file = "Pillow-9.1.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:769a7f131a2f43752455cc72f9f7a093c3ff3856bf976c5fb53a59d0ccc704f6"},
{file = "Pillow-9.1.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:488f3383cf5159907d48d32957ac6f9ea85ccdcc296c14eca1a4e396ecc32098"},
{file = "Pillow-9.1.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b525a356680022b0af53385944026d3486fc8c013638cf9900eb87c866afb4c"},
{file = "Pillow-9.1.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6e760cf01259a1c0a50f3c845f9cad1af30577fd8b670339b1659c6d0e7a41dd"},
{file = "Pillow-9.1.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4165205a13b16a29e1ac57efeee6be2dfd5b5408122d59ef2145bc3239fa340"},
{file = "Pillow-9.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937a54e5694684f74dcbf6e24cc453bfc5b33940216ddd8f4cd8f0f79167f765"},
{file = "Pillow-9.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:baf3be0b9446a4083cc0c5bb9f9c964034be5374b5bc09757be89f5d2fa247b8"},
{file = "Pillow-9.1.1.tar.gz", hash = "sha256:7502539939b53d7565f3d11d87c78e7ec900d3c72945d4ee0e2f250d598309a0"},
{file = "Pillow-9.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a9c9bc489f8ab30906d7a85afac4b4944a572a7432e00698a7239f44a44e6efb"},
{file = "Pillow-9.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:510cef4a3f401c246cfd8227b300828715dd055463cdca6176c2e4036df8bd4f"},
{file = "Pillow-9.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7888310f6214f19ab2b6df90f3f06afa3df7ef7355fc025e78a3044737fab1f5"},
{file = "Pillow-9.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:831e648102c82f152e14c1a0938689dbb22480c548c8d4b8b248b3e50967b88c"},
{file = "Pillow-9.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cc1d2451e8a3b4bfdb9caf745b58e6c7a77d2e469159b0d527a4554d73694d1"},
{file = "Pillow-9.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:136659638f61a251e8ed3b331fc6ccd124590eeff539de57c5f80ef3a9594e58"},
{file = "Pillow-9.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6e8c66f70fb539301e064f6478d7453e820d8a2c631da948a23384865cd95544"},
{file = "Pillow-9.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:37ff6b522a26d0538b753f0b4e8e164fdada12db6c6f00f62145d732d8a3152e"},
{file = "Pillow-9.2.0-cp310-cp310-win32.whl", hash = "sha256:c79698d4cd9318d9481d89a77e2d3fcaeff5486be641e60a4b49f3d2ecca4e28"},
{file = "Pillow-9.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:254164c57bab4b459f14c64e93df11eff5ded575192c294a0c49270f22c5d93d"},
{file = "Pillow-9.2.0-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:408673ed75594933714482501fe97e055a42996087eeca7e5d06e33218d05aa8"},
{file = "Pillow-9.2.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:727dd1389bc5cb9827cbd1f9d40d2c2a1a0c9b32dd2261db522d22a604a6eec9"},
{file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50dff9cc21826d2977ef2d2a205504034e3a4563ca6f5db739b0d1026658e004"},
{file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb6259196a589123d755380b65127ddc60f4c64b21fc3bb46ce3a6ea663659b0"},
{file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0554af24df2bf96618dac71ddada02420f946be943b181108cac55a7a2dcd4"},
{file = "Pillow-9.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:15928f824870535c85dbf949c09d6ae7d3d6ac2d6efec80f3227f73eefba741c"},
{file = "Pillow-9.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:bdd0de2d64688ecae88dd8935012c4a72681e5df632af903a1dca8c5e7aa871a"},
{file = "Pillow-9.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5b87da55a08acb586bad5c3aa3b86505f559b84f39035b233d5bf844b0834b1"},
{file = "Pillow-9.2.0-cp311-cp311-win32.whl", hash = "sha256:b6d5e92df2b77665e07ddb2e4dbd6d644b78e4c0d2e9272a852627cdba0d75cf"},
{file = "Pillow-9.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6bf088c1ce160f50ea40764f825ec9b72ed9da25346216b91361eef8ad1b8f8c"},
{file = "Pillow-9.2.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:2c58b24e3a63efd22554c676d81b0e57f80e0a7d3a5874a7e14ce90ec40d3069"},
{file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eef7592281f7c174d3d6cbfbb7ee5984a671fcd77e3fc78e973d492e9bf0eb3f"},
{file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dcd7b9c7139dc8258d164b55696ecd16c04607f1cc33ba7af86613881ffe4ac8"},
{file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a138441e95562b3c078746a22f8fca8ff1c22c014f856278bdbdd89ca36cff1b"},
{file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:93689632949aff41199090eff5474f3990b6823404e45d66a5d44304e9cdc467"},
{file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:f3fac744f9b540148fa7715a435d2283b71f68bfb6d4aae24482a890aed18b59"},
{file = "Pillow-9.2.0-cp37-cp37m-win32.whl", hash = "sha256:fa768eff5f9f958270b081bb33581b4b569faabf8774726b283edb06617101dc"},
{file = "Pillow-9.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:69bd1a15d7ba3694631e00df8de65a8cb031911ca11f44929c97fe05eb9b6c1d"},
{file = "Pillow-9.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:030e3460861488e249731c3e7ab59b07c7853838ff3b8e16aac9561bb345da14"},
{file = "Pillow-9.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:74a04183e6e64930b667d321524e3c5361094bb4af9083db5c301db64cd341f3"},
{file = "Pillow-9.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d33a11f601213dcd5718109c09a52c2a1c893e7461f0be2d6febc2879ec2402"},
{file = "Pillow-9.2.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fd6f5e3c0e4697fa7eb45b6e93996299f3feee73a3175fa451f49a74d092b9f"},
{file = "Pillow-9.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a647c0d4478b995c5e54615a2e5360ccedd2f85e70ab57fbe817ca613d5e63b8"},
{file = "Pillow-9.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:4134d3f1ba5f15027ff5c04296f13328fecd46921424084516bdb1b2548e66ff"},
{file = "Pillow-9.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:bc431b065722a5ad1dfb4df354fb9333b7a582a5ee39a90e6ffff688d72f27a1"},
{file = "Pillow-9.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1536ad017a9f789430fb6b8be8bf99d2f214c76502becc196c6f2d9a75b01b76"},
{file = "Pillow-9.2.0-cp38-cp38-win32.whl", hash = "sha256:2ad0d4df0f5ef2247e27fc790d5c9b5a0af8ade9ba340db4a73bb1a4a3e5fb4f"},
{file = "Pillow-9.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:ec52c351b35ca269cb1f8069d610fc45c5bd38c3e91f9ab4cbbf0aebc136d9c8"},
{file = "Pillow-9.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ed2c4ef2451de908c90436d6e8092e13a43992f1860275b4d8082667fbb2ffc"},
{file = "Pillow-9.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ad2f835e0ad81d1689f1b7e3fbac7b01bb8777d5a985c8962bedee0cc6d43da"},
{file = "Pillow-9.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea98f633d45f7e815db648fd7ff0f19e328302ac36427343e4432c84432e7ff4"},
{file = "Pillow-9.2.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7761afe0126d046974a01e030ae7529ed0ca6a196de3ec6937c11df0df1bc91c"},
{file = "Pillow-9.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a54614049a18a2d6fe156e68e188da02a046a4a93cf24f373bffd977e943421"},
{file = "Pillow-9.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:5aed7dde98403cd91d86a1115c78d8145c83078e864c1de1064f52e6feb61b20"},
{file = "Pillow-9.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:13b725463f32df1bfeacbf3dd197fb358ae8ebcd8c5548faa75126ea425ccb60"},
{file = "Pillow-9.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:808add66ea764ed97d44dda1ac4f2cfec4c1867d9efb16a33d158be79f32b8a4"},
{file = "Pillow-9.2.0-cp39-cp39-win32.whl", hash = "sha256:337a74fd2f291c607d220c793a8135273c4c2ab001b03e601c36766005f36885"},
{file = "Pillow-9.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:fac2d65901fb0fdf20363fbd345c01958a742f2dc62a8dd4495af66e3ff502a4"},
{file = "Pillow-9.2.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ad2277b185ebce47a63f4dc6302e30f05762b688f8dc3de55dbae4651872cdf3"},
{file = "Pillow-9.2.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c7b502bc34f6e32ba022b4a209638f9e097d7a9098104ae420eb8186217ebbb"},
{file = "Pillow-9.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d1f14f5f691f55e1b47f824ca4fdcb4b19b4323fe43cc7bb105988cad7496be"},
{file = "Pillow-9.2.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:dfe4c1fedfde4e2fbc009d5ad420647f7730d719786388b7de0999bf32c0d9fd"},
{file = "Pillow-9.2.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:f07f1f00e22b231dd3d9b9208692042e29792d6bd4f6639415d2f23158a80013"},
{file = "Pillow-9.2.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1802f34298f5ba11d55e5bb09c31997dc0c6aed919658dfdf0198a2fe75d5490"},
{file = "Pillow-9.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17d4cafe22f050b46d983b71c707162d63d796a1235cdf8b9d7a112e97b15bac"},
{file = "Pillow-9.2.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96b5e6874431df16aee0c1ba237574cb6dff1dcb173798faa6a9d8b399a05d0e"},
{file = "Pillow-9.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:0030fdbd926fb85844b8b92e2f9449ba89607231d3dd597a21ae72dc7fe26927"},
{file = "Pillow-9.2.0.tar.gz", hash = "sha256:75e636fd3e0fb872693f23ccb8a5ff2cd578801251f3a4f6854c6a5d437d3c04"},
]
pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
@ -677,19 +711,19 @@ zipp = [
{file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"},
]
zxing-cpp = [
{file = "zxing-cpp-1.3.0.tar.gz", hash = "sha256:5f30545afad01a278fc8c17efae11d82e36f8c2caa87c89096aec5a8d69103b2"},
{file = "zxing_cpp-1.3.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3a6e183b6c0aae9378f674f9e7714a39482595915cf15198d10b9ba8c33b25f"},
{file = "zxing_cpp-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88eadb723d20655caf81a6ba6ef64d74a266f57cbd782da82736c52a61a73fa5"},
{file = "zxing_cpp-1.3.0-cp310-cp310-win32.whl", hash = "sha256:15fb165ada1730ab0d96b67eb2d9827870d9ae534686e27541f3b3add15b96d7"},
{file = "zxing_cpp-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:8dbb17a31ee1ac2c946a96e83b170ecefbc87a52b9c35b41809d9afff77d8879"},
{file = "zxing_cpp-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:31578db20ba0668e010cb62e4718cb86f47563ec5122e29a0746651ff1e13735"},
{file = "zxing_cpp-1.3.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9253a3b6c8c143f3c22d172922226b10c8cc319d2554c73107fefce7e263daaa"},
{file = "zxing_cpp-1.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:250afd201f08bd1be8fd349766e32ef184a463b616c13102b2f80a4422695957"},
{file = "zxing_cpp-1.3.0-cp38-cp38-win32.whl", hash = "sha256:d2891dfba5c53b913867e7b01b8b430d801e15e54f53b3c05b9645dc824dfed3"},
{file = "zxing_cpp-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:6201e60cbefbc8de90c5f18e6e25c3cb1be19be8f369bf4dad3ab910b954f29d"},
{file = "zxing_cpp-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:44467984c1a65a332c8656926f30af1752c1ff774c6a030b95572e0a1543b23b"},
{file = "zxing_cpp-1.3.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0dbb54f8694063376d73be6f7dbddd39f3e7907ab885403d90cff7d518c54f7f"},
{file = "zxing_cpp-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3cff8a7fe960c2016bc8e217fcf02b9b1ac61b17fc5c0c5158f853088be4ad9"},
{file = "zxing_cpp-1.3.0-cp39-cp39-win32.whl", hash = "sha256:f75431cf7cddcb21c267d39a5895831a3c20abfa7676426974652d25b29ae429"},
{file = "zxing_cpp-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:de9dd0a2d01969e9828c5704d709b2559a417fea562bd2f308ebc8d4a9678b5e"},
{file = "zxing-cpp-1.4.0.tar.gz", hash = "sha256:3d3ec36954ecbf9b0f633dab4b8cebcf0059d8a27f7a5969c4e41a308111af38"},
{file = "zxing_cpp-1.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25b11d77cf6b9f7405af3ed6bacf4a6e0756ea74dfda7040ff53e7c58f352b05"},
{file = "zxing_cpp-1.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1f849205237d4bda462d0a4b745e72494f825e5b6b06581e05b58d34d9869aa"},
{file = "zxing_cpp-1.4.0-cp310-cp310-win32.whl", hash = "sha256:76e9777d943af3c51b6406b323b3f28cbf9e40cc65b53cf847fda08295f18e48"},
{file = "zxing_cpp-1.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:234d672e34e607ffc8e06639e79c8e1bf2ddb7c249134a6836569e92a2f2dd64"},
{file = "zxing_cpp-1.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1f66c61e43163740c59c58880c3a8c41ebd2109573c0494f255c9c96134e8c"},
{file = "zxing_cpp-1.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9418e1bd0775820a4933b60007b7f8a177e4ddd23692c1aaed2348fafc0a8e01"},
{file = "zxing_cpp-1.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bc1e48ddfd6692d183782f091fbf54e5e1d36d0070822b1eab14cfb580b1625"},
{file = "zxing_cpp-1.4.0-cp38-cp38-win32.whl", hash = "sha256:4f340b6907780e8eb0e6473fec43ea145c4dd3275e3c21d6f887c0e28e114f29"},
{file = "zxing_cpp-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:71772f81c4602133b2dba6a1107339ed965725001ce9a4caaf772598110351a1"},
{file = "zxing_cpp-1.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:331bec6b0ac8a9b339bc82956c52c022e7b2debfeb9102209483eb7538ed72d4"},
{file = "zxing_cpp-1.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b0844c6ad3c944452c980a025238ba3fbd3a414fd2c36e2bec1bc5bed03b21e"},
{file = "zxing_cpp-1.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a770aff618cd00dda3922de2f7085c1f84bbe02f2b6df114d19054ad41c52fb0"},
{file = "zxing_cpp-1.4.0-cp39-cp39-win32.whl", hash = "sha256:ebe67de6a4d3c48a5ee52211ecf2003301ab39bd7d7b7dfa72ae80be429cfcf9"},
{file = "zxing_cpp-1.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:d0e8b54b29497ed9238f31ce522ddb0189c0d6c4597787ef2eb823ca9fb42350"},
]

View File

@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:yubico_authenticator/android/init.dart' as android;
import 'package:yubico_authenticator/app/views/no_device_screen.dart';
import 'package:yubico_authenticator/app/views/device_error_screen.dart';
import 'package:yubico_authenticator/core/state.dart';
import 'package:yubico_authenticator/desktop/init.dart' as desktop;
import 'package:yubico_authenticator/oath/views/account_list.dart';
@ -55,7 +55,7 @@ void main() {
await tester.pumpWidget(initializedApp);
await tester.pump(const Duration(milliseconds: 500));
expect(find.byType(NoDeviceScreen), findsNothing,
expect(find.byType(DeviceErrorScreen), findsNothing,
reason: 'No YubiKey connected');
expect(find.byType(OathScreen), findsOneWidget);

View File

@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v3.0.3), do not edit directly.
// Autogenerated from Pigeon (v3.1.6), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name
// @dart = 2.12

View File

@ -12,11 +12,11 @@ import '../management/models.dart';
final _log = Logger('yubikeyDataCommandProvider');
final androidYubikeyProvider =
StateNotifierProvider<_YubikeyProvider, YubiKeyData?>((ref) {
return _YubikeyProvider(null, ref);
StateNotifierProvider<_YubikeyProvider, AsyncValue<YubiKeyData>>((ref) {
return _YubikeyProvider(const AsyncValue.loading(), ref);
});
class _YubikeyProvider extends StateNotifier<YubiKeyData?> {
class _YubikeyProvider extends StateNotifier<AsyncValue<YubiKeyData>> {
final Ref _ref;
_YubikeyProvider(super.yubiKeyData, this._ref);
@ -24,7 +24,7 @@ class _YubikeyProvider extends StateNotifier<YubiKeyData?> {
try {
if (input.isEmpty) {
_log.debug('Yubikey was detached.');
state = null;
state = const AsyncValue.loading();
// reset other providers when YubiKey is removed
_ref.refresh(androidStateProvider);
@ -49,15 +49,17 @@ class _YubikeyProvider extends StateNotifier<YubiKeyData?> {
// reset oath providers on key change
var yubiKeyData = YubiKeyData(deviceNode, name, deviceInfo);
if (state != yubiKeyData && state != null) {
_ref.refresh(androidStateProvider);
_ref.refresh(androidCredentialsProvider);
}
state.whenData((data) {
if (data != yubiKeyData) {
_ref.refresh(androidStateProvider);
_ref.refresh(androidCredentialsProvider);
}
});
state = yubiKeyData;
state = AsyncValue.data(yubiKeyData);
} on Exception catch (e) {
_log.debug('Invalid data for yubikey: $input. $e');
state = null;
state = AsyncValue.error(e);
}
}
}

View File

@ -35,18 +35,17 @@ class _AndroidSubPageNotifier extends CurrentAppNotifier {
final androidAttachedDevicesProvider =
StateNotifierProvider<AttachedDevicesNotifier, List<DeviceNode>>((ref) {
var currentDeviceData = ref.watch(androidDeviceDataProvider);
if (currentDeviceData != null) {
return _AndroidAttachedDevicesNotifier([currentDeviceData.node]);
}
return _AndroidAttachedDevicesNotifier([]);
List<DeviceNode> devs = currentDeviceData.maybeWhen(
data: (data) => [data.node], orElse: () => []);
return _AndroidAttachedDevicesNotifier(devs);
});
class _AndroidAttachedDevicesNotifier extends AttachedDevicesNotifier {
_AndroidAttachedDevicesNotifier(super.state);
}
final androidDeviceDataProvider =
Provider<YubiKeyData?>((ref) => ref.watch(androidYubikeyProvider));
final androidDeviceDataProvider = Provider<AsyncValue<YubiKeyData>>(
(ref) => ref.watch(androidYubikeyProvider));
final androidCurrentDeviceProvider =
StateNotifierProvider<CurrentDeviceNotifier, DeviceNode?>((ref) {

View File

@ -1,60 +1,67 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../widgets/toast.dart';
import 'models.dart';
import 'state.dart';
ScaffoldFeatureController showMessage(
void Function() showMessage(
BuildContext context,
String message, {
Duration duration = const Duration(seconds: 1),
}) {
final width = MediaQuery.of(context).size.width;
final narrow = width < 540;
return ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(message),
duration: duration,
behavior: narrow ? SnackBarBehavior.fixed : SnackBarBehavior.floating,
width: narrow ? null : 400,
));
}
Duration duration = const Duration(seconds: 2),
}) =>
showToast(context, message, duration: duration);
Future<void> showBottomMenu(
BuildContext context, List<MenuAction> actions) async {
MediaQuery? mediaQuery = context.findAncestorWidgetOfExactType<MediaQuery>();
var width = mediaQuery?.data.size.width ?? 0;
await showModalBottomSheet(
await showBlurDialog(
context: context,
constraints: width > 540 ? const BoxConstraints(maxWidth: 380) : null,
builder: (context) => SafeArea(child: _BottomMenu(actions)));
builder: (context) {
return AlertDialog(
title: const Text('Options'),
contentPadding: const EdgeInsets.only(bottom: 24, top: 4),
content: Column(
mainAxisSize: MainAxisSize.min,
children: actions
.map((a) => ListTile(
leading: a.icon,
title: Text(a.text),
contentPadding:
const EdgeInsets.symmetric(horizontal: 24),
enabled: a.action != null,
onTap: a.action == null
? null
: () {
Navigator.pop(context);
a.action?.call(context);
},
))
.toList(),
),
);
});
}
class _BottomMenu extends ConsumerWidget {
final List<MenuAction> actions;
const _BottomMenu(this.actions);
@override
Widget build(BuildContext context, WidgetRef ref) {
// If current device changes, we need to pop back to the main Page.
ref.listen<DeviceNode?>(currentDeviceProvider, (previous, next) {
Navigator.of(context).pop();
});
return Column(
mainAxisSize: MainAxisSize.min,
children: actions
.map((a) => ListTile(
leading: a.icon,
title: Text(a.text),
enabled: a.action != null,
onTap: a.action == null
? null
: () {
Navigator.pop(context);
a.action?.call(context);
},
))
.toList(),
Future<T?> showBlurDialog<T>({
required BuildContext context,
required Widget Function(BuildContext) builder,
RouteSettings? routeSettings,
}) =>
showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
barrierColor: Colors.black12,
pageBuilder: (ctx, anim1, anim2) => builder(ctx),
transitionDuration: const Duration(milliseconds: 150),
transitionBuilder: (ctx, anim1, anim2, child) => BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 20 * anim1.value, sigmaY: 20 * anim1.value),
child: FadeTransition(
opacity: anim1,
child: child,
),
),
routeSettings: routeSettings,
);
}
}

View File

@ -175,11 +175,11 @@ abstract class _YubiKeyData implements YubiKeyData {
_$_YubiKeyData;
@override
DeviceNode get node => throw _privateConstructorUsedError;
DeviceNode get node;
@override
String get name => throw _privateConstructorUsedError;
String get name;
@override
DeviceInfo get info => throw _privateConstructorUsedError;
DeviceInfo get info;
@override
@JsonKey(ignore: true)
_$$_YubiKeyDataCopyWith<_$_YubiKeyData> get copyWith =>
@ -454,11 +454,11 @@ abstract class UsbYubiKeyNode extends DeviceNode {
UsbYubiKeyNode._() : super._();
@override
DevicePath get path => throw _privateConstructorUsedError;
DevicePath get path;
@override
String get name => throw _privateConstructorUsedError;
UsbPid get pid => throw _privateConstructorUsedError;
DeviceInfo? get info => throw _privateConstructorUsedError;
String get name;
UsbPid get pid;
DeviceInfo? get info;
@override
@JsonKey(ignore: true)
_$$UsbYubiKeyNodeCopyWith<_$UsbYubiKeyNode> get copyWith =>
@ -613,9 +613,9 @@ abstract class NfcReaderNode extends DeviceNode {
NfcReaderNode._() : super._();
@override
DevicePath get path => throw _privateConstructorUsedError;
DevicePath get path;
@override
String get name => throw _privateConstructorUsedError;
String get name;
@override
@JsonKey(ignore: true)
_$$NfcReaderNodeCopyWith<_$NfcReaderNode> get copyWith =>
@ -762,11 +762,11 @@ abstract class _MenuAction implements MenuAction {
final void Function(BuildContext)? action}) = _$_MenuAction;
@override
String get text => throw _privateConstructorUsedError;
String get text;
@override
Widget get icon => throw _privateConstructorUsedError;
Widget get icon;
@override
void Function(BuildContext)? get action => throw _privateConstructorUsedError;
void Function(BuildContext)? get action;
@override
@JsonKey(ignore: true)
_$$_MenuActionCopyWith<_$_MenuAction> get copyWith =>
@ -914,11 +914,11 @@ abstract class _WindowState implements WindowState {
required final bool active}) = _$_WindowState;
@override
bool get focused => throw _privateConstructorUsedError;
bool get focused;
@override
bool get visible => throw _privateConstructorUsedError;
bool get visible;
@override
bool get active => throw _privateConstructorUsedError;
bool get active;
@override
@JsonKey(ignore: true)
_$$_WindowStateCopyWith<_$_WindowState> get copyWith =>

View File

@ -60,7 +60,7 @@ class AttachedDevicesNotifier extends StateNotifier<List<DeviceNode>> {
}
// Override with platform implementation
final currentDeviceDataProvider = Provider<YubiKeyData?>(
final currentDeviceDataProvider = Provider<AsyncValue<YubiKeyData>>(
(ref) => throw UnimplementedError(),
);
@ -77,8 +77,8 @@ abstract class CurrentDeviceNotifier extends StateNotifier<DeviceNode?> {
final currentAppProvider =
StateNotifierProvider<CurrentAppNotifier, Application>((ref) {
final notifier = CurrentAppNotifier(ref.watch(supportedAppsProvider));
ref.listen<YubiKeyData?>(currentDeviceDataProvider, (_, data) {
notifier._notifyDeviceChanged(data);
ref.listen<AsyncValue<YubiKeyData>>(currentDeviceDataProvider, (_, data) {
notifier._notifyDeviceChanged(data.whenOrNull(data: ((data) => data)));
}, fireImmediately: true);
return notifier;
});

View File

@ -49,7 +49,7 @@ class AppFailurePage extends ConsumerWidget {
icon: const Icon(Icons.lock_open),
style: AppTheme.primaryOutlinedButtonStyle(context),
onPressed: () async {
final controller = showMessage(
final closeMessage = showMessage(
context, 'Elevating permissions...',
duration: const Duration(seconds: 30));
try {
@ -59,7 +59,7 @@ class AppFailurePage extends ConsumerWidget {
showMessage(context, 'Permission denied');
}
} finally {
controller.close();
closeMessage();
}
}),
];

View File

@ -9,12 +9,14 @@ class AppPage extends ConsumerWidget {
final Widget? title;
final Widget child;
final List<Widget> actions;
final List<PopupMenuEntry> keyActions;
final bool centered;
AppPage({
super.key,
this.title,
required this.child,
this.actions = const [],
this.keyActions = const [],
this.centered = false,
});
@ -82,10 +84,10 @@ class AppPage extends ConsumerWidget {
title: title,
centerTitle: true,
titleTextStyle: Theme.of(context).textTheme.titleLarge,
actions: const [
actions: [
Padding(
padding: EdgeInsets.only(right: 6),
child: DeviceButton(),
padding: const EdgeInsets.only(right: 6),
child: DeviceButton(actions: keyActions),
),
],
),

View File

@ -1,75 +1,80 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../widgets/custom_icons.dart';
import '../models.dart';
import '../state.dart';
import 'device_images.dart';
class DeviceAvatar extends StatelessWidget {
final bool selected;
final Widget child;
final Widget? badge;
final double? radius;
const DeviceAvatar(
{super.key,
this.selected = false,
required this.child,
this.badge,
this.radius});
const DeviceAvatar({super.key, required this.child, this.badge, this.radius});
factory DeviceAvatar.yubiKeyData(YubiKeyData data,
{bool selected = false, double? radius}) =>
factory DeviceAvatar.yubiKeyData(YubiKeyData data, {double? radius}) =>
DeviceAvatar(
badge: data.node is NfcReaderNode ? nfcIcon : null,
selected: selected,
radius: radius,
child: getProductImage(data.info, data.name),
);
factory DeviceAvatar.deviceNode(DeviceNode node,
{bool selected = false, double? radius}) =>
factory DeviceAvatar.deviceNode(DeviceNode node, {double? radius}) =>
node.map(
usbYubiKey: (node) {
final info = node.info;
if (info != null) {
return DeviceAvatar.yubiKeyData(
YubiKeyData(node, node.name, info),
selected: selected,
radius: radius,
);
}
return DeviceAvatar(
selected: selected,
radius: radius,
child: const Icon(Icons.device_unknown),
);
},
nfcReader: (_) => DeviceAvatar(
selected: selected,
radius: radius,
child: nfcIcon,
),
);
factory DeviceAvatar.currentDevice(WidgetRef ref, {double? radius}) {
final deviceNode = ref.watch(currentDeviceProvider);
if (deviceNode != null) {
return ref.watch(currentDeviceDataProvider).maybeWhen(
data: (data) => DeviceAvatar.yubiKeyData(
data,
radius: radius,
),
orElse: () => DeviceAvatar.deviceNode(
deviceNode,
radius: radius,
),
);
} else {
return DeviceAvatar(
radius: radius,
child: const Icon(Icons.usb),
);
}
}
@override
Widget build(BuildContext context) {
final radius = this.radius ?? 24;
final radius = this.radius ?? 20;
return Stack(
alignment: AlignmentDirectional.bottomEnd,
children: [
CircleAvatar(
radius: radius,
backgroundColor: selected
? Theme.of(context).colorScheme.primary
: Colors.transparent,
child: CircleAvatar(
radius: radius - 1,
backgroundColor: Theme.of(context).colorScheme.background,
child: IconTheme(
data: IconTheme.of(context).copyWith(
size: radius,
),
child: child,
backgroundColor: Theme.of(context).colorScheme.background,
child: IconTheme(
data: IconTheme.of(context).copyWith(
size: radius,
),
child: child,
),
),
if (badge != null)

View File

@ -1,57 +1,127 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../message.dart';
import '../state.dart';
import 'device_avatar.dart';
import 'device_picker_dialog.dart';
import 'device_utils.dart';
class _CircledDeviceAvatar extends ConsumerWidget {
final double radius;
const _CircledDeviceAvatar(this.radius);
@override
Widget build(BuildContext context, WidgetRef ref) => CircleAvatar(
radius: radius,
backgroundColor: Theme.of(context).colorScheme.primary,
child: IconTheme(
// Force the standard icon theme
data: IconTheme.of(context),
child: DeviceAvatar.currentDevice(ref, radius: radius - 1),
),
);
}
class DeviceButton extends ConsumerWidget {
final double radius;
const DeviceButton({super.key, this.radius = 16});
final List<PopupMenuEntry> actions;
const DeviceButton({super.key, this.actions = const [], this.radius = 16});
@override
Widget build(BuildContext context, WidgetRef ref) {
final deviceNode = ref.watch(currentDeviceProvider);
final deviceData = ref.watch(currentDeviceDataProvider);
Widget deviceWidget;
if (deviceNode != null) {
if (deviceData != null) {
deviceWidget = DeviceAvatar.yubiKeyData(
deviceData,
selected: true,
radius: radius,
);
} else {
deviceWidget = DeviceAvatar.deviceNode(
deviceNode,
selected: true,
radius: radius,
);
}
} else {
deviceWidget = DeviceAvatar(
radius: radius,
selected: true,
child: const Icon(Icons.usb),
);
}
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: IconButton(
tooltip: 'Select YubiKey or device',
tooltip: 'More actions',
icon: OverflowBox(
maxHeight: 44,
maxWidth: 44,
child: deviceWidget,
child: _CircledDeviceAvatar(radius),
),
onPressed: () {
showDialog(
final withContext = ref.read(withContextProvider);
showMenu(
context: context,
builder: (context) => const DevicePickerDialog(),
routeSettings: const RouteSettings(name: 'device_picker'),
position: const RelativeRect.fromLTRB(100, 0, 0, 0),
items: <PopupMenuEntry>[
PopupMenuItem(
padding: const EdgeInsets.only(left: 11, right: 16),
onTap: () {
// Wait for menu to close, and use the main context to open
Timer.run(() {
withContext(
(context) async {
await showBlurDialog(
context: context,
builder: (context) => const DevicePickerDialog(),
routeSettings:
const RouteSettings(name: 'device_picker'),
);
},
);
});
},
child: _SlideInWidget(radius: radius),
),
if (actions.isNotEmpty) const PopupMenuDivider(),
...actions,
],
);
},
),
);
}
}
class _SlideInWidget extends ConsumerStatefulWidget {
final double radius;
const _SlideInWidget({required this.radius});
@override
ConsumerState<_SlideInWidget> createState() => _SlideInWidgetState();
}
class _SlideInWidgetState extends ConsumerState<_SlideInWidget>
with SingleTickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
)..forward();
late final Animation<Offset> _offsetAnimation = Tween<Offset>(
begin: const Offset(0.9, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
));
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final messages = getDeviceMessages(
ref.watch(currentDeviceProvider),
ref.watch(currentDeviceDataProvider),
);
return SlideTransition(
position: _offsetAnimation,
child: ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
minLeadingWidth: 0,
horizontalTitleGap: 13,
leading: _CircledDeviceAvatar(widget.radius),
title: Text(messages.removeAt(0)),
subtitle: Text(messages.first),
),
);
}
}

View File

@ -12,9 +12,10 @@ import 'device_avatar.dart';
import 'graphics.dart';
import 'message_page.dart';
class NoDeviceScreen extends ConsumerWidget {
final DeviceNode? node;
const NoDeviceScreen(this.node, {super.key});
class DeviceErrorScreen extends ConsumerWidget {
final DeviceNode node;
final Object? error;
const DeviceErrorScreen(this.node, {this.error, super.key});
Widget _buildUsbPid(BuildContext context, WidgetRef ref, UsbPid pid) {
if (pid.usbInterfaces == UsbInterface.fido.value) {
@ -29,7 +30,7 @@ class NoDeviceScreen extends ConsumerWidget {
label: const Text('Unlock'),
icon: const Icon(Icons.lock_open),
onPressed: () async {
final controller = showMessage(
final closeMessage = showMessage(
context, 'Elevating permissions...',
duration: const Duration(seconds: 30));
try {
@ -39,7 +40,7 @@ class NoDeviceScreen extends ConsumerWidget {
showMessage(context, 'Permission denied');
}
} finally {
controller.close();
closeMessage();
}
},
),
@ -55,12 +56,19 @@ class NoDeviceScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return node?.map(
usbYubiKey: (node) => _buildUsbPid(context, ref, node.pid),
nfcReader: (_) => const MessagePage(
message: 'Place your YubiKey on the NFC reader',
),
) ??
const MessagePage(message: 'Insert your YubiKey');
return node.map(
usbYubiKey: (node) => _buildUsbPid(context, ref, node.pid),
nfcReader: (node) {
final String message;
switch (error) {
case 'unknown-device':
message = 'Unrecognized device';
break;
default:
message = 'Place your YubiKey on the NFC reader';
}
return MessagePage(message: message);
},
);
}
}

View File

@ -2,28 +2,84 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../core/state.dart';
import '../../management/models.dart';
import '../models.dart';
import '../state.dart';
import 'device_avatar.dart';
import 'device_utils.dart';
String _getSubtitle(DeviceInfo info) {
final serial = info.serial;
var subtitle = '';
if (serial != null) {
subtitle += 'S/N: $serial ';
final _hiddenDevicesProvider =
StateNotifierProvider<_HiddenDevicesNotifier, List<String>>(
(ref) => _HiddenDevicesNotifier(ref.watch(prefProvider)));
class _HiddenDevicesNotifier extends StateNotifier<List<String>> {
static const String _key = 'DEVICE_PICKER_HIDDEN';
final SharedPreferences _prefs;
_HiddenDevicesNotifier(this._prefs) : super(_prefs.getStringList(_key) ?? []);
void showAll() {
state = [];
_prefs.setStringList(_key, state);
}
void hideDevice(DevicePath devicePath) {
state = [...state, devicePath.key];
_prefs.setStringList(_key, state);
}
subtitle += 'F/W: ${info.version}';
return subtitle;
}
class DevicePickerDialog extends ConsumerWidget {
class DevicePickerDialog extends StatefulWidget {
const DevicePickerDialog({super.key});
@override
State<StatefulWidget> createState() => _DevicePickerDialogState();
}
class _DevicePickerDialogState extends State<DevicePickerDialog> {
late FocusScopeNode _focus;
@override
void initState() {
super.initState();
_focus = FocusScopeNode();
}
@override
void dispose() {
_focus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// This keeps the focus in the dialog, even if the underlying page
// changes as it does when a new device is selected.
return FocusScope(
node: _focus,
autofocus: true,
onFocusChange: (focused) {
if (!focused) {
_focus.requestFocus();
}
},
child: const _DevicePickerContent(),
);
}
}
class _DevicePickerContent extends ConsumerWidget {
const _DevicePickerContent();
@override
Widget build(BuildContext context, WidgetRef ref) {
final devices = ref.watch(attachedDevicesProvider).toList();
final hidden = ref.watch(_hiddenDevicesProvider);
final devices = ref
.watch(attachedDevicesProvider)
.where((e) => !hidden.contains(e.path.key))
.toList();
final currentNode = ref.watch(currentDeviceProvider);
final Widget hero;
@ -33,24 +89,27 @@ class DevicePickerDialog extends ConsumerWidget {
devices.removeWhere((e) => e.path == currentNode.path);
hero = _CurrentDeviceRow(
currentNode,
data: ref.watch(currentDeviceDataProvider),
onTap: () {
Navigator.of(context).pop();
},
ref.watch(currentDeviceDataProvider),
);
} else {
hero = ListTile(
leading: DeviceAvatar(
selected: true,
child: Icon(Platform.isAndroid ? Icons.no_cell : Icons.usb),
),
title: Text(Platform.isAndroid ? 'No YubiKey' : 'USB'),
subtitle: Text(Platform.isAndroid
? 'Insert or tap a YubiKey'
: 'Insert a YubiKey'),
onTap: () {
Navigator.of(context).pop();
},
hero = Column(
children: [
_HeroAvatar(
child: DeviceAvatar(
radius: 64,
child: Icon(Platform.isAndroid ? Icons.no_cell : Icons.usb),
),
),
ListTile(
title:
Center(child: Text(Platform.isAndroid ? 'No YubiKey' : 'USB')),
subtitle: Center(
child: Text(Platform.isAndroid
? 'Insert or tap a YubiKey'
: 'Insert a YubiKey'),
),
),
],
);
showUsb = false;
}
@ -60,136 +119,194 @@ class DevicePickerDialog extends ConsumerWidget {
ListTile(
leading: const Padding(
padding: EdgeInsets.symmetric(horizontal: 4),
child: DeviceAvatar(
radius: 20,
child: Icon(Icons.usb),
),
child: DeviceAvatar(child: Icon(Icons.usb)),
),
title: const Text('USB'),
subtitle: const Text('No YubiKey present'),
onTap: () {
Navigator.of(context).pop();
ref.read(currentDeviceProvider.notifier).setCurrentDevice(null);
},
),
...devices.map(
(e) => _DeviceRow(
e,
info: e.map(
usbYubiKey: (node) => node.info,
nfcReader: (_) => null,
),
onTap: () {
Navigator.of(context).pop();
ref.read(currentDeviceProvider.notifier).setCurrentDevice(e);
},
(e) => e.map(
usbYubiKey: (node) => _DeviceRow(node, info: node.info),
nfcReader: (node) => _NfcDeviceRow(node),
),
),
];
return SimpleDialog(
children: [
hero,
if (others.isNotEmpty) const Divider(),
...others,
],
return GestureDetector(
onSecondaryTapDown: hidden.isEmpty
? null
: (details) {
showMenu(
context: context,
position: RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx,
0,
),
items: [
PopupMenuItem(
onTap: () {
ref.read(_hiddenDevicesProvider.notifier).showAll();
},
child: const ListTile(
title: Text('Show hidden devices'),
dense: true,
contentPadding: EdgeInsets.zero,
),
),
],
);
},
child: SimpleDialog(
children: [
hero,
if (others.isNotEmpty)
const Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
child: Divider(),
),
...others,
],
),
);
}
}
class _HeroAvatar extends StatelessWidget {
final Widget child;
const _HeroAvatar({required this.child});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
theme.colorScheme.background,
theme.colorScheme.background.withOpacity(0.4),
(theme.dialogTheme.backgroundColor ?? theme.dialogBackgroundColor)
.withOpacity(0),
],
),
),
padding: const EdgeInsets.all(12),
child: Theme(
// Give the avatar a transparent background
data: theme.copyWith(
colorScheme:
theme.colorScheme.copyWith(background: Colors.transparent)),
child: child,
),
);
}
}
class _CurrentDeviceRow extends StatelessWidget {
final DeviceNode node;
final YubiKeyData? data;
final Function() onTap;
final AsyncValue<YubiKeyData> data;
const _CurrentDeviceRow(
this.node, {
this.data,
required this.onTap,
});
const _CurrentDeviceRow(this.node, this.data);
@override
Widget build(BuildContext context) {
return node.when(usbYubiKey: (path, name, pid, info) {
if (info != null) {
return ListTile(
leading: DeviceAvatar.yubiKeyData(
data!,
selected: true,
),
title: Text(name),
subtitle: Text(_getSubtitle(info)),
onTap: onTap,
);
} else {
return ListTile(
leading: DeviceAvatar.deviceNode(
node,
selected: true,
),
title: Text(name),
subtitle: const Text('Device inaccessible'),
onTap: onTap,
);
}
}, nfcReader: (path, name) {
final info = data?.info;
if (info != null) {
return ListTile(
leading: DeviceAvatar.yubiKeyData(
data!,
selected: true,
),
title: Text(data!.name),
isThreeLine: true,
subtitle: Text('$name\n${_getSubtitle(info)}'),
onTap: onTap,
);
} else {
return ListTile(
leading: DeviceAvatar.deviceNode(
node,
selected: true,
),
title: const Text('No YubiKey present'),
subtitle: Text(name),
onTap: onTap,
);
}
});
final hero = data.maybeWhen(
data: (data) => DeviceAvatar.yubiKeyData(data, radius: 64),
orElse: () => DeviceAvatar.deviceNode(node, radius: 64),
);
final messages = getDeviceMessages(node, data);
return Column(
children: [
_HeroAvatar(child: hero),
ListTile(
title: Text(messages.removeAt(0), textAlign: TextAlign.center),
isThreeLine: messages.length > 1,
subtitle: Text(messages.join('\n'), textAlign: TextAlign.center),
)
],
);
}
}
class _DeviceRow extends StatelessWidget {
class _DeviceRow extends ConsumerWidget {
final DeviceNode node;
final DeviceInfo? info;
final Function() onTap;
const _DeviceRow(
this.node, {
required this.info,
required this.onTap,
});
const _DeviceRow(this.node, {this.info});
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
return ListTile(
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: DeviceAvatar.deviceNode(
node,
radius: 20,
),
child: DeviceAvatar.deviceNode(node),
),
title: Text(node.name),
subtitle: Text(
node.when(
usbYubiKey: (_, __, ___, info) =>
info == null ? 'Device inaccessible' : _getSubtitle(info),
info == null ? 'Device inaccessible' : getDeviceInfoString(info),
nfcReader: (_, __) => 'Select to scan',
),
),
onTap: onTap,
onTap: () {
ref.read(currentDeviceProvider.notifier).setCurrentDevice(node);
},
);
}
}
class _NfcDeviceRow extends ConsumerWidget {
final DeviceNode node;
const _NfcDeviceRow(this.node);
@override
Widget build(BuildContext context, WidgetRef ref) {
final hidden = ref.watch(_hiddenDevicesProvider);
return GestureDetector(
onSecondaryTapDown: (details) {
showMenu(
context: context,
position: RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx,
0,
),
items: [
PopupMenuItem(
enabled: hidden.isNotEmpty,
onTap: () {
ref.read(_hiddenDevicesProvider.notifier).showAll();
},
child: ListTile(
title: const Text('Show hidden devices'),
dense: true,
contentPadding: EdgeInsets.zero,
enabled: hidden.isNotEmpty,
),
),
PopupMenuItem(
onTap: () {
ref.read(_hiddenDevicesProvider.notifier).hideDevice(node.path);
},
child: const ListTile(
title: Text('Hide device'),
dense: true,
contentPadding: EdgeInsets.zero,
),
),
],
);
},
child: _DeviceRow(node),
);
}
}

44
lib/app/views/device_utils.dart Executable file
View File

@ -0,0 +1,44 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../management/models.dart';
import '../models.dart';
String getDeviceInfoString(DeviceInfo info) {
final serial = info.serial;
var subtitle = '';
if (serial != null) {
subtitle += 'S/N: $serial ';
}
subtitle += 'F/W: ${info.version}';
return subtitle;
}
List<String> getDeviceMessages(DeviceNode? node, AsyncValue<YubiKeyData> data) {
if (node == null) {
return ['Insert a YubiKey', 'USB'];
}
final messages = data.whenOrNull(
data: (data) => [getDeviceInfoString(data.info)],
error: (error, _) {
switch (error) {
case 'unknown-device':
return ['Unrecognized device'];
case 'device-inaccessible':
return ['Device inacessible'];
}
return null;
},
) ??
['No YubiKey present'];
final name = data.asData?.value.name;
if (name != null) {
messages.insert(0, name);
}
if (node is NfcReaderNode) {
messages.add(node.name);
}
return messages;
}

View File

@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../management/views/management_screen.dart';
import '../../about_page.dart';
import '../../settings_page.dart';
import '../message.dart';
import '../models.dart';
import '../state.dart';
@ -35,7 +36,8 @@ class MainPageDrawer extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final supportedApps = ref.watch(supportedAppsProvider);
final data = ref.watch(currentDeviceDataProvider);
final data =
ref.watch(currentDeviceDataProvider).whenOrNull(data: (data) => data);
final currentApp = ref.watch(currentAppProvider);
MediaQuery? mediaQuery =
@ -76,7 +78,7 @@ class MainPageDrawer extends ConsumerWidget {
icon: Icon(Application.management._icon),
onTap: () {
if (shouldPop) Navigator.of(context).pop();
showDialog(
showBlurDialog(
context: context,
builder: (context) => ManagementScreen(data),
);
@ -92,7 +94,7 @@ class MainPageDrawer extends ConsumerWidget {
onTap: () {
final nav = Navigator.of(context);
if (shouldPop) nav.pop();
showDialog(
showBlurDialog(
context: context,
builder: (context) => const SettingsPage(),
routeSettings: const RouteSettings(name: 'settings'),
@ -105,7 +107,7 @@ class MainPageDrawer extends ConsumerWidget {
onTap: () {
final nav = Navigator.of(context);
if (shouldPop) nav.pop();
showDialog(
showBlurDialog(
context: context,
builder: (context) => const AboutPage(),
routeSettings: const RouteSettings(name: 'about'),

View File

@ -2,12 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'message_page.dart';
import 'no_device_screen.dart';
import 'device_error_screen.dart';
import '../models.dart';
import '../state.dart';
import '../../fido/views/fido_screen.dart';
import '../../oath/views/oath_screen.dart';
import '../../management/views/management_screen.dart';
class MainPage extends ConsumerWidget {
const MainPage({super.key});
@ -21,7 +20,7 @@ class MainPage extends ConsumerWidget {
},
);
// If the current device changes, we need to pop any open dialogs.
ref.listen<YubiKeyData?>(currentDeviceDataProvider, (_, __) {
ref.listen<AsyncValue<YubiKeyData>>(currentDeviceDataProvider, (_, __) {
Navigator.of(context).popUntil((route) {
return route.isFirst ||
[
@ -32,31 +31,36 @@ class MainPage extends ConsumerWidget {
].contains(route.settings.name);
});
});
final deviceData = ref.watch(currentDeviceDataProvider);
if (deviceData == null) {
final node = ref.watch(currentDeviceProvider);
return NoDeviceScreen(node);
}
final app = ref.watch(currentAppProvider);
if (app.getAvailability(deviceData) != Availability.enabled) {
return const MessagePage(
header: 'Application disabled',
message: 'Enable the application on your YubiKey to access',
);
}
switch (app) {
case Application.oath:
return OathScreen(deviceData.node.path);
case Application.management:
return ManagementScreen(deviceData);
case Application.fido:
return FidoScreen(deviceData);
default:
return const MessagePage(
header: 'Not implemented',
message: 'This section has not yet been implemented',
);
final deviceNode = ref.watch(currentDeviceProvider);
if (deviceNode == null) {
return const MessagePage(message: 'Insert your YubiKey');
} else {
return ref.watch(currentDeviceDataProvider).when(
data: (data) {
final app = ref.watch(currentAppProvider);
if (app.getAvailability(data) != Availability.enabled) {
return const MessagePage(
header: 'Application disabled',
message: 'Enable the application on your YubiKey to access',
);
}
switch (app) {
case Application.oath:
return OathScreen(data.node.path);
case Application.fido:
return FidoScreen(data);
default:
return const MessagePage(
header: 'Not supported',
message: 'This application is not supported',
);
}
},
loading: () => DeviceErrorScreen(deviceNode),
error: (error, _) => DeviceErrorScreen(deviceNode, error: error),
);
}
}
}

View File

@ -8,6 +8,7 @@ class MessagePage extends StatelessWidget {
final String? header;
final String? message;
final List<Widget> actions;
final List<PopupMenuEntry> keyActions;
const MessagePage({
super.key,
@ -16,6 +17,7 @@ class MessagePage extends StatelessWidget {
this.header,
this.message,
this.actions = const [],
this.keyActions = const [],
});
@override
@ -23,6 +25,7 @@ class MessagePage extends StatelessWidget {
title: title,
centered: true,
actions: actions,
keyActions: keyActions,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import '../message.dart';
abstract class UserInteractionController {
void updateContent({String? title, String? description, Widget? icon});
void close();
@ -56,15 +58,28 @@ class _UserInteractionDialogState extends State<_UserInteractionDialog> {
Widget? icon = widget.controller.icon;
return AlertDialog(
scrollable: true,
title: Text(widget.controller.title),
content: SizedBox(
width: 300,
height: 160,
width: 100,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null) icon,
if (icon != null)
Padding(
padding: const EdgeInsets.all(24),
child: IconTheme(
data: IconTheme.of(context).copyWith(size: 36),
child: icon,
),
),
Text(
widget.controller.title,
style: Theme.of(context).textTheme.headline6,
),
Text(
widget.controller.description,
textAlign: TextAlign.center,
softWrap: true,
),
],
@ -110,7 +125,7 @@ UserInteractionController promptUserInteraction(
}
},
);
showDialog(
showBlurDialog(
context: context,
builder: (context) {
return WillPopScope(

View File

@ -88,7 +88,7 @@ enum UsbPid {
final suffix = UsbInterface.values
.where((e) => e.value & usbInterfaces != 0)
.map((e) => e.name.toUpperCase())
.join(' ');
.join('+');
return '$prefix $suffix';
}
}

View File

@ -151,11 +151,11 @@ abstract class _Version extends Version {
const _Version._() : super._();
@override
int get major => throw _privateConstructorUsedError;
int get major;
@override
int get minor => throw _privateConstructorUsedError;
int get minor;
@override
int get patch => throw _privateConstructorUsedError;
int get patch;
@override
@JsonKey(ignore: true)
_$$_VersionCopyWith<_$_Version> get copyWith =>
@ -284,9 +284,9 @@ abstract class _Pair<T1, T2> implements Pair<T1, T2> {
factory _Pair(final T1 first, final T2 second) = _$_Pair<T1, T2>;
@override
T1 get first => throw _privateConstructorUsedError;
T1 get first;
@override
T2 get second => throw _privateConstructorUsedError;
T2 get second;
@override
@JsonKey(ignore: true)
_$$_PairCopyWith<T1, T2, _$_Pair<T1, T2>> get copyWith =>

View File

@ -193,7 +193,8 @@ class _DesktopDevicesNotifier extends AttachedDevicesNotifier {
}
final _desktopDeviceDataProvider =
StateNotifierProvider<CurrentDeviceDataNotifier, YubiKeyData?>((ref) {
StateNotifierProvider<CurrentDeviceDataNotifier, AsyncValue<YubiKeyData>>(
(ref) {
final notifier = CurrentDeviceDataNotifier(
ref.watch(rpcProvider),
ref.watch(currentDeviceProvider),
@ -207,23 +208,26 @@ final _desktopDeviceDataProvider =
return notifier;
});
final desktopDeviceDataProvider = Provider<YubiKeyData?>(
final desktopDeviceDataProvider = Provider<AsyncValue<YubiKeyData>>(
(ref) {
return ref.watch(_desktopDeviceDataProvider);
},
);
class CurrentDeviceDataNotifier extends StateNotifier<YubiKeyData?> {
class CurrentDeviceDataNotifier extends StateNotifier<AsyncValue<YubiKeyData>> {
final RpcSession _rpc;
final DeviceNode? _deviceNode;
Timer? _pollTimer;
CurrentDeviceDataNotifier(this._rpc, this._deviceNode) : super(null) {
CurrentDeviceDataNotifier(this._rpc, this._deviceNode)
: super(const AsyncValue.loading()) {
final dev = _deviceNode;
if (dev is UsbYubiKeyNode) {
final info = dev.info;
if (info != null) {
state = YubiKeyData(dev, dev.name, info);
state = AsyncValue.data(YubiKeyData(dev, dev.name, info));
} else {
state = const AsyncValue.error('device-inaccessible');
}
}
}
@ -254,10 +258,10 @@ class CurrentDeviceDataNotifier extends StateNotifier<YubiKeyData?> {
var result = await _rpc.command('get', node.path.segments);
if (mounted) {
if (result['data']['present']) {
state = YubiKeyData(node, result['data']['name'],
DeviceInfo.fromJson(result['data']['info']));
state = AsyncValue.data(YubiKeyData(node, result['data']['name'],
DeviceInfo.fromJson(result['data']['info'])));
} else {
state = null;
state = AsyncValue.error(result['data']['status']);
}
}
} on RpcError catch (e) {
@ -265,7 +269,7 @@ class CurrentDeviceDataNotifier extends StateNotifier<YubiKeyData?> {
}
if (mounted) {
_pollTimer = Timer(
state == null ? _nfcAttachPollDelay : _nfcDetachPollDelay,
state is AsyncData ? _nfcDetachPollDelay : _nfcAttachPollDelay,
_pollReader);
}
}

View File

@ -272,7 +272,7 @@ abstract class Success implements RpcResponse {
factory Success.fromJson(Map<String, dynamic> json) = _$Success.fromJson;
@override
Map<String, dynamic> get body => throw _privateConstructorUsedError;
Map<String, dynamic> get body;
@override
@JsonKey(ignore: true)
_$$SuccessCopyWith<_$Success> get copyWith =>
@ -446,9 +446,9 @@ abstract class Signal implements RpcResponse {
factory Signal.fromJson(Map<String, dynamic> json) = _$Signal.fromJson;
String get status => throw _privateConstructorUsedError;
String get status;
@override
Map<String, dynamic> get body => throw _privateConstructorUsedError;
Map<String, dynamic> get body;
@override
@JsonKey(ignore: true)
_$$SignalCopyWith<_$Signal> get copyWith =>
@ -633,10 +633,10 @@ abstract class RpcError implements RpcResponse {
factory RpcError.fromJson(Map<String, dynamic> json) = _$RpcError.fromJson;
String get status => throw _privateConstructorUsedError;
String get message => throw _privateConstructorUsedError;
String get status;
String get message;
@override
Map<String, dynamic> get body => throw _privateConstructorUsedError;
Map<String, dynamic> get body;
@override
@JsonKey(ignore: true)
_$$RpcErrorCopyWith<_$RpcError> get copyWith =>
@ -780,9 +780,9 @@ abstract class _RpcState implements RpcState {
factory _RpcState.fromJson(Map<String, dynamic> json) = _$_RpcState.fromJson;
@override
String get version => throw _privateConstructorUsedError;
String get version;
@override
bool get isAdmin => throw _privateConstructorUsedError;
bool get isAdmin;
@override
@JsonKey(ignore: true)
_$$_RpcStateCopyWith<_$_RpcState> get copyWith =>

View File

@ -2,13 +2,14 @@ import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:yubico_authenticator/app/logging.dart';
import 'package:yubico_authenticator/app/views/user_interaction.dart';
import '../../app/logging.dart';
import '../../app/models.dart';
import '../../app/state.dart';
import '../../app/views/user_interaction.dart';
import '../../core/models.dart';
import '../../oath/models.dart';
import '../../oath/state.dart';
@ -254,6 +255,7 @@ class _DesktopCredentialListNotifier extends OathCredentialListNotifier {
controller = await _withContext(
(context) async => promptUserInteraction(
context,
icon: const Icon(Icons.touch_app),
title: 'Touch Required',
description: 'Touch the button on your YubiKey now.',
),

View File

@ -162,9 +162,9 @@ abstract class _FidoState extends FidoState {
_$_FidoState.fromJson;
@override
Map<String, dynamic> get info => throw _privateConstructorUsedError;
Map<String, dynamic> get info;
@override
bool get unlocked => throw _privateConstructorUsedError;
bool get unlocked;
@override
@JsonKey(ignore: true)
_$$_FidoStateCopyWith<_$_FidoState> get copyWith =>
@ -471,8 +471,8 @@ abstract class _PinFailure implements PinResult {
factory _PinFailure(final int retries, final bool authBlocked) =
_$_PinFailure;
int get retries => throw _privateConstructorUsedError;
bool get authBlocked => throw _privateConstructorUsedError;
int get retries;
bool get authBlocked;
@JsonKey(ignore: true)
_$$_PinFailureCopyWith<_$_PinFailure> get copyWith =>
throw _privateConstructorUsedError;
@ -620,9 +620,9 @@ abstract class _Fingerprint extends Fingerprint {
_$_Fingerprint.fromJson;
@override
String get templateId => throw _privateConstructorUsedError;
String get templateId;
@override
String? get name => throw _privateConstructorUsedError;
String? get name;
@override
@JsonKey(ignore: true)
_$$_FingerprintCopyWith<_$_Fingerprint> get copyWith =>
@ -828,7 +828,7 @@ class _$_EventCapture implements _EventCapture {
abstract class _EventCapture implements FingerprintEvent {
factory _EventCapture(final int remaining) = _$_EventCapture;
int get remaining => throw _privateConstructorUsedError;
int get remaining;
@JsonKey(ignore: true)
_$$_EventCaptureCopyWith<_$_EventCapture> get copyWith =>
throw _privateConstructorUsedError;
@ -978,7 +978,7 @@ class _$_EventComplete implements _EventComplete {
abstract class _EventComplete implements FingerprintEvent {
factory _EventComplete(final Fingerprint fingerprint) = _$_EventComplete;
Fingerprint get fingerprint => throw _privateConstructorUsedError;
Fingerprint get fingerprint;
@JsonKey(ignore: true)
_$$_EventCompleteCopyWith<_$_EventComplete> get copyWith =>
throw _privateConstructorUsedError;
@ -1118,7 +1118,7 @@ class _$_EventError implements _EventError {
abstract class _EventError implements FingerprintEvent {
factory _EventError(final int code) = _$_EventError;
int get code => throw _privateConstructorUsedError;
int get code;
@JsonKey(ignore: true)
_$$_EventErrorCopyWith<_$_EventError> get copyWith =>
throw _privateConstructorUsedError;
@ -1304,13 +1304,13 @@ abstract class _FidoCredential implements FidoCredential {
_$_FidoCredential.fromJson;
@override
String get rpId => throw _privateConstructorUsedError;
String get rpId;
@override
String get credentialId => throw _privateConstructorUsedError;
String get credentialId;
@override
String get userId => throw _privateConstructorUsedError;
String get userId;
@override
String get userName => throw _privateConstructorUsedError;
String get userName;
@override
@JsonKey(ignore: true)
_$$_FidoCredentialCopyWith<_$_FidoCredential> get copyWith =>

View File

@ -10,6 +10,7 @@ import 'package:yubico_authenticator/app/logging.dart';
import '../../app/message.dart';
import '../../desktop/models.dart';
import '../../widgets/responsive_dialog.dart';
import '../../widgets/utf8_utils.dart';
import '../state.dart';
import '../../fido/models.dart';
import '../../app/models.dart';
@ -179,6 +180,8 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
TextFormField(
focusNode: _nameFocus,
maxLength: 15,
inputFormatters: [limitBytesLength(15)],
buildCounter: buildByteCounterFor(_label),
autofocus: true,
decoration: InputDecoration(
enabled: _fingerprint != null,

View File

@ -6,7 +6,7 @@ import '../../app/models.dart';
import '../../app/views/app_page.dart';
import '../../app/views/graphics.dart';
import '../../app/views/message_page.dart';
import '../../theme.dart';
import '../../widgets/menu_list_tile.dart';
import '../models.dart';
import '../state.dart';
import 'pin_dialog.dart';
@ -27,7 +27,7 @@ class FidoLockedPage extends ConsumerWidget {
graphic: noFingerprints,
header: 'No fingerprints',
message: 'Set a PIN to register fingerprints',
actions: _buildActions(context),
keyActions: _buildActions(context),
);
} else {
return MessagePage(
@ -36,7 +36,7 @@ class FidoLockedPage extends ConsumerWidget {
header: state.credMgmt ? 'No discoverable accounts' : 'Ready to use',
message:
'Optionally set a PIN to protect access to your YubiKey\nRegister as a Security Key on websites',
actions: _buildActions(context),
keyActions: _buildActions(context),
);
}
}
@ -47,13 +47,13 @@ class FidoLockedPage extends ConsumerWidget {
graphic: manageAccounts,
header: 'Ready to use',
message: 'Register as a Security Key on websites',
actions: _buildActions(context),
keyActions: _buildActions(context),
);
}
return AppPage(
title: const Text('WebAuthn'),
actions: _buildActions(context),
keyActions: _buildActions(context),
child: Column(
children: [
_PinEntryForm(state, node),
@ -62,48 +62,26 @@ class FidoLockedPage extends ConsumerWidget {
);
}
List<Widget> _buildActions(BuildContext context) => [
List<PopupMenuEntry> _buildActions(BuildContext context) => [
if (!state.hasPin)
OutlinedButton.icon(
style: state.bioEnroll != null
? AppTheme.primaryOutlinedButtonStyle(context)
: null,
label: const Text('Set PIN'),
icon: const Icon(Icons.pin),
onPressed: () {
showDialog(
buildMenuItem(
title: const Text('Set PIN'),
leading: const Icon(Icons.pin),
action: () {
showBlurDialog(
context: context,
builder: (context) => FidoPinDialog(node.path, state),
);
},
),
OutlinedButton.icon(
label: const Text('Options'),
icon: const Icon(Icons.tune),
onPressed: () {
showBottomMenu(context, [
if (state.hasPin)
MenuAction(
text: 'Change PIN',
icon: const Icon(Icons.pin),
action: (context) {
showDialog(
context: context,
builder: (context) => FidoPinDialog(node.path, state),
);
},
),
MenuAction(
text: 'Reset FIDO',
icon: const Icon(Icons.delete),
action: (context) {
showDialog(
context: context,
builder: (context) => ResetDialog(node),
);
},
),
]);
buildMenuItem(
title: const Text('Reset FIDO'),
leading: const Icon(Icons.delete),
action: () {
showBlurDialog(
context: context,
builder: (context) => ResetDialog(node),
);
},
),
];

View File

@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../app/message.dart';
import '../../desktop/models.dart';
import '../../widgets/responsive_dialog.dart';
import '../../widgets/utf8_utils.dart';
import '../models.dart';
import '../state.dart';
import '../../app/models.dart';
@ -69,8 +70,9 @@ class _RenameAccountDialogState extends ConsumerState<RenameFingerprintDialog> {
const Text('This will change the label of the fingerprint.'),
TextFormField(
initialValue: _label,
// TODO: Make this field count UTF-8 bytes instead of characters.
maxLength: 15,
inputFormatters: [limitBytesLength(15)],
buildCounter: buildByteCounterFor(_label),
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Label',

View File

@ -6,8 +6,8 @@ import '../../app/models.dart';
import '../../app/views/app_page.dart';
import '../../app/views/graphics.dart';
import '../../app/views/message_page.dart';
import '../../theme.dart';
import '../../widgets/list_title.dart';
import '../../widgets/menu_list_tile.dart';
import '../models.dart';
import '../state.dart';
import 'add_fingerprint_dialog.dart';
@ -57,7 +57,7 @@ class FidoUnlockedPage extends ConsumerWidget {
children: [
IconButton(
onPressed: () {
showDialog(
showBlurDialog(
context: context,
builder: (context) =>
DeleteCredentialDialog(node.path, cred),
@ -96,7 +96,7 @@ class FidoUnlockedPage extends ConsumerWidget {
children: [
IconButton(
onPressed: () {
showDialog(
showBlurDialog(
context: context,
builder: (context) =>
RenameFingerprintDialog(node.path, fp),
@ -105,7 +105,7 @@ class FidoUnlockedPage extends ConsumerWidget {
icon: const Icon(Icons.edit_outlined)),
IconButton(
onPressed: () {
showDialog(
showBlurDialog(
context: context,
builder: (context) =>
DeleteFingerprintDialog(node.path, fp),
@ -121,7 +121,7 @@ class FidoUnlockedPage extends ConsumerWidget {
if (children.isNotEmpty) {
return AppPage(
title: const Text('WebAuthn'),
actions: _buildActions(context),
keyActions: _buildKeyActions(context),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, children: children),
);
@ -133,7 +133,7 @@ class FidoUnlockedPage extends ConsumerWidget {
graphic: noFingerprints,
header: 'No fingerprints',
message: 'Add one or more (up to five) fingerprints',
actions: _buildActions(context, fingerprintPrimary: true),
keyActions: _buildKeyActions(context),
);
}
@ -142,7 +142,7 @@ class FidoUnlockedPage extends ConsumerWidget {
graphic: manageAccounts,
header: 'No discoverable accounts',
message: 'Register as a Security Key on websites',
actions: _buildActions(context),
keyActions: _buildKeyActions(context),
);
}
@ -152,49 +152,36 @@ class FidoUnlockedPage extends ConsumerWidget {
child: const CircularProgressIndicator(),
);
List<Widget> _buildActions(BuildContext context,
{bool fingerprintPrimary = false}) =>
[
List<PopupMenuEntry> _buildKeyActions(BuildContext context) => [
if (state.bioEnroll != null)
OutlinedButton.icon(
style: fingerprintPrimary
? AppTheme.primaryOutlinedButtonStyle(context)
: null,
label: const Text('Add fingerprint'),
icon: const Icon(Icons.fingerprint),
onPressed: () {
showDialog(
buildMenuItem(
title: const Text('Add fingerprint'),
leading: const Icon(Icons.fingerprint),
action: () {
showBlurDialog(
context: context,
builder: (context) => AddFingerprintDialog(node.path),
);
},
),
OutlinedButton.icon(
label: const Text('Options'),
icon: const Icon(Icons.tune),
onPressed: () {
showBottomMenu(context, [
MenuAction(
text: 'Change PIN',
icon: const Icon(Icons.pin),
action: (context) {
showDialog(
context: context,
builder: (context) => FidoPinDialog(node.path, state),
);
},
),
MenuAction(
text: 'Reset FIDO',
icon: const Icon(Icons.delete),
action: (context) {
showDialog(
context: context,
builder: (context) => ResetDialog(node),
);
},
),
]);
buildMenuItem(
title: const Text('Change PIN'),
leading: const Icon(Icons.pin),
action: () {
showBlurDialog(
context: context,
builder: (context) => FidoPinDialog(node.path, state),
);
},
),
buildMenuItem(
title: const Text('Reset FIDO'),
leading: const Icon(Icons.delete),
action: () {
showBlurDialog(
context: context,
builder: (context) => ResetDialog(node),
);
},
),
];

View File

@ -208,14 +208,13 @@ abstract class _DeviceConfig implements DeviceConfig {
_$_DeviceConfig.fromJson;
@override
Map<Transport, int> get enabledCapabilities =>
throw _privateConstructorUsedError;
Map<Transport, int> get enabledCapabilities;
@override
int? get autoEjectTimeout => throw _privateConstructorUsedError;
int? get autoEjectTimeout;
@override
int? get challengeResponseTimeout => throw _privateConstructorUsedError;
int? get challengeResponseTimeout;
@override
int? get deviceFlags => throw _privateConstructorUsedError;
int? get deviceFlags;
@override
@JsonKey(ignore: true)
_$$_DeviceConfigCopyWith<_$_DeviceConfig> get copyWith =>
@ -514,22 +513,21 @@ abstract class _DeviceInfo implements DeviceInfo {
_$_DeviceInfo.fromJson;
@override
DeviceConfig get config => throw _privateConstructorUsedError;
DeviceConfig get config;
@override
int? get serial => throw _privateConstructorUsedError;
int? get serial;
@override
Version get version => throw _privateConstructorUsedError;
Version get version;
@override
FormFactor get formFactor => throw _privateConstructorUsedError;
FormFactor get formFactor;
@override
Map<Transport, int> get supportedCapabilities =>
throw _privateConstructorUsedError;
Map<Transport, int> get supportedCapabilities;
@override
bool get isLocked => throw _privateConstructorUsedError;
bool get isLocked;
@override
bool get isFips => throw _privateConstructorUsedError;
bool get isFips;
@override
bool get isSky => throw _privateConstructorUsedError;
bool get isSky;
@override
@JsonKey(ignore: true)
_$$_DeviceInfoCopyWith<_$_DeviceInfo> get copyWith =>

View File

@ -180,7 +180,7 @@ class _ManagementScreenState extends ConsumerState<ManagementScreen> {
context,
'Reconfiguring YubiKey...',
duration: const Duration(seconds: 8),
).close;
);
}
await ref
.read(managementStateProvider(widget.deviceData.node.path).notifier)

View File

@ -125,26 +125,18 @@ class CredentialData with _$CredentialData {
);
}
Uri toUri() {
final path = issuer != null ? '$issuer:$name' : name;
var uri = 'otpauth://${oathType.name}/$path?secret=$secret';
switch (oathType) {
case OathType.hotp:
uri += '&counter=$counter';
break;
case OathType.totp:
uri += '&period=$period';
break;
}
if (issuer != null) {
uri += '&issuer=$issuer';
}
if (digits != 6) {
uri += '&digits=$digits';
}
if (hashAlgorithm != HashAlgorithm.sha1) {
uri += '&algorithm=${hashAlgorithm.name}';
}
return Uri.parse(uri);
}
Uri toUri() => Uri(
scheme: 'otpauth',
host: oathType.name,
path: issuer != null ? '$issuer:$name' : name,
queryParameters: {
'secret': secret,
if (oathType == OathType.totp) 'period': period.toString(),
if (oathType == OathType.hotp) 'counter': counter.toString(),
if (issuer != null) 'issuer': issuer!,
if (digits != 6) 'digits': digits.toString(),
if (hashAlgorithm != HashAlgorithm.sha1)
'algorithm': hashAlgorithm.name,
},
);
}

View File

@ -253,19 +253,19 @@ abstract class _OathCredential implements OathCredential {
_$_OathCredential.fromJson;
@override
String get deviceId => throw _privateConstructorUsedError;
String get deviceId;
@override
String get id => throw _privateConstructorUsedError;
String get id;
@override
String? get issuer => throw _privateConstructorUsedError;
String? get issuer;
@override
String get name => throw _privateConstructorUsedError;
String get name;
@override
OathType get oathType => throw _privateConstructorUsedError;
OathType get oathType;
@override
int get period => throw _privateConstructorUsedError;
int get period;
@override
bool get touchRequired => throw _privateConstructorUsedError;
bool get touchRequired;
@override
@JsonKey(ignore: true)
_$$_OathCredentialCopyWith<_$_OathCredential> get copyWith =>
@ -424,11 +424,11 @@ abstract class _OathCode implements OathCode {
factory _OathCode.fromJson(Map<String, dynamic> json) = _$_OathCode.fromJson;
@override
String get value => throw _privateConstructorUsedError;
String get value;
@override
int get validFrom => throw _privateConstructorUsedError;
int get validFrom;
@override
int get validTo => throw _privateConstructorUsedError;
int get validTo;
@override
@JsonKey(ignore: true)
_$$_OathCodeCopyWith<_$_OathCode> get copyWith =>
@ -599,9 +599,9 @@ abstract class _OathPair implements OathPair {
factory _OathPair.fromJson(Map<String, dynamic> json) = _$_OathPair.fromJson;
@override
OathCredential get credential => throw _privateConstructorUsedError;
OathCredential get credential;
@override
OathCode? get code => throw _privateConstructorUsedError;
OathCode? get code;
@override
@JsonKey(ignore: true)
_$$_OathPairCopyWith<_$_OathPair> get copyWith =>
@ -838,17 +838,17 @@ abstract class _OathState implements OathState {
_$_OathState.fromJson;
@override
String get deviceId => throw _privateConstructorUsedError;
String get deviceId;
@override
Version get version => throw _privateConstructorUsedError;
Version get version;
@override
bool get hasKey => throw _privateConstructorUsedError;
bool get hasKey;
@override
bool get remembered => throw _privateConstructorUsedError;
bool get remembered;
@override
bool get locked => throw _privateConstructorUsedError;
bool get locked;
@override
KeystoreState get keystore => throw _privateConstructorUsedError;
KeystoreState get keystore;
@override
@JsonKey(ignore: true)
_$$_OathStateCopyWith<_$_OathState> get copyWith =>
@ -1126,21 +1126,21 @@ abstract class _CredentialData extends CredentialData {
_$_CredentialData.fromJson;
@override
String? get issuer => throw _privateConstructorUsedError;
String? get issuer;
@override
String get name => throw _privateConstructorUsedError;
String get name;
@override
String get secret => throw _privateConstructorUsedError;
String get secret;
@override
OathType get oathType => throw _privateConstructorUsedError;
OathType get oathType;
@override
HashAlgorithm get hashAlgorithm => throw _privateConstructorUsedError;
HashAlgorithm get hashAlgorithm;
@override
int get digits => throw _privateConstructorUsedError;
int get digits;
@override
int get period => throw _privateConstructorUsedError;
int get period;
@override
int get counter => throw _privateConstructorUsedError;
int get counter;
@override
@JsonKey(ignore: true)
_$$_CredentialDataCopyWith<_$_CredentialData> get copyWith =>

View File

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -59,15 +60,39 @@ abstract class OathCredentialListNotifier
Future<void> deleteAccount(OathCredential credential);
}
final credentialsProvider = Provider.autoDispose<List<OathCredential>?>((ref) {
final credentialsProvider = StateNotifierProvider.autoDispose<
_CredentialsProviderNotifier, List<OathCredential>?>((ref) {
final provider = _CredentialsProviderNotifier();
final node = ref.watch(currentDeviceProvider);
if (node != null) {
return ref.watch(credentialListProvider(node.path)
.select((pairs) => pairs?.map((e) => e.credential).toList()));
ref.listen<List<OathPair>?>(credentialListProvider(node.path),
(previous, next) {
provider._updatePairs(next);
});
}
return null;
return provider;
});
class _CredentialsProviderNotifier
extends StateNotifier<List<OathCredential>?> {
_CredentialsProviderNotifier() : super(null);
void _updatePairs(List<OathPair>? pairs) {
if (mounted) {
if (pairs == null) {
if (state != null) {
state = null;
}
} else {
final creds = pairs.map((p) => p.credential).toList();
if (!const ListEquality().equals(creds, state)) {
state = creds;
}
}
}
}
}
final codeProvider =
Provider.autoDispose.family<OathCode?, OathCredential>((ref, credential) {
final node = ref.watch(currentDeviceProvider);

View File

@ -3,11 +3,11 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../app/message.dart';
import '../../app/shortcuts.dart';
import '../../app/state.dart';
import '../../core/models.dart';
import '../../core/state.dart';
import '../../widgets/dialog_frame.dart';
import '../models.dart';
import 'account_mixin.dart';
@ -24,7 +24,7 @@ class AccountDialog extends ConsumerWidget with AccountMixin {
// Replace this dialog with a new one, for the renamed credential.
await ref.read(withContextProvider)((context) async {
Navigator.of(context).pop();
await showDialog(
await showBlurDialog(
context: context,
builder: (context) {
return AccountDialog(renamed);
@ -94,7 +94,7 @@ class AccountDialog extends ConsumerWidget with AccountMixin {
@override
Widget build(BuildContext context, WidgetRef ref) {
// TODO: Solve this in a cleaner way
if (ref.watch(currentDeviceDataProvider) == null) {
if (ref.watch(currentDeviceDataProvider) is! AsyncData) {
// The rest of this method assumes there is a device, and will throw an exception if not.
// This will never be shown, as the dialog will be immediately closed
return const SizedBox();
@ -120,70 +120,68 @@ class AccountDialog extends ConsumerWidget with AccountMixin {
return null;
}),
},
child: Focus(
child: FocusScope(
autofocus: true,
child: DialogFrame(
child: AlertDialog(
title: Center(
child: Text(
title,
overflow: TextOverflow.fade,
style: Theme.of(context).textTheme.headlineSmall,
maxLines: 1,
softWrap: false,
),
child: AlertDialog(
title: Center(
child: Text(
title,
overflow: TextOverflow.fade,
style: Theme.of(context).textTheme.headlineSmall,
maxLines: 1,
softWrap: false,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),
content: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
if (subtitle != null)
Text(
subtitle!,
overflow: TextOverflow.fade,
maxLines: 1,
softWrap: false,
// This is what ListTile uses for subtitle
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).textTheme.caption!.color,
),
),
const SizedBox(height: 12.0),
DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.rectangle,
color: CardTheme.of(context).color,
borderRadius: const BorderRadius.all(Radius.circular(30.0)),
),
child: Center(
child: FittedBox(
child: DefaultTextStyle.merge(
style: const TextStyle(fontSize: 28),
child: IconTheme(
data: IconTheme.of(context).copyWith(size: 24),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 8.0),
child: buildCodeView(ref),
),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),
content: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
if (subtitle != null)
Text(
subtitle!,
overflow: TextOverflow.fade,
maxLines: 1,
softWrap: false,
// This is what ListTile uses for subtitle
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).textTheme.caption!.color,
),
),
const SizedBox(height: 12.0),
DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.rectangle,
color: CardTheme.of(context).color,
borderRadius: const BorderRadius.all(Radius.circular(30.0)),
),
child: Center(
child: FittedBox(
child: DefaultTextStyle.merge(
style: const TextStyle(fontSize: 28),
child: IconTheme(
data: IconTheme.of(context).copyWith(size: 24),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 8.0),
child: buildCodeView(ref),
),
),
),
),
),
],
),
actionsPadding: const EdgeInsets.only(top: 10.0, right: -16.0),
actions: [
Center(
child: FittedBox(
alignment: Alignment.center,
child: Row(children: _buildActions(context, ref)),
),
)
),
],
),
actionsPadding: const EdgeInsets.only(top: 10.0, right: -16.0),
actions: [
Center(
child: FittedBox(
alignment: Alignment.center,
child: Row(children: _buildActions(context, ref)),
),
)
],
),
),
);

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../app/models.dart';
@ -8,65 +7,14 @@ import '../models.dart';
import '../state.dart';
import 'account_view.dart';
class AccountList extends ConsumerStatefulWidget {
class AccountList extends ConsumerWidget {
final DevicePath devicePath;
final OathState oathState;
const AccountList(this.devicePath, this.oathState, {super.key});
@override
ConsumerState<AccountList> createState() => _AccountListState();
}
class _AccountListState extends ConsumerState<AccountList> {
List<OathCredential> _credentials = [];
Map<OathCredential, FocusNode> _focusNodes = {};
@override
void dispose() {
super.dispose();
for (var e in _focusNodes.values) {
e.dispose();
}
_focusNodes.clear();
}
void _updateFocusNodes() {
_focusNodes = {
for (var cred in _credentials)
cred: _focusNodes[cred] ??
FocusNode(
onKeyEvent: (node, event) {
if (event is KeyDownEvent) {
int index = -1;
ScrollPositionAlignmentPolicy policy =
ScrollPositionAlignmentPolicy.explicit;
if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
index = _credentials.indexOf(cred) + 1;
policy = ScrollPositionAlignmentPolicy.keepVisibleAtEnd;
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
index = _credentials.indexOf(cred) - 1;
policy = ScrollPositionAlignmentPolicy.keepVisibleAtStart;
}
if (index >= 0 && index < _credentials.length) {
final targetNode = _focusNodes[_credentials[index]]!;
targetNode.requestFocus();
Scrollable.ensureVisible(
targetNode.context!,
alignmentPolicy: policy,
);
return KeyEventResult.handled;
}
}
return KeyEventResult.ignored;
},
)
};
_focusNodes.removeWhere((cred, _) => !_credentials.contains(cred));
}
@override
Widget build(BuildContext context) {
final accounts = ref.watch(credentialListProvider(widget.devicePath));
Widget build(BuildContext context, WidgetRef ref) {
final accounts = ref.watch(credentialListProvider(devicePath));
if (accounts == null) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
@ -88,27 +36,24 @@ class _AccountListState extends ConsumerState<AccountList> {
final creds =
credentials.where((entry) => !favorites.contains(entry.credential.id));
_credentials =
pinnedCreds.followedBy(creds).map((e) => e.credential).toList();
_updateFocusNodes();
return Column(
children: [
if (pinnedCreds.isNotEmpty) const ListTitle('Pinned'),
...pinnedCreds.map(
(entry) => AccountView(
entry.credential,
focusNode: _focusNodes[entry.credential],
return FocusTraversalGroup(
policy: WidgetOrderTraversalPolicy(),
child: Column(
children: [
if (pinnedCreds.isNotEmpty) const ListTitle('Pinned'),
...pinnedCreds.map(
(entry) => AccountView(
entry.credential,
),
),
),
if (creds.isNotEmpty) const ListTitle('Accounts'),
...creds.map(
(entry) => AccountView(
entry.credential,
focusNode: _focusNodes[entry.credential],
if (creds.isNotEmpty) const ListTitle('Accounts'),
...creds.map(
(entry) => AccountView(
entry.credential,
),
),
),
],
],
),
);
}
}

View File

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
@ -78,7 +79,7 @@ mixin AccountMixin {
Future<OathCredential?> renameCredential(
BuildContext context, WidgetRef ref) async {
final node = ref.read(currentDeviceProvider)!;
return await showDialog(
return await showBlurDialog(
context: context,
builder: (context) => RenameAccountDialog(node, credential),
);
@ -87,7 +88,7 @@ mixin AccountMixin {
@protected
Future<bool> deleteCredential(BuildContext context, WidgetRef ref) async {
final node = ref.read(currentDeviceProvider)!;
return await showDialog(
return await showBlurDialog(
context: context,
builder: (context) => DeleteAccountDialog(node, credential),
) ??
@ -95,67 +96,72 @@ mixin AccountMixin {
}
@protected
List<MenuAction> buildActions(BuildContext context, WidgetRef ref) {
final deviceData = ref.watch(currentDeviceDataProvider);
if (deviceData == null) {
return [];
}
final code = getCode(ref);
final expired = isExpired(code, ref);
final manual =
credential.touchRequired || credential.oathType == OathType.hotp;
final ready = expired || credential.oathType == OathType.hotp;
final pinned = isPinned(ref);
List<MenuAction> buildActions(BuildContext context, WidgetRef ref) =>
ref.watch(currentDeviceDataProvider).maybeWhen(
data: (data) {
final code = getCode(ref);
final expired = isExpired(code, ref);
final manual = credential.touchRequired ||
credential.oathType == OathType.hotp;
final ready = expired || credential.oathType == OathType.hotp;
final pinned = isPinned(ref);
return [
MenuAction(
text: 'Copy to clipboard',
icon: const Icon(Icons.copy),
action: code == null || expired
? null
: (context) {
Clipboard.setData(ClipboardData(text: code.value));
showMessage(context, 'Code copied to clipboard');
},
),
if (manual)
MenuAction(
text: 'Calculate',
icon: const Icon(Icons.refresh),
action: ready
? (context) async {
try {
await calculateCode(context, ref);
} on CancellationException catch (_) {
// ignored
}
}
: null,
),
MenuAction(
text: pinned ? 'Unpin account' : 'Pin account',
icon: pinned ? pushPinStrokeIcon : const Icon(Icons.push_pin_outlined),
action: (context) {
ref.read(favoritesProvider.notifier).toggleFavorite(credential.id);
},
),
if (deviceData.info.version.isAtLeast(5, 3))
MenuAction(
icon: const Icon(Icons.edit_outlined),
text: 'Rename account',
action: (context) async {
await renameCredential(context, ref);
},
),
MenuAction(
text: 'Delete account',
icon: const Icon(Icons.delete_outline),
action: (context) async {
await deleteCredential(context, ref);
},
),
];
}
final shortcut = Platform.isMacOS ? '\u2318 C' : 'Ctrl+C';
return [
MenuAction(
text: 'Copy to clipboard ($shortcut)',
icon: const Icon(Icons.copy),
action: code == null || expired
? null
: (context) {
Clipboard.setData(ClipboardData(text: code.value));
showMessage(context, 'Code copied to clipboard');
},
),
if (manual)
MenuAction(
text: 'Calculate',
icon: const Icon(Icons.refresh),
action: ready
? (context) async {
try {
await calculateCode(context, ref);
} on CancellationException catch (_) {
// ignored
}
}
: null,
),
MenuAction(
text: pinned ? 'Unpin account' : 'Pin account',
icon: pinned
? pushPinStrokeIcon
: const Icon(Icons.push_pin_outlined),
action: (context) {
ref
.read(favoritesProvider.notifier)
.toggleFavorite(credential.id);
},
),
if (data.info.version.isAtLeast(5, 3))
MenuAction(
icon: const Icon(Icons.edit_outlined),
text: 'Rename account',
action: (context) async {
await renameCredential(context, ref);
},
),
MenuAction(
text: 'Delete account',
icon: const Icon(Icons.delete_outline),
action: (context) async {
await deleteCredential(context, ref);
},
),
];
},
orElse: () => [],
);
@protected
Widget buildCodeView(WidgetRef ref) {

View File

@ -1,13 +1,13 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:yubico_authenticator/cancellation_exception.dart';
import 'package:yubico_authenticator/app/state.dart';
import '../../app/message.dart';
import '../../app/shortcuts.dart';
import '../../app/state.dart';
import '../../widgets/menu_list_tile.dart';
import '../models.dart';
import '../state.dart';
import 'account_dialog.dart';
@ -16,8 +16,7 @@ import 'account_mixin.dart';
class AccountView extends ConsumerWidget with AccountMixin {
@override
final OathCredential credential;
final FocusNode? focusNode;
AccountView(this.credential, {super.key, this.focusNode});
AccountView(this.credential, {super.key});
Color _iconColor(int shade) {
final colors = [
@ -47,22 +46,16 @@ class AccountView extends ConsumerWidget with AccountMixin {
List<PopupMenuItem> _buildPopupMenu(BuildContext context, WidgetRef ref) {
return buildActions(context, ref).map((e) {
final action = e.action;
return PopupMenuItem(
enabled: action != null,
onTap: () {
// As soon as onTap returns, the Navigator is popped,
// closing the topmost item. Since we sometimes open new dialogs in
// the action, make sure that happens *after* the pop.
Timer(Duration.zero, () {
action?.call(context);
});
},
child: ListTile(
leading: e.icon,
title: Text(e.text),
dense: true,
contentPadding: EdgeInsets.zero,
),
return buildMenuItem(
leading: e.icon,
title: Text(e.text),
action: action != null
? () {
ref.read(withContextProvider)((context) async {
action.call(context);
});
}
: null,
);
}).toList();
}
@ -122,13 +115,10 @@ class AccountView extends ConsumerWidget with AccountMixin {
return ListTile(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
focusNode: focusNode,
onTap: () {
showDialog(
showBlurDialog(
context: context,
builder: (context) {
return AccountDialog(credential);
},
builder: (context) => AccountDialog(credential),
);
},
onLongPress: triggerCopy,

View File

@ -6,14 +6,15 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:yubico_authenticator/cancellation_exception.dart';
import 'package:yubico_authenticator/app/logging.dart';
import '../../app/logging.dart';
import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/state.dart';
import '../../desktop/models.dart';
import '../../widgets/file_drop_target.dart';
import '../../widgets/responsive_dialog.dart';
import '../../widgets/utf8_utils.dart';
import '../models.dart';
import '../state.dart';
import 'utils.dart';
@ -213,6 +214,8 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
autofocus: true,
enabled: issuerRemaining > 0,
maxLength: max(issuerRemaining, 1),
inputFormatters: [limitBytesLength(issuerRemaining)],
buildCounter: buildByteCounterFor(_issuerController.text),
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Issuer (optional)',
@ -232,6 +235,8 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
key: const Key('name'),
controller: _accountController,
maxLength: max(nameRemaining, 1),
buildCounter: buildByteCounterFor(_accountController.text),
inputFormatters: [limitBytesLength(nameRemaining)],
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Account name',

View File

@ -12,7 +12,7 @@ import '../../app/views/app_loading_screen.dart';
import '../../app/views/app_page.dart';
import '../../app/views/graphics.dart';
import '../../app/views/message_page.dart';
import '../../theme.dart';
import '../../widgets/menu_list_tile.dart';
import '../models.dart';
import '../state.dart';
import 'account_list.dart';
@ -52,34 +52,26 @@ class _LockedView extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) => AppPage(
title: const Text('Authenticator'),
actions: [
OutlinedButton.icon(
label: const Text('Options'),
icon: const Icon(Icons.tune),
onPressed: () {
showBottomMenu(context, [
MenuAction(
text: 'Manage password',
icon: const Icon(Icons.password),
action: (context) {
showDialog(
context: context,
builder: (context) =>
ManagePasswordDialog(devicePath, oathState),
);
},
),
MenuAction(
text: 'Reset OATH',
icon: const Icon(Icons.delete),
action: (context) {
showDialog(
context: context,
builder: (context) => ResetDialog(devicePath),
);
},
),
]);
keyActions: [
buildMenuItem(
title: const Text('Manage password'),
leading: const Icon(Icons.password),
action: () {
showBlurDialog(
context: context,
builder: (context) =>
ManagePasswordDialog(devicePath, oathState),
);
},
),
buildMenuItem(
title: const Text('Reset OATH'),
leading: const Icon(Icons.delete),
action: () {
showBlurDialog(
context: context,
builder: (context) => ResetDialog(devicePath),
);
},
),
],
@ -124,14 +116,17 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
@override
Widget build(BuildContext context) {
final isEmpty = ref.watch(credentialListProvider(widget.devicePath)
.select((value) => value?.isEmpty == true));
if (isEmpty) {
final credentials = ref.watch(credentialsProvider);
if (credentials?.isEmpty == true) {
return MessagePage(
title: const Text('Authenticator'),
graphic: noAccounts,
header: 'No accounts',
actions: _buildActions(context, true),
keyActions: _buildActions(
context,
used: 0,
capacity: widget.oathState.version.isAtLeast(4) ? 32 : null,
),
);
}
return Actions(
@ -143,98 +138,92 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
return null;
}),
},
child: Focus(
autofocus: true,
child: AppPage(
title: Focus(
canRequestFocus: false,
onKeyEvent: (node, event) {
if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
node.focusInDirection(TraversalDirection.down);
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
child: Builder(builder: (context) {
return TextFormField(
key: const Key('search_accounts'),
controller: searchController,
focusNode: searchFocus,
style: Theme.of(context).textTheme.titleSmall,
decoration: const InputDecoration(
hintText: 'Search accounts',
isDense: true,
prefixIcon: Icon(Icons.search_outlined),
prefixIconConstraints: BoxConstraints(
minHeight: 30,
minWidth: 30,
),
border: InputBorder.none,
child: AppPage(
title: Focus(
canRequestFocus: false,
onKeyEvent: (node, event) {
if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
node.focusInDirection(TraversalDirection.down);
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
child: Builder(builder: (context) {
return TextFormField(
key: const Key('search_accounts'),
controller: searchController,
focusNode: searchFocus,
style: Theme.of(context).textTheme.titleSmall,
decoration: const InputDecoration(
hintText: 'Search accounts',
isDense: true,
prefixIcon: Icon(Icons.search_outlined),
prefixIconConstraints: BoxConstraints(
minHeight: 30,
minWidth: 30,
),
onChanged: (value) {
ref.read(searchProvider.notifier).setFilter(value);
},
textInputAction: TextInputAction.next,
onFieldSubmitted: (value) {
Focus.of(context).focusInDirection(TraversalDirection.down);
},
);
}),
),
actions: _buildActions(context, false),
child: AccountList(widget.devicePath, widget.oathState),
border: InputBorder.none,
),
onChanged: (value) {
ref.read(searchProvider.notifier).setFilter(value);
},
textInputAction: TextInputAction.next,
onFieldSubmitted: (value) {
Focus.of(context).focusInDirection(TraversalDirection.down);
},
);
}),
),
keyActions: _buildActions(
context,
used: credentials?.length ?? 0,
capacity: widget.oathState.version.isAtLeast(4) ? 32 : null,
),
child: AccountList(widget.devicePath, widget.oathState),
),
);
}
List<Widget> _buildActions(BuildContext context, bool isEmpty) {
List<PopupMenuEntry> _buildActions(BuildContext context,
{required int used, int? capacity}) {
return [
OutlinedButton.icon(
style: isEmpty ? AppTheme.primaryOutlinedButtonStyle(context) : null,
label: const Text('Add account'),
icon: const Icon(Icons.person_add_alt_1),
onPressed: () {
showDialog(
context: context,
builder: (context) => OathAddAccountPage(
widget.devicePath,
widget.oathState,
openQrScanner: Platform.isAndroid,
),
);
},
),
OutlinedButton.icon(
label: const Text('Options'),
icon: const Icon(Icons.tune),
onPressed: () {
showBottomMenu(context, [
MenuAction(
text:
widget.oathState.hasKey ? 'Manage password' : 'Set password',
icon: const Icon(Icons.password),
action: (context) {
showDialog(
buildMenuItem(
title: const Text('Add account'),
leading: const Icon(Icons.person_add_alt_1),
trailing: capacity != null ? '$used/$capacity' : null,
action: capacity == null || capacity > used
? () {
showBlurDialog(
context: context,
builder: (context) =>
ManagePasswordDialog(widget.devicePath, widget.oathState),
builder: (context) => OathAddAccountPage(
widget.devicePath,
widget.oathState,
openQrScanner: Platform.isAndroid,
),
);
},
),
MenuAction(
text: 'Reset OATH',
icon: const Icon(Icons.delete),
action: (context) {
showDialog(
context: context,
builder: (context) => ResetDialog(widget.devicePath),
);
},
),
]);
},
}
: null,
),
buildMenuItem(
title: Text(
widget.oathState.hasKey ? 'Manage password' : 'Set password'),
leading: const Icon(Icons.password),
action: () {
showBlurDialog(
context: context,
builder: (context) =>
ManagePasswordDialog(widget.devicePath, widget.oathState),
);
}),
buildMenuItem(
title: const Text('Reset OATH'),
leading: const Icon(Icons.delete),
action: () {
showBlurDialog(
context: context,
builder: (context) => ResetDialog(widget.devicePath),
);
}),
];
}
}

View File

@ -8,6 +8,7 @@ import '../../app/message.dart';
import '../../app/models.dart';
import '../../desktop/models.dart';
import '../../widgets/responsive_dialog.dart';
import '../../widgets/utf8_utils.dart';
import '../models.dart';
import '../state.dart';
import 'utils.dart';
@ -100,6 +101,8 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
initialValue: _issuer,
enabled: issuerRemaining > 0,
maxLength: issuerRemaining > 0 ? issuerRemaining : null,
buildCounter: buildByteCounterFor(_issuer),
inputFormatters: [limitBytesLength(issuerRemaining)],
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Issuer (optional)',
@ -115,6 +118,8 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
TextFormField(
initialValue: _account,
maxLength: nameRemaining,
inputFormatters: [limitBytesLength(nameRemaining)],
buildCounter: buildByteCounterFor(_account),
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'Account name',

View File

@ -1,5 +1,6 @@
import 'dart:math';
import '../../widgets/utf8_utils.dart';
import '../models.dart';
import '../../core/models.dart';
@ -18,7 +19,7 @@ Pair<int, int> getRemainingKeySpace(
// Non-standard TOTP periods are stored as part of this data, as a "D/"- prefix.
remaining -= '$period/'.length;
}
int issuerSpace = issuer.length;
int issuerSpace = byteLength(issuer);
if (issuer.isNotEmpty) {
// Issuer is separated from name with a ":", if present.
issuerSpace += 1;
@ -26,7 +27,7 @@ Pair<int, int> getRemainingKeySpace(
return Pair(
// Always reserve at least one character for name
remaining - 1 - max(name.length, 1),
remaining - 1 - max(byteLength(name), 1),
remaining - issuerSpace,
);
}

View File

@ -1,22 +0,0 @@
import 'package:flutter/material.dart';
class DialogFrame extends StatelessWidget {
final Widget child;
const DialogFrame({super.key, required this.child});
@override
Widget build(BuildContext context) => GestureDetector(
onTap: () {
Navigator.of(context).pop();
},
// Shows Snackbars above modal
child: Scaffold(
backgroundColor: Colors.transparent,
body: GestureDetector(
behavior: HitTestBehavior.deferToChild,
onTap: () {}, // Block onTap of parent gesture detector
child: child,
),
),
);
}

31
lib/widgets/menu_list_tile.dart Executable file
View File

@ -0,0 +1,31 @@
import 'dart:async';
import 'package:flutter/material.dart';
PopupMenuItem buildMenuItem({
required Widget title,
Widget? leading,
String? trailing,
void Function()? action,
}) =>
PopupMenuItem(
enabled: action != null,
onTap: () {
// Wait for popup menu to close before running action.
Timer.run(action!);
},
child: ListTile(
enabled: action != null,
dense: true,
contentPadding: EdgeInsets.zero,
minLeadingWidth: 0,
title: title,
leading: leading,
trailing: trailing != null
? Opacity(
opacity: 0.5,
child: Text(trailing, textScaleFactor: 0.7),
)
: null,
),
);

View File

@ -1,7 +1,5 @@
import 'package:flutter/material.dart';
import 'dialog_frame.dart';
class ResponsiveDialog extends StatefulWidget {
final Widget? title;
final Widget child;
@ -48,25 +46,23 @@ class _ResponsiveDialogState extends State<ResponsiveDialog> {
final cancelText = widget.onCancel == null && widget.actions.isEmpty
? 'Close'
: 'Cancel';
return DialogFrame(
child: AlertDialog(
title: widget.title,
scrollable: true,
content: SizedBox(
width: 380,
child: Container(key: _childKey, child: widget.child),
),
actions: [
TextButton(
child: Text(cancelText),
onPressed: () {
widget.onCancel?.call();
Navigator.of(context).pop();
},
),
...widget.actions
],
return AlertDialog(
title: widget.title,
scrollable: true,
content: SizedBox(
width: 380,
child: Container(key: _childKey, child: widget.child),
),
actions: [
TextButton(
child: Text(cancelText),
onPressed: () {
widget.onCancel?.call();
Navigator.of(context).pop();
},
),
...widget.actions
],
);
}
}));

126
lib/widgets/toast.dart Executable file
View File

@ -0,0 +1,126 @@
import 'dart:async';
import 'package:flutter/material.dart';
class Toast extends StatefulWidget {
final String message;
final Duration duration;
final void Function() onComplete;
final Color? backgroundColor;
final TextStyle? textStyle;
const Toast(
this.message,
this.duration, {
required this.onComplete,
this.backgroundColor,
this.textStyle,
super.key,
});
@override
State<StatefulWidget> createState() => _ToastState();
}
class _ToastState extends State<Toast> with SingleTickerProviderStateMixin {
late AnimationController _animator;
late Tween<double> _tween;
late Animation<double> _opacity;
@override
void initState() {
super.initState();
_animator = AnimationController(
duration: const Duration(milliseconds: 100), vsync: this);
_tween = Tween(begin: 0, end: 1);
_opacity = _tween.animate(_animator)
..addListener(() {
setState(() {});
});
_animate();
}
void _animate() async {
await _animator.forward();
if (mounted) {
await Future.delayed(widget.duration);
}
if (mounted) {
await _animator.reverse();
}
widget.onComplete();
}
@override
void dispose() {
_animator.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _opacity,
child: Material(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(4.0)),
),
color: widget.backgroundColor,
child: Center(
child: Text(
widget.message,
style: widget.textStyle,
)),
),
);
}
}
void Function() showToast(
BuildContext context,
String message, {
Duration duration = const Duration(seconds: 2),
}) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final bool isThemeDark = theme.brightness == Brightness.dark;
final Color backgroundColor = isThemeDark
? colorScheme.onSurface
: Color.alphaBlend(
colorScheme.onSurface.withOpacity(0.80), colorScheme.surface);
final textStyle =
ThemeData(brightness: isThemeDark ? Brightness.light : Brightness.dark)
.textTheme
.subtitle1;
OverlayEntry? entry;
void close() {
if (entry != null && entry.mounted) {
entry.remove();
}
}
entry = OverlayEntry(builder: (context) {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
height: 50,
width: 400,
margin: const EdgeInsets.all(8),
child: Toast(
message,
duration,
backgroundColor: backgroundColor,
textStyle: textStyle,
onComplete: close,
),
),
);
});
Timer.run(() {
Overlay.of(context)!.insert(entry!);
});
return close;
}

29
lib/widgets/utf8_utils.dart Executable file
View File

@ -0,0 +1,29 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// Get the number of bytes used by a String when encoded to UTF-8.
int byteLength(String value) => utf8.encode(value).length;
/// Builds a counter widget showing number of bytes used/available.
///
/// Set this as a [TextField.buildCounter] callback to show the number of bytes
/// used rather than number of characters. [currentValue] should always match
/// the input text value to measure.
InputCounterWidgetBuilder buildByteCounterFor(String currentValue) =>
(context, {required currentLength, required isFocused, maxLength}) => Text(
maxLength != null ? '${byteLength(currentValue)}/$maxLength' : '',
style: Theme.of(context).textTheme.caption,
);
/// Limits the input in length based on the byte length when encoded.
/// This is generally used together with [buildByteCounterFor].
TextInputFormatter limitBytesLength(int maxByteLength) =>
TextInputFormatter.withFunction((oldValue, newValue) {
final newLength = byteLength(newValue.text);
if (newLength <= maxByteLength) {
return newValue;
}
return oldValue;
});

View File

@ -7,14 +7,14 @@ packages:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "38.0.0"
version: "41.0.0"
analyzer:
dependency: "direct overridden"
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "3.4.1"
version: "4.2.0"
archive:
dependency: transitive
description:
@ -98,7 +98,7 @@ packages:
name: built_value
url: "https://pub.dartlang.org"
source: hosted
version: "8.3.2"
version: "8.3.3"
characters:
dependency: transitive
description:
@ -189,7 +189,7 @@ packages:
name: ffi
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
version: "2.0.1"
file:
dependency: transitive
description:
@ -244,7 +244,7 @@ packages:
name: freezed
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.3+1"
version: "2.0.4"
freezed_annotation:
dependency: "direct main"
description:
@ -270,7 +270,7 @@ packages:
name: glob
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
version: "2.1.0"
graphs:
dependency: transitive
description:
@ -284,7 +284,7 @@ packages:
name: http_multi_server
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.0"
version: "3.2.1"
http_parser:
dependency: transitive
description:
@ -373,7 +373,7 @@ packages:
name: package_config
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
version: "2.1.0"
path:
dependency: transitive
description:
@ -408,7 +408,7 @@ packages:
name: pigeon
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.5"
version: "3.2.3"
platform:
dependency: transitive
description:
@ -429,7 +429,7 @@ packages:
name: pool
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.0"
version: "1.5.1"
process:
dependency: transitive
description:
@ -534,14 +534,14 @@ packages:
name: shelf
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
version: "1.3.1"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
version: "1.0.2"
sky_engine:
dependency: transitive
description: flutter
@ -644,7 +644,7 @@ packages:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.3"
version: "6.1.4"
url_launcher_android:
dependency: transitive
description:
@ -679,14 +679,14 @@ packages:
name: url_launcher_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
version: "2.1.0"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.11"
version: "2.0.12"
url_launcher_windows:
dependency: transitive
description:

7
pubspec.yaml Executable file → Normal file
View File

@ -43,7 +43,7 @@ dependencies:
collection: ^1.16.0
shared_preferences: ^2.0.12
flutter_riverpod: ^1.0.0
json_annotation: ^4.4.0
json_annotation: ^4.5.0
freezed_annotation: ^2.0.3
window_manager: ^0.2.0
qrscanner_zxing:
@ -51,9 +51,6 @@ dependencies:
desktop_drop: ^0.3.3
url_launcher: ^6.1.2
dependency_overrides:
analyzer: ^3.4.1
dev_dependencies:
integration_test:
sdk: flutter
@ -70,7 +67,7 @@ dev_dependencies:
build_runner: ^2.1.4
freezed: ^2.0.3
json_serializable: ^6.0.0
pigeon: ^3.0.3
pigeon: ^3.1.6
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

BIN
resources/win/license.rtf Normal file

Binary file not shown.

View File

@ -0,0 +1,19 @@
$version=6.0.0-dev.0
echo "Renaming the Actions folder and moving it"
mv yubioath-desktop-* release
echo "Signing the executables"
signtool.exe sign /fd SHA256 /t http://timestamp.digicert.com/scripts/timstamp.dll release/authenticator.exe
signtool.exe sign /fd SHA256 /t http://timestamp.digicert.com/scripts/timstamp.dll release/helper/authenticator-helper.exe
echo "Setting env var and building installer"
$env:SRCDIR = ".\release\"
heat dir .\release -out fragment.wxs -gg -scom -srd -sfrag -dr INSTALLDIR -cg ApplicationFiles -var env.SRCDIR
candle .\fragment.wxs .\resources\win\yubioath-desktop.wxs -ext WixUtilExtension -arch x64
light fragment.wixobj yubioath-desktop.wixobj -ext WixUIExtension -ext WixUtilExtension -o yubioath-desktop-$version-win64.msi
echo "Signing the installer"
signtool.exe sign /d "Yubico Authenticator" /fd SHA256 /t http://timestamp.digicert.com/scripts/timstamp.dll yubioath-desktop-$version-win64.msi
echo "All done"

View File

@ -10,6 +10,7 @@
<WixVariable Id="WixUIDialogBmp" Value="resources\icons\yubico-msi-background.png" />
<WixVariable Id="WixUIBannerBmp" Value="resources\icons\yubico-msi-y-banner.png" />
<WixVariable Id="WixUILicenseRtf" Value="license.rtf" />
<Icon Id="icon.ico" SourceFile="resources\icons\com.yubico.yubioath.ico"/>
<Property Id="ARPPRODUCTICON" Value="icon.ico" />
@ -31,6 +32,11 @@
<UI>
<UIRef Id="WixUI_InstallDir" />
<Publish Dialog="WelcomeDlg"
Control="Next"
Event="NewDialog"
Value="LicenseAgreementDlg"
Order="2">1</Publish>
<Publish Dialog="LicenseAgreementDlg"
Control="Next"
Event="NewDialog"
Value="InstallDirDlg"

View File

@ -131,7 +131,15 @@ def update_helper_version(buf):
)
return buf
# release-win.ps1
def update_release_win(buf):
return sub1(
rf'\$version={version_pattern}',
f'$version={version}',
buf,
)
update_file("pubspec.yaml", update_pubspec)
update_file("windows/runner/Runner.rc", update_runner_rc)
update_file("helper/version_info.txt", update_helper_version)
update_file("helper/version_info.txt", update_helper_version)
update_file("resources/win/release-win.ps1", update_release_win)