mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-23 09:56:23 +03:00
Merge PR #196.
This commit is contained in:
commit
a6e4f6f8e2
2
.github/workflows/android.yaml
vendored
2
.github/workflows/android.yaml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version: '3.3.0'
|
||||
flutter-version: '3.0.5'
|
||||
- run: |
|
||||
flutter config
|
||||
flutter --version
|
||||
|
2
.github/workflows/linux.yml
vendored
2
.github/workflows/linux.yml
vendored
@ -33,7 +33,7 @@ jobs:
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version: '3.3.0'
|
||||
flutter-version: '3.0.5'
|
||||
- run: flutter config --enable-linux-desktop
|
||||
- run: flutter --version
|
||||
|
||||
|
2
.github/workflows/macos.yml
vendored
2
.github/workflows/macos.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
||||
with:
|
||||
channel: 'stable'
|
||||
architecture: 'x64'
|
||||
flutter-version: '3.3.0'
|
||||
flutter-version: '3.0.5'
|
||||
- run: flutter config --enable-macos-desktop
|
||||
- run: flutter --version
|
||||
|
||||
|
2
.github/workflows/windows.yml
vendored
2
.github/workflows/windows.yml
vendored
@ -29,7 +29,7 @@ jobs:
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version: '3.3.0'
|
||||
flutter-version: '3.0.5'
|
||||
- run: flutter config --enable-windows-desktop
|
||||
- run: flutter --version
|
||||
|
||||
|
@ -14,6 +14,7 @@ import 'core/state.dart';
|
||||
import 'desktop/state.dart';
|
||||
import 'version.dart';
|
||||
import 'widgets/responsive_dialog.dart';
|
||||
import 'widgets/choice_filter_chip.dart';
|
||||
|
||||
final _log = Logger('about');
|
||||
|
||||
@ -141,8 +142,8 @@ class AboutPage extends ConsumerWidget {
|
||||
const LoggingPanel(),
|
||||
if (isDesktop) ...[
|
||||
const SizedBox(height: 12.0),
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(Icons.bug_report_outlined),
|
||||
ActionChip(
|
||||
avatar: const Icon(Icons.bug_report_outlined),
|
||||
label: const Text('Run diagnostics'),
|
||||
onPressed: () async {
|
||||
_log.info('Running diagnostics...');
|
||||
@ -174,31 +175,33 @@ class LoggingPanel extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Column(
|
||||
final logLevel = ref.watch(logLevelProvider);
|
||||
return Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 4.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
const SizedBox(height: 12.0),
|
||||
DropdownButtonFormField<Level>(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Log level',
|
||||
border: OutlineInputBorder(),
|
||||
ChoiceFilterChip<Level>(
|
||||
avatar: Icon(
|
||||
Icons.insights,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
value: ref.watch(logLevelProvider),
|
||||
items: Levels.LEVELS
|
||||
.map((e) => DropdownMenuItem(
|
||||
value: e,
|
||||
child: Text(e.name.toUpperCase()),
|
||||
))
|
||||
.toList(),
|
||||
value: logLevel,
|
||||
items: Levels.LEVELS,
|
||||
selected: logLevel != Level.INFO,
|
||||
labelBuilder: (value) => Text(
|
||||
'Log level: ${value.name[0]}${value.name.substring(1).toLowerCase()}'),
|
||||
itemBuilder: (value) =>
|
||||
Text('${value.name[0]}${value.name.substring(1).toLowerCase()}'),
|
||||
onChanged: (level) {
|
||||
ref.read(logLevelProvider.notifier).setLogLevel(level!);
|
||||
ref.read(logLevelProvider.notifier).setLogLevel(level);
|
||||
_log.debug('Log level set to $level');
|
||||
showMessage(context, 'Log level set to $level');
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12.0),
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(Icons.copy),
|
||||
label: const Text('Copy log to clipboard'),
|
||||
ActionChip(
|
||||
avatar: const Icon(Icons.copy),
|
||||
label: const Text('Copy log'),
|
||||
onPressed: () async {
|
||||
_log.info('Copying log to clipboard ($version)...');
|
||||
final logs = await ref.read(logLevelProvider.notifier).getLogs();
|
||||
|
@ -6,7 +6,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../desktop/models.dart';
|
||||
import '../../desktop/state.dart';
|
||||
import '../../theme.dart';
|
||||
import '../message.dart';
|
||||
import 'graphics.dart';
|
||||
import 'message_page.dart';
|
||||
@ -45,27 +44,27 @@ class AppFailurePage extends ConsumerWidget {
|
||||
header = null;
|
||||
message = AppLocalizations.of(context)!.appFailurePage_txt_info;
|
||||
actions = [
|
||||
OutlinedButton.icon(
|
||||
label: Text(AppLocalizations.of(context)!
|
||||
.appFailurePage_btn_unlock),
|
||||
icon: const Icon(Icons.lock_open),
|
||||
style: AppTheme.primaryOutlinedButtonStyle(context),
|
||||
onPressed: () async {
|
||||
final closeMessage = showMessage(
|
||||
context,
|
||||
AppLocalizations.of(context)!
|
||||
.appFailurePage_msg_permission,
|
||||
duration: const Duration(seconds: 30));
|
||||
try {
|
||||
if (await ref.read(rpcProvider).elevate()) {
|
||||
ref.refresh(rpcProvider);
|
||||
} else {
|
||||
showMessage(context, 'Permission denied');
|
||||
}
|
||||
} finally {
|
||||
closeMessage();
|
||||
ElevatedButton.icon(
|
||||
label: Text(
|
||||
AppLocalizations.of(context)!.appFailurePage_btn_unlock),
|
||||
icon: const Icon(Icons.lock_open),
|
||||
onPressed: () async {
|
||||
final closeMessage = showMessage(
|
||||
context,
|
||||
AppLocalizations.of(context)!
|
||||
.appFailurePage_msg_permission,
|
||||
duration: const Duration(seconds: 30));
|
||||
try {
|
||||
if (await ref.read(rpcProvider).elevate()) {
|
||||
ref.refresh(rpcProvider);
|
||||
} else {
|
||||
showMessage(context, 'Permission denied');
|
||||
}
|
||||
}),
|
||||
} finally {
|
||||
closeMessage();
|
||||
}
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
break;
|
||||
|
@ -35,7 +35,12 @@ class DeviceButton extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return IconButton(
|
||||
tooltip: 'More actions',
|
||||
icon: _CircledDeviceAvatar(radius),
|
||||
// TODO: Remove OverflowBox on Flutter 3.3
|
||||
icon: OverflowBox(
|
||||
maxHeight: 44,
|
||||
maxWidth: 44,
|
||||
child: _CircledDeviceAvatar(radius),
|
||||
),
|
||||
onPressed: () {
|
||||
final withContext = ref.read(withContextProvider);
|
||||
|
||||
|
@ -5,7 +5,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../core/models.dart';
|
||||
import '../../desktop/state.dart';
|
||||
import '../../theme.dart';
|
||||
import '../message.dart';
|
||||
import '../models.dart';
|
||||
import 'device_avatar.dart';
|
||||
@ -25,8 +24,7 @@ class DeviceErrorScreen extends ConsumerWidget {
|
||||
graphic: noPermission,
|
||||
message: 'Managing this device requires elevated privileges.',
|
||||
actions: [
|
||||
OutlinedButton.icon(
|
||||
style: AppTheme.primaryOutlinedButtonStyle(context),
|
||||
ElevatedButton.icon(
|
||||
label: const Text('Unlock'),
|
||||
icon: const Icon(Icons.lock_open),
|
||||
onPressed: () async {
|
||||
|
@ -159,6 +159,7 @@ class _PinEntryFormState extends ConsumerState<_PinEntryForm> {
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_isObscure ? Icons.visibility : Icons.visibility_off,
|
||||
color: IconTheme.of(context).color,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
@ -187,10 +188,11 @@ class _PinEntryFormState extends ConsumerState<_PinEntryForm> {
|
||||
dense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 0),
|
||||
minLeadingWidth: 0,
|
||||
trailing: ElevatedButton(
|
||||
trailing: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.lock_open),
|
||||
label: const Text('Unlock'),
|
||||
onPressed:
|
||||
_pinController.text.isNotEmpty && !_blocked ? _submit : null,
|
||||
child: const Text('Unlock'),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -31,9 +31,8 @@ class _CapabilityForm extends StatelessWidget {
|
||||
children: Capability.values
|
||||
.where((c) => capabilities & c.value != 0)
|
||||
.map((c) => FilterChip(
|
||||
showCheckmark: true,
|
||||
selected: enabled & c.value != 0,
|
||||
label: Text(c.name),
|
||||
selected: enabled & c.value != 0,
|
||||
onSelected: (_) {
|
||||
onChanged(enabled ^ c.value);
|
||||
},
|
||||
|
@ -76,9 +76,17 @@ class AccountDialog extends ConsumerWidget with AccountMixin {
|
||||
backgroundColor: action != null ? color.first : theme.secondary,
|
||||
foregroundColor: color.second,
|
||||
child: IconButton(
|
||||
/* use this in Flutter 3.3
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: action != null ? color.first : theme.secondary,
|
||||
foregroundColor: color.second,
|
||||
disabledBackgroundColor: theme.onSecondary.withOpacity(0.2),
|
||||
fixedSize: const Size.square(38),
|
||||
),*/
|
||||
icon: e.icon,
|
||||
iconSize: 22,
|
||||
tooltip: e.text,
|
||||
// Remove the following line in Flutter 3.3:
|
||||
disabledColor: theme.onSecondary.withOpacity(0.2),
|
||||
onPressed: action != null
|
||||
? () {
|
||||
|
@ -5,13 +5,14 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:yubico_authenticator/cancellation_exception.dart';
|
||||
|
||||
import '../../app/logging.dart';
|
||||
import '../../app/message.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../cancellation_exception.dart';
|
||||
import '../../desktop/models.dart';
|
||||
import '../../widgets/choice_filter_chip.dart';
|
||||
import '../../widgets/file_drop_target.dart';
|
||||
import '../../widgets/responsive_dialog.dart';
|
||||
import '../../widgets/utf8_utils.dart';
|
||||
@ -56,6 +57,16 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
_scanQrCode(QrScanner qrScanner) async {
|
||||
try {
|
||||
setState(() {
|
||||
// If we have a previous scan result stored, clear it
|
||||
if (_qrState == _QrScanState.success) {
|
||||
_issuerController.text = '';
|
||||
_accountController.text = '';
|
||||
_secretController.text = '';
|
||||
_oathType = defaultOathType;
|
||||
_hashAlgorithm = defaultHashAlgorithm;
|
||||
_periodController.text = '$defaultPeriod';
|
||||
_digits = defaultDigits;
|
||||
}
|
||||
_qrState = _QrScanState.scanning;
|
||||
});
|
||||
final otpauth = await qrScanner.scanQr();
|
||||
@ -265,6 +276,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_isObscure ? Icons.visibility : Icons.visibility_off,
|
||||
color: IconTheme.of(context).color,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
@ -292,30 +304,18 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
if (qrScanner != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
if (_qrState != _QrScanState.scanning) ...[
|
||||
OutlinedButton.icon(
|
||||
style: OutlinedButton.styleFrom(
|
||||
fixedSize: const Size.fromWidth(132)),
|
||||
onPressed: () {
|
||||
_scanQrCode(qrScanner);
|
||||
},
|
||||
icon: const Icon(Icons.qr_code),
|
||||
label: const Text('Scan QR code'),
|
||||
)
|
||||
] else ...[
|
||||
OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
fixedSize: const Size.fromWidth(132)),
|
||||
onPressed: null,
|
||||
child: const SizedBox.square(
|
||||
dimension: 16.0,
|
||||
child: CircularProgressIndicator()),
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
child: ActionChip(
|
||||
avatar: _qrState != _QrScanState.scanning
|
||||
? (_qrState == _QrScanState.success
|
||||
? const Icon(Icons.qr_code)
|
||||
: const Icon(Icons.qr_code_scanner_outlined))
|
||||
: const CircularProgressIndicator(strokeWidth: 2.0),
|
||||
label: _qrState == _QrScanState.success
|
||||
? const Text('Scanned QR code')
|
||||
: const Text('Scan QR code'),
|
||||
onPressed: () {
|
||||
_scanQrCode(qrScanner);
|
||||
}),
|
||||
),
|
||||
const Divider(),
|
||||
Wrap(
|
||||
@ -333,103 +333,60 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
});
|
||||
},
|
||||
),
|
||||
Chip(
|
||||
backgroundColor: ChipTheme.of(context).selectedColor,
|
||||
label: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<OathType>(
|
||||
value: _oathType,
|
||||
isDense: true,
|
||||
underline: null,
|
||||
items: OathType.values
|
||||
.map((e) => DropdownMenuItem(
|
||||
value: e,
|
||||
child: Text(e.displayName),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: _qrState != _QrScanState.success
|
||||
? (type) {
|
||||
setState(() {
|
||||
_oathType = type ?? OathType.totp;
|
||||
});
|
||||
}
|
||||
: null,
|
||||
),
|
||||
),
|
||||
ChoiceFilterChip<OathType>(
|
||||
items: OathType.values,
|
||||
value: _oathType,
|
||||
selected: _oathType != defaultOathType,
|
||||
itemBuilder: (value) => Text(value.displayName),
|
||||
onChanged: _qrState != _QrScanState.success
|
||||
? (value) {
|
||||
setState(() {
|
||||
_oathType = value;
|
||||
});
|
||||
}
|
||||
: null,
|
||||
),
|
||||
Chip(
|
||||
backgroundColor: ChipTheme.of(context).selectedColor,
|
||||
label: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<HashAlgorithm>(
|
||||
value: _hashAlgorithm,
|
||||
isDense: true,
|
||||
underline: null,
|
||||
items: HashAlgorithm.values
|
||||
.where((alg) =>
|
||||
alg != HashAlgorithm.sha512 ||
|
||||
widget.state.version.isAtLeast(4, 3, 1))
|
||||
.map((e) => DropdownMenuItem(
|
||||
value: e,
|
||||
child: Text(e.displayName),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: _qrState != _QrScanState.success
|
||||
? (type) {
|
||||
setState(() {
|
||||
_hashAlgorithm = type ?? HashAlgorithm.sha1;
|
||||
});
|
||||
}
|
||||
: null,
|
||||
),
|
||||
),
|
||||
ChoiceFilterChip<HashAlgorithm>(
|
||||
items: HashAlgorithm.values,
|
||||
value: _hashAlgorithm,
|
||||
selected: _hashAlgorithm != defaultHashAlgorithm,
|
||||
itemBuilder: (value) => Text(value.displayName),
|
||||
onChanged: _qrState != _QrScanState.success
|
||||
? (value) {
|
||||
setState(() {
|
||||
_hashAlgorithm = value;
|
||||
});
|
||||
}
|
||||
: null,
|
||||
),
|
||||
if (_oathType == OathType.totp)
|
||||
Chip(
|
||||
backgroundColor: ChipTheme.of(context).selectedColor,
|
||||
label: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<int>(
|
||||
value: int.tryParse(_periodController.text) ??
|
||||
defaultPeriod,
|
||||
isDense: true,
|
||||
underline: null,
|
||||
items: _periodValues
|
||||
.map((e) => DropdownMenuItem(
|
||||
value: e,
|
||||
child: Text('$e sec'),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: _qrState != _QrScanState.success
|
||||
? (period) {
|
||||
setState(() {
|
||||
_periodController.text =
|
||||
'${period ?? defaultPeriod}';
|
||||
});
|
||||
}
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
Chip(
|
||||
backgroundColor: ChipTheme.of(context).selectedColor,
|
||||
label: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<int>(
|
||||
value: _digits,
|
||||
isDense: true,
|
||||
underline: null,
|
||||
items: _digitsValues
|
||||
.map((e) => DropdownMenuItem(
|
||||
value: e,
|
||||
child: Text('$e digits'),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: _qrState != _QrScanState.success
|
||||
? (digits) {
|
||||
setState(() {
|
||||
_digits = digits ?? defaultDigits;
|
||||
});
|
||||
}
|
||||
: null,
|
||||
),
|
||||
ChoiceFilterChip<int>(
|
||||
items: _periodValues,
|
||||
value:
|
||||
int.tryParse(_periodController.text) ?? defaultPeriod,
|
||||
selected:
|
||||
int.tryParse(_periodController.text) != defaultPeriod,
|
||||
itemBuilder: ((value) => Text('$value sec')),
|
||||
onChanged: _qrState != _QrScanState.success
|
||||
? (period) {
|
||||
setState(() {
|
||||
_periodController.text = '$period';
|
||||
});
|
||||
}
|
||||
: null,
|
||||
),
|
||||
ChoiceFilterChip<int>(
|
||||
items: _digitsValues,
|
||||
value: _digits,
|
||||
selected: _digits != defaultDigits,
|
||||
itemBuilder: (value) => Text('$value digits'),
|
||||
onChanged: _qrState != _QrScanState.success
|
||||
? (digits) {
|
||||
setState(() {
|
||||
_digits = digits;
|
||||
});
|
||||
}
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -288,6 +288,7 @@ class _UnlockFormState extends ConsumerState<_UnlockForm> {
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_isObscure ? Icons.visibility : Icons.visibility_off,
|
||||
color: IconTheme.of(context).color,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
@ -327,9 +328,10 @@ class _UnlockFormState extends ConsumerState<_UnlockForm> {
|
||||
padding: const EdgeInsets.only(top: 12.0, right: 18.0, bottom: 4.0),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: ElevatedButton(
|
||||
child: ElevatedButton.icon(
|
||||
label: const Text('Unlock'),
|
||||
icon: const Icon(Icons.lock_open),
|
||||
onPressed: _passwordController.text.isNotEmpty ? _submit : null,
|
||||
child: const Text('Unlock'),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -41,8 +41,8 @@ class AppTheme {
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: primaryBlue,
|
||||
onPrimary: Colors.white,
|
||||
primary: primaryBlue,
|
||||
)),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
@ -57,12 +57,12 @@ class AppTheme {
|
||||
color: Colors.grey.shade300,
|
||||
),
|
||||
chipTheme: ChipThemeData(
|
||||
backgroundColor: Colors.transparent,
|
||||
selectedColor: const Color(0xffd2dbdf),
|
||||
side: BorderSide(width: 1, color: Colors.grey.shade400),
|
||||
backgroundColor: Colors.transparent, // Remove 3.3
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8)), // Remove 3.3
|
||||
selectedColor: const Color(0xffd2dbdf),
|
||||
side: _ChipBorder(color: Colors.grey.shade400),
|
||||
checkmarkColor: Colors.black,
|
||||
),
|
||||
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
||||
backgroundColor: primaryBlue,
|
||||
@ -127,12 +127,12 @@ class AppTheme {
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.black,
|
||||
backgroundColor: primaryGreen,
|
||||
onPrimary: Colors.black,
|
||||
primary: primaryGreen,
|
||||
)),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.white70,
|
||||
primary: Colors.white70,
|
||||
side: const BorderSide(width: 1, color: Colors.white12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@ -143,17 +143,15 @@ class AppTheme {
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
chipTheme: ChipThemeData(
|
||||
backgroundColor: Colors.transparent,
|
||||
selectedColor: Colors.white12,
|
||||
side: const BorderSide(width: 1, color: Colors.white12),
|
||||
backgroundColor: Colors.transparent, // Remove 3.3
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8)), // Remove 3.3
|
||||
selectedColor: Colors.white12,
|
||||
side: const _ChipBorder(color: Colors.white12),
|
||||
labelStyle: TextStyle(
|
||||
// Should match titleMedium
|
||||
color: Colors.grey.shade200,
|
||||
fontWeight: FontWeight.w300,
|
||||
fontSize: 16),
|
||||
color: Colors.grey.shade200,
|
||||
),
|
||||
checkmarkColor: Colors.grey.shade200,
|
||||
),
|
||||
dialogTheme: const DialogTheme(
|
||||
backgroundColor: Color(0xff323232),
|
||||
@ -190,12 +188,17 @@ class AppTheme {
|
||||
fontSize: 16),
|
||||
),
|
||||
);
|
||||
|
||||
static ButtonStyle primaryOutlinedButtonStyle(BuildContext context) =>
|
||||
OutlinedButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
side:
|
||||
BorderSide(width: 1, color: Theme.of(context).colorScheme.primary),
|
||||
);
|
||||
}
|
||||
|
||||
/// This fixes the issue with FilterChip resizing vertically on toggle.
|
||||
class _ChipBorder extends BorderSide implements MaterialStateBorderSide {
|
||||
const _ChipBorder({super.color});
|
||||
|
||||
@override
|
||||
BorderSide? resolve(Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
return const BorderSide(width: 1, color: Colors.transparent);
|
||||
}
|
||||
return BorderSide(width: 1, color: color);
|
||||
}
|
||||
}
|
||||
|
105
lib/widgets/choice_filter_chip.dart
Executable file
105
lib/widgets/choice_filter_chip.dart
Executable file
@ -0,0 +1,105 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ChoiceFilterChip<T> extends StatefulWidget {
|
||||
final T value;
|
||||
final List<T> items;
|
||||
final Widget Function(T value) itemBuilder;
|
||||
final Widget Function(T value)? labelBuilder;
|
||||
final void Function(T value)? onChanged;
|
||||
final Widget? avatar;
|
||||
final bool selected;
|
||||
const ChoiceFilterChip({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.items,
|
||||
required this.itemBuilder,
|
||||
required this.onChanged,
|
||||
this.avatar,
|
||||
this.selected = false,
|
||||
this.labelBuilder,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChoiceFilterChip<T>> createState() => _ChoiceFilterChipState<T>();
|
||||
}
|
||||
|
||||
class _ChoiceFilterChipState<T> extends State<ChoiceFilterChip<T>> {
|
||||
bool _showing = false;
|
||||
|
||||
Future<T?> _showPickerMenu() async {
|
||||
final RenderBox chipBox = context.findRenderObject()! as RenderBox;
|
||||
final RenderBox overlay =
|
||||
Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox;
|
||||
final RelativeRect position = RelativeRect.fromRect(
|
||||
Rect.fromPoints(
|
||||
chipBox.localToGlobal(chipBox.size.bottomLeft(Offset.zero),
|
||||
ancestor: overlay),
|
||||
chipBox.localToGlobal(chipBox.size.bottomRight(Offset.zero),
|
||||
ancestor: overlay),
|
||||
),
|
||||
Offset.zero & overlay.size,
|
||||
);
|
||||
|
||||
return await showMenu(
|
||||
context: context,
|
||||
position: position,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(4)),
|
||||
),
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
items: widget.items
|
||||
.map((e) => PopupMenuItem<T>(
|
||||
value: e,
|
||||
height: chipBox.size.height,
|
||||
textStyle: Theme.of(context).chipTheme.labelStyle,
|
||||
child: widget.itemBuilder(e),
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FilterChip(
|
||||
avatar: widget.avatar,
|
||||
labelPadding: const EdgeInsets.only(left: 4),
|
||||
label: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
(widget.labelBuilder ?? widget.itemBuilder).call(widget.value),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 6),
|
||||
child: Icon(
|
||||
_showing ? Icons.arrow_drop_up : Icons.arrow_drop_down,
|
||||
color: Theme.of(context).chipTheme.checkmarkColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
selected: widget.selected,
|
||||
showCheckmark: false,
|
||||
onSelected: widget.onChanged != null
|
||||
? (_) async {
|
||||
setState(() {
|
||||
_showing = true;
|
||||
});
|
||||
try {
|
||||
final selected = await _showPickerMenu();
|
||||
if (selected != null) {
|
||||
widget.onChanged?.call(selected);
|
||||
}
|
||||
} finally {
|
||||
// Give the menu some time to rollup before switching state.
|
||||
Timer(const Duration(milliseconds: 300), () {
|
||||
setState(() {
|
||||
_showing = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
@ -68,10 +68,14 @@ class _ToastState extends State<Toast> with SingleTickerProviderStateMixin {
|
||||
),
|
||||
color: widget.backgroundColor,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text(
|
||||
widget.message,
|
||||
style: widget.textStyle,
|
||||
)),
|
||||
widget.message,
|
||||
style: widget.textStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
|
||||
FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811
|
||||
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
|
||||
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
|
||||
shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727
|
||||
url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3
|
||||
|
41
pubspec.lock
41
pubspec.lock
@ -21,7 +21,7 @@ packages:
|
||||
name: archive
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.3.0"
|
||||
version: "3.1.11"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -35,7 +35,7 @@ packages:
|
||||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.9.0"
|
||||
version: "2.8.2"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -105,7 +105,14 @@ packages:
|
||||
name: characters
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
version: "1.2.0"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -119,7 +126,7 @@ packages:
|
||||
name: clock
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
version: "1.1.0"
|
||||
code_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -154,7 +161,7 @@ packages:
|
||||
name: crypto
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
version: "3.0.1"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -175,7 +182,7 @@ packages:
|
||||
name: fake_async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
version: "1.3.0"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -350,21 +357,21 @@ packages:
|
||||
name: matcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.12"
|
||||
version: "0.12.11"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.5"
|
||||
version: "0.1.4"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
version: "1.7.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -385,7 +392,7 @@ packages:
|
||||
name: path
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.2"
|
||||
version: "1.8.1"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -565,7 +572,7 @@ packages:
|
||||
name: source_span
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
version: "1.8.2"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -600,28 +607,28 @@ packages:
|
||||
name: string_scanner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
version: "1.1.0"
|
||||
sync_http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sync_http
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
version: "0.3.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
version: "1.2.0"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.12"
|
||||
version: "0.4.9"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -635,7 +642,7 @@ packages:
|
||||
name: typed_data
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
version: "1.3.0"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -705,7 +712,7 @@ packages:
|
||||
name: vm_service
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "9.0.0"
|
||||
version: "8.2.2"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -60,16 +60,16 @@ IDI_APP_ICON ICON "resources\\app_icon.ico"
|
||||
// Version
|
||||
//
|
||||
|
||||
#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
|
||||
#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
|
||||
#ifdef FLUTTER_BUILD_NUMBER
|
||||
#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER
|
||||
#else
|
||||
#define VERSION_AS_NUMBER 1,0,0,0
|
||||
#define VERSION_AS_NUMBER 6,0,0
|
||||
#endif
|
||||
|
||||
#if defined(FLUTTER_VERSION)
|
||||
#define VERSION_AS_STRING FLUTTER_VERSION
|
||||
#ifdef FLUTTER_BUILD_NAME
|
||||
#define VERSION_AS_STRING #FLUTTER_BUILD_NAME
|
||||
#else
|
||||
#define VERSION_AS_STRING "1.0.0"
|
||||
#define VERSION_AS_STRING "6.0.0-dev.0"
|
||||
#endif
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
@ -93,7 +93,7 @@ BEGIN
|
||||
VALUE "FileDescription", "Yubico Authenticator" "\0"
|
||||
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||
VALUE "InternalName", "authenticator" "\0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2022 Yubico. All rights reserved." "\0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2021 Yubico. All rights reserved." "\0"
|
||||
VALUE "OriginalFilename", "authenticator.exe" "\0"
|
||||
VALUE "ProductName", "Yubico Authenticator" "\0"
|
||||
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||
@ -118,4 +118,4 @@ END
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
#endif // not APSTUDIO_INVOKED
|
||||
#endif // not APSTUDIO_INVOKED
|
Loading…
Reference in New Issue
Block a user