diff --git a/lib/android/views/android_settings_page.dart b/lib/android/views/android_settings_page.dart index fadbf301..e92e1cab 100755 --- a/lib/android/views/android_settings_page.dart +++ b/lib/android/views/android_settings_page.dart @@ -17,7 +17,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:yubico_authenticator/oath/icon_provider/icon_pack_settings.dart'; import '../../app/state.dart'; import '../../core/state.dart'; @@ -169,8 +168,7 @@ class _AndroidSettingsPageState extends ConsumerState { SwitchListTile( title: const Text('Silence NFC sounds'), subtitle: nfcSilenceSounds - ? const Text( - 'No sounds will be played on NFC tap') + ? const Text('No sounds will be played on NFC tap') : const Text('Sound will play on NFC tap'), value: nfcSilenceSounds, key: keys.nfcSilenceSoundsSettings, @@ -204,7 +202,6 @@ class _AndroidSettingsPageState extends ConsumerState { ref.read(themeModeProvider.notifier).setThemeMode(newMode); }, ), - const IconPackSettings() ], ), ), diff --git a/lib/oath/icon_provider/icon_pack_dialog.dart b/lib/oath/icon_provider/icon_pack_dialog.dart new file mode 100644 index 00000000..3b360cc3 --- /dev/null +++ b/lib/oath/icon_provider/icon_pack_dialog.dart @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2023 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:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:yubico_authenticator/app/message.dart'; +import 'package:yubico_authenticator/app/state.dart'; +import 'package:yubico_authenticator/oath/icon_provider/icon_pack_manager.dart'; +import 'package:yubico_authenticator/widgets/responsive_dialog.dart'; + +class IconPackDialog extends ConsumerWidget { + const IconPackDialog({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + final packManager = ref.watch(iconPackManager); + final hasIconPack = packManager.hasIconPack; + + return ResponsiveDialog( + title: const Text('Manage icons'), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('By loading an external icon pack, the avatar icons ' + 'of the accounts will be easier to distinguish throught the issuers ' + 'familiar logos and colors.\n\n' + 'We recommend the Aegis icon packs which can be downloaded ' + 'from below.'), + TextButton( + child: const Text( + 'https://aegis-icons.github.io/', + style: TextStyle(decoration: TextDecoration.underline), + ), + onPressed: () async { + await launchUrl( + Uri.parse('https://aegis-icons.github.io/'), + mode: LaunchMode.externalApplication, + ); + }, + ), + const SizedBox(height: 8), + Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ + OutlinedButton( + onPressed: () async { + await _importIconPack(context, ref); + }, + child: Row(mainAxisSize: MainAxisSize.min, children: [ + const Icon(Icons.download, size: 16), + const SizedBox(width: 4), + hasIconPack + ? const Text('Replace icon pack') + : const Text('Load icon pack') + ]), + ), + if (hasIconPack) + OutlinedButton( + onPressed: () async { + final removePackStatus = + await ref.read(iconPackManager).removePack(); + await ref.read(withContextProvider)( + (context) async { + if (removePackStatus) { + showMessage(context, 'Icon pack removed'); + } else { + showMessage(context, 'Error removing icon pack'); + } + Navigator.pop(context); + }, + ); + }, + child: Row(mainAxisSize: MainAxisSize.min, children: const [ + Icon(Icons.delete_rounded, size: 16), + SizedBox(width: 4), + Text('Remove icon pack') + ]), + ) + ]), + const SizedBox(height: 16), + if (hasIconPack) + Text( + 'Loaded: ${packManager.iconPackName} (version: ${packManager.iconPackVersion})', + style: + TextStyle(fontSize: 11, color: theme.colorScheme.primary), + ) + else + const Text('') + ] + .map((e) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: e, + )) + .toList(), + ), + ), + ); + } + + Future _importIconPack(BuildContext context, WidgetRef ref) async { + final result = await FilePicker.platform.pickFiles( + allowedExtensions: ['zip'], + type: FileType.custom, + allowMultiple: false, + lockParentWindow: true, + dialogTitle: 'Choose icon pack'); + if (result != null && result.files.isNotEmpty) { + final importStatus = + await ref.read(iconPackManager).importPack(result.paths.first!); + + await ref.read(withContextProvider)((context) async { + if (importStatus) { + showMessage(context, 'Icon pack imported'); + } else { + showMessage(context, + 'Error importing icon pack: ${ref.read(iconPackManager).lastError}'); + } + }); + } + + return false; + } +} diff --git a/lib/oath/icon_provider/icon_pack_settings.dart b/lib/oath/icon_provider/icon_pack_settings.dart deleted file mode 100644 index 5951265e..00000000 --- a/lib/oath/icon_provider/icon_pack_settings.dart +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (C) 2023 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:file_picker/file_picker.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:yubico_authenticator/app/message.dart'; -import 'package:yubico_authenticator/app/state.dart'; -import 'package:yubico_authenticator/oath/icon_provider/icon_pack_manager.dart'; -import 'package:yubico_authenticator/oath/state.dart'; -import 'package:yubico_authenticator/widgets/list_title.dart'; - -class IconPackSettings extends ConsumerWidget { - const IconPackSettings({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final packManager = ref.watch(iconPackManager); - final hasIconPack = packManager.hasIconPack; - final theme = Theme.of(context); - - return Column(children: [ - const ListTitle('Account icon pack'), - ListTile( - title: hasIconPack - ? const Text('Icon pack imported') - : const Text('Not using icon pack'), - subtitle: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - hasIconPack - ? Row( - children: [ - const Text('Name: ', style: TextStyle(fontSize: 11)), - Text( - '${packManager.iconPackName} (version: ${packManager.iconPackVersion})', - style: TextStyle( - fontSize: 11, color: theme.colorScheme.primary)), - ], - ) - : const Text('Tap to import', style: TextStyle(fontSize: 10)), - ], - ), - trailing: IconButton( - icon: const Icon(Icons.info_outline_rounded), - onPressed: () async { - await _showIconPackInfo(context, ref); - }, - ), - onTap: () async { - if (hasIconPack) { - await _removeOrChangeIconPack(context, ref); - } else { - await _importIconPack(context, ref); - } - - await ref.read(withContextProvider)((context) async { - ref.invalidate(credentialsProvider); - }); - }, - ), - ]); - } - - Future _importIconPack(BuildContext context, WidgetRef ref) async { - final result = await FilePicker.platform.pickFiles( - allowedExtensions: ['zip'], - type: FileType.custom, - allowMultiple: false, - lockParentWindow: true, - dialogTitle: 'Choose icon pack'); - if (result != null && result.files.isNotEmpty) { - final importStatus = - await ref.read(iconPackManager).importPack(result.paths.first!); - - await ref.read(withContextProvider)((context) async { - if (importStatus) { - showMessage(context, 'Icon pack imported'); - } else { - showMessage(context, - 'Error importing icon pack: ${ref.read(iconPackManager).lastError}'); - } - }); - } - - return false; - } - - Future _removeOrChangeIconPack( - BuildContext context, WidgetRef ref) async => - await showDialog( - context: context, - builder: (BuildContext context) { - return SimpleDialog( - children: [ - ListTile( - title: const Text('Replace icon pack'), - onTap: () async { - await _importIconPack(context, ref); - await ref.read(withContextProvider)((context) async { - Navigator.pop(context); - }); - }), - ListTile( - title: const Text('Remove icon pack'), - onTap: () async { - final removePackStatus = - await ref.read(iconPackManager).removePack(); - await ref.read(withContextProvider)( - (context) async { - if (removePackStatus) { - showMessage(context, 'Icon pack removed'); - } else { - showMessage(context, 'Error removing icon pack'); - } - Navigator.pop(context); - }, - ); - }), - ], - ); - }); - - Future _showIconPackInfo(BuildContext context, WidgetRef ref) async => - await showDialog( - context: context, - builder: (BuildContext context) { - return const SimpleDialog( - children: [ - ListTile( - title: Text('About icon packs'), - subtitle: Text('Icon packs contain icons for accounts. ' - 'To use an icon-pack, download and import one\n\n' - 'The supported format is aegis-icons.'), - ) - ], - ); - }); -} diff --git a/lib/oath/views/account_icon.dart b/lib/oath/views/account_icon.dart index 2c07dfd2..fd5c0ed7 100644 --- a/lib/oath/views/account_icon.dart +++ b/lib/oath/views/account_icon.dart @@ -17,7 +17,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:vector_graphics/vector_graphics.dart'; +import 'package:yubico_authenticator/app/message.dart'; import 'package:yubico_authenticator/oath/icon_provider/icon_file_loader.dart'; +import 'package:yubico_authenticator/oath/icon_provider/icon_pack_dialog.dart'; import 'package:yubico_authenticator/oath/icon_provider/icon_pack_manager.dart'; import 'package:yubico_authenticator/widgets/delayed_visibility.dart'; @@ -34,7 +36,7 @@ class AccountIcon extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final issuerImageFile = ref.watch(iconPackManager).getFileForIssuer(issuer); - return issuerImageFile != null + final issuerWidget = issuerImageFile != null ? VectorGraphic( width: 40, height: 40, @@ -53,5 +55,16 @@ class AccountIcon extends ConsumerWidget { ); }) : defaultWidget; + return IconButton( + onPressed: () async { + await showBlurDialog( + context: context, + builder: (context) => const IconPackDialog(), + ); + }, + icon: issuerWidget, + tooltip: 'Select icon', + padding: const EdgeInsets.all(1.0), + ); } } diff --git a/lib/settings_page.dart b/lib/settings_page.dart index 6bb4afbb..72878ba1 100755 --- a/lib/settings_page.dart +++ b/lib/settings_page.dart @@ -18,7 +18,6 @@ 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 'package:yubico_authenticator/oath/icon_provider/icon_pack_settings.dart'; import 'app/logging.dart'; import 'app/state.dart'; @@ -78,7 +77,6 @@ class SettingsPage extends ConsumerWidget { _log.debug('Set theme mode to $mode'); }, ), - const IconPackSettings() ], ), ),