mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-22 00:12:09 +03:00
Merge branch main into adamve/nfc_activity_widget
This commit is contained in:
commit
a82bcceecb
2
.github/workflows/android.yml
vendored
2
.github/workflows/android.yml
vendored
@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: set up JDK 17
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
|
4
.github/workflows/env
vendored
4
.github/workflows/env
vendored
@ -1,2 +1,2 @@
|
||||
FLUTTER=3.22.2
|
||||
PYVER=3.12.4
|
||||
FLUTTER=3.24.0
|
||||
PYVER=3.12.5
|
||||
|
8
.github/workflows/macos.yml
vendored
8
.github/workflows/macos.yml
vendored
@ -52,11 +52,17 @@ jobs:
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
architecture: 'x64'
|
||||
flutter-version: ${{ env.FLUTTER }}
|
||||
- run: flutter config --enable-macos-desktop
|
||||
- run: flutter --version
|
||||
|
||||
- name: Apply Flutter Patch
|
||||
run: |
|
||||
cd $FLUTTER_ROOT
|
||||
git apply $GITHUB_WORKSPACE/macos_assemble.patch
|
||||
env:
|
||||
GITHUB_WORKSPACE: ${{ github.workspace }}
|
||||
|
||||
- name: Run lints/tests
|
||||
env:
|
||||
SKIP: ${{ steps.cache-helper.outputs.cache-hit == 'true' && 'mypy,flake8,black,bandit' || ''}}
|
||||
|
@ -56,6 +56,7 @@ import com.yubico.authenticator.yubikit.NfcActivityDispatcher
|
||||
import com.yubico.authenticator.yubikit.NfcActivityListener
|
||||
import com.yubico.authenticator.yubikit.NfcActivityState
|
||||
import com.yubico.authenticator.yubikit.getDeviceInfo
|
||||
import com.yubico.authenticator.yubikit.withConnection
|
||||
import com.yubico.yubikit.android.YubiKitManager
|
||||
import com.yubico.yubikit.android.transport.nfc.NfcConfiguration
|
||||
import com.yubico.yubikit.android.transport.nfc.NfcNotAvailable
|
||||
@ -63,7 +64,14 @@ import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice
|
||||
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyManager
|
||||
import com.yubico.yubikit.android.transport.usb.UsbConfiguration
|
||||
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyManager
|
||||
import com.yubico.yubikit.core.Transport
|
||||
import com.yubico.yubikit.core.YubiKeyDevice
|
||||
import com.yubico.yubikit.core.smartcard.SmartCardConnection
|
||||
import com.yubico.yubikit.core.smartcard.scp.KeyRef
|
||||
import com.yubico.yubikit.core.smartcard.scp.Scp11KeyParams
|
||||
import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams
|
||||
import com.yubico.yubikit.core.smartcard.scp.ScpKid
|
||||
import com.yubico.yubikit.core.smartcard.scp.SecurityDomainSession
|
||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
@ -309,6 +317,25 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
return
|
||||
}
|
||||
|
||||
// If NFC and FIPS check for SCP11b key
|
||||
if (device.transport == Transport.NFC && deviceInfo.fipsCapable != 0) {
|
||||
logger.debug("Checking for usable SCP11b key...")
|
||||
deviceManager.scpKeyParams =
|
||||
device.withConnection<SmartCardConnection, ScpKeyParams?> { connection ->
|
||||
val scp = SecurityDomainSession(connection)
|
||||
val keyRef = scp.keyInformation.keys.firstOrNull { it.kid == ScpKid.SCP11b }
|
||||
keyRef?.let {
|
||||
val certs = scp.getCertificateBundle(it)
|
||||
if (certs.isNotEmpty()) Scp11KeyParams(
|
||||
keyRef,
|
||||
certs[certs.size - 1].publicKey
|
||||
) else null
|
||||
}?.also {
|
||||
logger.debug("Found SCP11b key: {}", keyRef)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val supportedContexts = DeviceManager.getSupportedContexts(deviceInfo)
|
||||
logger.debug("Connected key supports: {}", supportedContexts)
|
||||
if (!supportedContexts.contains(viewModel.appContext.value)) {
|
||||
@ -465,7 +492,7 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
}
|
||||
|
||||
private val sharedPreferencesListener = OnSharedPreferenceChangeListener { _, key ->
|
||||
if ( AppPreferences.PREF_NFC_SILENCE_SOUNDS == key) {
|
||||
if (AppPreferences.PREF_NFC_SILENCE_SOUNDS == key) {
|
||||
stopNfcDiscovery()
|
||||
startNfcDiscovery()
|
||||
}
|
||||
@ -531,6 +558,7 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
}
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"hasCamera" -> {
|
||||
val cameraService =
|
||||
getSystemService(CAMERA_SERVICE) as CameraManager
|
||||
@ -541,9 +569,11 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
"hasNfc" -> result.success(
|
||||
packageManager.hasSystemFeature(PackageManager.FEATURE_NFC)
|
||||
)
|
||||
|
||||
"isNfcEnabled" -> {
|
||||
val nfcAdapter = NfcAdapter.getDefaultAdapter(this@MainActivity)
|
||||
|
||||
@ -551,6 +581,7 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
nfcAdapter != null && nfcAdapter.isEnabled
|
||||
)
|
||||
}
|
||||
|
||||
"openNfcSettings" -> {
|
||||
startActivity(Intent(ACTION_NFC_SETTINGS))
|
||||
result.success(true)
|
||||
|
@ -24,6 +24,7 @@ import com.yubico.authenticator.MainViewModel
|
||||
import com.yubico.authenticator.OperationContext
|
||||
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
|
||||
import com.yubico.yubikit.core.YubiKeyDevice
|
||||
import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams
|
||||
import com.yubico.yubikit.management.Capability
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
@ -46,6 +47,15 @@ class DeviceManager(
|
||||
|
||||
private val deviceListeners = HashSet<DeviceListener>()
|
||||
|
||||
val deviceInfo: Info?
|
||||
get() = appViewModel.deviceInfo.value
|
||||
|
||||
var scpKeyParams: ScpKeyParams? = null
|
||||
set(value) {
|
||||
field = value
|
||||
logger.debug("SCP params set to {}", value)
|
||||
}
|
||||
|
||||
fun addDeviceListener(listener: DeviceListener) {
|
||||
deviceListeners.add(listener)
|
||||
}
|
||||
@ -157,6 +167,7 @@ class DeviceManager(
|
||||
|
||||
fun setDeviceInfo(deviceInfo: Info?) {
|
||||
appViewModel.setDeviceInfo(deviceInfo)
|
||||
scpKeyParams = null
|
||||
}
|
||||
|
||||
fun isUsbKeyConnected(): Boolean {
|
||||
|
@ -21,7 +21,7 @@ import com.yubico.yubikit.management.DeviceInfo
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
private fun DeviceInfo.capabilitiesFor(transport: Transport) : Int? =
|
||||
private fun DeviceInfo.capabilitiesFor(transport: Transport): Int? =
|
||||
when {
|
||||
hasTransport(transport) -> getSupportedCapabilities(transport)
|
||||
else -> null
|
||||
@ -30,7 +30,7 @@ private fun DeviceInfo.capabilitiesFor(transport: Transport) : Int? =
|
||||
@Serializable
|
||||
data class Info(
|
||||
@SerialName("config")
|
||||
val config : Config,
|
||||
val config: Config,
|
||||
@SerialName("serial")
|
||||
val serialNumber: Int?,
|
||||
@SerialName("version")
|
||||
@ -55,11 +55,19 @@ data class Info(
|
||||
val supportedCapabilities: Capabilities,
|
||||
@SerialName("fips_capable")
|
||||
val fipsCapable: Int,
|
||||
@SerialName("fips_approved")
|
||||
val fipsApproved: Int,
|
||||
@SerialName("reset_blocked")
|
||||
val resetBlocked: Int,
|
||||
) {
|
||||
constructor(name: String, isNfc: Boolean, usbPid: Int?, deviceInfo: DeviceInfo) : this(
|
||||
config = Config(deviceInfo.config),
|
||||
serialNumber = deviceInfo.serialNumber,
|
||||
version = Version(deviceInfo.version.major, deviceInfo.version.minor, deviceInfo.version.micro),
|
||||
version = Version(
|
||||
deviceInfo.version.major,
|
||||
deviceInfo.version.minor,
|
||||
deviceInfo.version.micro
|
||||
),
|
||||
formFactor = deviceInfo.formFactor.value,
|
||||
isLocked = deviceInfo.isLocked,
|
||||
isSky = deviceInfo.isSky,
|
||||
@ -72,6 +80,8 @@ data class Info(
|
||||
nfc = deviceInfo.capabilitiesFor(Transport.NFC),
|
||||
usb = deviceInfo.capabilitiesFor(Transport.USB),
|
||||
),
|
||||
fipsCapable = deviceInfo.fipsCapable
|
||||
fipsCapable = deviceInfo.fipsCapable,
|
||||
fipsApproved = deviceInfo.fipsApproved,
|
||||
resetBlocked = deviceInfo.resetBlocked,
|
||||
)
|
||||
}
|
||||
|
@ -22,7 +22,9 @@ val UnknownDevice = Info(
|
||||
usbPid = null,
|
||||
pinComplexity = false,
|
||||
supportedCapabilities = Capabilities(),
|
||||
fipsCapable = 0
|
||||
fipsCapable = 0,
|
||||
fipsApproved = 0,
|
||||
resetBlocked = 0
|
||||
)
|
||||
|
||||
fun unknownDeviceWithCapability(transport: Transport, bit: Int = 0) : Info {
|
||||
|
@ -26,7 +26,8 @@ enum class FidoActionDescription(private val value: Int) {
|
||||
DeleteFingerprint(4),
|
||||
RenameFingerprint(5),
|
||||
RegisterFingerprint(6),
|
||||
ActionFailure(7);
|
||||
EnableEnterpriseAttestation(7),
|
||||
ActionFailure(8);
|
||||
|
||||
val id: Int
|
||||
get() = value + dialogDescriptionFidoIndex
|
||||
|
@ -42,6 +42,7 @@ import com.yubico.yubikit.core.smartcard.SmartCardConnection
|
||||
import com.yubico.yubikit.core.util.Result
|
||||
import com.yubico.yubikit.fido.ctap.BioEnrollment
|
||||
import com.yubico.yubikit.fido.ctap.ClientPin
|
||||
import com.yubico.yubikit.fido.ctap.Config
|
||||
import com.yubico.yubikit.fido.ctap.CredentialManagement
|
||||
import com.yubico.yubikit.fido.ctap.Ctap2Session.InfoData
|
||||
import com.yubico.yubikit.fido.ctap.FingerprintBioEnrollment
|
||||
@ -159,6 +160,8 @@ class FidoManager(
|
||||
|
||||
"cancelRegisterFingerprint" -> cancelRegisterFingerprint()
|
||||
|
||||
"enableEnterpriseAttestation" -> enableEnterpriseAttestation()
|
||||
|
||||
else -> throw NotImplementedError()
|
||||
}
|
||||
}
|
||||
@ -603,6 +606,42 @@ class FidoManager(
|
||||
).toString()
|
||||
}
|
||||
|
||||
private suspend fun enableEnterpriseAttestation(): String =
|
||||
connectionHelper.useSession(FidoActionDescription.EnableEnterpriseAttestation) { fidoSession ->
|
||||
try {
|
||||
val uvAuthProtocol = getPreferredPinUvAuthProtocol(fidoSession.cachedInfo)
|
||||
val clientPin = ClientPin(fidoSession, uvAuthProtocol)
|
||||
val token = if (pinStore.hasPin()) {
|
||||
clientPin.getPinToken(
|
||||
pinStore.getPin(),
|
||||
ClientPin.PIN_PERMISSION_ACFG,
|
||||
null
|
||||
)
|
||||
} else null
|
||||
|
||||
val config = Config(fidoSession, uvAuthProtocol, token)
|
||||
config.enableEnterpriseAttestation()
|
||||
fidoViewModel.setSessionState(
|
||||
Session(
|
||||
fidoSession.info,
|
||||
pinStore.hasPin(),
|
||||
pinRetries
|
||||
)
|
||||
)
|
||||
return@useSession JSONObject(
|
||||
mapOf(
|
||||
"success" to true,
|
||||
)
|
||||
).toString()
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to enable enterprise attestation. ", e)
|
||||
return@useSession JSONObject(
|
||||
mapOf(
|
||||
"success" to false,
|
||||
)
|
||||
).toString()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDisconnected() {
|
||||
if (!resetHelper.inProgress) {
|
||||
|
@ -29,7 +29,8 @@ data class Options(
|
||||
val credMgmt: Boolean,
|
||||
val credentialMgmtPreview: Boolean,
|
||||
val bioEnroll: Boolean?,
|
||||
val alwaysUv: Boolean
|
||||
val alwaysUv: Boolean,
|
||||
val ep: Boolean?,
|
||||
) {
|
||||
constructor(infoData: InfoData) : this(
|
||||
infoData.getOptionsBoolean("clientPin") == true,
|
||||
@ -37,6 +38,7 @@ data class Options(
|
||||
infoData.getOptionsBoolean("credentialMgmtPreview") == true,
|
||||
infoData.getOptionsBoolean("bioEnroll"),
|
||||
infoData.getOptionsBoolean("alwaysUv") == true,
|
||||
infoData.getOptionsBoolean("ep"),
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
@ -56,6 +56,7 @@ import com.yubico.yubikit.core.smartcard.SW
|
||||
import com.yubico.yubikit.core.smartcard.SmartCardConnection
|
||||
import com.yubico.yubikit.core.smartcard.SmartCardProtocol
|
||||
import com.yubico.yubikit.core.util.Result
|
||||
import com.yubico.yubikit.management.Capability
|
||||
import com.yubico.yubikit.oath.CredentialData
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
@ -615,8 +616,10 @@ class OathManager(
|
||||
* @param connection the device SmartCard connection
|
||||
* @return a [YubiKitOathSession] which is unlocked or locked based on an internal parameter
|
||||
*/
|
||||
private fun getOathSession(connection: SmartCardConnection) : YubiKitOathSession {
|
||||
val session = YubiKitOathSession(connection)
|
||||
private fun getOathSession(connection: SmartCardConnection): YubiKitOathSession {
|
||||
// If OATH is FIPS capable, and we have scpKeyParams, we should use them
|
||||
val fips = (deviceManager.deviceInfo?.fipsCapable ?: 0) and Capability.OATH.bit != 0
|
||||
val session = YubiKitOathSession(connection, if (fips) deviceManager.scpKeyParams else null)
|
||||
|
||||
if (!unlockOnConnect.compareAndSet(false, true)) {
|
||||
tryToUnlockOathSession(session)
|
||||
|
@ -76,7 +76,9 @@ class SkyHelper(private val compatUtil: CompatUtil) {
|
||||
usbPid = pid.value,
|
||||
pinComplexity = false,
|
||||
supportedCapabilities = Capabilities(usb = 0),
|
||||
fipsCapable = 0
|
||||
fipsCapable = 0,
|
||||
fipsApproved = 0,
|
||||
resetBlocked = 0
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ allprojects {
|
||||
targetSdkVersion = 34
|
||||
compileSdkVersion = 34
|
||||
|
||||
yubiKitVersion = "2.6.0"
|
||||
yubiKitVersion = "2.7.0-alpha01"
|
||||
junitVersion = "4.13.2"
|
||||
mockitoVersion = "5.12.0"
|
||||
}
|
||||
|
@ -12,7 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .base import RpcException, encode_bytes
|
||||
from .base import RpcResponse, RpcException, encode_bytes
|
||||
from .device import RootNode
|
||||
|
||||
from queue import Queue
|
||||
@ -80,7 +80,7 @@ def _handle_incoming(event, recv, error, cmd_queue):
|
||||
def process(
|
||||
send: Callable[[Dict], None],
|
||||
recv: Callable[[], Dict],
|
||||
handler: Callable[[str, List, Dict, Event, Callable[[str], None]], Dict],
|
||||
handler: Callable[[str, List, Dict, Event, Callable[[str], None]], RpcResponse],
|
||||
) -> None:
|
||||
def error(status: str, message: str, body: Dict = {}):
|
||||
send(dict(kind="error", status=status, message=message, body=body))
|
||||
@ -88,8 +88,8 @@ def process(
|
||||
def signal(status: str, body: Dict = {}):
|
||||
send(dict(kind="signal", status=status, body=body))
|
||||
|
||||
def success(body: Dict):
|
||||
send(dict(kind="success", body=body))
|
||||
def success(response: RpcResponse):
|
||||
send(dict(kind="success", body=response.body, flags=response.flags))
|
||||
|
||||
event = Event()
|
||||
cmd_queue: Queue = Queue(1)
|
||||
|
@ -27,6 +27,12 @@ def encode_bytes(value: bytes) -> str:
|
||||
decode_bytes = bytes.fromhex
|
||||
|
||||
|
||||
class RpcResponse:
|
||||
def __init__(self, body, flags=None):
|
||||
self.body = body
|
||||
self.flags = flags or []
|
||||
|
||||
|
||||
class RpcException(Exception):
|
||||
"""An exception that is returned as the result of an RPC command.i
|
||||
|
||||
@ -116,16 +122,20 @@ class RpcNode:
|
||||
try:
|
||||
if target:
|
||||
traversed += [target[0]]
|
||||
return self.get_child(target[0])(
|
||||
response = self.get_child(target[0])(
|
||||
action, target[1:], params, event, signal, traversed
|
||||
)
|
||||
if action in self.list_actions():
|
||||
return self.get_action(action)(params, event, signal)
|
||||
if action in self.list_children():
|
||||
elif action in self.list_actions():
|
||||
response = self.get_action(action)(params, event, signal)
|
||||
elif action in self.list_children():
|
||||
traversed += [action]
|
||||
return self.get_child(action)(
|
||||
response = self.get_child(action)(
|
||||
"get", [], params, event, signal, traversed
|
||||
)
|
||||
|
||||
if isinstance(response, RpcResponse):
|
||||
return response
|
||||
return RpcResponse(response)
|
||||
except ChildResetException as e:
|
||||
self._close_child()
|
||||
raise StateResetException(e.message, traversed)
|
||||
|
@ -31,18 +31,21 @@ from ykman.base import PID
|
||||
from ykman.device import scan_devices, list_all_devices
|
||||
from ykman.diagnostics import get_diagnostics
|
||||
from ykman.logging import set_log_level
|
||||
from yubikit.core import TRANSPORT
|
||||
from yubikit.core import TRANSPORT, NotSupportedError
|
||||
from yubikit.core.smartcard import SmartCardConnection, ApduError, SW
|
||||
from yubikit.core.smartcard.scp import Scp11KeyParams
|
||||
from yubikit.core.otp import OtpConnection
|
||||
from yubikit.core.fido import FidoConnection
|
||||
from yubikit.support import get_name, read_info
|
||||
from yubikit.management import CAPABILITY
|
||||
from yubikit.securitydomain import SecurityDomainSession
|
||||
from yubikit.logging import LOG_LEVEL
|
||||
|
||||
from ykman.pcsc import list_devices, YK_READER_NAME
|
||||
from smartcard.Exceptions import SmartcardException, NoCardException
|
||||
from smartcard.pcsc.PCSCExceptions import EstablishContextException
|
||||
from smartcard.CardMonitoring import CardObserver, CardMonitor
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey
|
||||
from hashlib import sha256
|
||||
from dataclasses import asdict
|
||||
from typing import Mapping, Tuple
|
||||
@ -255,12 +258,24 @@ class AbstractDeviceNode(RpcNode):
|
||||
super().__init__()
|
||||
self._device = device
|
||||
self._info = info
|
||||
self._data = None
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
try:
|
||||
return super().__call__(*args, **kwargs)
|
||||
response = super().__call__(*args, **kwargs)
|
||||
if "device_info" in response.flags:
|
||||
# Clear DeviceInfo cache
|
||||
self._info = None
|
||||
self._data = None
|
||||
# Make sure any child node is re-opened after this,
|
||||
# as enabled applications may have changed
|
||||
super().close()
|
||||
|
||||
return response
|
||||
|
||||
except (SmartcardException, OSError):
|
||||
logger.exception("Device error")
|
||||
|
||||
self._child = None
|
||||
name = self._child_name
|
||||
self._child_name = None
|
||||
@ -273,6 +288,14 @@ class AbstractDeviceNode(RpcNode):
|
||||
logger.exception(f"Unable to create child {name}")
|
||||
raise NoSuchNodeException(name)
|
||||
|
||||
def get_data(self):
|
||||
if not self._data:
|
||||
self._data = self._refresh_data()
|
||||
return self._data
|
||||
|
||||
def _refresh_data(self):
|
||||
...
|
||||
|
||||
def _read_data(self, conn):
|
||||
pid = self._device.pid
|
||||
self._info = read_info(conn, pid)
|
||||
@ -293,7 +316,7 @@ class UsbDeviceNode(AbstractDeviceNode):
|
||||
connection = self._device.open_connection(conn_type)
|
||||
return ConnectionNode(self._device, connection, self._info)
|
||||
|
||||
def get_data(self):
|
||||
def _refresh_data(self):
|
||||
for conn_type in (SmartCardConnection, OtpConnection, FidoConnection):
|
||||
if self._supports_connection(conn_type):
|
||||
try:
|
||||
@ -332,7 +355,7 @@ class _ReaderObserver(CardObserver):
|
||||
def __init__(self, device):
|
||||
self.device = device
|
||||
self.card = None
|
||||
self.data = None
|
||||
self.needs_refresh = True
|
||||
|
||||
def update(self, observable, actions):
|
||||
added, removed = actions
|
||||
@ -343,7 +366,7 @@ class _ReaderObserver(CardObserver):
|
||||
break
|
||||
else:
|
||||
self.card = None
|
||||
self.data = None
|
||||
self.needs_refresh = True
|
||||
logger.debug(f"NFC card: {self.card}")
|
||||
|
||||
|
||||
@ -359,18 +382,24 @@ class ReaderDeviceNode(AbstractDeviceNode):
|
||||
super().close()
|
||||
|
||||
def get_data(self):
|
||||
if self._observer.data is None:
|
||||
card = self._observer.card
|
||||
if card is None:
|
||||
return dict(present=False, status="no-card")
|
||||
try:
|
||||
with self._device.open_connection(SmartCardConnection) as conn:
|
||||
self._observer.data = dict(self._read_data(conn), present=True)
|
||||
except NoCardException:
|
||||
return dict(present=False, status="no-card")
|
||||
except ValueError:
|
||||
self._observer.data = dict(present=False, status="unknown-device")
|
||||
return self._observer.data
|
||||
if self._observer.needs_refresh:
|
||||
self._data = None
|
||||
return super().get_data()
|
||||
|
||||
def _refresh_data(self):
|
||||
card = self._observer.card
|
||||
if card is None:
|
||||
return dict(present=False, status="no-card")
|
||||
try:
|
||||
with self._device.open_connection(SmartCardConnection) as conn:
|
||||
data = dict(self._read_data(conn), present=True)
|
||||
self._observer.needs_refresh = False
|
||||
return data
|
||||
except NoCardException:
|
||||
return dict(present=False, status="no-card")
|
||||
except ValueError:
|
||||
self._observer.needs_refresh = False
|
||||
return dict(present=False, status="unknown-device")
|
||||
|
||||
@action(closes_child=False)
|
||||
def get(self, params, event, signal):
|
||||
@ -381,7 +410,7 @@ class ReaderDeviceNode(AbstractDeviceNode):
|
||||
try:
|
||||
connection = self._device.open_connection(SmartCardConnection)
|
||||
info = read_info(connection)
|
||||
return ConnectionNode(self._device, connection, info)
|
||||
return ScpConnectionNode(self._device, connection, info)
|
||||
except (ValueError, SmartcardException, EstablishContextException) as e:
|
||||
logger.warning("Error opening connection", exc_info=True)
|
||||
raise ConnectionException(self._device.fingerprint, "ccid", e)
|
||||
@ -436,33 +465,36 @@ class ConnectionNode(RpcNode):
|
||||
self._info = read_info(self._connection, self._device.pid)
|
||||
return dict(version=self._info.version, serial=self._info.serial)
|
||||
|
||||
def _init_child_node(self, child_cls, capability=CAPABILITY(0)):
|
||||
return child_cls(self._connection)
|
||||
|
||||
@child(
|
||||
condition=lambda self: self._transport == TRANSPORT.USB
|
||||
or isinstance(self._connection, SmartCardConnection)
|
||||
)
|
||||
def management(self):
|
||||
return ManagementNode(self._connection)
|
||||
return self._init_child_node(ManagementNode)
|
||||
|
||||
@child(
|
||||
condition=lambda self: isinstance(self._connection, SmartCardConnection)
|
||||
and CAPABILITY.OATH in self.capabilities
|
||||
)
|
||||
def oath(self):
|
||||
return OathNode(self._connection)
|
||||
return self._init_child_node(OathNode, CAPABILITY.OATH)
|
||||
|
||||
@child(
|
||||
condition=lambda self: isinstance(self._connection, SmartCardConnection)
|
||||
and CAPABILITY.PIV in self.capabilities
|
||||
)
|
||||
def piv(self):
|
||||
return PivNode(self._connection)
|
||||
return self._init_child_node(PivNode, CAPABILITY.PIV)
|
||||
|
||||
@child(
|
||||
condition=lambda self: isinstance(self._connection, FidoConnection)
|
||||
and CAPABILITY.FIDO2 in self.capabilities
|
||||
)
|
||||
def ctap2(self):
|
||||
return Ctap2Node(self._connection)
|
||||
return self._init_child_node(Ctap2Node)
|
||||
|
||||
@child(
|
||||
condition=lambda self: CAPABILITY.OTP in self.capabilities
|
||||
@ -479,4 +511,30 @@ class ConnectionNode(RpcNode):
|
||||
)
|
||||
)
|
||||
def yubiotp(self):
|
||||
return YubiOtpNode(self._connection)
|
||||
return self._init_child_node(YubiOtpNode)
|
||||
|
||||
|
||||
class ScpConnectionNode(ConnectionNode):
|
||||
def __init__(self, device, connection, info):
|
||||
super().__init__(device, connection, info)
|
||||
|
||||
self.fips_capable = info.fips_capable
|
||||
self.scp_params = None
|
||||
try:
|
||||
scp = SecurityDomainSession(connection)
|
||||
|
||||
for ref in scp.get_key_information().keys():
|
||||
if ref.kid == 0x13:
|
||||
chain = scp.get_certificate_bundle(ref)
|
||||
if chain:
|
||||
pub_key = chain[-1].public_key()
|
||||
assert isinstance(pub_key, EllipticCurvePublicKey) # nosec
|
||||
self.scp_params = Scp11KeyParams(ref, pub_key)
|
||||
break
|
||||
except NotSupportedError:
|
||||
pass
|
||||
|
||||
def _init_child_node(self, child_cls, capability=CAPABILITY(0)):
|
||||
if capability in self.fips_capable:
|
||||
return child_cls(self._connection, self.scp_params)
|
||||
return child_cls(self._connection)
|
||||
|
@ -13,6 +13,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
from .base import (
|
||||
RpcResponse,
|
||||
RpcNode,
|
||||
action,
|
||||
child,
|
||||
@ -22,7 +23,7 @@ from .base import (
|
||||
PinComplexityException,
|
||||
)
|
||||
from fido2.ctap import CtapError
|
||||
from fido2.ctap2 import Ctap2, ClientPin
|
||||
from fido2.ctap2 import Ctap2, ClientPin, Config
|
||||
from fido2.ctap2.credman import CredentialManagement
|
||||
from fido2.ctap2.bio import BioEnrollment, FPBioEnrollment, CaptureError
|
||||
from fido2.pcsc import CtapPcscDevice
|
||||
@ -189,7 +190,7 @@ class Ctap2Node(RpcNode):
|
||||
raise InactivityException()
|
||||
self._info = self.ctap.get_info()
|
||||
self._token = None
|
||||
return dict()
|
||||
return RpcResponse(dict(), ["device_info"])
|
||||
|
||||
@action(condition=lambda self: self._info.options["clientPin"])
|
||||
def unlock(self, params, event, signal):
|
||||
@ -199,6 +200,8 @@ class Ctap2Node(RpcNode):
|
||||
permissions |= ClientPin.PERMISSION.CREDENTIAL_MGMT
|
||||
if BioEnrollment.is_supported(self._info):
|
||||
permissions |= ClientPin.PERMISSION.BIO_ENROLL
|
||||
if Config.is_supported(self._info):
|
||||
permissions |= ClientPin.PERMISSION.AUTHENTICATOR_CFG
|
||||
try:
|
||||
if permissions:
|
||||
self._token = self.client_pin.get_pin_token(pin, permissions)
|
||||
@ -224,10 +227,18 @@ class Ctap2Node(RpcNode):
|
||||
params.pop("new_pin"),
|
||||
)
|
||||
self._info = self.ctap.get_info()
|
||||
return dict()
|
||||
return RpcResponse(dict(), ["device_info"])
|
||||
except CtapError as e:
|
||||
return _handle_pin_error(e, self.client_pin)
|
||||
|
||||
@action(condition=lambda self: Config.is_supported(self._info))
|
||||
def enable_ep_attestation(self, params, event, signal):
|
||||
if self._info.options["clientPin"] and not self._token:
|
||||
raise AuthRequiredException()
|
||||
config = Config(self.ctap, self.client_pin.protocol, self._token)
|
||||
config._call(Config.CMD.ENABLE_ENTERPRISE_ATT)
|
||||
return dict()
|
||||
|
||||
@child(condition=lambda self: BioEnrollment.is_supported(self._info))
|
||||
def fingerprints(self):
|
||||
if not self._token:
|
||||
|
@ -12,7 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .base import RpcNode, action
|
||||
from .base import RpcResponse, RpcNode, action
|
||||
from yubikit.core import require_version, NotSupportedError, TRANSPORT, Connection
|
||||
from yubikit.core.smartcard import SmartCardConnection
|
||||
from yubikit.core.otp import OtpConnection
|
||||
@ -28,10 +28,10 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ManagementNode(RpcNode):
|
||||
def __init__(self, connection):
|
||||
def __init__(self, connection, scp_params=None):
|
||||
super().__init__()
|
||||
self._connection_type: Type[Connection] = type(connection)
|
||||
self.session = ManagementSession(connection)
|
||||
self.session = ManagementSession(connection, scp_params)
|
||||
|
||||
def get_data(self):
|
||||
try:
|
||||
@ -90,7 +90,7 @@ class ManagementNode(RpcNode):
|
||||
if reboot:
|
||||
enabled = config.enabled_capabilities.get(TRANSPORT.USB)
|
||||
self._await_reboot(serial, enabled)
|
||||
return dict()
|
||||
return RpcResponse(dict(), ["device_info"])
|
||||
|
||||
@action
|
||||
def set_mode(self, params, event, signal):
|
||||
@ -106,4 +106,4 @@ class ManagementNode(RpcNode):
|
||||
)
|
||||
def device_reset(self, params, event, signal):
|
||||
self.session.device_reset()
|
||||
return dict()
|
||||
return RpcResponse(dict(), ["device_info"])
|
||||
|
@ -13,6 +13,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
from .base import (
|
||||
RpcResponse,
|
||||
RpcNode,
|
||||
action,
|
||||
child,
|
||||
@ -77,9 +78,9 @@ class OathNode(RpcNode):
|
||||
logger.warning("Failed to unwrap access key", exc_info=True)
|
||||
return None
|
||||
|
||||
def __init__(self, connection):
|
||||
def __init__(self, connection, scp_params=None):
|
||||
super().__init__()
|
||||
self.session = OathSession(connection)
|
||||
self.session = OathSession(connection, scp_params)
|
||||
self._key_verifier = None
|
||||
|
||||
if self.session.locked:
|
||||
@ -193,7 +194,7 @@ class OathNode(RpcNode):
|
||||
self.session.set_key(key)
|
||||
self._set_key_verifier(key)
|
||||
remember &= self._remember_key(key if remember else None)
|
||||
return dict(remembered=remember)
|
||||
return RpcResponse(dict(remembered=remember), ["device_info"])
|
||||
|
||||
@action(condition=lambda self: self.session.has_key)
|
||||
def unset_key(self, params, event, signal):
|
||||
@ -207,7 +208,7 @@ class OathNode(RpcNode):
|
||||
self.session.reset()
|
||||
self._key_verifier = None
|
||||
self._remember_key(None)
|
||||
return dict()
|
||||
return RpcResponse(dict(), ["device_info"])
|
||||
|
||||
@child
|
||||
def accounts(self):
|
||||
|
@ -13,6 +13,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
from .base import (
|
||||
RpcResponse,
|
||||
RpcNode,
|
||||
action,
|
||||
child,
|
||||
@ -91,9 +92,9 @@ def _handle_pin_puk_error(e):
|
||||
|
||||
|
||||
class PivNode(RpcNode):
|
||||
def __init__(self, connection):
|
||||
def __init__(self, connection, scp_params=None):
|
||||
super().__init__()
|
||||
self.session = PivSession(connection)
|
||||
self.session = PivSession(connection, scp_params)
|
||||
self._pivman_data = get_pivman_data(self.session)
|
||||
self._authenticated = False
|
||||
|
||||
@ -212,7 +213,7 @@ class PivNode(RpcNode):
|
||||
store_key = params.pop("store_key", False)
|
||||
pivman_set_mgm_key(self.session, key, key_type, False, store_key)
|
||||
self._pivman_data = get_pivman_data(self.session)
|
||||
return dict()
|
||||
return RpcResponse(dict(), ["device_info"])
|
||||
|
||||
@action
|
||||
def change_pin(self, params, event, signal):
|
||||
@ -220,9 +221,9 @@ class PivNode(RpcNode):
|
||||
new_pin = params.pop("new_pin")
|
||||
try:
|
||||
pivman_change_pin(self.session, old_pin, new_pin)
|
||||
return RpcResponse(dict(), ["device_info"])
|
||||
except Exception as e:
|
||||
_handle_pin_puk_error(e)
|
||||
return dict()
|
||||
|
||||
@action
|
||||
def change_puk(self, params, event, signal):
|
||||
@ -230,9 +231,9 @@ class PivNode(RpcNode):
|
||||
new_puk = params.pop("new_puk")
|
||||
try:
|
||||
self.session.change_puk(old_puk, new_puk)
|
||||
return RpcResponse(dict(), ["device_info"])
|
||||
except Exception as e:
|
||||
_handle_pin_puk_error(e)
|
||||
return dict()
|
||||
|
||||
@action
|
||||
def unblock_pin(self, params, event, signal):
|
||||
@ -240,16 +241,16 @@ class PivNode(RpcNode):
|
||||
new_pin = params.pop("new_pin")
|
||||
try:
|
||||
self.session.unblock_pin(puk, new_pin)
|
||||
return RpcResponse(dict(), ["device_info"])
|
||||
except Exception as e:
|
||||
_handle_pin_puk_error(e)
|
||||
return dict()
|
||||
|
||||
@action
|
||||
def reset(self, params, event, signal):
|
||||
self.session.reset()
|
||||
self._authenticated = False
|
||||
self._pivman_data = get_pivman_data(self.session)
|
||||
return dict()
|
||||
return RpcResponse(dict(), ["device_info"])
|
||||
|
||||
@child
|
||||
def slots(self):
|
||||
@ -266,9 +267,11 @@ class PivNode(RpcNode):
|
||||
return dict(
|
||||
status=True,
|
||||
password=password is not None,
|
||||
key_type=KEY_TYPE.from_public_key(private_key.public_key())
|
||||
if private_key
|
||||
else None,
|
||||
key_type=(
|
||||
KEY_TYPE.from_public_key(private_key.public_key())
|
||||
if private_key
|
||||
else None
|
||||
),
|
||||
cert_info=_get_cert_info(certificate),
|
||||
)
|
||||
except InvalidPasswordError:
|
||||
@ -413,9 +416,11 @@ class SlotNode(RpcNode):
|
||||
id=f"{int(self.slot):02x}",
|
||||
name=self.slot.name,
|
||||
metadata=_metadata_dict(self.metadata),
|
||||
certificate=self.certificate.public_bytes(encoding=Encoding.PEM).decode()
|
||||
if self.certificate
|
||||
else None,
|
||||
certificate=(
|
||||
self.certificate.public_bytes(encoding=Encoding.PEM).decode()
|
||||
if self.certificate
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
@action(condition=lambda self: self.certificate or self.metadata)
|
||||
@ -492,16 +497,20 @@ class SlotNode(RpcNode):
|
||||
|
||||
return dict(
|
||||
metadata=_metadata_dict(metadata),
|
||||
public_key=private_key.public_key()
|
||||
.public_bytes(
|
||||
encoding=Encoding.PEM, format=PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
.decode()
|
||||
if private_key
|
||||
else None,
|
||||
certificate=self.certificate.public_bytes(encoding=Encoding.PEM).decode()
|
||||
if certs
|
||||
else None,
|
||||
public_key=(
|
||||
private_key.public_key()
|
||||
.public_bytes(
|
||||
encoding=Encoding.PEM, format=PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
.decode()
|
||||
if private_key
|
||||
else None
|
||||
),
|
||||
certificate=(
|
||||
self.certificate.public_bytes(encoding=Encoding.PEM).decode()
|
||||
if certs
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
@action
|
||||
|
@ -40,9 +40,9 @@ _FAIL_MSG = (
|
||||
|
||||
|
||||
class YubiOtpNode(RpcNode):
|
||||
def __init__(self, connection):
|
||||
def __init__(self, connection, scp_params=None):
|
||||
super().__init__()
|
||||
self.session = YubiOtpSession(connection)
|
||||
self.session = YubiOtpSession(connection, scp_params)
|
||||
|
||||
def get_data(self):
|
||||
state = self.session.get_config_state()
|
||||
|
359
helper/poetry.lock
generated
359
helper/poetry.lock
generated
@ -28,63 +28,78 @@ testing = ["jaraco.test", "pytest (!=8.0.*)", "pytest (>=6,!=8.1.*)", "pytest-ch
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "1.16.0"
|
||||
version = "1.17.0"
|
||||
description = "Foreign Function Interface for Python calling C code."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"},
|
||||
{file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"},
|
||||
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"},
|
||||
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"},
|
||||
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"},
|
||||
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"},
|
||||
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"},
|
||||
{file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"},
|
||||
{file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"},
|
||||
{file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"},
|
||||
{file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"},
|
||||
{file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"},
|
||||
{file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"},
|
||||
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"},
|
||||
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"},
|
||||
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"},
|
||||
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"},
|
||||
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"},
|
||||
{file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"},
|
||||
{file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"},
|
||||
{file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"},
|
||||
{file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"},
|
||||
{file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"},
|
||||
{file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"},
|
||||
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"},
|
||||
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"},
|
||||
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"},
|
||||
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"},
|
||||
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"},
|
||||
{file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"},
|
||||
{file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"},
|
||||
{file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"},
|
||||
{file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"},
|
||||
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"},
|
||||
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"},
|
||||
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"},
|
||||
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"},
|
||||
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"},
|
||||
{file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"},
|
||||
{file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"},
|
||||
{file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"},
|
||||
{file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"},
|
||||
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"},
|
||||
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"},
|
||||
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"},
|
||||
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"},
|
||||
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"},
|
||||
{file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"},
|
||||
{file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"},
|
||||
{file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"},
|
||||
{file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"},
|
||||
{file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"},
|
||||
{file = "cffi-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb"},
|
||||
{file = "cffi-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a"},
|
||||
{file = "cffi-1.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42"},
|
||||
{file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d"},
|
||||
{file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2"},
|
||||
{file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab"},
|
||||
{file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b"},
|
||||
{file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206"},
|
||||
{file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa"},
|
||||
{file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f"},
|
||||
{file = "cffi-1.17.0-cp310-cp310-win32.whl", hash = "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc"},
|
||||
{file = "cffi-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2"},
|
||||
{file = "cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720"},
|
||||
{file = "cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9"},
|
||||
{file = "cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb"},
|
||||
{file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424"},
|
||||
{file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d"},
|
||||
{file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8"},
|
||||
{file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6"},
|
||||
{file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"},
|
||||
{file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8"},
|
||||
{file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb"},
|
||||
{file = "cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9"},
|
||||
{file = "cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0"},
|
||||
{file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"},
|
||||
{file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"},
|
||||
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"},
|
||||
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"},
|
||||
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"},
|
||||
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"},
|
||||
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"},
|
||||
{file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"},
|
||||
{file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"},
|
||||
{file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"},
|
||||
{file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"},
|
||||
{file = "cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118"},
|
||||
{file = "cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7"},
|
||||
{file = "cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377"},
|
||||
{file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb"},
|
||||
{file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555"},
|
||||
{file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204"},
|
||||
{file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f"},
|
||||
{file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0"},
|
||||
{file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4"},
|
||||
{file = "cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a"},
|
||||
{file = "cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7"},
|
||||
{file = "cffi-1.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c"},
|
||||
{file = "cffi-1.17.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e"},
|
||||
{file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b"},
|
||||
{file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e"},
|
||||
{file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401"},
|
||||
{file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c"},
|
||||
{file = "cffi-1.17.0-cp38-cp38-win32.whl", hash = "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499"},
|
||||
{file = "cffi-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c"},
|
||||
{file = "cffi-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2"},
|
||||
{file = "cffi-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759"},
|
||||
{file = "cffi-1.17.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4"},
|
||||
{file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82"},
|
||||
{file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf"},
|
||||
{file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058"},
|
||||
{file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932"},
|
||||
{file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693"},
|
||||
{file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3"},
|
||||
{file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4"},
|
||||
{file = "cffi-1.17.0-cp39-cp39-win32.whl", hash = "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb"},
|
||||
{file = "cffi-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29"},
|
||||
{file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -117,43 +132,38 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "42.0.8"
|
||||
version = "43.0.0"
|
||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-win32.whl", hash = "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-win_amd64.whl", hash = "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a"},
|
||||
{file = "cryptography-42.0.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe"},
|
||||
{file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c"},
|
||||
{file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71"},
|
||||
{file = "cryptography-42.0.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d"},
|
||||
{file = "cryptography-42.0.8-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c"},
|
||||
{file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842"},
|
||||
{file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648"},
|
||||
{file = "cryptography-42.0.8-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad"},
|
||||
{file = "cryptography-42.0.8.tar.gz", hash = "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2"},
|
||||
{file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"},
|
||||
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"},
|
||||
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"},
|
||||
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"},
|
||||
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"},
|
||||
{file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"},
|
||||
{file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"},
|
||||
{file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"},
|
||||
{file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"},
|
||||
{file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"},
|
||||
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"},
|
||||
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"},
|
||||
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"},
|
||||
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"},
|
||||
{file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"},
|
||||
{file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"},
|
||||
{file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"},
|
||||
{file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"},
|
||||
{file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"},
|
||||
{file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"},
|
||||
{file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"},
|
||||
{file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"},
|
||||
{file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"},
|
||||
{file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"},
|
||||
{file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"},
|
||||
{file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"},
|
||||
{file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -166,18 +176,18 @@ nox = ["nox"]
|
||||
pep8test = ["check-sdist", "click", "mypy", "ruff"]
|
||||
sdist = ["build"]
|
||||
ssh = ["bcrypt (>=3.1.5)"]
|
||||
test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
|
||||
test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
|
||||
test-randomorder = ["pytest-randomly"]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.2.1"
|
||||
version = "1.2.2"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"},
|
||||
{file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"},
|
||||
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
|
||||
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@ -202,13 +212,13 @@ pcsc = ["pyscard (>=1.9,<3)"]
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "8.0.0"
|
||||
version = "8.2.0"
|
||||
description = "Read metadata from Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"},
|
||||
{file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"},
|
||||
{file = "importlib_metadata-8.2.0-py3-none-any.whl", hash = "sha256:11901fa0c2f97919b288679932bb64febaeacf289d18ac84dd68cb2e74213369"},
|
||||
{file = "importlib_metadata-8.2.0.tar.gz", hash = "sha256:72e8d4399996132204f9a16dcc751af254a48f8d1b20b9ff0f98d4a8f901e73d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -221,21 +231,21 @@ test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "p
|
||||
|
||||
[[package]]
|
||||
name = "importlib-resources"
|
||||
version = "6.4.0"
|
||||
version = "6.4.2"
|
||||
description = "Read resources from Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"},
|
||||
{file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"},
|
||||
{file = "importlib_resources-6.4.2-py3-none-any.whl", hash = "sha256:8bba8c54a8a3afaa1419910845fa26ebd706dc716dd208d9b158b4b6966f5c5c"},
|
||||
{file = "importlib_resources-6.4.2.tar.gz", hash = "sha256:6cbfbefc449cc6e2095dd184691b7a12a04f40bc75dd4c55d31c34f174cdf57a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"]
|
||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
@ -286,21 +296,21 @@ testing = ["portend", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytes
|
||||
|
||||
[[package]]
|
||||
name = "jaraco-functools"
|
||||
version = "4.0.1"
|
||||
version = "4.0.2"
|
||||
description = "Functools like those found in stdlib"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "jaraco.functools-4.0.1-py3-none-any.whl", hash = "sha256:3b24ccb921d6b593bdceb56ce14799204f473976e2a9d4b15b04d0f2c2326664"},
|
||||
{file = "jaraco_functools-4.0.1.tar.gz", hash = "sha256:d33fa765374c0611b52f8b3a795f8900869aa88c84769d4d1746cd68fb28c3e8"},
|
||||
{file = "jaraco.functools-4.0.2-py3-none-any.whl", hash = "sha256:c9d16a3ed4ccb5a889ad8e0b7a343401ee5b2a71cee6ed192d3f68bc351e94e3"},
|
||||
{file = "jaraco_functools-4.0.2.tar.gz", hash = "sha256:3460c74cd0d32bf82b9576bbb3527c4364d5b27a21f5158a62aed6c4b42e23f5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
more-itertools = "*"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
testing = ["jaraco.classes", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
|
||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
test = ["jaraco.classes", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "jeepney"
|
||||
@ -319,13 +329,13 @@ trio = ["async_generator", "trio"]
|
||||
|
||||
[[package]]
|
||||
name = "keyring"
|
||||
version = "25.2.1"
|
||||
version = "25.3.0"
|
||||
description = "Store and access your passwords safely."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "keyring-25.2.1-py3-none-any.whl", hash = "sha256:2458681cdefc0dbc0b7eb6cf75d0b98e59f9ad9b2d4edd319d18f68bdca95e50"},
|
||||
{file = "keyring-25.2.1.tar.gz", hash = "sha256:daaffd42dbda25ddafb1ad5fec4024e5bbcfe424597ca1ca452b299861e49f1b"},
|
||||
{file = "keyring-25.3.0-py3-none-any.whl", hash = "sha256:8d963da00ccdf06e356acd9bf3b743208878751032d8599c6cc89eb51310ffae"},
|
||||
{file = "keyring-25.3.0.tar.gz", hash = "sha256:8d85a1ea5d6db8515b59e1c5d1d1678b03cf7fc8b8dcfb1651e8c4a524eb42ef"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -340,8 +350,8 @@ SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""}
|
||||
|
||||
[package.extras]
|
||||
completion = ["shtab (>=1.1.0)"]
|
||||
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
testing = ["pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
|
||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
test = ["pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "macholib"
|
||||
@ -359,13 +369,13 @@ altgraph = ">=0.17"
|
||||
|
||||
[[package]]
|
||||
name = "more-itertools"
|
||||
version = "10.3.0"
|
||||
version = "10.4.0"
|
||||
description = "More routines for operating on iterables, beyond itertools"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "more-itertools-10.3.0.tar.gz", hash = "sha256:e5d93ef411224fbcef366a6e8ddc4c5781bc6359d43412a65dd5964e46111463"},
|
||||
{file = "more_itertools-10.3.0-py3-none-any.whl", hash = "sha256:ea6a02e24a9161e51faad17a8782b92a0df82c12c1c8886fec7f0c3fa1a1b320"},
|
||||
{file = "more-itertools-10.4.0.tar.gz", hash = "sha256:fe0e63c4ab068eac62410ab05cccca2dc71ec44ba8ef29916a0090df061cf923"},
|
||||
{file = "more_itertools-10.4.0-py3-none-any.whl", hash = "sha256:0f7d9f83a0a8dcfa8a2694a770590d98a67ea943e3d9f5298309a484758c4e27"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -381,44 +391,44 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "1.10.1"
|
||||
version = "1.11.1"
|
||||
description = "Optional static typing for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"},
|
||||
{file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"},
|
||||
{file = "mypy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a"},
|
||||
{file = "mypy-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9"},
|
||||
{file = "mypy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d"},
|
||||
{file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"},
|
||||
{file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"},
|
||||
{file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"},
|
||||
{file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"},
|
||||
{file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"},
|
||||
{file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"},
|
||||
{file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"},
|
||||
{file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"},
|
||||
{file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"},
|
||||
{file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"},
|
||||
{file = "mypy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c"},
|
||||
{file = "mypy-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade"},
|
||||
{file = "mypy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37"},
|
||||
{file = "mypy-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7"},
|
||||
{file = "mypy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d"},
|
||||
{file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"},
|
||||
{file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"},
|
||||
{file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"},
|
||||
{file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"},
|
||||
{file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"},
|
||||
{file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"},
|
||||
{file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"},
|
||||
{file = "mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c"},
|
||||
{file = "mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411"},
|
||||
{file = "mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03"},
|
||||
{file = "mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4"},
|
||||
{file = "mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58"},
|
||||
{file = "mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5"},
|
||||
{file = "mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca"},
|
||||
{file = "mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de"},
|
||||
{file = "mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"},
|
||||
{file = "mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72"},
|
||||
{file = "mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8"},
|
||||
{file = "mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a"},
|
||||
{file = "mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417"},
|
||||
{file = "mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e"},
|
||||
{file = "mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525"},
|
||||
{file = "mypy-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2"},
|
||||
{file = "mypy-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b"},
|
||||
{file = "mypy-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0"},
|
||||
{file = "mypy-1.11.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd"},
|
||||
{file = "mypy-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb"},
|
||||
{file = "mypy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe"},
|
||||
{file = "mypy-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c"},
|
||||
{file = "mypy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69"},
|
||||
{file = "mypy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74"},
|
||||
{file = "mypy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b"},
|
||||
{file = "mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54"},
|
||||
{file = "mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
mypy-extensions = ">=1.0.0"
|
||||
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
|
||||
typing-extensions = ">=4.1.0"
|
||||
typing-extensions = ">=4.6.0"
|
||||
|
||||
[package.extras]
|
||||
dmypy = ["psutil (>=4.0)"]
|
||||
@ -584,23 +594,23 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller"
|
||||
version = "6.8.0"
|
||||
version = "6.10.0"
|
||||
description = "PyInstaller bundles a Python application and all its dependencies into a single package."
|
||||
optional = false
|
||||
python-versions = "<3.13,>=3.8"
|
||||
python-versions = "<3.14,>=3.8"
|
||||
files = [
|
||||
{file = "pyinstaller-6.8.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:5ff6bc2784c1026f8e2f04aa3760cbed41408e108a9d4cf1dd52ee8351a3f6e1"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:39ac424d2ee2457d2ab11a5091436e75a0cccae207d460d180aa1fcbbafdd528"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_i686.whl", hash = "sha256:355832a3acc7de90a255ecacd4b9f9e166a547a79c8905d49f14e3a75c1acdb9"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:6303c7a009f47e6a96ef65aed49f41e36ece8d079b9193ca92fe807403e5fe80"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2b71509468c811968c0b5decb5bbe85b6292ea52d7b1f26313d2aabb673fa9a5"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ff31c5b99e05a4384bbe2071df67ec8b2b347640a375eae9b40218be2f1754c6"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:000c36b13fe4cd8d0d8c2bc855b1ddcf39867b5adf389e6b5ca45b25fa3e619d"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:fe0af018d7d5077180e3144ada89a4da5df8d07716eb7e9482834a56dc57a4e8"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-win32.whl", hash = "sha256:d257f6645c7334cbd66f38a4fac62c3ad614cc46302b2b5d9f8cc48c563bce0e"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-win_amd64.whl", hash = "sha256:81cccfa9b16699b457f4788c5cc119b50f3cd4d0db924955f15c33f2ad27a50d"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-win_arm64.whl", hash = "sha256:1c3060a263758cf7f0144ab4c016097b20451b2469d468763414665db1bb743d"},
|
||||
{file = "pyinstaller-6.8.0.tar.gz", hash = "sha256:3f4b6520f4423fe19bcc2fd63ab7238851ae2bdcbc98f25bc5d2f97cc62012e9"},
|
||||
{file = "pyinstaller-6.10.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:d60fb22859e11483af735aec115fdde09467cdbb29edd9844839f2c920b748c0"},
|
||||
{file = "pyinstaller-6.10.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:46d75359668993ddd98630a3669dc5249f3c446e35239b43bc7f4155bc574748"},
|
||||
{file = "pyinstaller-6.10.0-py3-none-manylinux2014_i686.whl", hash = "sha256:3398a98fa17d47ccb31f8779ecbdacec025f7adb2f22757a54b706ac8b4fe906"},
|
||||
{file = "pyinstaller-6.10.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e9989f354ae4ed8a3bec7bdb37ae0d170751d6520e500f049c7cd0632d31d5c3"},
|
||||
{file = "pyinstaller-6.10.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:b7c90c91921b3749083115b28f30f40abf2bb481ceff196d2b2ce0eaa2b3d429"},
|
||||
{file = "pyinstaller-6.10.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6cf876d7d93b8b4f28d1ad57fa24645cf43119c79e985dd5e5f7a801245e6f53"},
|
||||
{file = "pyinstaller-6.10.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:db05e3f2f10f9f78c56f1fb163d9cb453433429fe4281218ebaf1ebfd39ba942"},
|
||||
{file = "pyinstaller-6.10.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:28eca3817f176fdc19747e1afcf434f13bb9f17a644f611be2c5a61b1f498ed7"},
|
||||
{file = "pyinstaller-6.10.0-py3-none-win32.whl", hash = "sha256:703e041718987e46ba0568a2c71ecf2459fddef57cf9edf3efeed4a53e3dae3f"},
|
||||
{file = "pyinstaller-6.10.0-py3-none-win_amd64.whl", hash = "sha256:95b55966e563e8b8f31a43882aea10169e9a11fdf38e626d86a2907b640c0701"},
|
||||
{file = "pyinstaller-6.10.0-py3-none-win_arm64.whl", hash = "sha256:308e0a8670c9c9ac0cebbf1bbb492e71b6675606f2ec78bc4adfc830d209e087"},
|
||||
{file = "pyinstaller-6.10.0.tar.gz", hash = "sha256:143840f8056ff7b910bf8f16f6cd92cc10a6c2680bb76d0a25d558d543d21270"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -609,7 +619,7 @@ importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""}
|
||||
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
|
||||
packaging = ">=22.0"
|
||||
pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""}
|
||||
pyinstaller-hooks-contrib = ">=2024.6"
|
||||
pyinstaller-hooks-contrib = ">=2024.8"
|
||||
pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""}
|
||||
setuptools = ">=42.0.0"
|
||||
|
||||
@ -619,13 +629,13 @@ hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller-hooks-contrib"
|
||||
version = "2024.7"
|
||||
version = "2024.8"
|
||||
description = "Community maintained hooks for PyInstaller"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pyinstaller_hooks_contrib-2024.7-py2.py3-none-any.whl", hash = "sha256:8bf0775771fbaf96bcd2f4dfd6f7ae6c1dd1b1efe254c7e50477b3c08e7841d8"},
|
||||
{file = "pyinstaller_hooks_contrib-2024.7.tar.gz", hash = "sha256:fd5f37dcf99bece184e40642af88be16a9b89613ecb958a8bd1136634fc9fac5"},
|
||||
{file = "pyinstaller_hooks_contrib-2024.8-py3-none-any.whl", hash = "sha256:0057fe9a5c398d3f580e73e58793a1d4a8315ca91c3df01efea1c14ed557825a"},
|
||||
{file = "pyinstaller_hooks_contrib-2024.8.tar.gz", hash = "sha256:29b68d878ab739e967055b56a93eb9b58e529d5b054fbab7a2f2bacf80cef3e2"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -662,13 +672,13 @@ pyro = ["Pyro"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.2.2"
|
||||
version = "8.3.2"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"},
|
||||
{file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"},
|
||||
{file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"},
|
||||
{file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -676,7 +686,7 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=1.5,<2.0"
|
||||
pluggy = ">=1.5,<2"
|
||||
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
@ -707,13 +717,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pywin32-ctypes"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
description = "A (partial) reimplementation of pywin32 using ctypes/cffi"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"},
|
||||
{file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"},
|
||||
{file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"},
|
||||
{file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -733,18 +743,19 @@ jeepney = ">=0.6"
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "70.2.0"
|
||||
version = "72.2.0"
|
||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "setuptools-70.2.0-py3-none-any.whl", hash = "sha256:b8b8060bb426838fbe942479c90296ce976249451118ef566a5a0b7d8b78fb05"},
|
||||
{file = "setuptools-70.2.0.tar.gz", hash = "sha256:bd63e505105011b25c3c11f753f7e3b8465ea739efddaccef8f0efac2137bac1"},
|
||||
{file = "setuptools-72.2.0-py3-none-any.whl", hash = "sha256:f11dd94b7bae3a156a95ec151f24e4637fb4fa19c878e4d191bfb8b2d82728c4"},
|
||||
{file = "setuptools-72.2.0.tar.gz", hash = "sha256:80aacbf633704e9c8bfa1d99fa5dd4dc59573efcf9e4042c13d3bcef91ac2ef9"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
|
||||
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
|
||||
core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"]
|
||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
|
||||
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
@ -800,13 +811,13 @@ pywin32 = {version = ">=223", markers = "sys_platform == \"win32\""}
|
||||
|
||||
[[package]]
|
||||
name = "zipp"
|
||||
version = "3.19.2"
|
||||
version = "3.20.0"
|
||||
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"},
|
||||
{file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"},
|
||||
{file = "zipp-3.20.0-py3-none-any.whl", hash = "sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d"},
|
||||
{file = "zipp-3.20.0.tar.gz", hash = "sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
|
@ -94,9 +94,10 @@ class RpcShell(cmd.Cmd):
|
||||
cmd = target.pop() if target else ""
|
||||
node = self.get_node(target)
|
||||
if node:
|
||||
names = [n + "/" for n in node.get("children", [])]
|
||||
body = node.get("body", {})
|
||||
names = [n + "/" for n in body.get("children", [])]
|
||||
if not nodes_only:
|
||||
actions = node.get("actions", [])
|
||||
actions = body.get("actions", [])
|
||||
if "get" in actions:
|
||||
actions.remove("get")
|
||||
names += actions
|
||||
@ -104,10 +105,10 @@ class RpcShell(cmd.Cmd):
|
||||
return res
|
||||
return []
|
||||
|
||||
def completedefault(self, cmd, text, *args):
|
||||
def completedefault(self, cmd, text, *args): # type: ignore
|
||||
return self.completepath(text)
|
||||
|
||||
def completenames(self, cmd, text, *ignored):
|
||||
def completenames(self, cmd, text, *ignored): # type: ignore
|
||||
return self.completepath(text)
|
||||
|
||||
def emptyline(self):
|
||||
|
@ -161,6 +161,29 @@ class _FidoStateNotifier extends FidoStateNotifier {
|
||||
throw decodedException;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> enableEnterpriseAttestation() async {
|
||||
try {
|
||||
final response = jsonDecode(await _methods.invokeMethod(
|
||||
'enableEnterpriseAttestation',
|
||||
));
|
||||
|
||||
if (response['success'] == true) {
|
||||
_log.debug('Enterprise attestation enabled');
|
||||
}
|
||||
} on PlatformException catch (pe) {
|
||||
var decodedException = pe.decode();
|
||||
if (decodedException is CancellationException) {
|
||||
_log.debug('User cancelled unlock FIDO operation');
|
||||
throw decodedException;
|
||||
}
|
||||
|
||||
_log.debug(
|
||||
'Platform exception during enable enterprise attestation: $pe');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final androidFingerprintProvider = AsyncNotifierProvider.autoDispose
|
||||
|
@ -64,6 +64,8 @@ enum _DDesc {
|
||||
fidoDeleteCredential,
|
||||
fidoDeleteFingerprint,
|
||||
fidoRenameFingerprint,
|
||||
fidoRegisterFingerprint,
|
||||
fidoEnableEnterpriseAttestation,
|
||||
fidoActionFailure,
|
||||
// Others
|
||||
invalid;
|
||||
@ -89,7 +91,9 @@ enum _DDesc {
|
||||
dialogDescriptionFidoIndex + 3: fidoDeleteCredential,
|
||||
dialogDescriptionFidoIndex + 4: fidoDeleteFingerprint,
|
||||
dialogDescriptionFidoIndex + 5: fidoRenameFingerprint,
|
||||
dialogDescriptionFidoIndex + 6: fidoActionFailure,
|
||||
dialogDescriptionFidoIndex + 6: fidoRegisterFingerprint,
|
||||
dialogDescriptionFidoIndex + 7: fidoEnableEnterpriseAttestation,
|
||||
dialogDescriptionFidoIndex + 8: fidoActionFailure,
|
||||
}[id] ??
|
||||
_DDesc.invalid;
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ Future<T?> showBlurDialog<T>({
|
||||
required BuildContext context,
|
||||
required Widget Function(BuildContext) builder,
|
||||
RouteSettings? routeSettings,
|
||||
Color barrierColor = const Color(0x00cccccc),
|
||||
Color barrierColor = const Color(0x33000000),
|
||||
}) async =>
|
||||
await showGeneralDialog<T>(
|
||||
context: context,
|
||||
|
@ -9,9 +9,9 @@ part of 'models.dart';
|
||||
_$KeyCustomizationImpl _$$KeyCustomizationImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$KeyCustomizationImpl(
|
||||
serial: json['serial'] as int,
|
||||
serial: (json['serial'] as num).toInt(),
|
||||
name: json['name'] as String?,
|
||||
color: const _ColorConverter().fromJson(json['color'] as int?),
|
||||
color: const _ColorConverter().fromJson((json['color'] as num?)?.toInt()),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$KeyCustomizationImplToJson(
|
||||
|
@ -30,8 +30,10 @@ class AppListItem<T> extends ConsumerStatefulWidget {
|
||||
final String? semanticTitle;
|
||||
final Widget? trailing;
|
||||
final List<ActionItem> Function(BuildContext context)? buildPopupActions;
|
||||
final Widget Function(BuildContext context)? itemBuilder;
|
||||
final Intent? tapIntent;
|
||||
final Intent? doubleTapIntent;
|
||||
final Color? tileColor;
|
||||
final bool selected;
|
||||
|
||||
const AppListItem(
|
||||
@ -43,8 +45,10 @@ class AppListItem<T> extends ConsumerStatefulWidget {
|
||||
this.subtitle,
|
||||
this.trailing,
|
||||
this.buildPopupActions,
|
||||
this.itemBuilder,
|
||||
this.tapIntent,
|
||||
this.doubleTapIntent,
|
||||
this.tileColor,
|
||||
this.selected = false,
|
||||
});
|
||||
|
||||
@ -78,7 +82,7 @@ class _AppListItemState<T> extends ConsumerState<AppListItem> {
|
||||
item: widget.item,
|
||||
child: InkWell(
|
||||
focusNode: _focusNode,
|
||||
borderRadius: BorderRadius.circular(48),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onSecondaryTapDown: buildPopupActions == null
|
||||
? null
|
||||
: (details) {
|
||||
@ -118,57 +122,62 @@ class _AppListItemState<T> extends ConsumerState<AppListItem> {
|
||||
: () {
|
||||
Actions.invoke(context, doubleTapIntent);
|
||||
},
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: [
|
||||
const SizedBox(height: 64),
|
||||
ListTile(
|
||||
mouseCursor:
|
||||
widget.tapIntent != null ? SystemMouseCursors.click : null,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(48)),
|
||||
selectedTileColor: colorScheme.secondaryContainer,
|
||||
selectedColor: colorScheme.onSecondaryContainer,
|
||||
selected: widget.selected,
|
||||
leading: widget.leading,
|
||||
title: subtitle == null
|
||||
// We use SizedBox to fill entire space
|
||||
? SizedBox(
|
||||
height: 48,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
widget.title,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
widget.title,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
),
|
||||
subtitle: subtitle != null
|
||||
? Text(
|
||||
subtitle,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
)
|
||||
: null,
|
||||
trailing: trailing == null
|
||||
? null
|
||||
: Focus(
|
||||
skipTraversal: true,
|
||||
descendantsAreTraversable: false,
|
||||
child: trailing,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: widget.itemBuilder != null
|
||||
? widget.itemBuilder!.call(context)
|
||||
: Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: [
|
||||
const SizedBox(height: 64),
|
||||
ListTile(
|
||||
mouseCursor: widget.tapIntent != null
|
||||
? SystemMouseCursors.click
|
||||
: null,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16)),
|
||||
selectedTileColor: colorScheme.secondaryContainer,
|
||||
selectedColor: colorScheme.onSecondaryContainer,
|
||||
tileColor: widget.tileColor,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
selected: widget.selected,
|
||||
leading: widget.leading,
|
||||
title: subtitle == null
|
||||
// We use SizedBox to fill entire space
|
||||
? SizedBox(
|
||||
height: 48,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
widget.title,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
widget.title,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
),
|
||||
subtitle: subtitle != null
|
||||
? Text(
|
||||
subtitle,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
)
|
||||
: null,
|
||||
trailing: trailing == null
|
||||
? null
|
||||
: Focus(
|
||||
skipTraversal: true,
|
||||
descendantsAreTraversable: false,
|
||||
child: trailing,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -22,6 +22,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../core/state.dart';
|
||||
import '../../management/models.dart';
|
||||
@ -29,18 +30,29 @@ import '../../widgets/delayed_visibility.dart';
|
||||
import '../../widgets/file_drop_target.dart';
|
||||
import '../message.dart';
|
||||
import '../shortcuts.dart';
|
||||
import '../state.dart';
|
||||
import 'fs_dialog.dart';
|
||||
import 'keys.dart';
|
||||
import 'navigation.dart';
|
||||
|
||||
final _navigationProvider = StateNotifierProvider<_NavigationProvider, bool>(
|
||||
(ref) => _NavigationProvider());
|
||||
final _navigationVisibilityProvider =
|
||||
StateNotifierProvider<_VisibilityNotifier, bool>((ref) =>
|
||||
_VisibilityNotifier('NAVIGATION_VISIBILITY', ref.watch(prefProvider)));
|
||||
|
||||
class _NavigationProvider extends StateNotifier<bool> {
|
||||
_NavigationProvider() : super(true);
|
||||
final _detailViewVisibilityProvider =
|
||||
StateNotifierProvider<_VisibilityNotifier, bool>((ref) =>
|
||||
_VisibilityNotifier('DETAIL_VIEW_VISIBILITY', ref.watch(prefProvider)));
|
||||
|
||||
class _VisibilityNotifier extends StateNotifier<bool> {
|
||||
final String _key;
|
||||
final SharedPreferences _prefs;
|
||||
_VisibilityNotifier(this._key, this._prefs)
|
||||
: super(_prefs.getBool(_key) ?? true);
|
||||
|
||||
void toggleExpanded() {
|
||||
state = !state;
|
||||
final newValue = !state;
|
||||
state = newValue;
|
||||
_prefs.setBool(_key, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
@ -308,14 +320,17 @@ class _AppPageState extends ConsumerState<AppPage> {
|
||||
|
||||
Widget? _buildAppBarTitle(
|
||||
BuildContext context, bool hasRail, bool hasManage, bool fullyExpanded) {
|
||||
final showNavigation = ref.watch(_navigationProvider);
|
||||
final showNavigation = ref.watch(_navigationVisibilityProvider);
|
||||
final showDetailView = ref.watch(_detailViewVisibilityProvider);
|
||||
|
||||
EdgeInsets padding;
|
||||
if (fullyExpanded) {
|
||||
padding = EdgeInsets.only(left: showNavigation ? 280 : 72, right: 320);
|
||||
padding = EdgeInsets.only(
|
||||
left: showNavigation ? 280 : 72, right: showDetailView ? 320 : 0.0);
|
||||
} else if (!hasRail && hasManage) {
|
||||
padding = const EdgeInsets.only(right: 320);
|
||||
} else if (hasRail && hasManage) {
|
||||
padding = const EdgeInsets.only(left: 72, right: 320);
|
||||
padding = EdgeInsets.only(left: 72, right: showDetailView ? 320 : 0.0);
|
||||
} else if (hasRail && !hasManage) {
|
||||
padding = const EdgeInsets.only(left: 72);
|
||||
} else {
|
||||
@ -344,21 +359,23 @@ class _AppPageState extends ConsumerState<AppPage> {
|
||||
}
|
||||
|
||||
Widget _buildMainContent(BuildContext context, bool expanded) {
|
||||
final actions = widget.actionsBuilder?.call(context, expanded) ?? [];
|
||||
final showDetailView = ref.watch(_detailViewVisibilityProvider);
|
||||
final actions =
|
||||
widget.actionsBuilder?.call(context, expanded && showDetailView) ?? [];
|
||||
final content = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: widget.centered
|
||||
? CrossAxisAlignment.center
|
||||
: CrossAxisAlignment.start,
|
||||
children: [
|
||||
widget.builder(context, expanded),
|
||||
widget.builder(context, expanded && showDetailView),
|
||||
if (actions.isNotEmpty)
|
||||
Align(
|
||||
alignment:
|
||||
widget.centered ? Alignment.center : Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16, bottom: 0, left: 16, right: 16),
|
||||
top: 16, bottom: 0, left: 18, right: 18),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
@ -369,7 +386,7 @@ class _AppPageState extends ConsumerState<AppPage> {
|
||||
if (widget.footnote != null)
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(bottom: 16, top: 33, left: 16, right: 16),
|
||||
const EdgeInsets.only(bottom: 16, top: 33, left: 18, right: 18),
|
||||
child: Opacity(
|
||||
opacity: 0.6,
|
||||
child: Text(
|
||||
@ -399,7 +416,7 @@ class _AppPageState extends ConsumerState<AppPage> {
|
||||
alignment: Alignment.topLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16.0, right: 16.0, bottom: 24.0, top: 4.0),
|
||||
left: 18.0, right: 18.0, bottom: 24.0, top: 4.0),
|
||||
child: _buildTitle(context),
|
||||
),
|
||||
),
|
||||
@ -452,7 +469,7 @@ class _AppPageState extends ConsumerState<AppPage> {
|
||||
child: Padding(
|
||||
key: _sliverTitleWrapperGlobalKey,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16.0, right: 16.0, bottom: 12.0, top: 4.0),
|
||||
left: 18.0, right: 18.0, bottom: 12.0, top: 4.0),
|
||||
child: _buildTitle(context),
|
||||
),
|
||||
),
|
||||
@ -499,7 +516,8 @@ class _AppPageState extends ConsumerState<AppPage> {
|
||||
BuildContext context, bool hasDrawer, bool hasRail, bool hasManage) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final fullyExpanded = !hasDrawer && hasRail && hasManage;
|
||||
final showNavigation = ref.watch(_navigationProvider);
|
||||
final showNavigation = ref.watch(_navigationVisibilityProvider);
|
||||
final showDetailView = ref.watch(_detailViewVisibilityProvider);
|
||||
final hasDetailsOrKeyActions =
|
||||
widget.detailViewBuilder != null || widget.keyActionsBuilder != null;
|
||||
var body = _buildMainContent(context, hasManage);
|
||||
@ -518,187 +536,211 @@ class _AppPageState extends ConsumerState<AppPage> {
|
||||
);
|
||||
}
|
||||
if (hasRail || hasManage) {
|
||||
body = Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (hasRail && (!fullyExpanded || !showNavigation))
|
||||
SizedBox(
|
||||
width: 72,
|
||||
child: _VisibilityListener(
|
||||
targetKey: _navKey,
|
||||
controller: _navController,
|
||||
child: SingleChildScrollView(
|
||||
child: NavigationContent(
|
||||
key: _navKey,
|
||||
shouldPop: false,
|
||||
extended: false,
|
||||
body = GestureDetector(
|
||||
behavior: HitTestBehavior.deferToChild,
|
||||
onTap: () {
|
||||
Actions.invoke(context, const EscapeIntent());
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
},
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (hasRail && (!fullyExpanded || !showNavigation))
|
||||
SizedBox(
|
||||
width: 72,
|
||||
child: _VisibilityListener(
|
||||
targetKey: _navKey,
|
||||
controller: _navController,
|
||||
child: SingleChildScrollView(
|
||||
child: NavigationContent(
|
||||
key: _navKey,
|
||||
shouldPop: false,
|
||||
extended: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (fullyExpanded && showNavigation)
|
||||
SizedBox(
|
||||
width: 280,
|
||||
child: _VisibilityListener(
|
||||
controller: _navController,
|
||||
targetKey: _navExpandedKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: NavigationContent(
|
||||
key: _navExpandedKey,
|
||||
shouldPop: false,
|
||||
extended: true,
|
||||
if (fullyExpanded && showNavigation)
|
||||
SizedBox(
|
||||
width: 280,
|
||||
child: _VisibilityListener(
|
||||
controller: _navController,
|
||||
targetKey: _navExpandedKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: NavigationContent(
|
||||
key: _navExpandedKey,
|
||||
shouldPop: false,
|
||||
extended: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: body),
|
||||
if (hasManage &&
|
||||
!hasDetailsOrKeyActions &&
|
||||
widget.capabilities != null &&
|
||||
widget.capabilities?.first != Capability.u2f)
|
||||
// Add a placeholder for the Manage/Details column. Exceptions are:
|
||||
// - the "Security Key" because it does not have any actions/details.
|
||||
// - pages without Capabilities
|
||||
const SizedBox(width: 336), // simulate column
|
||||
if (hasManage && hasDetailsOrKeyActions && showDetailView)
|
||||
_VisibilityListener(
|
||||
controller: _detailsController,
|
||||
targetKey: _detailsViewGlobalKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: SizedBox(
|
||||
width: 320,
|
||||
child: Column(
|
||||
key: _detailsViewGlobalKey,
|
||||
children: [
|
||||
if (widget.detailViewBuilder != null)
|
||||
widget.detailViewBuilder!(context),
|
||||
if (widget.keyActionsBuilder != null)
|
||||
widget.keyActionsBuilder!(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.deferToChild,
|
||||
onTap: () {
|
||||
Actions.invoke(context, const EscapeIntent());
|
||||
},
|
||||
child: Stack(children: [
|
||||
Container(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
body
|
||||
]),
|
||||
)),
|
||||
if (hasManage &&
|
||||
!hasDetailsOrKeyActions &&
|
||||
widget.capabilities != null &&
|
||||
widget.capabilities?.first != Capability.u2f)
|
||||
// Add a placeholder for the Manage/Details column. Exceptions are:
|
||||
// - the "Security Key" because it does not have any actions/details.
|
||||
// - pages without Capabilities
|
||||
const SizedBox(width: 336), // simulate column
|
||||
if (hasManage && hasDetailsOrKeyActions)
|
||||
_VisibilityListener(
|
||||
controller: _detailsController,
|
||||
targetKey: _detailsViewGlobalKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: SizedBox(
|
||||
width: 320,
|
||||
child: Column(
|
||||
key: _detailsViewGlobalKey,
|
||||
children: [
|
||||
if (widget.detailViewBuilder != null)
|
||||
widget.detailViewBuilder!(context),
|
||||
if (widget.keyActionsBuilder != null)
|
||||
widget.keyActionsBuilder!(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return Scaffold(
|
||||
key: scaffoldGlobalKey,
|
||||
appBar: AppBar(
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(1.0),
|
||||
child: ListenableBuilder(
|
||||
listenable: _scrolledUnderController,
|
||||
builder: (context, child) {
|
||||
final visible = _scrolledUnderController.someIsScrolledUnder;
|
||||
return AnimatedOpacity(
|
||||
opacity: visible ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
height: 1.0,
|
||||
),
|
||||
);
|
||||
},
|
||||
appBar: _GestureDetectorAppBar(
|
||||
onTap: () {
|
||||
Actions.invoke(context, const EscapeIntent());
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
},
|
||||
appBar: AppBar(
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(1.0),
|
||||
child: ListenableBuilder(
|
||||
listenable: _scrolledUnderController,
|
||||
builder: (context, child) {
|
||||
final visible = _scrolledUnderController.someIsScrolledUnder;
|
||||
return AnimatedOpacity(
|
||||
opacity: visible ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
color: Theme.of(context).hoverColor,
|
||||
height: 1.0,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
scrolledUnderElevation: 0.0,
|
||||
leadingWidth: hasRail ? 84 : null,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
title: _buildAppBarTitle(
|
||||
context,
|
||||
hasRail,
|
||||
hasManage,
|
||||
fullyExpanded,
|
||||
),
|
||||
leading: hasRail
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: IconButton(
|
||||
icon: Icon(Symbols.menu, semanticLabel: navigationText),
|
||||
tooltip: navigationText,
|
||||
onPressed: fullyExpanded
|
||||
? () {
|
||||
ref
|
||||
.read(_navigationProvider.notifier)
|
||||
.toggleExpanded();
|
||||
}
|
||||
: () {
|
||||
scaffoldGlobalKey.currentState?.openDrawer();
|
||||
},
|
||||
),
|
||||
)),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
)
|
||||
: Builder(
|
||||
builder: (context) {
|
||||
// Need to wrap with builder to get Scaffold context
|
||||
return IconButton(
|
||||
onPressed: () => Scaffold.of(context).openDrawer(),
|
||||
icon: const Icon(Symbols.menu),
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
if (widget.actionButtonBuilder == null &&
|
||||
(widget.keyActionsBuilder != null && !hasManage))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: IconButton(
|
||||
key: actionsIconButtonKey,
|
||||
onPressed: () {
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
barrierColor: Colors.transparent,
|
||||
builder: (context) => FsDialog(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 32),
|
||||
child: widget.keyActionsBuilder!(context),
|
||||
scrolledUnderElevation: 0.0,
|
||||
leadingWidth: hasRail ? 84 : null,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
title: _buildAppBarTitle(
|
||||
context,
|
||||
hasRail,
|
||||
hasManage,
|
||||
fullyExpanded,
|
||||
),
|
||||
centerTitle: true,
|
||||
leading: hasRail
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: IconButton(
|
||||
icon: Icon(Symbols.menu, semanticLabel: navigationText),
|
||||
tooltip: navigationText,
|
||||
onPressed: fullyExpanded
|
||||
? () {
|
||||
ref
|
||||
.read(
|
||||
_navigationVisibilityProvider.notifier)
|
||||
.toggleExpanded();
|
||||
}
|
||||
: () {
|
||||
scaffoldGlobalKey.currentState?.openDrawer();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: widget.keyActionsBadge
|
||||
? Badge(
|
||||
child: Icon(Symbols.more_vert,
|
||||
semanticLabel: l10n.s_configure_yk),
|
||||
)
|
||||
: Icon(Symbols.more_vert,
|
||||
semanticLabel: l10n.s_configure_yk),
|
||||
iconSize: 24,
|
||||
tooltip: l10n.s_configure_yk,
|
||||
padding: const EdgeInsets.all(12),
|
||||
)),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
)
|
||||
: Builder(
|
||||
builder: (context) {
|
||||
// Need to wrap with builder to get Scaffold context
|
||||
return IconButton(
|
||||
onPressed: () => Scaffold.of(context).openDrawer(),
|
||||
icon: const Icon(Symbols.menu),
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
if (widget.actionButtonBuilder == null &&
|
||||
(widget.keyActionsBuilder != null &&
|
||||
(!hasManage || !showDetailView)))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: IconButton(
|
||||
key: actionsIconButtonKey,
|
||||
onPressed: () {
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
barrierColor: Colors.transparent,
|
||||
builder: (context) => FsDialog(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 32),
|
||||
child: widget.keyActionsBuilder!(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: widget.keyActionsBadge
|
||||
? Badge(
|
||||
child: Icon(Symbols.more_vert,
|
||||
semanticLabel: l10n.s_configure_yk),
|
||||
)
|
||||
: Icon(Symbols.more_vert,
|
||||
semanticLabel: l10n.s_configure_yk),
|
||||
iconSize: 24,
|
||||
tooltip: l10n.s_configure_yk,
|
||||
padding: const EdgeInsets.all(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.actionButtonBuilder != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: widget.actionButtonBuilder!.call(context),
|
||||
),
|
||||
],
|
||||
if (hasManage &&
|
||||
(widget.keyActionsBuilder != null ||
|
||||
widget.detailViewBuilder != null))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: IconButton(
|
||||
key: toggleDetailViewIconButtonKey,
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(_detailViewVisibilityProvider.notifier)
|
||||
.toggleExpanded();
|
||||
},
|
||||
icon: const Icon(Symbols.view_sidebar),
|
||||
iconSize: 24,
|
||||
tooltip: showDetailView
|
||||
? l10n.s_collapse_sidebar
|
||||
: l10n.s_expand_sidebar,
|
||||
padding: const EdgeInsets.all(12),
|
||||
),
|
||||
),
|
||||
if (widget.actionButtonBuilder != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: widget.actionButtonBuilder!.call(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
drawer: hasDrawer ? _buildDrawer(context) : null,
|
||||
body: body,
|
||||
@ -706,23 +748,66 @@ class _AppPageState extends ConsumerState<AppPage> {
|
||||
}
|
||||
}
|
||||
|
||||
class CapabilityBadge extends StatelessWidget {
|
||||
final Capability capability;
|
||||
class _GestureDetectorAppBar extends StatelessWidget
|
||||
implements PreferredSizeWidget {
|
||||
final AppBar appBar;
|
||||
final void Function() onTap;
|
||||
|
||||
const CapabilityBadge(this.capability, {super.key});
|
||||
const _GestureDetectorAppBar({required this.appBar, required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.deferToChild, onTap: onTap, child: appBar);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
}
|
||||
|
||||
class CapabilityBadge extends ConsumerWidget {
|
||||
final Capability capability;
|
||||
final bool noTooltip;
|
||||
|
||||
const CapabilityBadge(this.capability, {super.key, this.noTooltip = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final text = Text(capability.getDisplayName(l10n));
|
||||
final (fipsCapable, fipsApproved) = ref
|
||||
.watch(currentDeviceDataProvider)
|
||||
.valueOrNull
|
||||
?.info
|
||||
.getFipsStatus(capability) ??
|
||||
(false, false);
|
||||
final label = fipsCapable
|
||||
? Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.shield,
|
||||
color: colorScheme.onSecondaryContainer,
|
||||
size: 12,
|
||||
fill: fipsApproved ? 1 : 0,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
text,
|
||||
],
|
||||
)
|
||||
: text;
|
||||
return Badge(
|
||||
backgroundColor: colorScheme.secondaryContainer,
|
||||
textColor: colorScheme.onSecondaryContainer,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
largeSize: MediaQuery.of(context).textScaler.scale(20),
|
||||
label: Text(
|
||||
capability.getDisplayName(l10n),
|
||||
),
|
||||
label: fipsCapable && !noTooltip
|
||||
? Tooltip(
|
||||
message:
|
||||
fipsApproved ? l10n.l_fips_approved : l10n.l_fips_capable,
|
||||
child: label,
|
||||
)
|
||||
: label,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,8 @@ const _prefix = 'app.keys';
|
||||
const deviceInfoListTile = Key('$_prefix.device_info_list_tile');
|
||||
const noDeviceAvatar = Key('$_prefix.no_device_avatar');
|
||||
const actionsIconButtonKey = Key('$_prefix.actions_icon_button');
|
||||
const toggleDetailViewIconButtonKey =
|
||||
Key('$_prefix.toggle_detail_view_icon_button');
|
||||
|
||||
// drawer items
|
||||
const homeDrawer = Key('$_prefix.drawer.home');
|
||||
|
@ -71,9 +71,9 @@ class MessagePage extends StatelessWidget {
|
||||
delayedContent: delayedContent,
|
||||
builder: (context, _) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 16.0,
|
||||
left: 18.0,
|
||||
top: 0.0,
|
||||
right: 16.0,
|
||||
right: 18.0,
|
||||
bottom: centered && actionsBuilder == null ? 96 : 0),
|
||||
child: SizedBox(
|
||||
width: centered ? 250 : 350,
|
||||
|
@ -163,6 +163,8 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
|
||||
_subscription = null;
|
||||
}, onError: (e) {
|
||||
_log.error('Error performing FIDO reset', e);
|
||||
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
final String errorMessage;
|
||||
// TODO: Make this cleaner than importing desktop specific RpcError.
|
||||
|
@ -175,7 +175,7 @@ UserInteractionController _dialogUserInteraction(
|
||||
builder: (context) {
|
||||
return PopScope(
|
||||
canPop: onCancel != null,
|
||||
onPopInvoked: (didPop) {
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (didPop) {
|
||||
wasPopped = true;
|
||||
if (!completed && onCancel != null) {
|
||||
|
@ -21,6 +21,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../app/models.dart';
|
||||
import '../widgets/flex_box.dart';
|
||||
|
||||
bool get isDesktop => const [
|
||||
TargetPlatform.windows,
|
||||
@ -119,3 +120,20 @@ final featureProvider = Provider<FeatureProvider>((ref) {
|
||||
|
||||
return isEnabled;
|
||||
});
|
||||
|
||||
class LayoutNotifier extends StateNotifier<FlexLayout> {
|
||||
final String _key;
|
||||
final SharedPreferences _prefs;
|
||||
LayoutNotifier(this._key, this._prefs)
|
||||
: super(_fromName(_prefs.getString(_key)));
|
||||
|
||||
void setLayout(FlexLayout layout) {
|
||||
state = layout;
|
||||
_prefs.setString(_key, layout.name);
|
||||
}
|
||||
|
||||
static FlexLayout _fromName(String? name) => FlexLayout.values.firstWhere(
|
||||
(element) => element.name == name,
|
||||
orElse: () => FlexLayout.list,
|
||||
);
|
||||
}
|
||||
|
@ -87,15 +87,16 @@ class UsbDeviceNotifier extends StateNotifier<List<UsbYubiKeyNode>> {
|
||||
return;
|
||||
}
|
||||
|
||||
final pids = {
|
||||
for (var e in (scan['pids'] as Map).entries)
|
||||
UsbPid.fromValue(int.parse(e.key)): e.value as int
|
||||
};
|
||||
final numDevices = pids.values.fold<int>(0, (a, b) => a + b);
|
||||
final numDevices =
|
||||
(scan['pids'] as Map).values.fold<int>(0, (a, b) => a + b as int);
|
||||
if (_usbState != scan['state'] || state.length != numDevices) {
|
||||
var usbResult = await rpc.command('get', ['usb']);
|
||||
_log.info('USB state change', jsonEncode(usbResult));
|
||||
_usbState = usbResult['data']['state'];
|
||||
final pids = {
|
||||
for (var e in (usbResult['data']['pids'] as Map).entries)
|
||||
UsbPid.fromValue(int.parse(e.key)): e.value as int
|
||||
};
|
||||
List<UsbYubiKeyNode> usbDevices = [];
|
||||
|
||||
for (String id in (usbResult['children'] as Map).keys) {
|
||||
@ -224,11 +225,12 @@ final _desktopDeviceDataProvider =
|
||||
ref.watch(rpcProvider).valueOrNull,
|
||||
ref.watch(currentDeviceProvider),
|
||||
);
|
||||
if (notifier._deviceNode is NfcReaderNode) {
|
||||
// If this is an NFC reader, listen on WindowState.
|
||||
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
|
||||
notifier._notifyWindowState(windowState);
|
||||
}, fireImmediately: true);
|
||||
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
|
||||
notifier._notifyWindowState(windowState);
|
||||
});
|
||||
if (notifier._deviceNode is NfcReaderNode &&
|
||||
ref.read(windowStateProvider).active) {
|
||||
notifier._pollCard();
|
||||
}
|
||||
return notifier;
|
||||
});
|
||||
@ -243,6 +245,7 @@ class CurrentDeviceDataNotifier extends StateNotifier<AsyncValue<YubiKeyData>> {
|
||||
final RpcSession? _rpc;
|
||||
final DeviceNode? _deviceNode;
|
||||
Timer? _pollTimer;
|
||||
StreamSubscription? _flagSubscription;
|
||||
|
||||
CurrentDeviceDataNotifier(this._rpc, this._deviceNode)
|
||||
: super(const AsyncValue.loading()) {
|
||||
@ -255,11 +258,27 @@ class CurrentDeviceDataNotifier extends StateNotifier<AsyncValue<YubiKeyData>> {
|
||||
state = AsyncValue.error('device-inaccessible', StackTrace.current);
|
||||
}
|
||||
}
|
||||
_flagSubscription = _rpc?.flags.listen(
|
||||
(flag) {
|
||||
if (flag == 'device_info') {
|
||||
_pollDevice();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _pollDevice() {
|
||||
switch (_deviceNode) {
|
||||
case UsbYubiKeyNode _:
|
||||
_refreshUsb();
|
||||
case NfcReaderNode _:
|
||||
_pollCard();
|
||||
}
|
||||
}
|
||||
|
||||
void _notifyWindowState(WindowState windowState) {
|
||||
if (windowState.active) {
|
||||
_pollCard();
|
||||
_pollDevice();
|
||||
} else {
|
||||
_pollTimer?.cancel();
|
||||
// TODO: Should we clear the key here?
|
||||
@ -271,25 +290,39 @@ class CurrentDeviceDataNotifier extends StateNotifier<AsyncValue<YubiKeyData>> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_flagSubscription?.cancel();
|
||||
_pollTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _refreshUsb() async {
|
||||
final node = _deviceNode!;
|
||||
var result = await _rpc?.command('get', node.path.segments);
|
||||
if (mounted && result != null) {
|
||||
final newState = YubiKeyData(node, result['data']['name'],
|
||||
DeviceInfo.fromJson(result['data']['info']));
|
||||
if (state.valueOrNull != newState) {
|
||||
_log.info('Configuration change in current USB device');
|
||||
state = AsyncValue.data(newState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _pollCard() async {
|
||||
_pollTimer?.cancel();
|
||||
final node = _deviceNode!;
|
||||
try {
|
||||
_log.debug('Polling for NFC device changes...');
|
||||
var result = await _rpc?.command('get', node.path.segments);
|
||||
if (mounted && result != null) {
|
||||
if (result['data']['present']) {
|
||||
final oldState = state.valueOrNull;
|
||||
final newState = YubiKeyData(node, result['data']['name'],
|
||||
DeviceInfo.fromJson(result['data']['info']));
|
||||
if (oldState != null && oldState != newState) {
|
||||
// Ensure state is cleared
|
||||
state = const AsyncValue.loading();
|
||||
} else {
|
||||
if (oldState != newState) {
|
||||
if (oldState != null) {
|
||||
// Ensure state is cleared
|
||||
state = const AsyncValue.loading();
|
||||
}
|
||||
state = AsyncValue.data(newState);
|
||||
}
|
||||
} else {
|
||||
|
@ -184,6 +184,12 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> enableEnterpriseAttestation() async {
|
||||
await _session.command('enable_ep_attestation');
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
final desktopFingerprintProvider = AsyncNotifierProvider.autoDispose
|
||||
|
@ -21,7 +21,8 @@ part 'models.g.dart';
|
||||
|
||||
@Freezed(unionKey: 'kind')
|
||||
class RpcResponse with _$RpcResponse {
|
||||
factory RpcResponse.success(Map<String, dynamic> body) = Success;
|
||||
factory RpcResponse.success(Map<String, dynamic> body, List<String> flags) =
|
||||
Success;
|
||||
factory RpcResponse.signal(String status, Map<String, dynamic> body) = Signal;
|
||||
factory RpcResponse.error(
|
||||
String status, String message, Map<String, dynamic> body) = RpcError;
|
||||
|
@ -34,7 +34,8 @@ mixin _$RpcResponse {
|
||||
Map<String, dynamic> get body => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function(Map<String, dynamic> body) success,
|
||||
required TResult Function(Map<String, dynamic> body, List<String> flags)
|
||||
success,
|
||||
required TResult Function(String status, Map<String, dynamic> body) signal,
|
||||
required TResult Function(
|
||||
String status, String message, Map<String, dynamic> body)
|
||||
@ -43,7 +44,7 @@ mixin _$RpcResponse {
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function(Map<String, dynamic> body)? success,
|
||||
TResult? Function(Map<String, dynamic> body, List<String> flags)? success,
|
||||
TResult? Function(String status, Map<String, dynamic> body)? signal,
|
||||
TResult? Function(String status, String message, Map<String, dynamic> body)?
|
||||
error,
|
||||
@ -51,7 +52,7 @@ mixin _$RpcResponse {
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(Map<String, dynamic> body)? success,
|
||||
TResult Function(Map<String, dynamic> body, List<String> flags)? success,
|
||||
TResult Function(String status, Map<String, dynamic> body)? signal,
|
||||
TResult Function(String status, String message, Map<String, dynamic> body)?
|
||||
error,
|
||||
@ -127,7 +128,7 @@ abstract class _$$SuccessImplCopyWith<$Res>
|
||||
__$$SuccessImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({Map<String, dynamic> body});
|
||||
$Res call({Map<String, dynamic> body, List<String> flags});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -142,12 +143,17 @@ class __$$SuccessImplCopyWithImpl<$Res>
|
||||
@override
|
||||
$Res call({
|
||||
Object? body = null,
|
||||
Object? flags = null,
|
||||
}) {
|
||||
return _then(_$SuccessImpl(
|
||||
null == body
|
||||
? _value._body
|
||||
: body // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,
|
||||
null == flags
|
||||
? _value._flags
|
||||
: flags // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -155,8 +161,10 @@ class __$$SuccessImplCopyWithImpl<$Res>
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SuccessImpl implements Success {
|
||||
_$SuccessImpl(final Map<String, dynamic> body, {final String? $type})
|
||||
_$SuccessImpl(final Map<String, dynamic> body, final List<String> flags,
|
||||
{final String? $type})
|
||||
: _body = body,
|
||||
_flags = flags,
|
||||
$type = $type ?? 'success';
|
||||
|
||||
factory _$SuccessImpl.fromJson(Map<String, dynamic> json) =>
|
||||
@ -170,12 +178,20 @@ class _$SuccessImpl implements Success {
|
||||
return EqualUnmodifiableMapView(_body);
|
||||
}
|
||||
|
||||
final List<String> _flags;
|
||||
@override
|
||||
List<String> get flags {
|
||||
if (_flags is EqualUnmodifiableListView) return _flags;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_flags);
|
||||
}
|
||||
|
||||
@JsonKey(name: 'kind')
|
||||
final String $type;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'RpcResponse.success(body: $body)';
|
||||
return 'RpcResponse.success(body: $body, flags: $flags)';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -183,13 +199,16 @@ class _$SuccessImpl implements Success {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SuccessImpl &&
|
||||
const DeepCollectionEquality().equals(other._body, _body));
|
||||
const DeepCollectionEquality().equals(other._body, _body) &&
|
||||
const DeepCollectionEquality().equals(other._flags, _flags));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, const DeepCollectionEquality().hash(_body));
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
const DeepCollectionEquality().hash(_body),
|
||||
const DeepCollectionEquality().hash(_flags));
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@ -200,37 +219,38 @@ class _$SuccessImpl implements Success {
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function(Map<String, dynamic> body) success,
|
||||
required TResult Function(Map<String, dynamic> body, List<String> flags)
|
||||
success,
|
||||
required TResult Function(String status, Map<String, dynamic> body) signal,
|
||||
required TResult Function(
|
||||
String status, String message, Map<String, dynamic> body)
|
||||
error,
|
||||
}) {
|
||||
return success(body);
|
||||
return success(body, flags);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function(Map<String, dynamic> body)? success,
|
||||
TResult? Function(Map<String, dynamic> body, List<String> flags)? success,
|
||||
TResult? Function(String status, Map<String, dynamic> body)? signal,
|
||||
TResult? Function(String status, String message, Map<String, dynamic> body)?
|
||||
error,
|
||||
}) {
|
||||
return success?.call(body);
|
||||
return success?.call(body, flags);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(Map<String, dynamic> body)? success,
|
||||
TResult Function(Map<String, dynamic> body, List<String> flags)? success,
|
||||
TResult Function(String status, Map<String, dynamic> body)? signal,
|
||||
TResult Function(String status, String message, Map<String, dynamic> body)?
|
||||
error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (success != null) {
|
||||
return success(body);
|
||||
return success(body, flags);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
@ -278,12 +298,14 @@ class _$SuccessImpl implements Success {
|
||||
}
|
||||
|
||||
abstract class Success implements RpcResponse {
|
||||
factory Success(final Map<String, dynamic> body) = _$SuccessImpl;
|
||||
factory Success(final Map<String, dynamic> body, final List<String> flags) =
|
||||
_$SuccessImpl;
|
||||
|
||||
factory Success.fromJson(Map<String, dynamic> json) = _$SuccessImpl.fromJson;
|
||||
|
||||
@override
|
||||
Map<String, dynamic> get body;
|
||||
List<String> get flags;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$SuccessImplCopyWith<_$SuccessImpl> get copyWith =>
|
||||
@ -380,7 +402,8 @@ class _$SignalImpl implements Signal {
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function(Map<String, dynamic> body) success,
|
||||
required TResult Function(Map<String, dynamic> body, List<String> flags)
|
||||
success,
|
||||
required TResult Function(String status, Map<String, dynamic> body) signal,
|
||||
required TResult Function(
|
||||
String status, String message, Map<String, dynamic> body)
|
||||
@ -392,7 +415,7 @@ class _$SignalImpl implements Signal {
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function(Map<String, dynamic> body)? success,
|
||||
TResult? Function(Map<String, dynamic> body, List<String> flags)? success,
|
||||
TResult? Function(String status, Map<String, dynamic> body)? signal,
|
||||
TResult? Function(String status, String message, Map<String, dynamic> body)?
|
||||
error,
|
||||
@ -403,7 +426,7 @@ class _$SignalImpl implements Signal {
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(Map<String, dynamic> body)? success,
|
||||
TResult Function(Map<String, dynamic> body, List<String> flags)? success,
|
||||
TResult Function(String status, Map<String, dynamic> body)? signal,
|
||||
TResult Function(String status, String message, Map<String, dynamic> body)?
|
||||
error,
|
||||
@ -570,7 +593,8 @@ class _$RpcErrorImpl implements RpcError {
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function(Map<String, dynamic> body) success,
|
||||
required TResult Function(Map<String, dynamic> body, List<String> flags)
|
||||
success,
|
||||
required TResult Function(String status, Map<String, dynamic> body) signal,
|
||||
required TResult Function(
|
||||
String status, String message, Map<String, dynamic> body)
|
||||
@ -582,7 +606,7 @@ class _$RpcErrorImpl implements RpcError {
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function(Map<String, dynamic> body)? success,
|
||||
TResult? Function(Map<String, dynamic> body, List<String> flags)? success,
|
||||
TResult? Function(String status, Map<String, dynamic> body)? signal,
|
||||
TResult? Function(String status, String message, Map<String, dynamic> body)?
|
||||
error,
|
||||
@ -593,7 +617,7 @@ class _$RpcErrorImpl implements RpcError {
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(Map<String, dynamic> body)? success,
|
||||
TResult Function(Map<String, dynamic> body, List<String> flags)? success,
|
||||
TResult Function(String status, Map<String, dynamic> body)? signal,
|
||||
TResult Function(String status, String message, Map<String, dynamic> body)?
|
||||
error,
|
||||
|
@ -9,12 +9,14 @@ part of 'models.dart';
|
||||
_$SuccessImpl _$$SuccessImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SuccessImpl(
|
||||
json['body'] as Map<String, dynamic>,
|
||||
(json['flags'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
$type: json['kind'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SuccessImplToJson(_$SuccessImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'body': instance.body,
|
||||
'flags': instance.flags,
|
||||
'kind': instance.$type,
|
||||
};
|
||||
|
||||
|
@ -102,8 +102,12 @@ class RpcSession {
|
||||
final String executable;
|
||||
late _RpcConnection _connection;
|
||||
final StreamController<_Request> _requests = StreamController();
|
||||
final StreamController<String> _flags = StreamController();
|
||||
late final Stream<String> flags;
|
||||
|
||||
RpcSession(this.executable);
|
||||
RpcSession(this.executable) {
|
||||
flags = _flags.stream.asBroadcastStream();
|
||||
}
|
||||
|
||||
static void _logEntry(String entry) {
|
||||
try {
|
||||
@ -230,7 +234,7 @@ class RpcSession {
|
||||
|
||||
Future<Map<String, dynamic>> command(String action, List<String>? target,
|
||||
{Map? params, Signaler? signal}) {
|
||||
var request = _Request(action, target ?? [], params ?? {}, signal);
|
||||
final request = _Request(action, target ?? [], params ?? {}, signal);
|
||||
_requests.add(request);
|
||||
return request.completer.future;
|
||||
}
|
||||
@ -278,6 +282,10 @@ class RpcSession {
|
||||
},
|
||||
success: (success) {
|
||||
request.completer.complete(success.body);
|
||||
for (final flag in success.flags) {
|
||||
_log.traffic('FLAG', flag);
|
||||
_flags.add(flag);
|
||||
}
|
||||
completed = true;
|
||||
},
|
||||
error: (error) {
|
||||
|
@ -21,6 +21,8 @@ final actions = fido.feature('actions');
|
||||
final actionsPin = actions.feature('pin');
|
||||
final actionsAddFingerprint = actions.feature('addFingerprint');
|
||||
final actionsReset = actions.feature('reset');
|
||||
final enableEnterpriseAttestation =
|
||||
actions.feature('enableEnterpriseAttestation');
|
||||
|
||||
final credentials = fido.feature('credentials');
|
||||
|
||||
|
@ -25,6 +25,8 @@ const _credentialInfo = '$_prefix.credential.info';
|
||||
// Key actions
|
||||
const managePinAction = Key('$_keyAction.manage_pin');
|
||||
const addFingerprintAction = Key('$_keyAction.add_fingerprint');
|
||||
const enableEnterpriseAttestation =
|
||||
Key('$_keyAction.enable_enterprise_attestation');
|
||||
const newPin = Key('$_keyAction.new_pin');
|
||||
const confirmPin = Key('$_keyAction.confirm_pin');
|
||||
const currentPin = Key('$_keyAction.current_pin');
|
||||
|
@ -50,6 +50,8 @@ class FidoState with _$FidoState {
|
||||
bool get forcePinChange => info['force_pin_change'] == true;
|
||||
|
||||
bool get pinBlocked => pinRetries == 0;
|
||||
|
||||
bool? get enterpriseAttestation => info['options']['ep'];
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
@ -10,7 +10,7 @@ _$FidoStateImpl _$$FidoStateImplFromJson(Map<String, dynamic> json) =>
|
||||
_$FidoStateImpl(
|
||||
info: json['info'] as Map<String, dynamic>,
|
||||
unlocked: json['unlocked'] as bool,
|
||||
pinRetries: json['pin_retries'] as int?,
|
||||
pinRetries: (json['pin_retries'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$FidoStateImplToJson(_$FidoStateImpl instance) =>
|
||||
|
@ -18,6 +18,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../app/models.dart';
|
||||
import '../core/state.dart';
|
||||
import '../widgets/flex_box.dart';
|
||||
import 'models.dart';
|
||||
|
||||
final passkeysSearchProvider =
|
||||
@ -32,6 +33,11 @@ class PasskeysSearchNotifier extends StateNotifier<String> {
|
||||
}
|
||||
}
|
||||
|
||||
final passkeysLayoutProvider =
|
||||
StateNotifierProvider<LayoutNotifier, FlexLayout>(
|
||||
(ref) => LayoutNotifier('FIDO_PASSKEYS_LAYOUT', ref.watch(prefProvider)),
|
||||
);
|
||||
|
||||
final fidoStateProvider = AsyncNotifierProvider.autoDispose
|
||||
.family<FidoStateNotifier, FidoState, DevicePath>(
|
||||
() => throw UnimplementedError(),
|
||||
@ -41,6 +47,7 @@ abstract class FidoStateNotifier extends ApplicationStateNotifier<FidoState> {
|
||||
Stream<InteractionEvent> reset();
|
||||
Future<PinResult> setPin(String newPin, {String? oldPin});
|
||||
Future<PinResult> unlock(String pin);
|
||||
Future<void> enableEnterpriseAttestation();
|
||||
}
|
||||
|
||||
final fingerprintProvider = AsyncNotifierProvider.autoDispose
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022-2023 Yubico.
|
||||
* Copyright (C) 2022-2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -123,6 +123,8 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
|
||||
});
|
||||
}, onError: (error, stacktrace) {
|
||||
_log.error('Error adding fingerprint', error, stacktrace);
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final String errorMessage;
|
||||
@ -255,7 +257,11 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
|
||||
});
|
||||
},
|
||||
onFieldSubmitted: (_) {
|
||||
_submit();
|
||||
if (_label.isNotEmpty) {
|
||||
_submit();
|
||||
} else {
|
||||
_nameFocus.requestFocus();
|
||||
}
|
||||
},
|
||||
).init(),
|
||||
)
|
||||
|
50
lib/fido/views/enterprise_attestation_dialog.dart
Normal file
50
lib/fido/views/enterprise_attestation_dialog.dart
Normal file
@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../app/message.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../widgets/responsive_dialog.dart';
|
||||
import '../state.dart';
|
||||
|
||||
class EnableEnterpriseAttestationDialog extends ConsumerWidget {
|
||||
final DevicePath devicePath;
|
||||
const EnableEnterpriseAttestationDialog(this.devicePath, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return ResponsiveDialog(
|
||||
title: Text(l10n.s_enable_ep_attestation),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(fidoStateProvider(devicePath).notifier)
|
||||
.enableEnterpriseAttestation();
|
||||
await ref.read(withContextProvider)((context) async {
|
||||
Navigator.of(context).pop();
|
||||
showMessage(context, l10n.s_ep_attestation_enabled);
|
||||
});
|
||||
},
|
||||
child: Text(l10n.s_enable),
|
||||
),
|
||||
],
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.p_enable_ep_attestation_desc),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: e,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -331,15 +331,18 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
|
||||
}),
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: fingerprints
|
||||
.map((fp) => _FingerprintListItem(
|
||||
fp,
|
||||
expanded: expanded,
|
||||
selected: fp == _selected,
|
||||
))
|
||||
.toList()),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: fingerprints
|
||||
.map((fp) => _FingerprintListItem(
|
||||
fp,
|
||||
expanded: expanded,
|
||||
selected: fp == _selected,
|
||||
))
|
||||
.toList()),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -25,6 +25,7 @@ import '../features.dart' as features;
|
||||
import '../keys.dart' as keys;
|
||||
import '../models.dart';
|
||||
import 'add_fingerprint_dialog.dart';
|
||||
import 'enterprise_attestation_dialog.dart';
|
||||
import 'pin_dialog.dart';
|
||||
|
||||
bool passkeysShowActionsNotifier(FidoState state) {
|
||||
@ -50,6 +51,13 @@ Widget _fidoBuildActions(BuildContext context, DeviceNode node, FidoState state,
|
||||
Theme.of(context).colorScheme;
|
||||
final authBlocked = state.pinBlocked;
|
||||
|
||||
final enterpriseAttestation = state.enterpriseAttestation;
|
||||
final showEnterpriseAttestation = enterpriseAttestation != null &&
|
||||
!(state.alwaysUv && !state.hasPin) &&
|
||||
!(!state.unlocked && state.hasPin);
|
||||
final canEnableEnterpriseAttestation =
|
||||
enterpriseAttestation == false && showEnterpriseAttestation;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (fingerprints != null)
|
||||
@ -115,8 +123,27 @@ Widget _fidoBuildActions(BuildContext context, DeviceNode node, FidoState state,
|
||||
}
|
||||
: null,
|
||||
),
|
||||
if (showEnterpriseAttestation)
|
||||
ActionListItem(
|
||||
key: keys.enableEnterpriseAttestation,
|
||||
feature: features.enableEnterpriseAttestation,
|
||||
icon: const Icon(Symbols.local_police),
|
||||
title: l10n.s_ep_attestation,
|
||||
subtitle:
|
||||
enterpriseAttestation ? l10n.s_enabled : l10n.s_disabled,
|
||||
onTap: canEnableEnterpriseAttestation
|
||||
? (context) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
EnableEnterpriseAttestationDialog(node.path),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ import '../../exception/no_data_exception.dart';
|
||||
import '../../management/models.dart';
|
||||
import '../../widgets/app_input_decoration.dart';
|
||||
import '../../widgets/app_text_form_field.dart';
|
||||
import '../../widgets/flex_box.dart';
|
||||
import '../../widgets/list_title.dart';
|
||||
import '../features.dart' as features;
|
||||
import '../models.dart';
|
||||
@ -219,6 +220,7 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
|
||||
late FocusNode searchFocus;
|
||||
late TextEditingController searchController;
|
||||
FidoCredential? _selected;
|
||||
bool _canRequestFocus = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -374,60 +376,103 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
child: Builder(builder: (context) {
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
return Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: AppTextFormField(
|
||||
key: searchField,
|
||||
controller: searchController,
|
||||
focusNode: searchFocus,
|
||||
// Use the default style, but with a smaller font size:
|
||||
style: textTheme.titleMedium
|
||||
?.copyWith(fontSize: textTheme.titleSmall?.fontSize),
|
||||
decoration: AppInputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(48),
|
||||
borderSide: BorderSide(
|
||||
width: 0,
|
||||
style: searchFocus.hasFocus
|
||||
? BorderStyle.solid
|
||||
: BorderStyle.none,
|
||||
final width = constraints.maxWidth;
|
||||
final showLayoutOptions = width > 600;
|
||||
return Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final layout = ref.watch(passkeysLayoutProvider);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10.0, vertical: 8.0),
|
||||
child: AppTextFormField(
|
||||
key: searchField,
|
||||
controller: searchController,
|
||||
canRequestFocus: _canRequestFocus,
|
||||
focusNode: searchFocus,
|
||||
// Use the default style, but with a smaller font size:
|
||||
style: textTheme.titleMedium
|
||||
?.copyWith(fontSize: textTheme.titleSmall?.fontSize),
|
||||
decoration: AppInputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(48),
|
||||
borderSide: BorderSide(
|
||||
width: 0,
|
||||
style: searchFocus.hasFocus
|
||||
? BorderStyle.solid
|
||||
: BorderStyle.none,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
fillColor: Theme.of(context).hoverColor,
|
||||
filled: true,
|
||||
hintText: l10n.s_search_passkeys,
|
||||
isDense: true,
|
||||
prefixIcon: const Padding(
|
||||
padding: EdgeInsetsDirectional.only(start: 8.0),
|
||||
child: Icon(Icons.search_outlined),
|
||||
),
|
||||
suffixIcons: [
|
||||
if (searchController.text.isNotEmpty)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
iconSize: 16,
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
ref
|
||||
.read(passkeysSearchProvider.notifier)
|
||||
.setFilter('');
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
if (searchController.text.isEmpty && showLayoutOptions)
|
||||
...FlexLayout.values.map(
|
||||
(e) => MouseRegion(
|
||||
onEnter: (event) {
|
||||
if (!searchFocus.hasFocus) {
|
||||
setState(() {
|
||||
_canRequestFocus = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
onExit: (event) {
|
||||
setState(() {
|
||||
_canRequestFocus = true;
|
||||
});
|
||||
},
|
||||
child: IconButton(
|
||||
tooltip: e.getDisplayName(l10n),
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(passkeysLayoutProvider.notifier)
|
||||
.setLayout(e);
|
||||
},
|
||||
icon: Icon(
|
||||
e.icon,
|
||||
color: e == layout
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
fillColor: Theme.of(context).hoverColor,
|
||||
filled: true,
|
||||
hintText: l10n.s_search_passkeys,
|
||||
isDense: true,
|
||||
prefixIcon: const Padding(
|
||||
padding: EdgeInsetsDirectional.only(start: 8.0),
|
||||
child: Icon(Icons.search_outlined),
|
||||
),
|
||||
suffixIcon: searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
iconSize: 16,
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
ref
|
||||
.read(passkeysSearchProvider.notifier)
|
||||
.setFilter('');
|
||||
setState(() {});
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
onChanged: (value) {
|
||||
ref.read(passkeysSearchProvider.notifier).setFilter(value);
|
||||
setState(() {});
|
||||
},
|
||||
textInputAction: TextInputAction.next,
|
||||
onFieldSubmitted: (value) {
|
||||
Focus.of(context).focusInDirection(TraversalDirection.down);
|
||||
},
|
||||
).init(),
|
||||
onChanged: (value) {
|
||||
ref
|
||||
.read(passkeysSearchProvider.notifier)
|
||||
.setFilter(value);
|
||||
setState(() {});
|
||||
},
|
||||
textInputAction: TextInputAction.next,
|
||||
onFieldSubmitted: (value) {
|
||||
Focus.of(context)
|
||||
.focusInDirection(TraversalDirection.down);
|
||||
},
|
||||
).init(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
@ -483,21 +528,37 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> {
|
||||
}),
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (filteredCredentials.isEmpty)
|
||||
Center(
|
||||
child: Text(l10n.s_no_passkeys),
|
||||
child: Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final layout = ref.watch(passkeysLayoutProvider);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (filteredCredentials.isEmpty)
|
||||
Center(
|
||||
child: Text(l10n.s_no_passkeys),
|
||||
),
|
||||
FlexBox<FidoCredential>(
|
||||
items: filteredCredentials,
|
||||
itemBuilder: (cred) => _CredentialListItem(
|
||||
cred,
|
||||
expanded: expanded,
|
||||
selected: _selected == cred,
|
||||
tileColor: layout == FlexLayout.grid
|
||||
? Theme.of(context).hoverColor
|
||||
: null,
|
||||
),
|
||||
layout: layout,
|
||||
cellMinWidth: 265,
|
||||
spacing: layout == FlexLayout.grid ? 4.0 : 0.0,
|
||||
runSpacing: layout == FlexLayout.grid ? 4.0 : 0.0,
|
||||
)
|
||||
],
|
||||
),
|
||||
...filteredCredentials.map(
|
||||
(cred) => _CredentialListItem(
|
||||
cred,
|
||||
expanded: expanded,
|
||||
selected: _selected == cred,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -518,9 +579,10 @@ class _CredentialListItem extends StatelessWidget {
|
||||
final FidoCredential credential;
|
||||
final bool selected;
|
||||
final bool expanded;
|
||||
final Color? tileColor;
|
||||
|
||||
const _CredentialListItem(this.credential,
|
||||
{required this.expanded, required this.selected});
|
||||
{required this.expanded, required this.selected, this.tileColor});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -533,6 +595,7 @@ class _CredentialListItem extends StatelessWidget {
|
||||
backgroundColor: colorScheme.secondary,
|
||||
child: const Icon(Symbols.passkey),
|
||||
),
|
||||
tileColor: tileColor,
|
||||
title: credential.rpId,
|
||||
subtitle: credential.userName,
|
||||
trailing: expanded
|
||||
|
@ -52,6 +52,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
final _currentPinFocus = FocusNode();
|
||||
final _newPinController = TextEditingController();
|
||||
final _newPinFocus = FocusNode();
|
||||
final _confirmPinFocus = FocusNode();
|
||||
String _confirmPin = '';
|
||||
String? _currentPinError;
|
||||
String? _newPinError;
|
||||
@ -68,6 +69,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
_currentPinFocus.dispose();
|
||||
_newPinController.dispose();
|
||||
_newPinFocus.dispose();
|
||||
_confirmPinFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -86,6 +88,9 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
final isValid =
|
||||
currentPinLenOk && newPinLenOk && _newPinController.text == _confirmPin;
|
||||
|
||||
final newPinEnabled = !_isBlocked && currentPinLenOk;
|
||||
final confirmPinEnabled = !_isBlocked && currentPinLenOk && newPinLenOk;
|
||||
|
||||
final deviceData = ref.read(currentDeviceDataProvider).valueOrNull;
|
||||
|
||||
final hasPinComplexity = deviceData?.info.pinComplexity ?? false;
|
||||
@ -148,11 +153,19 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
_isObscureCurrent ? l10n.s_show_pin : l10n.s_hide_pin,
|
||||
),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_currentIsWrong = false;
|
||||
});
|
||||
},
|
||||
onFieldSubmitted: (_) {
|
||||
if (_currentPinController.text.length < minPinLength) {
|
||||
_currentPinFocus.requestFocus();
|
||||
} else {
|
||||
_newPinFocus.requestFocus();
|
||||
}
|
||||
},
|
||||
).init(),
|
||||
],
|
||||
Text(hasPinComplexity
|
||||
@ -172,31 +185,43 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
decoration: AppInputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: l10n.s_new_pin,
|
||||
enabled: !_isBlocked && currentPinLenOk,
|
||||
enabled: newPinEnabled,
|
||||
errorText: _newIsWrong ? _newPinError : null,
|
||||
errorMaxLines: 3,
|
||||
prefixIcon: const Icon(Symbols.pin),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(_isObscureNew
|
||||
? Symbols.visibility
|
||||
: Symbols.visibility_off),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isObscureNew = !_isObscureNew;
|
||||
});
|
||||
},
|
||||
tooltip: _isObscureNew ? l10n.s_show_pin : l10n.s_hide_pin,
|
||||
suffixIcon: ExcludeFocusTraversal(
|
||||
excluding: !newPinEnabled,
|
||||
child: IconButton(
|
||||
icon: Icon(_isObscureNew
|
||||
? Symbols.visibility
|
||||
: Symbols.visibility_off),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isObscureNew = !_isObscureNew;
|
||||
});
|
||||
},
|
||||
tooltip: _isObscureNew ? l10n.s_show_pin : l10n.s_hide_pin,
|
||||
),
|
||||
),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_newIsWrong = false;
|
||||
});
|
||||
},
|
||||
onFieldSubmitted: (_) {
|
||||
if (_newPinController.text.length < minPinLength) {
|
||||
_newPinFocus.requestFocus();
|
||||
} else {
|
||||
_confirmPinFocus.requestFocus();
|
||||
}
|
||||
},
|
||||
).init(),
|
||||
AppTextFormField(
|
||||
key: confirmPin,
|
||||
initialValue: _confirmPin,
|
||||
focusNode: _confirmPinFocus,
|
||||
maxLength: maxPinLength,
|
||||
inputFormatters: [limitBytesLength(maxPinLength)],
|
||||
buildCounter: buildByteCounterFor(_confirmPin),
|
||||
@ -206,19 +231,22 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: l10n.s_confirm_pin,
|
||||
prefixIcon: const Icon(Symbols.pin),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(_isObscureConfirm
|
||||
? Symbols.visibility
|
||||
: Symbols.visibility_off),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isObscureConfirm = !_isObscureConfirm;
|
||||
});
|
||||
},
|
||||
tooltip:
|
||||
_isObscureConfirm ? l10n.s_show_pin : l10n.s_hide_pin,
|
||||
suffixIcon: ExcludeFocusTraversal(
|
||||
excluding: !confirmPinEnabled,
|
||||
child: IconButton(
|
||||
icon: Icon(_isObscureConfirm
|
||||
? Symbols.visibility
|
||||
: Symbols.visibility_off),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isObscureConfirm = !_isObscureConfirm;
|
||||
});
|
||||
},
|
||||
tooltip:
|
||||
_isObscureConfirm ? l10n.s_show_pin : l10n.s_hide_pin,
|
||||
),
|
||||
),
|
||||
enabled: !_isBlocked && currentPinLenOk && newPinLenOk,
|
||||
enabled: confirmPinEnabled,
|
||||
errorText:
|
||||
_newPinController.text.length == _confirmPin.length &&
|
||||
_newPinController.text != _confirmPin
|
||||
@ -226,6 +254,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
: null,
|
||||
helperText: '', // Prevents resizing when errorText shown
|
||||
),
|
||||
textInputAction: TextInputAction.done,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_confirmPin = value;
|
||||
@ -234,6 +263,8 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
onFieldSubmitted: (_) {
|
||||
if (isValid) {
|
||||
_submit();
|
||||
} else {
|
||||
_confirmPinFocus.requestFocus();
|
||||
}
|
||||
},
|
||||
).init(),
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022-2023 Yubico.
|
||||
* Copyright (C) 2022-2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -41,12 +41,20 @@ class RenameFingerprintDialog extends ConsumerStatefulWidget {
|
||||
|
||||
class _RenameAccountDialogState extends ConsumerState<RenameFingerprintDialog> {
|
||||
late String _label;
|
||||
late FocusNode _labelFocus;
|
||||
_RenameAccountDialogState();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_label = widget.fingerprint.name ?? '';
|
||||
_labelFocus = FocusNode();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_labelFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_submit() async {
|
||||
@ -93,7 +101,9 @@ class _RenameAccountDialogState extends ConsumerState<RenameFingerprintDialog> {
|
||||
Text(l10n.q_rename_target(widget.fingerprint.label)),
|
||||
Text(l10n.p_will_change_label_fp),
|
||||
AppTextFormField(
|
||||
autofocus: true,
|
||||
initialValue: _label,
|
||||
focusNode: _labelFocus,
|
||||
maxLength: 15,
|
||||
inputFormatters: [limitBytesLength(15)],
|
||||
buildCounter: buildByteCounterFor(_label),
|
||||
@ -110,6 +120,8 @@ class _RenameAccountDialogState extends ConsumerState<RenameFingerprintDialog> {
|
||||
onFieldSubmitted: (_) {
|
||||
if (_label.isNotEmpty) {
|
||||
_submit();
|
||||
} else {
|
||||
_labelFocus.requestFocus();
|
||||
}
|
||||
},
|
||||
).init(),
|
||||
|
@ -29,7 +29,6 @@ import '../../app/views/app_page.dart';
|
||||
import '../../core/models.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../../management/models.dart';
|
||||
import '../../widgets/choice_filter_chip.dart';
|
||||
import '../../widgets/product_image.dart';
|
||||
import 'key_actions.dart';
|
||||
import 'manage_label_dialog.dart';
|
||||
@ -72,7 +71,7 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
|
||||
homeBuildActions(context, widget.deviceData, ref),
|
||||
builder: (context, expanded) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -92,16 +91,14 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
|
||||
runSpacing: 8,
|
||||
children: Capability.values
|
||||
.where((c) => enabledCapabilities & c.value != 0)
|
||||
.map((c) => CapabilityBadge(c))
|
||||
.map((c) => CapabilityBadge(c, noTooltip: true))
|
||||
.toList(),
|
||||
),
|
||||
if (serial != null) ...[
|
||||
const SizedBox(height: 32.0),
|
||||
_DeviceColor(
|
||||
deviceData: widget.deviceData,
|
||||
initialCustomization: keyCustomization ??
|
||||
KeyCustomization(serial: serial))
|
||||
]
|
||||
if (widget.deviceData.info.fipsCapable != 0)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 38),
|
||||
child: _FipsLegend(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -131,6 +128,62 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
class _FipsLegend extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return Opacity(
|
||||
opacity: 0.6,
|
||||
child: Wrap(
|
||||
runSpacing: 0,
|
||||
spacing: 16,
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
const WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: 4),
|
||||
child: Icon(
|
||||
Symbols.shield,
|
||||
size: 12,
|
||||
fill: 0.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: l10n.l_fips_capable,
|
||||
style: Theme.of(context).textTheme.bodySmall),
|
||||
],
|
||||
),
|
||||
),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
const WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: 4),
|
||||
child: Icon(
|
||||
Symbols.shield,
|
||||
size: 12,
|
||||
fill: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: l10n.l_fips_approved,
|
||||
style: Theme.of(context).textTheme.bodySmall),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DeviceContent extends ConsumerWidget {
|
||||
final YubiKeyData deviceData;
|
||||
final KeyCustomization? initialCustomization;
|
||||
@ -147,6 +200,9 @@ class _DeviceContent extends ConsumerWidget {
|
||||
final label = initialCustomization?.name;
|
||||
String displayName = label != null ? '$label ($name)' : name;
|
||||
|
||||
final defaultColor = ref.watch(defaultColorProvider);
|
||||
final customColor = initialCustomization?.color;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -158,22 +214,114 @@ class _DeviceContent extends ConsumerWidget {
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
if (serial != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: IconButton(
|
||||
icon: const Icon(Symbols.edit),
|
||||
onPressed: () async {
|
||||
await ref.read(withContextProvider)((context) async {
|
||||
await _showManageLabelDialog(
|
||||
initialCustomization ??
|
||||
KeyCustomization(serial: serial),
|
||||
context,
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
if (serial != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.edit),
|
||||
tooltip: l10n.s_set_label,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
onPressed: () async {
|
||||
await ref.read(withContextProvider)((context) async {
|
||||
await _showManageLabelDialog(
|
||||
initialCustomization ??
|
||||
KeyCustomization(serial: serial),
|
||||
context,
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
PopupMenuButton(
|
||||
popUpAnimationStyle:
|
||||
AnimationStyle(duration: Duration.zero),
|
||||
menuPadding: EdgeInsets.zero,
|
||||
tooltip: l10n.s_set_color,
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
child: Center(
|
||||
child: Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 16,
|
||||
children: [
|
||||
...[
|
||||
Colors.teal,
|
||||
Colors.cyan,
|
||||
Colors.blueAccent,
|
||||
Colors.deepPurple,
|
||||
Colors.red,
|
||||
Colors.orange,
|
||||
Colors.yellow,
|
||||
// add nice color to devices with dynamic color
|
||||
if (isAndroid &&
|
||||
ref.read(androidSdkVersionProvider) >=
|
||||
31)
|
||||
Colors.lightGreen
|
||||
].map((e) => _ColorButton(
|
||||
color: e,
|
||||
isSelected:
|
||||
customColor?.value == e.value,
|
||||
onPressed: () {
|
||||
_updateColor(e, ref, serial);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)),
|
||||
|
||||
// "use default color" button
|
||||
RawMaterialButton(
|
||||
onPressed: () {
|
||||
_updateColor(null, ref, serial);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 26.0, minHeight: 26.0),
|
||||
fillColor: defaultColor,
|
||||
hoverColor: Colors.black12,
|
||||
shape: const CircleBorder(),
|
||||
child: Icon(
|
||||
customColor == null
|
||||
? Symbols.circle
|
||||
: Symbols.clear,
|
||||
fill: 1,
|
||||
size: 16,
|
||||
weight: 700,
|
||||
opticalSize: 20,
|
||||
color: defaultColor
|
||||
.computeLuminance() >
|
||||
0.7
|
||||
? Colors.grey // for bright colors
|
||||
: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
];
|
||||
},
|
||||
icon: Icon(
|
||||
Symbols.palette,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 3.0,
|
||||
width: 24.0,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(0.9)),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
@ -195,10 +343,42 @@ class _DeviceContent extends ConsumerWidget {
|
||||
.titleSmall
|
||||
?.apply(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
if (deviceData.info.pinComplexity)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: RichText(
|
||||
text: TextSpan(children: [
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: Icon(
|
||||
Symbols.check,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
)),
|
||||
TextSpan(
|
||||
text: l10n.l_pin_complexity,
|
||||
style: Theme.of(context).textTheme.titleSmall?.apply(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _updateColor(Color? color, WidgetRef ref, int serial) async {
|
||||
final manager = ref.read(keyCustomizationManagerProvider.notifier);
|
||||
await manager.set(
|
||||
serial: serial,
|
||||
name: initialCustomization?.name,
|
||||
color: color,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showManageLabelDialog(
|
||||
KeyCustomization keyCustomization, BuildContext context) async {
|
||||
await showBlurDialog(
|
||||
@ -210,97 +390,6 @@ class _DeviceContent extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _DeviceColor extends ConsumerWidget {
|
||||
final YubiKeyData deviceData;
|
||||
final KeyCustomization initialCustomization;
|
||||
const _DeviceColor(
|
||||
{required this.deviceData, required this.initialCustomization});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final defaultColor = ref.watch(defaultColorProvider);
|
||||
final customColor = initialCustomization.color;
|
||||
|
||||
return ChoiceFilterChip<Color?>(
|
||||
disableHover: true,
|
||||
value: customColor,
|
||||
items: const [null],
|
||||
selected: customColor != null && customColor.value != defaultColor.value,
|
||||
itemBuilder: (e) => Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
runSpacing: 8,
|
||||
spacing: 16,
|
||||
children: [
|
||||
...[
|
||||
Colors.teal,
|
||||
Colors.cyan,
|
||||
Colors.blueAccent,
|
||||
Colors.deepPurple,
|
||||
Colors.red,
|
||||
Colors.orange,
|
||||
Colors.yellow,
|
||||
// add nice color to devices with dynamic color
|
||||
if (isAndroid && ref.read(androidSdkVersionProvider) >= 31)
|
||||
Colors.lightGreen
|
||||
].map((e) => _ColorButton(
|
||||
color: e,
|
||||
isSelected: customColor?.value == e.value,
|
||||
onPressed: () {
|
||||
_updateColor(e, ref);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)),
|
||||
|
||||
// "use default color" button
|
||||
RawMaterialButton(
|
||||
onPressed: () {
|
||||
_updateColor(null, ref);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
constraints: const BoxConstraints(minWidth: 26.0, minHeight: 26.0),
|
||||
fillColor: defaultColor,
|
||||
hoverColor: Colors.black12,
|
||||
shape: const CircleBorder(),
|
||||
child: Icon(customColor == null ? Symbols.circle : Symbols.clear,
|
||||
fill: 1,
|
||||
size: 16,
|
||||
weight: 700,
|
||||
opticalSize: 20,
|
||||
color: defaultColor.computeLuminance() > 0.7
|
||||
? Colors.grey // for bright colors
|
||||
: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
labelBuilder: (e) => Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
constraints: const BoxConstraints(minWidth: 22.0, minHeight: 22.0),
|
||||
decoration: BoxDecoration(
|
||||
color: customColor ?? defaultColor, shape: BoxShape.circle),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
),
|
||||
Flexible(child: Text(l10n.s_color))
|
||||
],
|
||||
),
|
||||
onChanged: (e) {},
|
||||
);
|
||||
}
|
||||
|
||||
void _updateColor(Color? color, WidgetRef ref) async {
|
||||
final manager = ref.read(keyCustomizationManagerProvider.notifier);
|
||||
await manager.set(
|
||||
serial: initialCustomization.serial,
|
||||
name: initialCustomization.name,
|
||||
color: color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ColorButton extends StatefulWidget {
|
||||
final Color? color;
|
||||
final bool isSelected;
|
||||
|
@ -34,6 +34,7 @@ Widget homeBuildActions(
|
||||
BuildContext context, YubiKeyData? deviceData, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final hasFeature = ref.watch(featureProvider);
|
||||
final interfacesLocked = deviceData?.info.resetBlocked != 0;
|
||||
final managementAvailability = hasFeature(features.management) &&
|
||||
switch (deviceData?.info.version) {
|
||||
Version version => (version.major > 4 || // YK5 and up
|
||||
@ -56,16 +57,21 @@ Widget homeBuildActions(
|
||||
title: deviceData.info.version.major > 4
|
||||
? l10n.s_toggle_applications
|
||||
: l10n.s_toggle_interfaces,
|
||||
subtitle: deviceData.info.version.major > 4
|
||||
? l10n.l_toggle_applications_desc
|
||||
: l10n.l_toggle_interfaces_desc,
|
||||
onTap: (context) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ManagementScreen(deviceData),
|
||||
);
|
||||
},
|
||||
subtitle: interfacesLocked
|
||||
? 'Requires factory reset' // TODO: Replace with l10n
|
||||
: (deviceData.info.version.major > 4
|
||||
? l10n.l_toggle_applications_desc
|
||||
: l10n.l_toggle_interfaces_desc),
|
||||
onTap: interfacesLocked
|
||||
? null
|
||||
: (context) {
|
||||
Navigator.of(context)
|
||||
.popUntil((route) => route.isFirst);
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => ManagementScreen(deviceData),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (getResetCapabilities(hasFeature).any((c) =>
|
||||
c.value &
|
||||
|
@ -30,6 +30,9 @@
|
||||
"s_delete": "Löschen",
|
||||
"s_move": null,
|
||||
"s_quit": "Beenden",
|
||||
"s_enable": null,
|
||||
"s_enabled": null,
|
||||
"s_disabled": null,
|
||||
"s_status": null,
|
||||
"s_unlock": "Entsperren",
|
||||
"s_calculate": "Berechnen",
|
||||
@ -45,6 +48,8 @@
|
||||
"s_hide_window": "Fenster verstecken",
|
||||
"s_expand_navigation": null,
|
||||
"s_collapse_navigation": null,
|
||||
"s_expand_sidebar": null,
|
||||
"s_collapse_sidebar": null,
|
||||
"q_rename_target": "{label} umbenennen?",
|
||||
"@q_rename_target": {
|
||||
"placeholders": {
|
||||
@ -121,6 +126,12 @@
|
||||
"s_light_mode": "Heller Modus",
|
||||
"s_dark_mode": "Dunkler Modus",
|
||||
|
||||
"@_layout": {},
|
||||
"s_list_layout": null,
|
||||
"s_grid_layout": null,
|
||||
"s_mixed_layout": null,
|
||||
"s_select_layout": null,
|
||||
|
||||
"@_yubikey_selection": {},
|
||||
"s_select_to_scan": "Zum Scannen auswählen",
|
||||
"s_hide_device": "Gerät verstecken",
|
||||
@ -149,6 +160,8 @@
|
||||
}
|
||||
},
|
||||
"l_firmware_version": null,
|
||||
"l_fips_capable": null,
|
||||
"l_fips_approved": null,
|
||||
|
||||
"@_yubikey_interactions": {},
|
||||
"l_insert_yk": "YubiKey anschließen",
|
||||
@ -301,6 +314,10 @@
|
||||
"common_pin": {}
|
||||
}
|
||||
},
|
||||
"s_ep_attestation": null,
|
||||
"s_ep_attestation_enabled": null,
|
||||
"s_enable_ep_attestation": null,
|
||||
"p_enable_ep_attestation_desc": null,
|
||||
"s_pin_required": null,
|
||||
"p_pin_required_desc": null,
|
||||
"l_piv_pin_blocked": null,
|
||||
@ -330,6 +347,7 @@
|
||||
"l_warning_default_puk": null,
|
||||
"l_default_pin_used": null,
|
||||
"l_default_puk_used": null,
|
||||
"l_pin_complexity": null,
|
||||
|
||||
"@_passwords": {},
|
||||
"s_password": "Passwort",
|
||||
@ -339,6 +357,7 @@
|
||||
"s_show_password": null,
|
||||
"s_hide_password": null,
|
||||
"l_optional_password_protection": "Optionaler Passwortschutz",
|
||||
"l_password_protection": null,
|
||||
"s_new_password": "Neues Passwort",
|
||||
"s_current_password": "Aktuelles Passwort",
|
||||
"s_confirm_password": "Passwort bestätigen",
|
||||
@ -352,6 +371,7 @@
|
||||
"l_keystore_unavailable": "Passwortspeicher des Betriebssystems nicht verfügbar",
|
||||
"l_remember_pw_failed": "Konnte Passwort nicht speichern",
|
||||
"l_unlock_first": "Zuerst mit Passwort entsperren",
|
||||
"l_set_password_first": null,
|
||||
"l_enter_oath_pw": "Das OATH-Passwort für Ihren YubiKey eingeben",
|
||||
"p_enter_current_password_or_reset": "Geben Sie Ihr aktuelles Passwort ein. Wenn Sie Ihr Passwort nicht wissen, müssen Sie den YubiKey zurücksetzen.",
|
||||
"p_enter_new_password": "Geben Sie Ihr neues Passwort ein. Ein Passwort kann Buchstaben, Ziffern und spezielle Zeichen enthalten.",
|
||||
@ -410,6 +430,7 @@
|
||||
"s_pin_account": "Konto anpinnen",
|
||||
"s_unpin_account": "Konto nicht mehr anpinnen",
|
||||
"s_no_pinned_accounts": "Keine angepinnten Konten",
|
||||
"s_pinned": null,
|
||||
"l_pin_account_desc": null,
|
||||
"s_rename_account": "Konto umbenennen",
|
||||
"l_rename_account_desc": null,
|
||||
@ -885,6 +906,7 @@
|
||||
|
||||
"@_key_customization": {},
|
||||
"s_set_label": null,
|
||||
"s_set_color": null,
|
||||
"s_change_label": null,
|
||||
"s_color": null,
|
||||
"p_set_will_add_custom_name": null,
|
||||
|
@ -30,6 +30,9 @@
|
||||
"s_delete": "Delete",
|
||||
"s_move": "Move",
|
||||
"s_quit": "Quit",
|
||||
"s_enable": "Enable",
|
||||
"s_enabled": "Enabled",
|
||||
"s_disabled": "Disabled",
|
||||
"s_status": "Status",
|
||||
"s_unlock": "Unlock",
|
||||
"s_calculate": "Calculate",
|
||||
@ -45,6 +48,8 @@
|
||||
"s_hide_window": "Hide window",
|
||||
"s_expand_navigation": "Expand navigation",
|
||||
"s_collapse_navigation": "Collapse navigation",
|
||||
"s_expand_sidebar": "Expand sidebar",
|
||||
"s_collapse_sidebar": "Collapse sidebar",
|
||||
"q_rename_target": "Rename {label}?",
|
||||
"@q_rename_target": {
|
||||
"placeholders": {
|
||||
@ -121,6 +126,12 @@
|
||||
"s_light_mode": "Light mode",
|
||||
"s_dark_mode": "Dark mode",
|
||||
|
||||
"@_layout": {},
|
||||
"s_list_layout": "List layout",
|
||||
"s_grid_layout": "Grid layout",
|
||||
"s_mixed_layout": "Mixed layout",
|
||||
"s_select_layout": "Select layout",
|
||||
|
||||
"@_yubikey_selection": {},
|
||||
"s_select_to_scan": "Select to scan",
|
||||
"s_hide_device": "Hide device",
|
||||
@ -149,6 +160,8 @@
|
||||
}
|
||||
},
|
||||
"l_firmware_version": "Firmware version: {version}",
|
||||
"l_fips_capable": "FIPS capable",
|
||||
"l_fips_approved": "FIPS approved",
|
||||
|
||||
"@_yubikey_interactions": {},
|
||||
"l_insert_yk": "Insert your YubiKey",
|
||||
@ -301,6 +314,10 @@
|
||||
"common_pin": {}
|
||||
}
|
||||
},
|
||||
"s_ep_attestation": "Enterprise Attestation",
|
||||
"s_ep_attestation_enabled": "Enterprise Attestation enabled",
|
||||
"s_enable_ep_attestation": "Enable Enterprise Attestation",
|
||||
"p_enable_ep_attestation_desc": "This will enable Enterprise Attestation, allowing authorized domains to uniquely identify your YubiKey.",
|
||||
"s_pin_required": "PIN required",
|
||||
"p_pin_required_desc": "The action you are about to perform requires the PIV PIN to be entered.",
|
||||
"l_piv_pin_blocked": "Blocked, use PUK to reset",
|
||||
@ -330,6 +347,7 @@
|
||||
"l_warning_default_puk": "Warning: Default PUK used",
|
||||
"l_default_pin_used": "Default PIN used",
|
||||
"l_default_puk_used": "Default PUK used",
|
||||
"l_pin_complexity": "PIN complexity enforced",
|
||||
|
||||
"@_passwords": {},
|
||||
"s_password": "Password",
|
||||
@ -339,6 +357,7 @@
|
||||
"s_show_password": "Show password",
|
||||
"s_hide_password": "Hide password",
|
||||
"l_optional_password_protection": "Optional password protection",
|
||||
"l_password_protection": "Password protection of accounts",
|
||||
"s_new_password": "New password",
|
||||
"s_current_password": "Current password",
|
||||
"s_confirm_password": "Confirm password",
|
||||
@ -352,6 +371,7 @@
|
||||
"l_keystore_unavailable": "OS Keystore unavailable",
|
||||
"l_remember_pw_failed": "Failed to remember password",
|
||||
"l_unlock_first": "Unlock with password first",
|
||||
"l_set_password_first": "Set a password first",
|
||||
"l_enter_oath_pw": "Enter the OATH password for your YubiKey",
|
||||
"p_enter_current_password_or_reset": "Enter your current password. If you don't know your password, you'll need to reset the YubiKey.",
|
||||
"p_enter_new_password": "Enter your new password. A password may contain letters, numbers and special characters.",
|
||||
@ -410,6 +430,7 @@
|
||||
"s_pin_account": "Pin account",
|
||||
"s_unpin_account": "Unpin account",
|
||||
"s_no_pinned_accounts": "No pinned accounts",
|
||||
"s_pinned": "Pinned",
|
||||
"l_pin_account_desc": "Keep your important accounts together",
|
||||
"s_rename_account": "Rename account",
|
||||
"l_rename_account_desc": "Edit the issuer/name of the account",
|
||||
@ -885,6 +906,7 @@
|
||||
|
||||
"@_key_customization": {},
|
||||
"s_set_label": "Set label",
|
||||
"s_set_color": "Set color",
|
||||
"s_change_label": "Change label",
|
||||
"s_color": "Color",
|
||||
"p_set_will_add_custom_name": "This will give your YubiKey a custom name.",
|
||||
|
@ -30,6 +30,9 @@
|
||||
"s_delete": "Supprimer",
|
||||
"s_move": "Déplacer",
|
||||
"s_quit": "Quitter",
|
||||
"s_enable": null,
|
||||
"s_enabled": null,
|
||||
"s_disabled": null,
|
||||
"s_status": "État",
|
||||
"s_unlock": "Déverrouiller",
|
||||
"s_calculate": "Calculer",
|
||||
@ -45,6 +48,8 @@
|
||||
"s_hide_window": "Masquer fenêtre",
|
||||
"s_expand_navigation": "Développer la navigation",
|
||||
"s_collapse_navigation": "Réduire la navigation",
|
||||
"s_expand_sidebar": null,
|
||||
"s_collapse_sidebar": null,
|
||||
"q_rename_target": "Renommer {label}\u00a0?",
|
||||
"@q_rename_target": {
|
||||
"placeholders": {
|
||||
@ -121,6 +126,12 @@
|
||||
"s_light_mode": "Thème clair",
|
||||
"s_dark_mode": "Thème sombre",
|
||||
|
||||
"@_layout": {},
|
||||
"s_list_layout": null,
|
||||
"s_grid_layout": null,
|
||||
"s_mixed_layout": null,
|
||||
"s_select_layout": null,
|
||||
|
||||
"@_yubikey_selection": {},
|
||||
"s_select_to_scan": "Sélectionner pour scanner",
|
||||
"s_hide_device": "Masquer appareil",
|
||||
@ -149,6 +160,8 @@
|
||||
}
|
||||
},
|
||||
"l_firmware_version": "Version du firmware : {version}",
|
||||
"l_fips_capable": null,
|
||||
"l_fips_approved": null,
|
||||
|
||||
"@_yubikey_interactions": {},
|
||||
"l_insert_yk": "Insérez votre YubiKey",
|
||||
@ -301,6 +314,10 @@
|
||||
"common_pin": {}
|
||||
}
|
||||
},
|
||||
"s_ep_attestation": null,
|
||||
"s_ep_attestation_enabled": null,
|
||||
"s_enable_ep_attestation": null,
|
||||
"p_enable_ep_attestation_desc": null,
|
||||
"s_pin_required": "PIN requis",
|
||||
"p_pin_required_desc": "L'action que vous allez effectuer nécessite la saisie du PIN PIV.",
|
||||
"l_piv_pin_blocked": "Bloqué, utilisez PUK pour réinitialiser",
|
||||
@ -330,6 +347,7 @@
|
||||
"l_warning_default_puk": "Attention : PUK par défaut utilisé",
|
||||
"l_default_pin_used": "Code PIN par défaut utilisé",
|
||||
"l_default_puk_used": "PUK par défaut utilisé",
|
||||
"l_pin_complexity": null,
|
||||
|
||||
"@_passwords": {},
|
||||
"s_password": "Mot de passe",
|
||||
@ -339,6 +357,7 @@
|
||||
"s_show_password": "Montrer mot de passe",
|
||||
"s_hide_password": "Masquer mot de passe",
|
||||
"l_optional_password_protection": "Protection par mot de passe facultative",
|
||||
"l_password_protection": null,
|
||||
"s_new_password": "Nouveau mot de passe",
|
||||
"s_current_password": "Mot de passe actuel",
|
||||
"s_confirm_password": "Confirmer mot de passe",
|
||||
@ -352,6 +371,7 @@
|
||||
"l_keystore_unavailable": "OS Keystore indisponible",
|
||||
"l_remember_pw_failed": "Mémorisation mot de passe impossible",
|
||||
"l_unlock_first": "Débloquez d'abord avec mot de passe",
|
||||
"l_set_password_first": null,
|
||||
"l_enter_oath_pw": "Saisissez le mot de passe OATH de votre YubiKey",
|
||||
"p_enter_current_password_or_reset": "Saisissez votre mot de passe actuel. Vous ne connaissez votre mot de passe\u00a0? Réinitialisez la YubiKey.",
|
||||
"p_enter_new_password": "Saisissez votre nouveau mot de passe. Un mot de passe peut inclure des lettres, chiffres et caractères spéciaux.",
|
||||
@ -410,6 +430,7 @@
|
||||
"s_pin_account": "Épingler compte",
|
||||
"s_unpin_account": "Détacher compte",
|
||||
"s_no_pinned_accounts": "Aucun compte épinglé",
|
||||
"s_pinned": null,
|
||||
"l_pin_account_desc": "Conserver vos comptes importants ensemble",
|
||||
"s_rename_account": "Renommer compte",
|
||||
"l_rename_account_desc": "Modifier émetteur/nom du compte",
|
||||
@ -885,6 +906,7 @@
|
||||
|
||||
"@_key_customization": {},
|
||||
"s_set_label": "Définir l'étiquette",
|
||||
"s_set_color": null,
|
||||
"s_change_label": "Modifier l'étiquette",
|
||||
"s_color": "Couleur",
|
||||
"p_set_will_add_custom_name": "Cela donnera un nom personnalisé à votre YubiKey.",
|
||||
|
@ -30,6 +30,9 @@
|
||||
"s_delete": "削除",
|
||||
"s_move": "移動",
|
||||
"s_quit": "終了",
|
||||
"s_enable": null,
|
||||
"s_enabled": null,
|
||||
"s_disabled": null,
|
||||
"s_status": "ステータス",
|
||||
"s_unlock": "ロック解除",
|
||||
"s_calculate": "計算",
|
||||
@ -45,6 +48,8 @@
|
||||
"s_hide_window": "ウィンドウを非表示",
|
||||
"s_expand_navigation": "ナビゲーションを展開",
|
||||
"s_collapse_navigation": "ナビゲーションを閉じる",
|
||||
"s_expand_sidebar": null,
|
||||
"s_collapse_sidebar": null,
|
||||
"q_rename_target": "{label}の名前を変更しますか?",
|
||||
"@q_rename_target": {
|
||||
"placeholders": {
|
||||
@ -121,6 +126,12 @@
|
||||
"s_light_mode": "ライトモード",
|
||||
"s_dark_mode": "ダークモード",
|
||||
|
||||
"@_layout": {},
|
||||
"s_list_layout": null,
|
||||
"s_grid_layout": null,
|
||||
"s_mixed_layout": null,
|
||||
"s_select_layout": null,
|
||||
|
||||
"@_yubikey_selection": {},
|
||||
"s_select_to_scan": "選択してスキャン",
|
||||
"s_hide_device": "デバイスを非表示",
|
||||
@ -149,6 +160,8 @@
|
||||
}
|
||||
},
|
||||
"l_firmware_version": "ファームウェアバージョン: {version}",
|
||||
"l_fips_capable": null,
|
||||
"l_fips_approved": null,
|
||||
|
||||
"@_yubikey_interactions": {},
|
||||
"l_insert_yk": "YubiKeyを挿入してください",
|
||||
@ -301,6 +314,10 @@
|
||||
"common_pin": {}
|
||||
}
|
||||
},
|
||||
"s_ep_attestation": null,
|
||||
"s_ep_attestation_enabled": null,
|
||||
"s_enable_ep_attestation": null,
|
||||
"p_enable_ep_attestation_desc": null,
|
||||
"s_pin_required": "PINが必要",
|
||||
"p_pin_required_desc": "実行しようとしているアクションでは、PIV PINを入力する必要があります。",
|
||||
"l_piv_pin_blocked": "ブロックされています。リセットするにはPUKを使用してください",
|
||||
@ -330,6 +347,7 @@
|
||||
"l_warning_default_puk": "警告: デフォルトのPUKが使用されています",
|
||||
"l_default_pin_used": "デフォルトのPINが使用されています",
|
||||
"l_default_puk_used": "既定のPUKを使用",
|
||||
"l_pin_complexity": null,
|
||||
|
||||
"@_passwords": {},
|
||||
"s_password": "パスワード",
|
||||
@ -339,6 +357,7 @@
|
||||
"s_show_password": "パスワードを表示",
|
||||
"s_hide_password": "パスワードを非表示",
|
||||
"l_optional_password_protection": "オプションのパスワード保護",
|
||||
"l_password_protection": null,
|
||||
"s_new_password": "新しいパスワード",
|
||||
"s_current_password": "現在のパスワード",
|
||||
"s_confirm_password": "パスワードを確認",
|
||||
@ -352,6 +371,7 @@
|
||||
"l_keystore_unavailable": "OSのキーストアを利用できません",
|
||||
"l_remember_pw_failed": "パスワードを記憶できませんでした",
|
||||
"l_unlock_first": "最初にパスワードでロックを解除",
|
||||
"l_set_password_first": null,
|
||||
"l_enter_oath_pw": "YubiKeyのOATHパスワードを入力",
|
||||
"p_enter_current_password_or_reset": "現在のパスワードを入力してください。パスワードがわからない場合は、YubiKeyをリセットする必要があります。",
|
||||
"p_enter_new_password": "新しいパスワードを入力してください。パスワードには文字、数字、特殊文字を含めることができます。",
|
||||
@ -410,6 +430,7 @@
|
||||
"s_pin_account": "アカウントをピン留めする",
|
||||
"s_unpin_account": "アカウントのピン留めを解除",
|
||||
"s_no_pinned_accounts": "ピン留めされたアカウントはありません",
|
||||
"s_pinned": null,
|
||||
"l_pin_account_desc": "重要なアカウントをまとめて保持",
|
||||
"s_rename_account": "アカウント名を変更",
|
||||
"l_rename_account_desc": "アカウントの発行者/名前を編集",
|
||||
@ -885,6 +906,7 @@
|
||||
|
||||
"@_key_customization": {},
|
||||
"s_set_label": "ラベルを設定",
|
||||
"s_set_color": null,
|
||||
"s_change_label": "ラベルを変更",
|
||||
"s_color": "色",
|
||||
"p_set_will_add_custom_name": "これにより、YubiKey にカスタム名を付けることができます。",
|
||||
|
@ -30,6 +30,9 @@
|
||||
"s_delete": "Usuń",
|
||||
"s_move": null,
|
||||
"s_quit": "Wyjdź",
|
||||
"s_enable": null,
|
||||
"s_enabled": null,
|
||||
"s_disabled": null,
|
||||
"s_status": "Status",
|
||||
"s_unlock": "Odblokuj",
|
||||
"s_calculate": "Oblicz",
|
||||
@ -45,6 +48,8 @@
|
||||
"s_hide_window": "Ukryj okno",
|
||||
"s_expand_navigation": null,
|
||||
"s_collapse_navigation": null,
|
||||
"s_expand_sidebar": null,
|
||||
"s_collapse_sidebar": null,
|
||||
"q_rename_target": "Zmienić nazwę {label}?",
|
||||
"@q_rename_target": {
|
||||
"placeholders": {
|
||||
@ -121,6 +126,12 @@
|
||||
"s_light_mode": "Jasny",
|
||||
"s_dark_mode": "Ciemny",
|
||||
|
||||
"@_layout": {},
|
||||
"s_list_layout": null,
|
||||
"s_grid_layout": null,
|
||||
"s_mixed_layout": null,
|
||||
"s_select_layout": null,
|
||||
|
||||
"@_yubikey_selection": {},
|
||||
"s_select_to_scan": "Wybierz, aby skanować",
|
||||
"s_hide_device": "Ukryj urządzenie",
|
||||
@ -149,6 +160,8 @@
|
||||
}
|
||||
},
|
||||
"l_firmware_version": null,
|
||||
"l_fips_capable": null,
|
||||
"l_fips_approved": null,
|
||||
|
||||
"@_yubikey_interactions": {},
|
||||
"l_insert_yk": "Podłącz klucz YubiKey",
|
||||
@ -301,6 +314,10 @@
|
||||
"common_pin": {}
|
||||
}
|
||||
},
|
||||
"s_ep_attestation": null,
|
||||
"s_ep_attestation_enabled": null,
|
||||
"s_enable_ep_attestation": null,
|
||||
"p_enable_ep_attestation_desc": null,
|
||||
"s_pin_required": "Wymagany PIN",
|
||||
"p_pin_required_desc": "Czynność, którą zamierzasz wykonać, wymaga wprowadzenia kodu PIN PIV.",
|
||||
"l_piv_pin_blocked": "Zablokowano, użyj PUK, aby zresetować",
|
||||
@ -330,6 +347,7 @@
|
||||
"l_warning_default_puk": null,
|
||||
"l_default_pin_used": null,
|
||||
"l_default_puk_used": null,
|
||||
"l_pin_complexity": null,
|
||||
|
||||
"@_passwords": {},
|
||||
"s_password": "Hasło",
|
||||
@ -339,6 +357,7 @@
|
||||
"s_show_password": "Pokaż hasło",
|
||||
"s_hide_password": "Ukryj hasło",
|
||||
"l_optional_password_protection": "Opcjonalna ochrona hasłem",
|
||||
"l_password_protection": null,
|
||||
"s_new_password": "Nowe hasło",
|
||||
"s_current_password": "Aktualne hasło",
|
||||
"s_confirm_password": "Potwierdź hasło",
|
||||
@ -352,6 +371,7 @@
|
||||
"l_keystore_unavailable": "Magazyn kluczy systemu operacyjnego jest niedostępny",
|
||||
"l_remember_pw_failed": "Nie udało się zapamiętać hasła",
|
||||
"l_unlock_first": "Najpierw odblokuj hasłem",
|
||||
"l_set_password_first": null,
|
||||
"l_enter_oath_pw": "Wprowadź hasło OATH dla klucza YubiKey",
|
||||
"p_enter_current_password_or_reset": "Wprowadź aktualne hasło. Jeśli go nie znasz, musisz zresetować klucz YubiKey.",
|
||||
"p_enter_new_password": "Wprowadź nowe hasło. Może ono zawierać litery, cyfry i znaki specjalne.",
|
||||
@ -410,6 +430,7 @@
|
||||
"s_pin_account": "Przypnij konto",
|
||||
"s_unpin_account": "Odepnij konto",
|
||||
"s_no_pinned_accounts": "Brak przypiętych kont",
|
||||
"s_pinned": null,
|
||||
"l_pin_account_desc": "Przechowuj ważne konta razem",
|
||||
"s_rename_account": "Zmień nazwę konta",
|
||||
"l_rename_account_desc": "Edytuj wydawcę/nazwę konta",
|
||||
@ -885,6 +906,7 @@
|
||||
|
||||
"@_key_customization": {},
|
||||
"s_set_label": null,
|
||||
"s_set_color": null,
|
||||
"s_change_label": null,
|
||||
"s_color": null,
|
||||
"p_set_will_add_custom_name": null,
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import 'dart:developer' as developer;
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
@ -27,6 +28,7 @@ import 'app/state.dart';
|
||||
import 'core/state.dart';
|
||||
import 'desktop/init.dart' as desktop;
|
||||
import 'error_page.dart';
|
||||
import 'version.dart';
|
||||
|
||||
final _log = Logger('main');
|
||||
|
||||
@ -43,6 +45,12 @@ void main(List<String> argv) async {
|
||||
_initializeDebugLogging();
|
||||
throw UnimplementedError('Platform not supported');
|
||||
}
|
||||
_log.info('Running Yubico Authenticator...', {
|
||||
'app_version': version,
|
||||
'dart': Platform.version,
|
||||
'os': Platform.operatingSystem,
|
||||
'os_version': Platform.operatingSystemVersion,
|
||||
});
|
||||
runApp(initializedApp);
|
||||
} catch (e) {
|
||||
_log.warning('Platform initialization failed: $e');
|
||||
|
@ -78,6 +78,8 @@ class DeviceConfig with _$DeviceConfig {
|
||||
|
||||
@freezed
|
||||
class DeviceInfo with _$DeviceInfo {
|
||||
const DeviceInfo._(); // Added constructor
|
||||
|
||||
factory DeviceInfo(
|
||||
DeviceConfig config,
|
||||
int? serial,
|
||||
@ -88,8 +90,17 @@ class DeviceInfo with _$DeviceInfo {
|
||||
bool isFips,
|
||||
bool isSky,
|
||||
bool pinComplexity,
|
||||
int fipsCapable) = _DeviceInfo;
|
||||
int fipsCapable,
|
||||
int fipsApproved,
|
||||
int resetBlocked) = _DeviceInfo;
|
||||
|
||||
factory DeviceInfo.fromJson(Map<String, dynamic> json) =>
|
||||
_$DeviceInfoFromJson(json);
|
||||
|
||||
/// Gets the tuple fipsCapable, fipsApproved for the given capability.
|
||||
(bool fipsCapable, bool fipsApproved) getFipsStatus(Capability capability) {
|
||||
final capable = fipsCapable & capability.value != 0;
|
||||
final approved = capable && fipsApproved & capability.value != 0;
|
||||
return (capable, approved);
|
||||
}
|
||||
}
|
||||
|
@ -247,6 +247,8 @@ mixin _$DeviceInfo {
|
||||
bool get isSky => throw _privateConstructorUsedError;
|
||||
bool get pinComplexity => throw _privateConstructorUsedError;
|
||||
int get fipsCapable => throw _privateConstructorUsedError;
|
||||
int get fipsApproved => throw _privateConstructorUsedError;
|
||||
int get resetBlocked => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
@ -270,7 +272,9 @@ abstract class $DeviceInfoCopyWith<$Res> {
|
||||
bool isFips,
|
||||
bool isSky,
|
||||
bool pinComplexity,
|
||||
int fipsCapable});
|
||||
int fipsCapable,
|
||||
int fipsApproved,
|
||||
int resetBlocked});
|
||||
|
||||
$DeviceConfigCopyWith<$Res> get config;
|
||||
$VersionCopyWith<$Res> get version;
|
||||
@ -299,6 +303,8 @@ class _$DeviceInfoCopyWithImpl<$Res, $Val extends DeviceInfo>
|
||||
Object? isSky = null,
|
||||
Object? pinComplexity = null,
|
||||
Object? fipsCapable = null,
|
||||
Object? fipsApproved = null,
|
||||
Object? resetBlocked = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
config: null == config
|
||||
@ -341,6 +347,14 @@ class _$DeviceInfoCopyWithImpl<$Res, $Val extends DeviceInfo>
|
||||
? _value.fipsCapable
|
||||
: fipsCapable // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
fipsApproved: null == fipsApproved
|
||||
? _value.fipsApproved
|
||||
: fipsApproved // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
resetBlocked: null == resetBlocked
|
||||
? _value.resetBlocked
|
||||
: resetBlocked // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
@ -379,7 +393,9 @@ abstract class _$$DeviceInfoImplCopyWith<$Res>
|
||||
bool isFips,
|
||||
bool isSky,
|
||||
bool pinComplexity,
|
||||
int fipsCapable});
|
||||
int fipsCapable,
|
||||
int fipsApproved,
|
||||
int resetBlocked});
|
||||
|
||||
@override
|
||||
$DeviceConfigCopyWith<$Res> get config;
|
||||
@ -408,6 +424,8 @@ class __$$DeviceInfoImplCopyWithImpl<$Res>
|
||||
Object? isSky = null,
|
||||
Object? pinComplexity = null,
|
||||
Object? fipsCapable = null,
|
||||
Object? fipsApproved = null,
|
||||
Object? resetBlocked = null,
|
||||
}) {
|
||||
return _then(_$DeviceInfoImpl(
|
||||
null == config
|
||||
@ -450,13 +468,21 @@ class __$$DeviceInfoImplCopyWithImpl<$Res>
|
||||
? _value.fipsCapable
|
||||
: fipsCapable // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
null == fipsApproved
|
||||
? _value.fipsApproved
|
||||
: fipsApproved // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
null == resetBlocked
|
||||
? _value.resetBlocked
|
||||
: resetBlocked // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$DeviceInfoImpl implements _DeviceInfo {
|
||||
class _$DeviceInfoImpl extends _DeviceInfo {
|
||||
_$DeviceInfoImpl(
|
||||
this.config,
|
||||
this.serial,
|
||||
@ -467,8 +493,11 @@ class _$DeviceInfoImpl implements _DeviceInfo {
|
||||
this.isFips,
|
||||
this.isSky,
|
||||
this.pinComplexity,
|
||||
this.fipsCapable)
|
||||
: _supportedCapabilities = supportedCapabilities;
|
||||
this.fipsCapable,
|
||||
this.fipsApproved,
|
||||
this.resetBlocked)
|
||||
: _supportedCapabilities = supportedCapabilities,
|
||||
super._();
|
||||
|
||||
factory _$DeviceInfoImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$DeviceInfoImplFromJson(json);
|
||||
@ -500,10 +529,14 @@ class _$DeviceInfoImpl implements _DeviceInfo {
|
||||
final bool pinComplexity;
|
||||
@override
|
||||
final int fipsCapable;
|
||||
@override
|
||||
final int fipsApproved;
|
||||
@override
|
||||
final int resetBlocked;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DeviceInfo(config: $config, serial: $serial, version: $version, formFactor: $formFactor, supportedCapabilities: $supportedCapabilities, isLocked: $isLocked, isFips: $isFips, isSky: $isSky, pinComplexity: $pinComplexity, fipsCapable: $fipsCapable)';
|
||||
return 'DeviceInfo(config: $config, serial: $serial, version: $version, formFactor: $formFactor, supportedCapabilities: $supportedCapabilities, isLocked: $isLocked, isFips: $isFips, isSky: $isSky, pinComplexity: $pinComplexity, fipsCapable: $fipsCapable, fipsApproved: $fipsApproved, resetBlocked: $resetBlocked)';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -525,7 +558,11 @@ class _$DeviceInfoImpl implements _DeviceInfo {
|
||||
(identical(other.pinComplexity, pinComplexity) ||
|
||||
other.pinComplexity == pinComplexity) &&
|
||||
(identical(other.fipsCapable, fipsCapable) ||
|
||||
other.fipsCapable == fipsCapable));
|
||||
other.fipsCapable == fipsCapable) &&
|
||||
(identical(other.fipsApproved, fipsApproved) ||
|
||||
other.fipsApproved == fipsApproved) &&
|
||||
(identical(other.resetBlocked, resetBlocked) ||
|
||||
other.resetBlocked == resetBlocked));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@ -541,7 +578,9 @@ class _$DeviceInfoImpl implements _DeviceInfo {
|
||||
isFips,
|
||||
isSky,
|
||||
pinComplexity,
|
||||
fipsCapable);
|
||||
fipsCapable,
|
||||
fipsApproved,
|
||||
resetBlocked);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@ -557,7 +596,7 @@ class _$DeviceInfoImpl implements _DeviceInfo {
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _DeviceInfo implements DeviceInfo {
|
||||
abstract class _DeviceInfo extends DeviceInfo {
|
||||
factory _DeviceInfo(
|
||||
final DeviceConfig config,
|
||||
final int? serial,
|
||||
@ -568,7 +607,10 @@ abstract class _DeviceInfo implements DeviceInfo {
|
||||
final bool isFips,
|
||||
final bool isSky,
|
||||
final bool pinComplexity,
|
||||
final int fipsCapable) = _$DeviceInfoImpl;
|
||||
final int fipsCapable,
|
||||
final int fipsApproved,
|
||||
final int resetBlocked) = _$DeviceInfoImpl;
|
||||
_DeviceInfo._() : super._();
|
||||
|
||||
factory _DeviceInfo.fromJson(Map<String, dynamic> json) =
|
||||
_$DeviceInfoImpl.fromJson;
|
||||
@ -594,6 +636,10 @@ abstract class _DeviceInfo implements DeviceInfo {
|
||||
@override
|
||||
int get fipsCapable;
|
||||
@override
|
||||
int get fipsApproved;
|
||||
@override
|
||||
int get resetBlocked;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$DeviceInfoImplCopyWith<_$DeviceInfoImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
|
@ -46,6 +46,8 @@ _$DeviceInfoImpl _$$DeviceInfoImplFromJson(Map<String, dynamic> json) =>
|
||||
json['is_sky'] as bool,
|
||||
json['pin_complexity'] as bool,
|
||||
(json['fips_capable'] as num).toInt(),
|
||||
(json['fips_approved'] as num).toInt(),
|
||||
(json['reset_blocked'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$DeviceInfoImplToJson(_$DeviceInfoImpl instance) =>
|
||||
@ -61,6 +63,8 @@ Map<String, dynamic> _$$DeviceInfoImplToJson(_$DeviceInfoImpl instance) =>
|
||||
'is_sky': instance.isSky,
|
||||
'pin_complexity': instance.pinComplexity,
|
||||
'fips_capable': instance.fipsCapable,
|
||||
'fips_approved': instance.fipsApproved,
|
||||
'reset_blocked': instance.resetBlocked,
|
||||
};
|
||||
|
||||
const _$FormFactorEnumMap = {
|
||||
|
@ -258,3 +258,5 @@ class CredentialData with _$CredentialData {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
enum OathLayout { list, grid, mixed }
|
||||
|
@ -13,7 +13,7 @@ _$OathCredentialImpl _$$OathCredentialImplFromJson(Map<String, dynamic> json) =>
|
||||
const _IssuerConverter().fromJson(json['issuer'] as String?),
|
||||
json['name'] as String,
|
||||
$enumDecode(_$OathTypeEnumMap, json['oath_type']),
|
||||
json['period'] as int,
|
||||
(json['period'] as num).toInt(),
|
||||
json['touch_required'] as bool,
|
||||
);
|
||||
|
||||
@ -37,8 +37,8 @@ const _$OathTypeEnumMap = {
|
||||
_$OathCodeImpl _$$OathCodeImplFromJson(Map<String, dynamic> json) =>
|
||||
_$OathCodeImpl(
|
||||
json['value'] as String,
|
||||
json['valid_from'] as int,
|
||||
json['valid_to'] as int,
|
||||
(json['valid_from'] as num).toInt(),
|
||||
(json['valid_to'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$OathCodeImplToJson(_$OathCodeImpl instance) =>
|
||||
@ -98,9 +98,9 @@ _$CredentialDataImpl _$$CredentialDataImplFromJson(Map<String, dynamic> json) =>
|
||||
hashAlgorithm:
|
||||
$enumDecodeNullable(_$HashAlgorithmEnumMap, json['hash_algorithm']) ??
|
||||
defaultHashAlgorithm,
|
||||
digits: json['digits'] as int? ?? defaultDigits,
|
||||
period: json['period'] as int? ?? defaultPeriod,
|
||||
counter: json['counter'] as int? ?? defaultCounter,
|
||||
digits: (json['digits'] as num?)?.toInt() ?? defaultDigits,
|
||||
period: (json['period'] as num?)?.toInt() ?? defaultPeriod,
|
||||
counter: (json['counter'] as num?)?.toInt() ?? defaultCounter,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$CredentialDataImplToJson(
|
||||
|
@ -38,6 +38,53 @@ class AccountsSearchNotifier extends StateNotifier<String> {
|
||||
}
|
||||
}
|
||||
|
||||
final oathLayoutProvider =
|
||||
StateNotifierProvider.autoDispose<OathLayoutNotfier, OathLayout>((ref) {
|
||||
final device = ref.watch(currentDeviceProvider);
|
||||
List<OathPair> credentials = device != null
|
||||
? ref.read(filteredCredentialsProvider(
|
||||
ref.read(credentialListProvider(device.path)) ?? []))
|
||||
: [];
|
||||
final favorites = ref.watch(favoritesProvider);
|
||||
final pinnedCreds =
|
||||
credentials.where((entry) => favorites.contains(entry.credential.id));
|
||||
return OathLayoutNotfier('OATH_STATE_LAYOUT', ref.watch(prefProvider),
|
||||
credentials, pinnedCreds.toList());
|
||||
});
|
||||
|
||||
class OathLayoutNotfier extends StateNotifier<OathLayout> {
|
||||
final String _key;
|
||||
final SharedPreferences _prefs;
|
||||
OathLayoutNotfier(this._key, this._prefs, List<OathPair> credentials,
|
||||
List<OathPair> pinnedCredentials)
|
||||
: super(
|
||||
_fromName(_prefs.getString(_key), credentials, pinnedCredentials));
|
||||
|
||||
void setLayout(OathLayout layout) {
|
||||
state = layout;
|
||||
_prefs.setString(_key, layout.name);
|
||||
}
|
||||
|
||||
static OathLayout _fromName(String? name, List<OathPair> credentials,
|
||||
List<OathPair> pinnedCredentials) {
|
||||
final layout = OathLayout.values.firstWhere(
|
||||
(element) => element.name == name,
|
||||
orElse: () => OathLayout.list,
|
||||
);
|
||||
// Default to list view if current key does not have
|
||||
// pinned credentials
|
||||
if (layout == OathLayout.mixed) {
|
||||
if (pinnedCredentials.isEmpty) {
|
||||
return OathLayout.list;
|
||||
}
|
||||
if (pinnedCredentials.length == credentials.length) {
|
||||
return OathLayout.grid;
|
||||
}
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
}
|
||||
|
||||
final oathStateProvider = AsyncNotifierProvider.autoDispose
|
||||
.family<OathStateNotifier, OathState, DevicePath>(
|
||||
() => throw UnimplementedError(),
|
||||
|
@ -26,7 +26,6 @@ import '../../app/shortcuts.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../../widgets/circle_timer.dart';
|
||||
import '../../widgets/custom_icons.dart';
|
||||
import '../features.dart' as features;
|
||||
import '../keys.dart' as keys;
|
||||
import '../models.dart';
|
||||
@ -90,7 +89,7 @@ class AccountHelper {
|
||||
ActionItem(
|
||||
key: keys.togglePinAction,
|
||||
feature: features.accountsPin,
|
||||
icon: pinned ? pushPinStrokeIcon : const Icon(Symbols.push_pin),
|
||||
icon: Icon(pinned ? Symbols.keep_off : Symbols.keep),
|
||||
title: pinned ? l10n.s_unpin_account : l10n.s_pin_account,
|
||||
subtitle: l10n.l_pin_account_desc,
|
||||
intent: TogglePinIntent(credential),
|
||||
|
@ -18,6 +18,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../widgets/flex_box.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'account_view.dart';
|
||||
@ -32,6 +33,9 @@ class AccountList extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final theme = Theme.of(context);
|
||||
final labelStyle =
|
||||
theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.primary);
|
||||
final credentials = ref.watch(filteredCredentialsProvider(accounts));
|
||||
final favorites = ref.watch(favoritesProvider);
|
||||
if (credentials.isEmpty) {
|
||||
@ -45,32 +49,71 @@ class AccountList extends ConsumerWidget {
|
||||
final creds =
|
||||
credentials.where((entry) => !favorites.contains(entry.credential.id));
|
||||
|
||||
final oathLayout = ref.watch(oathLayoutProvider);
|
||||
final pinnedLayout =
|
||||
(oathLayout == OathLayout.grid || oathLayout == OathLayout.mixed)
|
||||
? FlexLayout.grid
|
||||
: FlexLayout.list;
|
||||
final normalLayout =
|
||||
oathLayout == OathLayout.grid ? FlexLayout.grid : FlexLayout.list;
|
||||
|
||||
return FocusTraversalGroup(
|
||||
policy: WidgetOrderTraversalPolicy(),
|
||||
child: Column(
|
||||
children: [
|
||||
...pinnedCreds.map(
|
||||
(entry) => AccountView(
|
||||
entry.credential,
|
||||
expanded: expanded,
|
||||
selected: entry.credential == selected,
|
||||
),
|
||||
),
|
||||
if (pinnedCreds.isNotEmpty && creds.isNotEmpty)
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (pinnedCreds.isNotEmpty) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 18, bottom: 8),
|
||||
child: Text(l10n.s_pinned, style: labelStyle),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
child: FlexBox<OathPair>(
|
||||
items: pinnedCreds.toList(),
|
||||
itemBuilder: (value) => AccountView(
|
||||
value.credential,
|
||||
expanded: expanded,
|
||||
selected: value.credential == selected,
|
||||
large: pinnedLayout == FlexLayout.grid,
|
||||
),
|
||||
cellMinWidth: 250,
|
||||
spacing: pinnedLayout == FlexLayout.grid ? 4.0 : 0.0,
|
||||
runSpacing: pinnedLayout == FlexLayout.grid ? 4.0 : 0.0,
|
||||
layout: pinnedLayout,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (pinnedCreds.isNotEmpty && creds.isNotEmpty) ...[
|
||||
const SizedBox(height: 24),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 18, bottom: 8),
|
||||
child: Text(
|
||||
l10n.s_accounts,
|
||||
style: labelStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Divider(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
child: FlexBox<OathPair>(
|
||||
items: creds.toList(),
|
||||
itemBuilder: (value) => AccountView(
|
||||
value.credential,
|
||||
expanded: expanded,
|
||||
selected: value.credential == selected,
|
||||
large: normalLayout == FlexLayout.grid,
|
||||
),
|
||||
cellMinWidth: 250,
|
||||
spacing: normalLayout == FlexLayout.grid ? 4.0 : 0.0,
|
||||
runSpacing: normalLayout == FlexLayout.grid ? 4.0 : 0.0,
|
||||
layout: normalLayout,
|
||||
),
|
||||
),
|
||||
...creds.map(
|
||||
(entry) => AccountView(
|
||||
entry.credential,
|
||||
expanded: expanded,
|
||||
selected: entry.credential == selected,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -30,8 +30,12 @@ class AccountView extends ConsumerStatefulWidget {
|
||||
final OathCredential credential;
|
||||
final bool expanded;
|
||||
final bool selected;
|
||||
final bool large;
|
||||
const AccountView(this.credential,
|
||||
{super.key, required this.expanded, required this.selected});
|
||||
{super.key,
|
||||
required this.expanded,
|
||||
required this.selected,
|
||||
this.large = false});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _AccountViewState();
|
||||
@ -116,6 +120,95 @@ class _AccountViewState extends ConsumerState<AccountView> {
|
||||
? CopyIntent<OathCredential>(credential)
|
||||
: null,
|
||||
buildPopupActions: (_) => helper.buildActions(),
|
||||
itemBuilder: widget.large
|
||||
? (context) {
|
||||
return ListTile(
|
||||
mouseCursor: !(isDesktop && !widget.expanded)
|
||||
? SystemMouseCursors.click
|
||||
: null,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16)),
|
||||
selectedTileColor:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
selectedColor:
|
||||
Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
selected: widget.selected,
|
||||
tileColor: Theme.of(context).hoverColor,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
title: Column(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AccountIcon(
|
||||
issuer: credential.issuer,
|
||||
defaultWidget: circleAvatar),
|
||||
const SizedBox(width: 12),
|
||||
Flexible(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
helper.title,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface),
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
),
|
||||
if (subtitle != null)
|
||||
Text(
|
||||
subtitle,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant),
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Focus(
|
||||
skipTraversal: true,
|
||||
descendantsAreTraversable: false,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
helper.code != null
|
||||
? FilledButton.tonalIcon(
|
||||
icon: helper.buildCodeIcon(),
|
||||
label: helper.buildCodeLabel(),
|
||||
style: buttonStyle,
|
||||
onPressed:
|
||||
Actions.handler(context, openIntent),
|
||||
)
|
||||
: FilledButton.tonal(
|
||||
style: buttonStyle,
|
||||
onPressed:
|
||||
Actions.handler(context, openIntent),
|
||||
child: helper.buildCodeIcon()),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import '../../app/message.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../app/views/action_list.dart';
|
||||
import '../../management/models.dart';
|
||||
import '../features.dart' as features;
|
||||
import '../icon_provider/icon_pack_dialog.dart';
|
||||
import '../keys.dart' as keys;
|
||||
@ -39,6 +40,28 @@ Widget oathBuildActions(
|
||||
}) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final capacity = oathState.capacity;
|
||||
final (fipsCapable, fipsApproved) = ref
|
||||
.watch(currentDeviceDataProvider)
|
||||
.valueOrNull
|
||||
?.info
|
||||
.getFipsStatus(Capability.oath) ??
|
||||
(false, false);
|
||||
|
||||
final String? subtitle;
|
||||
final bool enabled;
|
||||
if (used == null) {
|
||||
subtitle = l10n.l_unlock_first;
|
||||
enabled = false;
|
||||
} else if (fipsCapable & !fipsApproved) {
|
||||
subtitle = l10n.l_set_password_first;
|
||||
enabled = false;
|
||||
} else if (capacity != null) {
|
||||
subtitle = l10n.l_accounts_used(used, capacity);
|
||||
enabled = capacity > used;
|
||||
} else {
|
||||
subtitle = null;
|
||||
enabled = true;
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
@ -47,14 +70,10 @@ Widget oathBuildActions(
|
||||
feature: features.actionsAdd,
|
||||
key: keys.addAccountAction,
|
||||
title: l10n.s_add_account,
|
||||
subtitle: used == null
|
||||
? l10n.l_unlock_first
|
||||
: (capacity != null
|
||||
? l10n.l_accounts_used(used, capacity)
|
||||
: null),
|
||||
subtitle: subtitle,
|
||||
actionStyle: ActionStyle.primary,
|
||||
icon: const Icon(Symbols.person_add_alt),
|
||||
onTap: used != null && (capacity == null || capacity > used)
|
||||
onTap: enabled
|
||||
? (context) async {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
await addOathAccount(context, ref, devicePath, oathState);
|
||||
@ -82,7 +101,7 @@ Widget oathBuildActions(
|
||||
feature: features.actionsPassword,
|
||||
title:
|
||||
oathState.hasKey ? l10n.s_manage_password : l10n.s_set_password,
|
||||
subtitle: l10n.l_optional_password_protection,
|
||||
subtitle: l10n.l_password_protection,
|
||||
icon: const Icon(Symbols.password),
|
||||
onTap: (context) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022-2023 Yubico.
|
||||
* Copyright (C) 2022-2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -22,6 +22,7 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import '../../app/message.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../management/models.dart';
|
||||
import '../../widgets/app_input_decoration.dart';
|
||||
import '../../widgets/app_text_field.dart';
|
||||
import '../../widgets/focus_utils.dart';
|
||||
@ -33,6 +34,7 @@ import '../state.dart';
|
||||
class ManagePasswordDialog extends ConsumerStatefulWidget {
|
||||
final DevicePath path;
|
||||
final OathState state;
|
||||
|
||||
const ManagePasswordDialog(this.path, this.state, {super.key});
|
||||
|
||||
@override
|
||||
@ -43,6 +45,8 @@ class ManagePasswordDialog extends ConsumerStatefulWidget {
|
||||
class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
final _currentPasswordController = TextEditingController();
|
||||
final _currentPasswordFocus = FocusNode();
|
||||
final _newPasswordFocus = FocusNode();
|
||||
final _confirmPasswordFocus = FocusNode();
|
||||
String _newPassword = '';
|
||||
String _confirmPassword = '';
|
||||
bool _currentIsWrong = false;
|
||||
@ -54,6 +58,8 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
void dispose() {
|
||||
_currentPasswordController.dispose();
|
||||
_currentPasswordFocus.dispose();
|
||||
_newPasswordFocus.dispose();
|
||||
_confirmPasswordFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -82,12 +88,22 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final fipsCapable = ref.watch(currentDeviceDataProvider).maybeWhen(
|
||||
data: (data) => data.info.getFipsStatus(Capability.oath).$1,
|
||||
orElse: () => false);
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final isValid = !_currentIsWrong &&
|
||||
_newPassword.isNotEmpty &&
|
||||
_newPassword == _confirmPassword &&
|
||||
(!widget.state.hasKey || _currentPasswordController.text.isNotEmpty);
|
||||
|
||||
final newPasswordEnabled =
|
||||
!widget.state.hasKey || _currentPasswordController.text.isNotEmpty;
|
||||
|
||||
final confirmPasswordEnabled =
|
||||
(!widget.state.hasKey || _currentPasswordController.text.isNotEmpty) &&
|
||||
_newPassword.isNotEmpty;
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: Text(
|
||||
widget.state.hasKey ? l10n.s_manage_password : l10n.s_set_password),
|
||||
@ -137,42 +153,52 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
_currentIsWrong = false;
|
||||
});
|
||||
},
|
||||
onSubmitted: (_) {
|
||||
if (_currentPasswordController.text.isEmpty) {
|
||||
_currentPasswordFocus.requestFocus();
|
||||
} else {
|
||||
_newPasswordFocus.requestFocus();
|
||||
}
|
||||
},
|
||||
).init(),
|
||||
Wrap(
|
||||
spacing: 4.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
key: keys.removePasswordButton,
|
||||
onPressed: _currentPasswordController.text.isNotEmpty &&
|
||||
!_currentIsWrong
|
||||
? () async {
|
||||
final result = await ref
|
||||
.read(oathStateProvider(widget.path).notifier)
|
||||
.unsetPassword(_currentPasswordController.text);
|
||||
if (result) {
|
||||
if (mounted) {
|
||||
await ref.read(withContextProvider)(
|
||||
(context) async {
|
||||
Navigator.of(context).pop();
|
||||
showMessage(context, l10n.s_password_removed);
|
||||
if (!fipsCapable)
|
||||
OutlinedButton(
|
||||
key: keys.removePasswordButton,
|
||||
onPressed: _currentPasswordController.text.isNotEmpty &&
|
||||
!_currentIsWrong
|
||||
? () async {
|
||||
final result = await ref
|
||||
.read(oathStateProvider(widget.path).notifier)
|
||||
.unsetPassword(
|
||||
_currentPasswordController.text);
|
||||
if (result) {
|
||||
if (mounted) {
|
||||
await ref.read(withContextProvider)(
|
||||
(context) async {
|
||||
Navigator.of(context).pop();
|
||||
showMessage(
|
||||
context, l10n.s_password_removed);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
_currentPasswordController.selection =
|
||||
TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: _currentPasswordController
|
||||
.text.length);
|
||||
_currentPasswordFocus.requestFocus();
|
||||
setState(() {
|
||||
_currentIsWrong = true;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
_currentPasswordController.selection =
|
||||
TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: _currentPasswordController
|
||||
.text.length);
|
||||
_currentPasswordFocus.requestFocus();
|
||||
setState(() {
|
||||
_currentIsWrong = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: Text(l10n.s_remove_password),
|
||||
),
|
||||
: null,
|
||||
child: Text(l10n.s_remove_password),
|
||||
),
|
||||
if (widget.state.remembered)
|
||||
OutlinedButton(
|
||||
child: Text(l10n.s_clear_saved_password),
|
||||
@ -197,24 +223,27 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
autofocus: !widget.state.hasKey,
|
||||
obscureText: _isObscureNew,
|
||||
autofillHints: const [AutofillHints.newPassword],
|
||||
focusNode: _newPasswordFocus,
|
||||
decoration: AppInputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: l10n.s_new_password,
|
||||
prefixIcon: const Icon(Symbols.password),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(_isObscureNew
|
||||
? Symbols.visibility
|
||||
: Symbols.visibility_off),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isObscureNew = !_isObscureNew;
|
||||
});
|
||||
},
|
||||
tooltip: _isObscureNew
|
||||
? l10n.s_show_password
|
||||
: l10n.s_hide_password),
|
||||
enabled: !widget.state.hasKey ||
|
||||
_currentPasswordController.text.isNotEmpty,
|
||||
suffixIcon: ExcludeFocusTraversal(
|
||||
excluding: !newPasswordEnabled,
|
||||
child: IconButton(
|
||||
icon: Icon(_isObscureNew
|
||||
? Symbols.visibility
|
||||
: Symbols.visibility_off),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isObscureNew = !_isObscureNew;
|
||||
});
|
||||
},
|
||||
tooltip: _isObscureNew
|
||||
? l10n.s_show_password
|
||||
: l10n.s_hide_password),
|
||||
),
|
||||
enabled: newPasswordEnabled,
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
onChanged: (value) {
|
||||
@ -223,34 +252,38 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
});
|
||||
},
|
||||
onSubmitted: (_) {
|
||||
if (isValid) {
|
||||
_submit();
|
||||
if (_newPassword.isNotEmpty) {
|
||||
_confirmPasswordFocus.requestFocus();
|
||||
} else if (_newPassword.isEmpty) {
|
||||
_newPasswordFocus.requestFocus();
|
||||
}
|
||||
},
|
||||
).init(),
|
||||
AppTextField(
|
||||
key: keys.confirmPasswordField,
|
||||
obscureText: _isObscureConfirm,
|
||||
focusNode: _confirmPasswordFocus,
|
||||
autofillHints: const [AutofillHints.newPassword],
|
||||
decoration: AppInputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: l10n.s_confirm_password,
|
||||
prefixIcon: const Icon(Symbols.password),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(_isObscureConfirm
|
||||
? Symbols.visibility
|
||||
: Symbols.visibility_off),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isObscureConfirm = !_isObscureConfirm;
|
||||
});
|
||||
},
|
||||
tooltip: _isObscureConfirm
|
||||
? l10n.s_show_password
|
||||
: l10n.s_hide_password),
|
||||
enabled: (!widget.state.hasKey ||
|
||||
_currentPasswordController.text.isNotEmpty) &&
|
||||
_newPassword.isNotEmpty,
|
||||
suffixIcon: ExcludeFocusTraversal(
|
||||
excluding: !confirmPasswordEnabled,
|
||||
child: IconButton(
|
||||
icon: Icon(_isObscureConfirm
|
||||
? Symbols.visibility
|
||||
: Symbols.visibility_off),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isObscureConfirm = !_isObscureConfirm;
|
||||
});
|
||||
},
|
||||
tooltip: _isObscureConfirm
|
||||
? l10n.s_show_password
|
||||
: l10n.s_hide_password),
|
||||
),
|
||||
enabled: confirmPasswordEnabled,
|
||||
errorText: _newPassword.length == _confirmPassword.length &&
|
||||
_newPassword != _confirmPassword
|
||||
? l10n.l_password_mismatch
|
||||
@ -266,6 +299,8 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
onSubmitted: (_) {
|
||||
if (isValid) {
|
||||
_submit();
|
||||
} else {
|
||||
_confirmPasswordFocus.requestFocus();
|
||||
}
|
||||
},
|
||||
).init(),
|
||||
|
@ -53,6 +53,19 @@ import 'key_actions.dart';
|
||||
import 'unlock_form.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
extension on OathLayout {
|
||||
IconData get _icon => switch (this) {
|
||||
OathLayout.list => Symbols.list,
|
||||
OathLayout.grid => Symbols.grid_view,
|
||||
OathLayout.mixed => Symbols.vertical_split
|
||||
};
|
||||
String getDisplayName(AppLocalizations l10n) => switch (this) {
|
||||
OathLayout.list => l10n.s_list_layout,
|
||||
OathLayout.grid => l10n.s_grid_layout,
|
||||
OathLayout.mixed => l10n.s_mixed_layout
|
||||
};
|
||||
}
|
||||
|
||||
class OathScreen extends ConsumerWidget {
|
||||
final DevicePath devicePath;
|
||||
|
||||
@ -123,6 +136,7 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
||||
late FocusNode searchFocus;
|
||||
late TextEditingController searchController;
|
||||
OathCredential? _selected;
|
||||
bool _canRequestFocus = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -376,60 +390,171 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
child: Builder(builder: (context) {
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
final width = constraints.maxWidth;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
return Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: AppTextFormField(
|
||||
key: searchField,
|
||||
controller: searchController,
|
||||
focusNode: searchFocus,
|
||||
// Use the default style, but with a smaller font size:
|
||||
style: textTheme.titleMedium
|
||||
?.copyWith(fontSize: textTheme.titleSmall?.fontSize),
|
||||
decoration: AppInputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(48),
|
||||
borderSide: BorderSide(
|
||||
width: 0,
|
||||
style: searchFocus.hasFocus
|
||||
? BorderStyle.solid
|
||||
: BorderStyle.none,
|
||||
return Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final credentials = ref.watch(filteredCredentialsProvider(
|
||||
ref.watch(credentialListProvider(widget.devicePath)) ??
|
||||
[]));
|
||||
final favorites = ref.watch(favoritesProvider);
|
||||
final pinnedCreds = credentials
|
||||
.where((entry) => favorites.contains(entry.credential.id));
|
||||
|
||||
final availableLayouts = pinnedCreds.isEmpty ||
|
||||
pinnedCreds.length == credentials.length
|
||||
? OathLayout.values
|
||||
.where((element) => element != OathLayout.mixed)
|
||||
: OathLayout.values;
|
||||
final oathLayout = ref.watch(oathLayoutProvider);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10.0, vertical: 8.0),
|
||||
child: AppTextFormField(
|
||||
key: searchField,
|
||||
controller: searchController,
|
||||
canRequestFocus: _canRequestFocus,
|
||||
focusNode: searchFocus,
|
||||
// Use the default style, but with a smaller font size:
|
||||
style: textTheme.titleMedium
|
||||
?.copyWith(fontSize: textTheme.titleSmall?.fontSize),
|
||||
decoration: AppInputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(48),
|
||||
borderSide: BorderSide(
|
||||
width: 0,
|
||||
style: searchFocus.hasFocus
|
||||
? BorderStyle.solid
|
||||
: BorderStyle.none,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
fillColor: Theme.of(context).hoverColor,
|
||||
filled: true,
|
||||
hintText: l10n.s_search_accounts,
|
||||
isDense: true,
|
||||
prefixIcon: const Padding(
|
||||
padding: EdgeInsetsDirectional.only(start: 8.0),
|
||||
child: Icon(Icons.search_outlined),
|
||||
),
|
||||
suffixIcons: [
|
||||
if (searchController.text.isNotEmpty)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
iconSize: 16,
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
ref
|
||||
.read(accountsSearchProvider.notifier)
|
||||
.setFilter('');
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
if (searchController.text.isEmpty) ...[
|
||||
if (width >= 450)
|
||||
...availableLayouts.map(
|
||||
(e) => MouseRegion(
|
||||
onEnter: (event) {
|
||||
if (!searchFocus.hasFocus) {
|
||||
setState(() {
|
||||
_canRequestFocus = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
onExit: (event) {
|
||||
setState(() {
|
||||
_canRequestFocus = true;
|
||||
});
|
||||
},
|
||||
child: IconButton(
|
||||
tooltip: e.getDisplayName(l10n),
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(oathLayoutProvider.notifier)
|
||||
.setLayout(e);
|
||||
},
|
||||
icon: Icon(
|
||||
e._icon,
|
||||
color: e == oathLayout
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (width < 450)
|
||||
MouseRegion(
|
||||
onEnter: (event) {
|
||||
if (!searchFocus.hasFocus) {
|
||||
setState(() {
|
||||
_canRequestFocus = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
onExit: (event) {
|
||||
setState(() {
|
||||
_canRequestFocus = true;
|
||||
});
|
||||
},
|
||||
child: PopupMenuButton(
|
||||
constraints: const BoxConstraints.tightFor(),
|
||||
tooltip: 'Select layout',
|
||||
popUpAnimationStyle:
|
||||
AnimationStyle(duration: Duration.zero),
|
||||
icon: Icon(
|
||||
oathLayout._icon,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
itemBuilder: (context) => [
|
||||
...availableLayouts.map(
|
||||
(e) => PopupMenuItem(
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
Tooltip(
|
||||
message: e.getDisplayName(l10n),
|
||||
child: Icon(
|
||||
e._icon,
|
||||
color: e == oathLayout
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
ref
|
||||
.read(oathLayoutProvider.notifier)
|
||||
.setLayout(e);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
fillColor: Theme.of(context).hoverColor,
|
||||
filled: true,
|
||||
hintText: l10n.s_search_accounts,
|
||||
isDense: true,
|
||||
prefixIcon: const Padding(
|
||||
padding: EdgeInsetsDirectional.only(start: 8.0),
|
||||
child: Icon(Icons.search_outlined),
|
||||
),
|
||||
suffixIcon: searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
iconSize: 16,
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
ref
|
||||
.read(accountsSearchProvider.notifier)
|
||||
.setFilter('');
|
||||
setState(() {});
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
onChanged: (value) {
|
||||
ref.read(accountsSearchProvider.notifier).setFilter(value);
|
||||
setState(() {});
|
||||
},
|
||||
textInputAction: TextInputAction.next,
|
||||
onFieldSubmitted: (value) {
|
||||
Focus.of(context).focusInDirection(TraversalDirection.down);
|
||||
},
|
||||
).init(),
|
||||
|
||||
onChanged: (value) {
|
||||
ref
|
||||
.read(accountsSearchProvider.notifier)
|
||||
.setFilter(value);
|
||||
setState(() {});
|
||||
},
|
||||
textInputAction: TextInputAction.next,
|
||||
onFieldSubmitted: (value) {
|
||||
Focus.of(context)
|
||||
.focusInDirection(TraversalDirection.down);
|
||||
},
|
||||
).init(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
@ -180,13 +180,17 @@ class _OtpScreenState extends ConsumerState<OtpScreen> {
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
child: Column(children: [
|
||||
...otpState.slots.map((e) => _SlotListItem(
|
||||
e,
|
||||
expanded: expanded,
|
||||
selected: e == selected,
|
||||
))
|
||||
]),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
child: Column(children: [
|
||||
...otpState.slots.map((e) => _SlotListItem(
|
||||
e,
|
||||
expanded: expanded,
|
||||
selected: e == selected,
|
||||
))
|
||||
]),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -9,8 +9,8 @@ part of 'models.dart';
|
||||
_$PinMetadataImpl _$$PinMetadataImplFromJson(Map<String, dynamic> json) =>
|
||||
_$PinMetadataImpl(
|
||||
json['default_value'] as bool,
|
||||
json['total_attempts'] as int,
|
||||
json['attempts_remaining'] as int,
|
||||
(json['total_attempts'] as num).toInt(),
|
||||
(json['attempts_remaining'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$PinMetadataImplToJson(_$PinMetadataImpl instance) =>
|
||||
@ -113,7 +113,7 @@ _$PivStateImpl _$$PivStateImplFromJson(Map<String, dynamic> json) =>
|
||||
authenticated: json['authenticated'] as bool,
|
||||
derivedKey: json['derived_key'] as bool,
|
||||
storedKey: json['stored_key'] as bool,
|
||||
pinAttempts: json['pin_attempts'] as int,
|
||||
pinAttempts: (json['pin_attempts'] as num).toInt(),
|
||||
chuid: json['chuid'] as String?,
|
||||
ccc: json['ccc'] as String?,
|
||||
metadata: json['metadata'] == null
|
||||
@ -157,7 +157,7 @@ Map<String, dynamic> _$$CertInfoImplToJson(_$CertInfoImpl instance) =>
|
||||
|
||||
_$PivSlotImpl _$$PivSlotImplFromJson(Map<String, dynamic> json) =>
|
||||
_$PivSlotImpl(
|
||||
slot: SlotId.fromJson(json['slot'] as int),
|
||||
slot: SlotId.fromJson((json['slot'] as num).toInt()),
|
||||
metadata: json['metadata'] == null
|
||||
? null
|
||||
: SlotMetadata.fromJson(json['metadata'] as Map<String, dynamic>),
|
||||
|
@ -310,7 +310,16 @@ class PivActions extends ConsumerWidget {
|
||||
}
|
||||
|
||||
List<ActionItem> buildSlotActions(
|
||||
PivState pivState, PivSlot slot, AppLocalizations l10n) {
|
||||
PivState pivState, PivSlot slot, bool fipsUnready, AppLocalizations l10n) {
|
||||
if (fipsUnready) {
|
||||
// TODO: Decide on final look and move strings to .arb file.
|
||||
return [
|
||||
ActionItem(
|
||||
icon: const Icon(Symbols.add),
|
||||
title: 'Provision slot',
|
||||
subtitle: 'Change from default PIN/PUK/Management key first'),
|
||||
];
|
||||
}
|
||||
final hasCert = slot.certInfo != null;
|
||||
final hasKey = slot.metadata != null;
|
||||
final canDeleteOrMoveKey = hasKey && pivState.version.isAtLeast(5, 7);
|
||||
|
@ -25,6 +25,7 @@ import '../../app/message.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../core/models.dart';
|
||||
import '../../management/models.dart';
|
||||
import '../../widgets/app_input_decoration.dart';
|
||||
import '../../widgets/app_text_field.dart';
|
||||
import '../../widgets/app_text_form_field.dart';
|
||||
@ -176,6 +177,17 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
? currentKeyOrPin.length >= 4
|
||||
: currentKeyOrPin.length == currentType.keyLength * 2;
|
||||
final newLenOk = _keyController.text.length == hexLength;
|
||||
final (fipsCapable, fipsApproved) = ref
|
||||
.watch(currentDeviceDataProvider)
|
||||
.valueOrNull
|
||||
?.info
|
||||
.getFipsStatus(Capability.piv) ??
|
||||
(false, false);
|
||||
final fipsUnready = fipsCapable && !fipsApproved;
|
||||
final managementKeyTypes = ManagementKeyType.values.toList();
|
||||
if (fipsCapable) {
|
||||
managementKeyTypes.remove(ManagementKeyType.tdes);
|
||||
}
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: Text(l10n.l_change_management_key),
|
||||
@ -334,7 +346,7 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
children: [
|
||||
if (widget.pivState.metadata != null)
|
||||
ChoiceFilterChip<ManagementKeyType>(
|
||||
items: ManagementKeyType.values,
|
||||
items: managementKeyTypes,
|
||||
value: _keyType,
|
||||
selected: _keyType != currentType,
|
||||
itemBuilder: (value) => Text(value.getDisplayName(l10n)),
|
||||
@ -344,16 +356,17 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||
});
|
||||
},
|
||||
),
|
||||
FilterChip(
|
||||
key: keys.pinLockManagementKeyChip,
|
||||
label: Text(l10n.s_protect_key),
|
||||
selected: _storeKey,
|
||||
onSelected: (value) {
|
||||
setState(() {
|
||||
_storeKey = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (!fipsUnready)
|
||||
FilterChip(
|
||||
key: keys.pinLockManagementKeyChip,
|
||||
label: Text(l10n.s_protect_key),
|
||||
selected: _storeKey,
|
||||
onSelected: (value) {
|
||||
setState(() {
|
||||
_storeKey = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
]),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
|
@ -165,8 +165,8 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||
final isBio = [FormFactor.usbABio, FormFactor.usbCBio]
|
||||
.contains(deviceData?.info.formFactor);
|
||||
|
||||
final fipsCapable = deviceData?.info.fipsCapable ?? 0;
|
||||
final isFipsCapable = fipsCapable & Capability.piv.value != 0;
|
||||
final isFipsCapable =
|
||||
deviceData?.info.getFipsStatus(Capability.piv).$1 ?? false;
|
||||
|
||||
// Old YubiKeys allowed a 4 digit PIN
|
||||
final currentMinPinLen = isFipsCapable
|
||||
|
@ -24,6 +24,7 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import '../../app/message.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/shortcuts.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../app/views/action_list.dart';
|
||||
import '../../app/views/app_failure_page.dart';
|
||||
import '../../app/views/app_list_item.dart';
|
||||
@ -57,6 +58,14 @@ class _PivScreenState extends ConsumerState<PivScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final hasFeature = ref.watch(featureProvider);
|
||||
final (fipsCapable, fipsApproved) = ref
|
||||
.watch(currentDeviceDataProvider)
|
||||
.valueOrNull
|
||||
?.info
|
||||
.getFipsStatus(Capability.piv) ??
|
||||
(false, false);
|
||||
final fipsUnready = fipsCapable && !fipsApproved;
|
||||
|
||||
return ref.watch(pivStateProvider(widget.devicePath)).when(
|
||||
loading: () => MessagePage(
|
||||
title: l10n.s_certificates,
|
||||
@ -168,8 +177,8 @@ class _PivScreenState extends ConsumerState<PivScreen> {
|
||||
ActionListSection.fromMenuActions(
|
||||
context,
|
||||
l10n.s_actions,
|
||||
actions:
|
||||
buildSlotActions(pivState, selected, l10n),
|
||||
actions: buildSlotActions(
|
||||
pivState, selected, fipsUnready, l10n),
|
||||
),
|
||||
],
|
||||
)
|
||||
@ -200,25 +209,30 @@ class _PivScreenState extends ConsumerState<PivScreen> {
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
...normalSlots.map(
|
||||
(e) => _CertificateListItem(
|
||||
pivState,
|
||||
e,
|
||||
expanded: expanded,
|
||||
selected: e == selected,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
child: Column(
|
||||
children: [
|
||||
...normalSlots.map(
|
||||
(e) => _CertificateListItem(
|
||||
pivState,
|
||||
e,
|
||||
expanded: expanded,
|
||||
selected: e == selected,
|
||||
fipsUnready: fipsUnready,
|
||||
),
|
||||
),
|
||||
),
|
||||
...shownRetiredSlots.map(
|
||||
(e) => _CertificateListItem(
|
||||
pivState,
|
||||
e,
|
||||
expanded: expanded,
|
||||
selected: e == selected,
|
||||
),
|
||||
)
|
||||
],
|
||||
...shownRetiredSlots.map(
|
||||
(e) => _CertificateListItem(
|
||||
pivState,
|
||||
e,
|
||||
expanded: expanded,
|
||||
selected: e == selected,
|
||||
fipsUnready: fipsUnready,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -235,9 +249,12 @@ class _CertificateListItem extends ConsumerWidget {
|
||||
final PivSlot pivSlot;
|
||||
final bool expanded;
|
||||
final bool selected;
|
||||
final bool fipsUnready;
|
||||
|
||||
const _CertificateListItem(this.pivState, this.pivSlot,
|
||||
{required this.expanded, required this.selected});
|
||||
{required this.expanded,
|
||||
required this.selected,
|
||||
required this.fipsUnready});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@ -272,8 +289,8 @@ class _CertificateListItem extends ConsumerWidget {
|
||||
),
|
||||
tapIntent: isDesktop && !expanded ? null : OpenIntent(pivSlot),
|
||||
doubleTapIntent: isDesktop && !expanded ? OpenIntent(pivSlot) : null,
|
||||
buildPopupActions: hasFeature(features.slots)
|
||||
? (context) => buildSlotActions(pivState, pivSlot, l10n)
|
||||
buildPopupActions: hasFeature(features.slots) && !fipsUnready
|
||||
? (context) => buildSlotActions(pivState, pivSlot, fipsUnready, l10n)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import '../../app/shortcuts.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../app/views/action_list.dart';
|
||||
import '../../app/views/fs_dialog.dart';
|
||||
import '../../management/models.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'actions.dart';
|
||||
@ -34,12 +35,13 @@ class SlotDialog extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// TODO: Solve this in a cleaner way
|
||||
final node = ref.watch(currentDeviceDataProvider).valueOrNull?.node;
|
||||
if (node == null) {
|
||||
var keyData = ref.watch(currentDeviceDataProvider).valueOrNull;
|
||||
if (keyData == null) {
|
||||
// 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();
|
||||
}
|
||||
final devicePath = keyData.node.path;
|
||||
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
@ -48,8 +50,11 @@ class SlotDialog extends ConsumerWidget {
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
);
|
||||
|
||||
final pivState = ref.watch(pivStateProvider(node.path)).valueOrNull;
|
||||
final slotData = ref.watch(pivSlotsProvider(node.path).select((value) =>
|
||||
final (fipsCapable, fipsApproved) =
|
||||
keyData.info.getFipsStatus(Capability.piv);
|
||||
|
||||
final pivState = ref.watch(pivStateProvider(devicePath)).valueOrNull;
|
||||
final slotData = ref.watch(pivSlotsProvider(devicePath).select((value) =>
|
||||
value.whenOrNull(
|
||||
data: (data) =>
|
||||
data.firstWhere((element) => element.slot == pivSlot))));
|
||||
@ -61,7 +66,7 @@ class SlotDialog extends ConsumerWidget {
|
||||
final certInfo = slotData.certInfo;
|
||||
final metadata = slotData.metadata;
|
||||
return PivActions(
|
||||
devicePath: node.path,
|
||||
devicePath: devicePath,
|
||||
pivState: pivState,
|
||||
builder: (context) => ItemShortcuts(
|
||||
item: slotData,
|
||||
@ -113,7 +118,8 @@ class SlotDialog extends ConsumerWidget {
|
||||
ActionListSection.fromMenuActions(
|
||||
context,
|
||||
l10n.s_actions,
|
||||
actions: buildSlotActions(pivState, slotData, l10n),
|
||||
actions: buildSlotActions(
|
||||
pivState, slotData, fipsCapable && !fipsApproved, l10n),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
final Widget pushPinStrokeIcon = Builder(builder: (context) {
|
||||
return CustomPaint(
|
||||
painter: _StrikethroughPainter(IconTheme.of(context).color ?? Colors.black),
|
||||
child: ClipPath(
|
||||
clipper: _StrikethroughClipper(), child: const Icon(Symbols.push_pin)),
|
||||
);
|
||||
});
|
||||
|
||||
class _StrikethroughClipper extends CustomClipper<Path> {
|
||||
@override
|
||||
Path getClip(Size size) {
|
||||
Path path = Path()
|
||||
..moveTo(0, 2)
|
||||
..lineTo(0, size.height)
|
||||
..lineTo(size.width - 2, size.height)
|
||||
..lineTo(0, 2)
|
||||
..moveTo(2, 0)
|
||||
..lineTo(size.width, size.height - 2)
|
||||
..lineTo(size.width, 0)
|
||||
..lineTo(2, 0)
|
||||
..close();
|
||||
return path;
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class _StrikethroughPainter extends CustomPainter {
|
||||
final Color color;
|
||||
_StrikethroughPainter(this.color);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..strokeWidth = 1.3;
|
||||
|
||||
canvas.drawLine(Offset(size.width * 0.15, size.height * 0.15),
|
||||
Offset(size.width * 0.8, size.height * 0.8), paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
||||
return false;
|
||||
}
|
||||
}
|
111
lib/widgets/flex_box.dart
Normal file
111
lib/widgets/flex_box.dart
Normal file
@ -0,0 +1,111 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
enum FlexLayout {
|
||||
list,
|
||||
grid;
|
||||
|
||||
IconData get icon => switch (this) {
|
||||
FlexLayout.list => Symbols.list,
|
||||
FlexLayout.grid => Symbols.grid_view
|
||||
};
|
||||
String getDisplayName(AppLocalizations l10n) => switch (this) {
|
||||
FlexLayout.list => l10n.s_list_layout,
|
||||
FlexLayout.grid => l10n.s_grid_layout
|
||||
};
|
||||
}
|
||||
|
||||
class FlexBox<T> extends StatelessWidget {
|
||||
final List<T> items;
|
||||
final Widget Function(T value) itemBuilder;
|
||||
final FlexLayout layout;
|
||||
final double cellMinWidth;
|
||||
final double spacing;
|
||||
final double runSpacing;
|
||||
const FlexBox({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.itemBuilder,
|
||||
required this.cellMinWidth,
|
||||
this.layout = FlexLayout.list,
|
||||
this.spacing = 0.0,
|
||||
this.runSpacing = 0.0,
|
||||
});
|
||||
|
||||
int _getItemsPerRow(double width) {
|
||||
// Calculate the maximum number of cells that can fit in one row
|
||||
int cellsPerRow = (width / (cellMinWidth + spacing)).floor();
|
||||
|
||||
// Ensure there's at least one cell per row
|
||||
if (cellsPerRow < 1) {
|
||||
cellsPerRow = 1;
|
||||
}
|
||||
|
||||
// Calculate the total width needed for the calculated number of cells and spacing
|
||||
double totalWidthNeeded =
|
||||
cellsPerRow * cellMinWidth + (cellsPerRow - 1) * spacing;
|
||||
|
||||
// Adjust the number of cells per row if the calculated total width exceeds the available width
|
||||
if (totalWidthNeeded > width) {
|
||||
cellsPerRow = cellsPerRow - 1 > 0 ? cellsPerRow - 1 : 1;
|
||||
}
|
||||
|
||||
return cellsPerRow;
|
||||
}
|
||||
|
||||
List<List<T>> getChunks(int itemsPerChunk) {
|
||||
List<List<T>> chunks = [];
|
||||
final numChunks = (items.length / itemsPerChunk).ceil();
|
||||
for (int i = 0; i < numChunks; i++) {
|
||||
final index = i * itemsPerChunk;
|
||||
int endIndex = index + itemsPerChunk;
|
||||
|
||||
if (endIndex > items.length) {
|
||||
endIndex = items.length;
|
||||
}
|
||||
|
||||
chunks.add(items.sublist(index, endIndex));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final width = constraints.maxWidth;
|
||||
final itemsPerRow =
|
||||
layout == FlexLayout.grid ? _getItemsPerRow(width) : 1;
|
||||
final chunks = getChunks(itemsPerRow);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
for (final c in chunks) ...[
|
||||
if (chunks.indexOf(c) > 0) SizedBox(height: runSpacing),
|
||||
Row(
|
||||
children: [
|
||||
for (final entry in c) ...[
|
||||
Flexible(
|
||||
child: itemBuilder(entry),
|
||||
),
|
||||
if (itemsPerRow != 1 && c.indexOf(entry) != c.length - 1)
|
||||
SizedBox(width: spacing),
|
||||
],
|
||||
if (c.length < itemsPerRow) ...[
|
||||
// Prevents resizing when an item is removed
|
||||
SizedBox(width: 8 * (itemsPerRow - c.length).toDouble()),
|
||||
Spacer(
|
||||
flex: itemsPerRow - c.length,
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
]
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -102,7 +102,7 @@ class _ResponsiveDialogState extends State<ResponsiveDialog> {
|
||||
...widget.actions
|
||||
],
|
||||
),
|
||||
onPopInvoked: (didPop) {
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (didPop) {
|
||||
widget.onCancel?.call();
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class Toast extends StatefulWidget {
|
||||
final String message;
|
||||
@ -149,5 +150,7 @@ void Function() showToast(
|
||||
Overlay.of(context).insert(entry!);
|
||||
});
|
||||
|
||||
SemanticsService.announce(message, TextDirection.ltr);
|
||||
|
||||
return close;
|
||||
}
|
||||
|
@ -63,4 +63,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7
|
||||
|
||||
COCOAPODS: 1.14.3
|
||||
COCOAPODS: 1.15.2
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Cocoa
|
||||
import FlutterMacOS
|
||||
|
||||
@NSApplicationMain
|
||||
@main
|
||||
class AppDelegate: FlutterAppDelegate {
|
||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
// Keep app running if window closes
|
||||
|
12
macos_assemble.patch
Normal file
12
macos_assemble.patch
Normal file
@ -0,0 +1,12 @@
|
||||
diff --git a/packages/flutter_tools/bin/macos_assemble.sh b/packages/flutter_tools/bin/macos_assemble.sh
|
||||
index 40c6a5051f..a7f05d9113 100755
|
||||
--- a/packages/flutter_tools/bin/macos_assemble.sh
|
||||
+++ b/packages/flutter_tools/bin/macos_assemble.sh
|
||||
@@ -222,6 +222,7 @@ EmbedFrameworks() {
|
||||
|
||||
# Iterate through all .frameworks in native assets directory.
|
||||
for native_asset in "${native_assets_path}"*.framework; do
|
||||
+ [ -e "$native_asset" ] || continue # Skip when there are no matches.
|
||||
# Codesign the framework inside the app bundle.
|
||||
RunCommand codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${xcode_frameworks_dir}/$(basename "$native_asset")"
|
||||
done
|
179
pubspec.lock
179
pubspec.lock
@ -5,18 +5,23 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
|
||||
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "67.0.0"
|
||||
version: "72.0.0"
|
||||
_macros:
|
||||
dependency: transitive
|
||||
description: dart
|
||||
source: sdk
|
||||
version: "0.3.2"
|
||||
analyzer:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
|
||||
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.4.1"
|
||||
version: "6.7.0"
|
||||
analyzer_plugin:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -101,18 +106,18 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7"
|
||||
sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.11"
|
||||
version: "2.4.12"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe
|
||||
sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.3.1"
|
||||
version: "7.3.2"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -197,18 +202,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32"
|
||||
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.4+1"
|
||||
version: "0.3.4+2"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||
sha256: "1dceb0cf05cb63a7852c11560060e53ec2f182079a16ced6f4395c5b0875baf8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
version: "3.0.4"
|
||||
custom_lint:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -261,10 +266,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
|
||||
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
version: "2.1.3"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -277,10 +282,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: "824f5b9f389bfc4dddac3dea76cd70c51092d9dff0b2ece7ef4f53db8547d258"
|
||||
sha256: "1375f8685ca6f0412effecc2db834757e9d0e3e055468053e563794b0755cdcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.0.6"
|
||||
version: "8.1.1"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -316,10 +321,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e
|
||||
sha256: "9d98bd47ef9d34e803d438f17fd32b116d31009f534a6fa5ce3a1167f189a6de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.20"
|
||||
version: "2.0.21"
|
||||
flutter_riverpod:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -342,18 +347,18 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: freezed
|
||||
sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1
|
||||
sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.2"
|
||||
version: "2.5.7"
|
||||
freezed_annotation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: freezed_annotation
|
||||
sha256: f54946fdb1fa7b01f780841937b1a80783a20b393485f3f6cdf336fd6f4705f2
|
||||
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
version: "2.4.4"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -379,10 +384,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: graphs
|
||||
sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19
|
||||
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
version: "2.3.2"
|
||||
hotreloader:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -395,10 +400,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
|
||||
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
version: "1.2.2"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -464,18 +469,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
|
||||
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.4"
|
||||
version: "10.0.5"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
|
||||
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
version: "3.0.5"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -515,6 +520,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
macros:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: macros
|
||||
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.2-main.4"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -527,18 +540,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.0"
|
||||
version: "0.11.1"
|
||||
material_symbols_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: material_symbols_icons
|
||||
sha256: a2c78726048c755f0f90fd2b7c8799cd94338e2e9b7ab6498ae56503262c14bc
|
||||
sha256: "8f4abdb6bc714526ccf66e825b7391d7ca65239484ad92be71980fe73a57521c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2762.0"
|
||||
version: "4.2780.0"
|
||||
menu_base:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -551,10 +564,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
|
||||
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.0"
|
||||
version: "1.15.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -591,18 +604,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161
|
||||
sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.1.4"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: bca87b0165ffd7cdb9cad8edd22d18d2201e886d9a9f19b4fb3452ea7df3a72a
|
||||
sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.6"
|
||||
version: "2.2.10"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -631,10 +644,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
version: "2.3.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -647,10 +660,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
|
||||
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.4"
|
||||
version: "3.1.5"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -726,58 +739,58 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
|
||||
sha256: c272f9cabca5a81adc9b0894381e9c1def363e980f960fa903c604c471b22f68
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
version: "2.3.1"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577"
|
||||
sha256: a7e8467e9181cef109f601e3f65765685786c1a738a83d7fbbde377589c0d974
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
version: "2.3.1"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7"
|
||||
sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
version: "2.5.2"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
|
||||
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
version: "2.4.1"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
|
||||
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
version: "2.4.1"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a"
|
||||
sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.4.2"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
|
||||
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
version: "2.4.1"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -899,10 +912,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
|
||||
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
version: "0.7.2"
|
||||
test_res:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -946,26 +959,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf
|
||||
sha256: f0c73347dfcfa5b3db8bc06e1502668265d39c08f310c29bff4e28eea9699f79
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.3"
|
||||
version: "6.3.9"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89"
|
||||
sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
version: "6.3.1"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811
|
||||
sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
version: "3.2.0"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -986,26 +999,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a"
|
||||
sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
version: "2.3.3"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
|
||||
sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
version: "3.1.2"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8"
|
||||
sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.4.0"
|
||||
version: "4.4.2"
|
||||
vector_graphics:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1042,10 +1055,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
|
||||
sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.2.1"
|
||||
version: "14.2.4"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1058,26 +1071,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
|
||||
sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.1"
|
||||
version: "1.0.0"
|
||||
web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket
|
||||
sha256: "24301d8c293ce6fe327ffe6f59d8fd8834735f0ec36e4fd383ec7ff8a64aa078"
|
||||
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.5"
|
||||
version: "0.1.6"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: a2d56211ee4d35d9b344d9d4ce60f362e4f5d1aafb988302906bd732bc731276
|
||||
sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "3.0.1"
|
||||
webdriver:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1090,10 +1103,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
|
||||
sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.5.1"
|
||||
version: "5.5.4"
|
||||
window_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1128,5 +1141,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.4.3 <4.0.0"
|
||||
dart: ">=3.5.0-259.0.dev <4.0.0"
|
||||
flutter: ">=3.22.0"
|
||||
|
@ -70,7 +70,7 @@ dependencies:
|
||||
io: ^1.0.4
|
||||
base32: ^2.1.3
|
||||
convert: ^3.1.1
|
||||
material_symbols_icons: ^4.2719.3
|
||||
material_symbols_icons: ^4.2741.0
|
||||
|
||||
dev_dependencies:
|
||||
integration_test:
|
||||
|
Loading…
Reference in New Issue
Block a user