mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-22 08:22:16 +03:00
customize color and display name
This commit is contained in:
parent
9793af6405
commit
c7f2b651fd
@ -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.
|
||||
@ -38,6 +38,7 @@ import androidx.activity.viewModels
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.yubico.authenticator.logging.FlutterLog
|
||||
import com.yubico.authenticator.oath.AppLinkMethodChannel
|
||||
import com.yubico.authenticator.oath.OathManager
|
||||
@ -383,6 +384,11 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
methodCall.arguments as Boolean,
|
||||
)
|
||||
)
|
||||
|
||||
"getPrimaryColor" -> result.success(
|
||||
getPrimaryColor(this@MainActivity)
|
||||
)
|
||||
|
||||
"getAndroidSdkVersion" -> result.success(
|
||||
Build.VERSION.SDK_INT
|
||||
)
|
||||
@ -456,6 +462,30 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
return FLAG_SECURE != (window.attributes.flags and FLAG_SECURE)
|
||||
}
|
||||
|
||||
private fun getPrimaryColor(context: Context): Int? {
|
||||
if (DynamicColors.isDynamicColorAvailable()) {
|
||||
val dynamicColorContext = DynamicColors.wrapContextIfAvailable(
|
||||
context,
|
||||
com.google.android.material.R.style.ThemeOverlay_Material3_DynamicColors_DayNight
|
||||
)
|
||||
|
||||
val typedArray = dynamicColorContext.obtainStyledAttributes(
|
||||
intArrayOf(
|
||||
android.R.attr.colorPrimary,
|
||||
)
|
||||
)
|
||||
try {
|
||||
return if (typedArray.hasValue(0))
|
||||
typedArray.getColor(0, 0)
|
||||
else
|
||||
null
|
||||
} finally {
|
||||
typedArray.recycle()
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
private fun forcePortraitOrientation() {
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
@ -465,5 +495,5 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
}
|
||||
|
||||
private fun isPortraitOnly() = resources.getBoolean(R.bool.portrait_only);
|
||||
private fun isPortraitOnly() = resources.getBoolean(R.bool.portrait_only)
|
||||
}
|
||||
|
@ -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.
|
||||
@ -52,6 +52,10 @@ Future<int> getAndroidSdkVersion() async {
|
||||
return await appMethodsChannel.invokeMethod('getAndroidSdkVersion');
|
||||
}
|
||||
|
||||
Future<int?> getPrimaryColor() async {
|
||||
return await appMethodsChannel.invokeMethod('getPrimaryColor');
|
||||
}
|
||||
|
||||
Future<void> setPrimaryClip(String toClipboard, bool isSensitive) async {
|
||||
await appMethodsChannel.invokeMethod('setPrimaryClip',
|
||||
{'toClipboard': toClipboard, 'isSensitive': isSensitive});
|
||||
|
@ -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.
|
||||
@ -51,6 +51,9 @@ Future<Widget> initialize() async {
|
||||
|
||||
_initLicenses();
|
||||
|
||||
final primaryColor = await getPrimaryColor();
|
||||
final primaryColorInt = primaryColor != null ? Color(primaryColor) : null;
|
||||
|
||||
return ProviderScope(
|
||||
overrides: [
|
||||
supportedAppsProvider.overrideWith(implementedApps([
|
||||
@ -84,7 +87,8 @@ Future<Widget> initialize() async {
|
||||
androidNfcSupportProvider.overrideWithValue(await getHasNfc()),
|
||||
supportedThemesProvider.overrideWith(
|
||||
(ref) => ref.watch(androidSupportedThemesProvider),
|
||||
)
|
||||
),
|
||||
primaryColorProvider.overrideWithValue(primaryColorInt),
|
||||
],
|
||||
child: DismissKeyboard(
|
||||
child: YubicoAuthenticatorApp(page: Consumer(
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.
|
||||
@ -19,13 +19,13 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../theme.dart';
|
||||
import 'logging.dart';
|
||||
import 'shortcuts.dart';
|
||||
import 'state.dart';
|
||||
|
||||
class YubicoAuthenticatorApp extends StatelessWidget {
|
||||
final Widget page;
|
||||
|
||||
const YubicoAuthenticatorApp({required this.page, super.key});
|
||||
|
||||
@override
|
||||
@ -34,8 +34,8 @@ class YubicoAuthenticatorApp extends StatelessWidget {
|
||||
child: Consumer(
|
||||
builder: (context, ref, _) => MaterialApp(
|
||||
title: ref.watch(l10nProvider).app_name,
|
||||
theme: AppTheme.lightTheme,
|
||||
darkTheme: AppTheme.darkTheme,
|
||||
theme: ref.watch(lightThemeProvider),
|
||||
darkTheme: ref.watch(darkThemeProvider),
|
||||
themeMode: ref.watch(themeModeProvider),
|
||||
home: page,
|
||||
debugShowCheckedModeBanner: false,
|
||||
|
110
lib/app/key_customization.dart
Normal file
110
lib/app/key_customization.dart
Normal file
@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import 'logging.dart';
|
||||
import 'models.dart';
|
||||
|
||||
final _log = Logger('key_customization');
|
||||
|
||||
final keyCustomizationManagerProvider =
|
||||
Provider<KeyCustomizationManager>((ref) {
|
||||
final retval = KeyCustomizationManager();
|
||||
retval.read();
|
||||
return retval;
|
||||
});
|
||||
|
||||
class KeyCustomizationManager {
|
||||
var _customizations = <String, dynamic>{};
|
||||
|
||||
void read() async {
|
||||
final customizationFile = await _customizationFile;
|
||||
// get content
|
||||
if (!await customizationFile.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var customizationContent = await customizationFile.readAsString();
|
||||
_customizations =
|
||||
json.decode(String.fromCharCodes(base64Decode(customizationContent)));
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
KeyCustomization? get(String? serialNumber) {
|
||||
_log.debug('Getting customization for: $serialNumber');
|
||||
|
||||
if (serialNumber == null || serialNumber.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final sha = getSerialSha(serialNumber);
|
||||
|
||||
if (_customizations.containsKey(sha)) {
|
||||
return KeyCustomization(serialNumber, _customizations[sha]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
void set(KeyCustomization customization) {
|
||||
_log.debug(
|
||||
'Added: ${customization.serialNumber}: ${customization.properties}');
|
||||
final sha = getSerialSha(customization.serialNumber);
|
||||
_customizations[sha] = customization.properties;
|
||||
}
|
||||
|
||||
Future<void> write() async {
|
||||
final customizationFile = await _customizationFile;
|
||||
|
||||
try {
|
||||
await customizationFile.writeAsString(
|
||||
base64UrlEncode(utf8.encode(json.encode(_customizations))),
|
||||
flush: true);
|
||||
} catch (e) {
|
||||
_log.error('Error writing customization file: $e');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final _dataSubDir = 'customizations';
|
||||
|
||||
Future<Directory> get _dataDir async {
|
||||
final supportDirectory = await getApplicationSupportDirectory();
|
||||
return Directory(join(supportDirectory.path, _dataSubDir));
|
||||
}
|
||||
|
||||
Future<File> get _customizationFile async {
|
||||
final dataDir = await _dataDir;
|
||||
if (!await dataDir.exists()) {
|
||||
await dataDir.create();
|
||||
}
|
||||
return File(join(dataDir.path, 'key_customizations.dat'));
|
||||
}
|
||||
|
||||
String getSerialSha(String serialNumber) =>
|
||||
sha256.convert(utf8.encode(serialNumber)).toString();
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.
|
||||
@ -14,6 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
@ -144,3 +146,15 @@ class WindowState with _$WindowState {
|
||||
@Default(false) bool hidden,
|
||||
}) = _WindowState;
|
||||
}
|
||||
|
||||
class KeyCustomization {
|
||||
final String serialNumber;
|
||||
final Map<String, dynamic> properties;
|
||||
|
||||
const KeyCustomization(this.serialNumber, this.properties);
|
||||
|
||||
factory KeyCustomization.fromString(String serialNumber, String encodedJson) {
|
||||
final data = json.decode(String.fromCharCodes(base64Decode(encodedJson)));
|
||||
return KeyCustomization(serialNumber, data);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.
|
||||
@ -51,6 +51,10 @@ class NextDeviceIntent extends Intent {
|
||||
const NextDeviceIntent();
|
||||
}
|
||||
|
||||
class KeyCustomizationIntent extends Intent {
|
||||
const KeyCustomizationIntent();
|
||||
}
|
||||
|
||||
class SettingsIntent extends Intent {
|
||||
const SettingsIntent();
|
||||
}
|
||||
@ -61,26 +65,31 @@ class AboutIntent extends Intent {
|
||||
|
||||
class OpenIntent<T> extends Intent {
|
||||
final T target;
|
||||
|
||||
const OpenIntent(this.target);
|
||||
}
|
||||
|
||||
class CopyIntent<T> extends Intent {
|
||||
final T target;
|
||||
|
||||
const CopyIntent(this.target);
|
||||
}
|
||||
|
||||
class EditIntent<T> extends Intent {
|
||||
final T target;
|
||||
|
||||
const EditIntent(this.target);
|
||||
}
|
||||
|
||||
class DeleteIntent<T> extends Intent {
|
||||
final T target;
|
||||
|
||||
const DeleteIntent(this.target);
|
||||
}
|
||||
|
||||
class RefreshIntent<T> extends Intent {
|
||||
final T target;
|
||||
|
||||
const RefreshIntent(this.target);
|
||||
}
|
||||
|
||||
@ -92,6 +101,7 @@ SingleActivator ctrlOrCmd(LogicalKeyboardKey key) =>
|
||||
class ItemShortcuts<T> extends StatelessWidget {
|
||||
final T item;
|
||||
final Widget child;
|
||||
|
||||
const ItemShortcuts({super.key, required this.item, required this.child});
|
||||
|
||||
@override
|
||||
@ -112,6 +122,7 @@ class ItemShortcuts<T> extends StatelessWidget {
|
||||
/// Global keyboard shortcuts
|
||||
class GlobalShortcuts extends ConsumerWidget {
|
||||
final Widget child;
|
||||
|
||||
const GlobalShortcuts({super.key, required this.child});
|
||||
|
||||
@override
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.
|
||||
@ -25,6 +25,7 @@ import 'package:logging/logging.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../core/state.dart';
|
||||
import '../theme.dart';
|
||||
import 'features.dart' as features;
|
||||
import 'logging.dart';
|
||||
import 'models.dart';
|
||||
@ -139,6 +140,42 @@ class ThemeModeNotifier extends StateNotifier<ThemeMode> {
|
||||
orElse: () => supportedThemes.first);
|
||||
}
|
||||
|
||||
final primaryColorProvider = Provider<Color?>((ref) => null);
|
||||
|
||||
final darkThemeProvider = StateNotifierProvider<ThemeNotifier, ThemeData>(
|
||||
(ref) => ThemeNotifier(ref.watch(primaryColorProvider), ThemeMode.dark),
|
||||
);
|
||||
|
||||
final lightThemeProvider = StateNotifierProvider<ThemeNotifier, ThemeData>(
|
||||
(ref) => ThemeNotifier(ref.watch(primaryColorProvider), ThemeMode.light),
|
||||
);
|
||||
|
||||
class ThemeNotifier extends StateNotifier<ThemeData> {
|
||||
final ThemeMode _themeMode;
|
||||
|
||||
ThemeNotifier(Color? systemPrimaryColor, this._themeMode)
|
||||
: super(_get(systemPrimaryColor, _themeMode));
|
||||
|
||||
static ThemeData _getDefault(ThemeMode themeMode) =>
|
||||
themeMode == ThemeMode.light ? AppTheme.lightTheme : AppTheme.darkTheme;
|
||||
|
||||
static ThemeData _get(Color? primaryColor, ThemeMode themeMode) =>
|
||||
(primaryColor != null)
|
||||
? _getDefault(themeMode).copyWith(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
brightness: themeMode == ThemeMode.dark
|
||||
? Brightness.dark
|
||||
: Brightness.light,
|
||||
seedColor: primaryColor)
|
||||
.copyWith(primary: primaryColor))
|
||||
: _getDefault(themeMode);
|
||||
|
||||
void setPrimaryColor(Color? primaryColor) {
|
||||
_log.debug('Set primary color to $primaryColor');
|
||||
state = _get(primaryColor, _themeMode);
|
||||
}
|
||||
}
|
||||
|
||||
// Override with platform implementation
|
||||
final attachedDevicesProvider =
|
||||
NotifierProvider<AttachedDevicesNotifier, List<DeviceNode>>(
|
||||
|
252
lib/app/views/customize_page.dart
Executable file
252
lib/app/views/customize_page.dart
Executable file
@ -0,0 +1,252 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../../widgets/focus_utils.dart';
|
||||
import '../../widgets/responsive_dialog.dart';
|
||||
import '../key_customization.dart';
|
||||
import '../logging.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
|
||||
final _log = Logger('CustomizePage');
|
||||
|
||||
class CustomizePage extends ConsumerStatefulWidget {
|
||||
final KeyCustomization? initialCustomization;
|
||||
|
||||
const CustomizePage({super.key, required this.initialCustomization});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _CustomizePageState();
|
||||
}
|
||||
|
||||
class _CustomizePageState extends ConsumerState<CustomizePage> {
|
||||
String? _displayName;
|
||||
String? _displayColor;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_displayColor = widget.initialCustomization != null
|
||||
? widget.initialCustomization?.properties['display_color']
|
||||
: null;
|
||||
_displayName = widget.initialCustomization != null
|
||||
? widget.initialCustomization?.properties['display_name']
|
||||
: null;
|
||||
}
|
||||
|
||||
void updateColor(String? colorString) {
|
||||
setState(() {
|
||||
_displayColor = colorString;
|
||||
Color? color =
|
||||
colorString != null ? Color(int.parse(colorString, radix: 16)) : null;
|
||||
ref.watch(darkThemeProvider.notifier).setPrimaryColor(color);
|
||||
ref.watch(lightThemeProvider.notifier).setPrimaryColor(color);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: const Text('Customize key appearance'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
KeyCustomization newValue = KeyCustomization(
|
||||
widget.initialCustomization!.serialNumber, <String, dynamic>{
|
||||
'display_color': _displayColor,
|
||||
'display_name': _displayName
|
||||
});
|
||||
|
||||
_log.debug('Saving customization for '
|
||||
'${widget.initialCustomization!.serialNumber}: '
|
||||
'$_displayName/$_displayColor');
|
||||
|
||||
final manager = ref.read(keyCustomizationManagerProvider);
|
||||
manager.set(newValue);
|
||||
await manager.write();
|
||||
|
||||
await ref.read(withContextProvider)((context) async {
|
||||
FocusUtils.unfocus(context);
|
||||
final nav = Navigator.of(context);
|
||||
nav.pop();
|
||||
});
|
||||
},
|
||||
child: Text(l10n.s_save),
|
||||
),
|
||||
],
|
||||
child: Theme(
|
||||
// Make the headers use the primary color to pop a bit.
|
||||
// Once M3 is implemented this will probably not be needed.
|
||||
data: theme.copyWith(
|
||||
textTheme: theme.textTheme.copyWith(
|
||||
labelLarge: theme.textTheme.labelLarge
|
||||
?.copyWith(color: theme.colorScheme.primary)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
//controller: displayNameController,
|
||||
initialValue: _displayName,
|
||||
maxLength: 20,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Nick name',
|
||||
helperText: '', // Prevents dialog resizing when disabled
|
||||
prefixIcon: Icon(Icons.key),
|
||||
),
|
||||
textInputAction: TextInputAction.done,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_displayName = value.trim();
|
||||
});
|
||||
},
|
||||
onFieldSubmitted: (_) {},
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Text('Color: '),
|
||||
const SizedBox(width: 16),
|
||||
ColorButton(
|
||||
color: Colors.yellow,
|
||||
isSelected: _displayColor == 'FFFFEB3B',
|
||||
onPressed: () {
|
||||
updateColor('FFFFEB3B');
|
||||
},
|
||||
),
|
||||
ColorButton(
|
||||
color: Colors.orange,
|
||||
isSelected: _displayColor == 'FFFF9800',
|
||||
onPressed: () {
|
||||
updateColor('FFFF9800');
|
||||
},
|
||||
),
|
||||
ColorButton(
|
||||
color: Colors.red,
|
||||
isSelected: _displayColor == 'FFF44336',
|
||||
onPressed: () {
|
||||
updateColor('FFF44336');
|
||||
},
|
||||
),
|
||||
ColorButton(
|
||||
color: Colors.deepPurple,
|
||||
isSelected: _displayColor == 'FF673AB7',
|
||||
onPressed: () {
|
||||
updateColor('FF673AB7');
|
||||
},
|
||||
),
|
||||
ColorButton(
|
||||
color: Colors.green,
|
||||
isSelected: _displayColor == 'FF4CAF50',
|
||||
onPressed: () {
|
||||
updateColor('FF4CAF50');
|
||||
},
|
||||
),
|
||||
ColorButton(
|
||||
color: Colors.teal,
|
||||
isSelected: _displayColor == 'FF009688',
|
||||
onPressed: () {
|
||||
updateColor('FF009688');
|
||||
},
|
||||
),
|
||||
ColorButton(
|
||||
color: Colors.cyan,
|
||||
isSelected: _displayColor == 'FF00BCD4',
|
||||
onPressed: () {
|
||||
updateColor('FF00BCD4');
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.cancel_rounded),
|
||||
color: _displayColor == null
|
||||
? theme.colorScheme.surface
|
||||
: theme.colorScheme.onSurface,
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: const BorderSide(
|
||||
style: BorderStyle.solid,
|
||||
width: 0.4,
|
||||
),
|
||||
backgroundColor: _displayColor == null
|
||||
? theme.colorScheme.onSurface
|
||||
: theme.colorScheme.surface,
|
||||
),
|
||||
onPressed: () {
|
||||
updateColor(null);
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ColorButton extends StatefulWidget {
|
||||
final MaterialColor color;
|
||||
final bool isSelected;
|
||||
final Function()? onPressed;
|
||||
|
||||
const ColorButton(
|
||||
{super.key,
|
||||
required this.color,
|
||||
required this.isSelected,
|
||||
required this.onPressed});
|
||||
|
||||
@override
|
||||
State<ColorButton> createState() => _ColorButtonState();
|
||||
}
|
||||
|
||||
class _ColorButtonState extends State<ColorButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var style = OutlinedButton.styleFrom(
|
||||
side: BorderSide(
|
||||
color: widget.color,
|
||||
style: BorderStyle.solid,
|
||||
width: 0.4,
|
||||
),
|
||||
);
|
||||
if (widget.isSelected) {
|
||||
style = style.copyWith(
|
||||
backgroundColor: MaterialStatePropertyAll(widget.color.shade900));
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.circle),
|
||||
color: widget.color,
|
||||
style: style,
|
||||
onPressed: widget.onPressed,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.
|
||||
@ -26,29 +26,46 @@ import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'keys.dart';
|
||||
|
||||
class DeviceAvatar extends StatelessWidget {
|
||||
class DeviceAvatar extends ConsumerWidget {
|
||||
final Widget child;
|
||||
final Widget? decoration;
|
||||
final Color? backgroundColor;
|
||||
final Widget? badge;
|
||||
final double? radius;
|
||||
const DeviceAvatar({super.key, required this.child, this.badge, this.radius});
|
||||
|
||||
factory DeviceAvatar.yubiKeyData(YubiKeyData data, {double? radius}) =>
|
||||
DeviceAvatar(
|
||||
badge: isDesktop && data.node is NfcReaderNode ? nfcIcon : null,
|
||||
radius: radius,
|
||||
const DeviceAvatar(
|
||||
{super.key,
|
||||
required this.child,
|
||||
this.decoration,
|
||||
this.backgroundColor,
|
||||
this.badge,
|
||||
this.radius});
|
||||
|
||||
factory DeviceAvatar.yubiKeyData(YubiKeyData data, WidgetRef ref,
|
||||
{double? radius}) {
|
||||
return DeviceAvatar(
|
||||
backgroundColor: Colors.transparent,
|
||||
badge: isDesktop && data.node is NfcReaderNode ? nfcIcon : null,
|
||||
radius: radius,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
child: ProductImage(
|
||||
name: data.name,
|
||||
formFactor: data.info.formFactor,
|
||||
isNfc: data.info.supportedCapabilities.containsKey(Transport.nfc)),
|
||||
);
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
factory DeviceAvatar.deviceNode(DeviceNode node, {double? radius}) =>
|
||||
factory DeviceAvatar.deviceNode(DeviceNode node, WidgetRef ref,
|
||||
{double? radius}) =>
|
||||
node.map(
|
||||
usbYubiKey: (node) {
|
||||
final info = node.info;
|
||||
if (info != null) {
|
||||
return DeviceAvatar.yubiKeyData(
|
||||
YubiKeyData(node, node.name, info),
|
||||
ref,
|
||||
radius: radius,
|
||||
);
|
||||
}
|
||||
@ -73,10 +90,12 @@ class DeviceAvatar extends StatelessWidget {
|
||||
return ref.watch(currentDeviceDataProvider).maybeWhen(
|
||||
data: (data) => DeviceAvatar.yubiKeyData(
|
||||
data,
|
||||
ref,
|
||||
radius: radius,
|
||||
),
|
||||
orElse: () => DeviceAvatar.deviceNode(
|
||||
deviceNode,
|
||||
ref,
|
||||
radius: radius,
|
||||
),
|
||||
);
|
||||
@ -90,14 +109,15 @@ class DeviceAvatar extends StatelessWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final radius = this.radius ?? 20;
|
||||
return Stack(
|
||||
alignment: AlignmentDirectional.bottomEnd,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: radius,
|
||||
backgroundColor: Colors.transparent,
|
||||
backgroundColor:
|
||||
backgroundColor ?? Theme.of(context).colorScheme.surfaceVariant,
|
||||
child: IconTheme(
|
||||
data: IconTheme.of(context).copyWith(
|
||||
size: radius,
|
||||
@ -105,6 +125,7 @@ class DeviceAvatar extends StatelessWidget {
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
if (decoration != null) decoration!,
|
||||
if (badge != null)
|
||||
CircleAvatar(
|
||||
radius: radius / 3,
|
||||
|
@ -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:shared_preferences/shared_preferences.dart';
|
||||
import '../../android/state.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../../management/models.dart';
|
||||
import '../key_customization.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'device_avatar.dart';
|
||||
@ -34,6 +35,7 @@ final _hiddenDevicesProvider =
|
||||
class _HiddenDevicesNotifier extends StateNotifier<List<String>> {
|
||||
static const String _key = 'DEVICE_PICKER_HIDDEN';
|
||||
final SharedPreferences _prefs;
|
||||
|
||||
_HiddenDevicesNotifier(this._prefs) : super(_prefs.getStringList(_key) ?? []);
|
||||
|
||||
void showAll() {
|
||||
@ -49,7 +51,8 @@ class _HiddenDevicesNotifier extends StateNotifier<List<String>> {
|
||||
|
||||
class DevicePickerContent extends ConsumerWidget {
|
||||
final bool extended;
|
||||
const DevicePickerContent({super.key, this.extended = true});
|
||||
|
||||
const DevicePickerContent({required this.extended, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@ -188,6 +191,7 @@ class _DeviceRow extends StatelessWidget {
|
||||
final String subtitle;
|
||||
final bool extended;
|
||||
final bool selected;
|
||||
final Color? background;
|
||||
final void Function() onTap;
|
||||
|
||||
const _DeviceRow({
|
||||
@ -197,6 +201,7 @@ class _DeviceRow extends StatelessWidget {
|
||||
required this.subtitle,
|
||||
required this.extended,
|
||||
required this.selected,
|
||||
this.background,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@ -218,7 +223,8 @@ class _DeviceRow extends StatelessWidget {
|
||||
subtitle:
|
||||
Text(subtitle, overflow: TextOverflow.fade, softWrap: false),
|
||||
dense: true,
|
||||
tileColor: selected ? colorScheme.primary : null,
|
||||
tileColor:
|
||||
selected ? colorScheme.primary : background?.withOpacity(0.3),
|
||||
textColor: selected ? colorScheme.onPrimary : null,
|
||||
iconColor: selected ? colorScheme.onPrimary : null,
|
||||
onTap: onTap,
|
||||
@ -260,12 +266,35 @@ _DeviceRow _buildDeviceRow(
|
||||
: _getDeviceInfoString(context, info),
|
||||
nfcReader: (_, __) => l10n.s_select_to_scan,
|
||||
);
|
||||
|
||||
String displayName = node.name;
|
||||
Color? displayColor;
|
||||
if (info?.serial != null) {
|
||||
final properties = ref
|
||||
.read(keyCustomizationManagerProvider)
|
||||
.get(info?.serial?.toString())
|
||||
?.properties;
|
||||
var customizedName = properties?['display_name'];
|
||||
if (customizedName != null && customizedName != '') {
|
||||
displayName = customizedName + ' (${node.name})';
|
||||
}
|
||||
var displayColorCustomization = properties?['display_color'];
|
||||
if (displayColorCustomization != null) {
|
||||
displayColor = Color(int.parse(displayColorCustomization, radix: 16));
|
||||
}
|
||||
}
|
||||
|
||||
return _DeviceRow(
|
||||
key: ValueKey(node.path.key),
|
||||
leading: DeviceAvatar.deviceNode(node),
|
||||
title: node.name,
|
||||
leading: IconTheme(
|
||||
// Force the standard icon theme
|
||||
data: IconTheme.of(context),
|
||||
child: DeviceAvatar.deviceNode(node, ref),
|
||||
),
|
||||
title: displayName,
|
||||
subtitle: subtitle,
|
||||
extended: extended,
|
||||
background: displayColor,
|
||||
selected: false,
|
||||
onTap: () {
|
||||
ref.read(currentDeviceProvider.notifier).setCurrentDevice(node);
|
||||
@ -288,16 +317,37 @@ _DeviceRow _buildCurrentDeviceRow(
|
||||
final title = messages.removeAt(0);
|
||||
final subtitle = messages.join('\n');
|
||||
|
||||
String displayName = title;
|
||||
Color? displayColor;
|
||||
if (node is UsbYubiKeyNode) {
|
||||
if (node.info?.serial != null) {
|
||||
final properties = ref
|
||||
.read(keyCustomizationManagerProvider)
|
||||
.get(node.info?.serial.toString())
|
||||
?.properties;
|
||||
var customizedName = properties?['display_name'];
|
||||
if (customizedName != null && customizedName != '') {
|
||||
displayName = customizedName + ' (${node.name})';
|
||||
}
|
||||
var displayColorCustomization = properties?['display_color'];
|
||||
if (displayColorCustomization != null) {
|
||||
displayColor = Color(int.parse(displayColorCustomization, radix: 16));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _DeviceRow(
|
||||
key: keys.deviceInfoListTile,
|
||||
leading: data.maybeWhen(
|
||||
data: (data) =>
|
||||
DeviceAvatar.yubiKeyData(data, radius: extended ? null : 16),
|
||||
orElse: () => DeviceAvatar.deviceNode(node, radius: extended ? null : 16),
|
||||
DeviceAvatar.yubiKeyData(data, ref, radius: extended ? null : 16),
|
||||
orElse: () =>
|
||||
DeviceAvatar.deviceNode(node, ref, radius: extended ? null : 16),
|
||||
),
|
||||
title: title,
|
||||
title: displayName,
|
||||
subtitle: subtitle,
|
||||
extended: extended,
|
||||
background: displayColor,
|
||||
selected: true,
|
||||
onTap: () {},
|
||||
);
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Yubico.
|
||||
* Copyright (C) 2023-2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -111,6 +111,7 @@ extension on Application {
|
||||
class NavigationContent extends ConsumerWidget {
|
||||
final bool shouldPop;
|
||||
final bool extended;
|
||||
|
||||
const NavigationContent(
|
||||
{super.key, this.shouldPop = true, this.extended = false});
|
||||
|
||||
@ -181,6 +182,20 @@ class NavigationContent extends ConsumerWidget {
|
||||
},
|
||||
),
|
||||
],
|
||||
if (data.info.serial != null) ...[
|
||||
NavigationItem(
|
||||
leading: const Icon(Icons.settings_applications_sharp),
|
||||
title: 'Customize',
|
||||
collapsed: !extended,
|
||||
onTap: () {
|
||||
if (shouldPop) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
Actions.maybeInvoke(
|
||||
context, const KeyCustomizationIntent());
|
||||
},
|
||||
)
|
||||
],
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
],
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.
|
||||
@ -25,6 +25,7 @@ import 'package:logging/logging.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import '../app/key_customization.dart';
|
||||
import '../app/logging.dart';
|
||||
import '../app/models.dart';
|
||||
import '../app/state.dart';
|
||||
@ -215,5 +216,18 @@ class DesktopCurrentDeviceNotifier extends CurrentDeviceNotifier {
|
||||
setCurrentDevice(DeviceNode? device) {
|
||||
state = device;
|
||||
ref.read(prefProvider).setString(_lastDevice, device?.path.key ?? '');
|
||||
if (device != null &&
|
||||
device is UsbYubiKeyNode &&
|
||||
device.info?.serial != null) {
|
||||
final manager = ref.read(keyCustomizationManagerProvider);
|
||||
final customization = manager.get(device.info?.serial!.toString());
|
||||
String? displayColorCustomization =
|
||||
customization?.properties['display_color'];
|
||||
Color? displayColor;
|
||||
if (displayColorCustomization != null) {
|
||||
displayColor = Color(int.parse(displayColorCustomization, radix: 16));
|
||||
}
|
||||
ref.watch(darkThemeProvider.notifier).setPrimaryColor(displayColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user