mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-25 14:04:55 +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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.content.ContextCompat
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.google.android.material.color.DynamicColors
|
||||||
import com.yubico.authenticator.logging.FlutterLog
|
import com.yubico.authenticator.logging.FlutterLog
|
||||||
import com.yubico.authenticator.oath.AppLinkMethodChannel
|
import com.yubico.authenticator.oath.AppLinkMethodChannel
|
||||||
import com.yubico.authenticator.oath.OathManager
|
import com.yubico.authenticator.oath.OathManager
|
||||||
@ -383,6 +384,11 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
methodCall.arguments as Boolean,
|
methodCall.arguments as Boolean,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
"getPrimaryColor" -> result.success(
|
||||||
|
getPrimaryColor(this@MainActivity)
|
||||||
|
)
|
||||||
|
|
||||||
"getAndroidSdkVersion" -> result.success(
|
"getAndroidSdkVersion" -> result.success(
|
||||||
Build.VERSION.SDK_INT
|
Build.VERSION.SDK_INT
|
||||||
)
|
)
|
||||||
@ -456,6 +462,30 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
return FLAG_SECURE != (window.attributes.flags and FLAG_SECURE)
|
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")
|
@SuppressLint("SourceLockedOrientationActivity")
|
||||||
private fun forcePortraitOrientation() {
|
private fun forcePortraitOrientation() {
|
||||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
@ -465,5 +495,5 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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');
|
return await appMethodsChannel.invokeMethod('getAndroidSdkVersion');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int?> getPrimaryColor() async {
|
||||||
|
return await appMethodsChannel.invokeMethod('getPrimaryColor');
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> setPrimaryClip(String toClipboard, bool isSensitive) async {
|
Future<void> setPrimaryClip(String toClipboard, bool isSensitive) async {
|
||||||
await appMethodsChannel.invokeMethod('setPrimaryClip',
|
await appMethodsChannel.invokeMethod('setPrimaryClip',
|
||||||
{'toClipboard': toClipboard, 'isSensitive': isSensitive});
|
{'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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -51,6 +51,9 @@ Future<Widget> initialize() async {
|
|||||||
|
|
||||||
_initLicenses();
|
_initLicenses();
|
||||||
|
|
||||||
|
final primaryColor = await getPrimaryColor();
|
||||||
|
final primaryColorInt = primaryColor != null ? Color(primaryColor) : null;
|
||||||
|
|
||||||
return ProviderScope(
|
return ProviderScope(
|
||||||
overrides: [
|
overrides: [
|
||||||
supportedAppsProvider.overrideWith(implementedApps([
|
supportedAppsProvider.overrideWith(implementedApps([
|
||||||
@ -84,7 +87,8 @@ Future<Widget> initialize() async {
|
|||||||
androidNfcSupportProvider.overrideWithValue(await getHasNfc()),
|
androidNfcSupportProvider.overrideWithValue(await getHasNfc()),
|
||||||
supportedThemesProvider.overrideWith(
|
supportedThemesProvider.overrideWith(
|
||||||
(ref) => ref.watch(androidSupportedThemesProvider),
|
(ref) => ref.watch(androidSupportedThemesProvider),
|
||||||
)
|
),
|
||||||
|
primaryColorProvider.overrideWithValue(primaryColorInt),
|
||||||
],
|
],
|
||||||
child: DismissKeyboard(
|
child: DismissKeyboard(
|
||||||
child: YubicoAuthenticatorApp(page: Consumer(
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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_localizations/flutter_localizations.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../theme.dart';
|
|
||||||
import 'logging.dart';
|
import 'logging.dart';
|
||||||
import 'shortcuts.dart';
|
import 'shortcuts.dart';
|
||||||
import 'state.dart';
|
import 'state.dart';
|
||||||
|
|
||||||
class YubicoAuthenticatorApp extends StatelessWidget {
|
class YubicoAuthenticatorApp extends StatelessWidget {
|
||||||
final Widget page;
|
final Widget page;
|
||||||
|
|
||||||
const YubicoAuthenticatorApp({required this.page, super.key});
|
const YubicoAuthenticatorApp({required this.page, super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -34,8 +34,8 @@ class YubicoAuthenticatorApp extends StatelessWidget {
|
|||||||
child: Consumer(
|
child: Consumer(
|
||||||
builder: (context, ref, _) => MaterialApp(
|
builder: (context, ref, _) => MaterialApp(
|
||||||
title: ref.watch(l10nProvider).app_name,
|
title: ref.watch(l10nProvider).app_name,
|
||||||
theme: AppTheme.lightTheme,
|
theme: ref.watch(lightThemeProvider),
|
||||||
darkTheme: AppTheme.darkTheme,
|
darkTheme: ref.watch(darkThemeProvider),
|
||||||
themeMode: ref.watch(themeModeProvider),
|
themeMode: ref.watch(themeModeProvider),
|
||||||
home: page,
|
home: page,
|
||||||
debugShowCheckedModeBanner: false,
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -14,6 +14,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
@ -144,3 +146,15 @@ class WindowState with _$WindowState {
|
|||||||
@Default(false) bool hidden,
|
@Default(false) bool hidden,
|
||||||
}) = _WindowState;
|
}) = _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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -51,6 +51,10 @@ class NextDeviceIntent extends Intent {
|
|||||||
const NextDeviceIntent();
|
const NextDeviceIntent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class KeyCustomizationIntent extends Intent {
|
||||||
|
const KeyCustomizationIntent();
|
||||||
|
}
|
||||||
|
|
||||||
class SettingsIntent extends Intent {
|
class SettingsIntent extends Intent {
|
||||||
const SettingsIntent();
|
const SettingsIntent();
|
||||||
}
|
}
|
||||||
@ -61,26 +65,31 @@ class AboutIntent extends Intent {
|
|||||||
|
|
||||||
class OpenIntent<T> extends Intent {
|
class OpenIntent<T> extends Intent {
|
||||||
final T target;
|
final T target;
|
||||||
|
|
||||||
const OpenIntent(this.target);
|
const OpenIntent(this.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
class CopyIntent<T> extends Intent {
|
class CopyIntent<T> extends Intent {
|
||||||
final T target;
|
final T target;
|
||||||
|
|
||||||
const CopyIntent(this.target);
|
const CopyIntent(this.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
class EditIntent<T> extends Intent {
|
class EditIntent<T> extends Intent {
|
||||||
final T target;
|
final T target;
|
||||||
|
|
||||||
const EditIntent(this.target);
|
const EditIntent(this.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
class DeleteIntent<T> extends Intent {
|
class DeleteIntent<T> extends Intent {
|
||||||
final T target;
|
final T target;
|
||||||
|
|
||||||
const DeleteIntent(this.target);
|
const DeleteIntent(this.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
class RefreshIntent<T> extends Intent {
|
class RefreshIntent<T> extends Intent {
|
||||||
final T target;
|
final T target;
|
||||||
|
|
||||||
const RefreshIntent(this.target);
|
const RefreshIntent(this.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,6 +101,7 @@ SingleActivator ctrlOrCmd(LogicalKeyboardKey key) =>
|
|||||||
class ItemShortcuts<T> extends StatelessWidget {
|
class ItemShortcuts<T> extends StatelessWidget {
|
||||||
final T item;
|
final T item;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const ItemShortcuts({super.key, required this.item, required this.child});
|
const ItemShortcuts({super.key, required this.item, required this.child});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -112,6 +122,7 @@ class ItemShortcuts<T> extends StatelessWidget {
|
|||||||
/// Global keyboard shortcuts
|
/// Global keyboard shortcuts
|
||||||
class GlobalShortcuts extends ConsumerWidget {
|
class GlobalShortcuts extends ConsumerWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const GlobalShortcuts({super.key, required this.child});
|
const GlobalShortcuts({super.key, required this.child});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2022 Yubico.
|
* Copyright (C) 2022,2024 Yubico.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../core/state.dart';
|
import '../core/state.dart';
|
||||||
|
import '../theme.dart';
|
||||||
import 'features.dart' as features;
|
import 'features.dart' as features;
|
||||||
import 'logging.dart';
|
import 'logging.dart';
|
||||||
import 'models.dart';
|
import 'models.dart';
|
||||||
@ -139,6 +140,42 @@ class ThemeModeNotifier extends StateNotifier<ThemeMode> {
|
|||||||
orElse: () => supportedThemes.first);
|
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
|
// Override with platform implementation
|
||||||
final attachedDevicesProvider =
|
final attachedDevicesProvider =
|
||||||
NotifierProvider<AttachedDevicesNotifier, List<DeviceNode>>(
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 '../state.dart';
|
||||||
import 'keys.dart';
|
import 'keys.dart';
|
||||||
|
|
||||||
class DeviceAvatar extends StatelessWidget {
|
class DeviceAvatar extends ConsumerWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
final Widget? decoration;
|
||||||
|
final Color? backgroundColor;
|
||||||
final Widget? badge;
|
final Widget? badge;
|
||||||
final double? radius;
|
final double? radius;
|
||||||
const DeviceAvatar({super.key, required this.child, this.badge, this.radius});
|
|
||||||
|
|
||||||
factory DeviceAvatar.yubiKeyData(YubiKeyData data, {double? radius}) =>
|
const DeviceAvatar(
|
||||||
DeviceAvatar(
|
{super.key,
|
||||||
badge: isDesktop && data.node is NfcReaderNode ? nfcIcon : null,
|
required this.child,
|
||||||
radius: radius,
|
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(
|
child: ProductImage(
|
||||||
name: data.name,
|
name: data.name,
|
||||||
formFactor: data.info.formFactor,
|
formFactor: data.info.formFactor,
|
||||||
isNfc: data.info.supportedCapabilities.containsKey(Transport.nfc)),
|
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(
|
node.map(
|
||||||
usbYubiKey: (node) {
|
usbYubiKey: (node) {
|
||||||
final info = node.info;
|
final info = node.info;
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
return DeviceAvatar.yubiKeyData(
|
return DeviceAvatar.yubiKeyData(
|
||||||
YubiKeyData(node, node.name, info),
|
YubiKeyData(node, node.name, info),
|
||||||
|
ref,
|
||||||
radius: radius,
|
radius: radius,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -73,10 +90,12 @@ class DeviceAvatar extends StatelessWidget {
|
|||||||
return ref.watch(currentDeviceDataProvider).maybeWhen(
|
return ref.watch(currentDeviceDataProvider).maybeWhen(
|
||||||
data: (data) => DeviceAvatar.yubiKeyData(
|
data: (data) => DeviceAvatar.yubiKeyData(
|
||||||
data,
|
data,
|
||||||
|
ref,
|
||||||
radius: radius,
|
radius: radius,
|
||||||
),
|
),
|
||||||
orElse: () => DeviceAvatar.deviceNode(
|
orElse: () => DeviceAvatar.deviceNode(
|
||||||
deviceNode,
|
deviceNode,
|
||||||
|
ref,
|
||||||
radius: radius,
|
radius: radius,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -90,14 +109,15 @@ class DeviceAvatar extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final radius = this.radius ?? 20;
|
final radius = this.radius ?? 20;
|
||||||
return Stack(
|
return Stack(
|
||||||
alignment: AlignmentDirectional.bottomEnd,
|
alignment: AlignmentDirectional.bottomEnd,
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: radius,
|
radius: radius,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor:
|
||||||
|
backgroundColor ?? Theme.of(context).colorScheme.surfaceVariant,
|
||||||
child: IconTheme(
|
child: IconTheme(
|
||||||
data: IconTheme.of(context).copyWith(
|
data: IconTheme.of(context).copyWith(
|
||||||
size: radius,
|
size: radius,
|
||||||
@ -105,6 +125,7 @@ class DeviceAvatar extends StatelessWidget {
|
|||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (decoration != null) decoration!,
|
||||||
if (badge != null)
|
if (badge != null)
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: radius / 3,
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 '../../android/state.dart';
|
||||||
import '../../core/state.dart';
|
import '../../core/state.dart';
|
||||||
import '../../management/models.dart';
|
import '../../management/models.dart';
|
||||||
|
import '../key_customization.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import 'device_avatar.dart';
|
import 'device_avatar.dart';
|
||||||
@ -34,6 +35,7 @@ final _hiddenDevicesProvider =
|
|||||||
class _HiddenDevicesNotifier extends StateNotifier<List<String>> {
|
class _HiddenDevicesNotifier extends StateNotifier<List<String>> {
|
||||||
static const String _key = 'DEVICE_PICKER_HIDDEN';
|
static const String _key = 'DEVICE_PICKER_HIDDEN';
|
||||||
final SharedPreferences _prefs;
|
final SharedPreferences _prefs;
|
||||||
|
|
||||||
_HiddenDevicesNotifier(this._prefs) : super(_prefs.getStringList(_key) ?? []);
|
_HiddenDevicesNotifier(this._prefs) : super(_prefs.getStringList(_key) ?? []);
|
||||||
|
|
||||||
void showAll() {
|
void showAll() {
|
||||||
@ -49,7 +51,8 @@ class _HiddenDevicesNotifier extends StateNotifier<List<String>> {
|
|||||||
|
|
||||||
class DevicePickerContent extends ConsumerWidget {
|
class DevicePickerContent extends ConsumerWidget {
|
||||||
final bool extended;
|
final bool extended;
|
||||||
const DevicePickerContent({super.key, this.extended = true});
|
|
||||||
|
const DevicePickerContent({required this.extended, super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@ -188,6 +191,7 @@ class _DeviceRow extends StatelessWidget {
|
|||||||
final String subtitle;
|
final String subtitle;
|
||||||
final bool extended;
|
final bool extended;
|
||||||
final bool selected;
|
final bool selected;
|
||||||
|
final Color? background;
|
||||||
final void Function() onTap;
|
final void Function() onTap;
|
||||||
|
|
||||||
const _DeviceRow({
|
const _DeviceRow({
|
||||||
@ -197,6 +201,7 @@ class _DeviceRow extends StatelessWidget {
|
|||||||
required this.subtitle,
|
required this.subtitle,
|
||||||
required this.extended,
|
required this.extended,
|
||||||
required this.selected,
|
required this.selected,
|
||||||
|
this.background,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -218,7 +223,8 @@ class _DeviceRow extends StatelessWidget {
|
|||||||
subtitle:
|
subtitle:
|
||||||
Text(subtitle, overflow: TextOverflow.fade, softWrap: false),
|
Text(subtitle, overflow: TextOverflow.fade, softWrap: false),
|
||||||
dense: true,
|
dense: true,
|
||||||
tileColor: selected ? colorScheme.primary : null,
|
tileColor:
|
||||||
|
selected ? colorScheme.primary : background?.withOpacity(0.3),
|
||||||
textColor: selected ? colorScheme.onPrimary : null,
|
textColor: selected ? colorScheme.onPrimary : null,
|
||||||
iconColor: selected ? colorScheme.onPrimary : null,
|
iconColor: selected ? colorScheme.onPrimary : null,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
@ -260,12 +266,35 @@ _DeviceRow _buildDeviceRow(
|
|||||||
: _getDeviceInfoString(context, info),
|
: _getDeviceInfoString(context, info),
|
||||||
nfcReader: (_, __) => l10n.s_select_to_scan,
|
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(
|
return _DeviceRow(
|
||||||
key: ValueKey(node.path.key),
|
key: ValueKey(node.path.key),
|
||||||
leading: DeviceAvatar.deviceNode(node),
|
leading: IconTheme(
|
||||||
title: node.name,
|
// Force the standard icon theme
|
||||||
|
data: IconTheme.of(context),
|
||||||
|
child: DeviceAvatar.deviceNode(node, ref),
|
||||||
|
),
|
||||||
|
title: displayName,
|
||||||
subtitle: subtitle,
|
subtitle: subtitle,
|
||||||
extended: extended,
|
extended: extended,
|
||||||
|
background: displayColor,
|
||||||
selected: false,
|
selected: false,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ref.read(currentDeviceProvider.notifier).setCurrentDevice(node);
|
ref.read(currentDeviceProvider.notifier).setCurrentDevice(node);
|
||||||
@ -288,16 +317,37 @@ _DeviceRow _buildCurrentDeviceRow(
|
|||||||
final title = messages.removeAt(0);
|
final title = messages.removeAt(0);
|
||||||
final subtitle = messages.join('\n');
|
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(
|
return _DeviceRow(
|
||||||
key: keys.deviceInfoListTile,
|
key: keys.deviceInfoListTile,
|
||||||
leading: data.maybeWhen(
|
leading: data.maybeWhen(
|
||||||
data: (data) =>
|
data: (data) =>
|
||||||
DeviceAvatar.yubiKeyData(data, radius: extended ? null : 16),
|
DeviceAvatar.yubiKeyData(data, ref, radius: extended ? null : 16),
|
||||||
orElse: () => DeviceAvatar.deviceNode(node, radius: extended ? null : 16),
|
orElse: () =>
|
||||||
|
DeviceAvatar.deviceNode(node, ref, radius: extended ? null : 16),
|
||||||
),
|
),
|
||||||
title: title,
|
title: displayName,
|
||||||
subtitle: subtitle,
|
subtitle: subtitle,
|
||||||
extended: extended,
|
extended: extended,
|
||||||
|
background: displayColor,
|
||||||
selected: true,
|
selected: true,
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2023 Yubico.
|
* Copyright (C) 2023-2024 Yubico.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 {
|
class NavigationContent extends ConsumerWidget {
|
||||||
final bool shouldPop;
|
final bool shouldPop;
|
||||||
final bool extended;
|
final bool extended;
|
||||||
|
|
||||||
const NavigationContent(
|
const NavigationContent(
|
||||||
{super.key, this.shouldPop = true, this.extended = false});
|
{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),
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
|
import '../app/key_customization.dart';
|
||||||
import '../app/logging.dart';
|
import '../app/logging.dart';
|
||||||
import '../app/models.dart';
|
import '../app/models.dart';
|
||||||
import '../app/state.dart';
|
import '../app/state.dart';
|
||||||
@ -215,5 +216,18 @@ class DesktopCurrentDeviceNotifier extends CurrentDeviceNotifier {
|
|||||||
setCurrentDevice(DeviceNode? device) {
|
setCurrentDevice(DeviceNode? device) {
|
||||||
state = device;
|
state = device;
|
||||||
ref.read(prefProvider).setString(_lastDevice, device?.path.key ?? '');
|
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