mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-22 16:32:01 +03:00
use icon pack
This commit is contained in:
parent
47e577c947
commit
a456d813ac
179
lib/oath/issuer_icon_provider.dart
Normal file
179
lib/oath/issuer_icon_provider.dart
Normal file
@ -0,0 +1,179 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:vector_graphics/vector_graphics.dart';
|
||||
import 'package:vector_graphics_compiler/vector_graphics_compiler.dart';
|
||||
import 'package:yubico_authenticator/app/logging.dart';
|
||||
|
||||
final _log = Logger('issuer_icon_provider');
|
||||
|
||||
class IssuerIcon {
|
||||
final String filename;
|
||||
final String? category;
|
||||
final List<String> issuer;
|
||||
|
||||
const IssuerIcon(
|
||||
{required this.filename, required this.category, required this.issuer});
|
||||
}
|
||||
|
||||
class IssuerIconPack {
|
||||
final String uuid;
|
||||
final String name;
|
||||
final int version;
|
||||
final Directory directory;
|
||||
final List<IssuerIcon> icons;
|
||||
|
||||
const IssuerIconPack(
|
||||
{required this.uuid,
|
||||
required this.name,
|
||||
required this.version,
|
||||
required this.directory,
|
||||
required this.icons});
|
||||
}
|
||||
|
||||
class FileSystemCache {
|
||||
|
||||
late Directory cacheDirectory;
|
||||
|
||||
FileSystemCache();
|
||||
|
||||
void initialize() async {
|
||||
final documentsDirectory = await getApplicationDocumentsDirectory();
|
||||
cacheDirectory = Directory('${documentsDirectory.path}${Platform.pathSeparator}issuer_icons_cache${Platform.pathSeparator}');
|
||||
}
|
||||
|
||||
File _cachedFile(String fileName) => File('${cacheDirectory.path}${fileName}_cached');
|
||||
|
||||
Future<Uint8List?> getCachedFileData(String fileName) async {
|
||||
final file = _cachedFile(fileName);
|
||||
final exists = await file.exists();
|
||||
if (exists) {
|
||||
_log.debug('File $fileName exists in cache');
|
||||
} else {
|
||||
_log.debug('File $fileName does not exist in cache');
|
||||
}
|
||||
return (exists) ? file.readAsBytes() : null;
|
||||
}
|
||||
|
||||
Future<void> writeFileData(String fileName, Uint8List data) async {
|
||||
final file = _cachedFile(fileName);
|
||||
_log.debug('Storing $fileName to cache');
|
||||
if (!await file.exists()) {
|
||||
await file.create(recursive: true, exclusive: false);
|
||||
}
|
||||
await file.writeAsBytes(data, flush: true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CachingFileLoader extends BytesLoader {
|
||||
final File _file;
|
||||
final FileSystemCache _cache;
|
||||
|
||||
const CachingFileLoader(this._cache, this._file);
|
||||
|
||||
@override
|
||||
Future<ByteData> loadBytes(BuildContext? context) async {
|
||||
_log.debug('Reading ${_file.path}');
|
||||
|
||||
final cacheFileName = 'cache_${basename(_file.path)}';
|
||||
final cachedData = await _cache.getCachedFileData(cacheFileName);
|
||||
|
||||
if (cachedData != null) {
|
||||
return cachedData.buffer.asByteData();
|
||||
}
|
||||
|
||||
return await compute((File file) async {
|
||||
final fileData = await _file.readAsString();
|
||||
final TimelineTask task = TimelineTask()..start('encodeSvg');
|
||||
final Uint8List compiledBytes = encodeSvg(
|
||||
xml: fileData,
|
||||
debugName: _file.path,
|
||||
enableClippingOptimizer: false,
|
||||
enableMaskingOptimizer: false,
|
||||
enableOverdrawOptimizer: false,
|
||||
);
|
||||
task.finish();
|
||||
// for testing: await Future.delayed(const Duration(seconds: 5));
|
||||
|
||||
await _cache.writeFileData(cacheFileName, compiledBytes);
|
||||
|
||||
// sendAndExit will make sure this isn't copied.
|
||||
return compiledBytes.buffer.asByteData();
|
||||
}, _file, debugLabel: 'Load Bytes');
|
||||
}
|
||||
}
|
||||
|
||||
class IssuerIconProvider {
|
||||
final FileSystemCache _cache;
|
||||
late IssuerIconPack _issuerIconPack;
|
||||
|
||||
IssuerIconProvider(this._cache) {
|
||||
_cache.initialize();
|
||||
}
|
||||
|
||||
void readPack(String relativePackPath) async {
|
||||
final documentsDirectory = await getApplicationDocumentsDirectory();
|
||||
final packDirectory = Directory(
|
||||
'${documentsDirectory.path}${Platform.pathSeparator}$relativePackPath${Platform.pathSeparator}');
|
||||
final packFile = File('${packDirectory.path}pack.json');
|
||||
|
||||
_log.debug('Looking for file: ${packFile.path}');
|
||||
|
||||
if (!await packFile.exists()) {
|
||||
_log.debug('Failed to find icons pack ${packFile.path}');
|
||||
return;
|
||||
}
|
||||
|
||||
var packContent = await packFile.readAsString();
|
||||
Map<String, dynamic> pack = const JsonDecoder().convert(packContent);
|
||||
|
||||
final icons = List<IssuerIcon>.from(pack['icons'].map((icon) => IssuerIcon(
|
||||
filename: icon['filename'],
|
||||
category: icon['category'],
|
||||
issuer: List<String>.from(icon['issuer']))));
|
||||
|
||||
_issuerIconPack = IssuerIconPack(
|
||||
uuid: pack['uuid'],
|
||||
name: pack['name'],
|
||||
version: pack['version'],
|
||||
directory: packDirectory,
|
||||
icons: icons);
|
||||
_log.debug(
|
||||
'Parsed ${_issuerIconPack.name} with ${_issuerIconPack.icons.length} icons');
|
||||
}
|
||||
|
||||
VectorGraphic? issuerVectorGraphic(String issuer, Widget placeHolder) {
|
||||
final matching = _issuerIconPack.icons
|
||||
.where((element) => element.issuer.any((element) => element == issuer));
|
||||
final issuerImageFile = matching.isNotEmpty
|
||||
? File('${_issuerIconPack.directory.path}${matching.first.filename}')
|
||||
: null;
|
||||
return issuerImageFile != null && issuerImageFile.existsSync()
|
||||
? VectorGraphic(
|
||||
width: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.fill,
|
||||
loader: CachingFileLoader(_cache, issuerImageFile),
|
||||
placeholderBuilder: (BuildContext _) => placeHolder,
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
Image? issuerImage(String issuer) {
|
||||
final matching = _issuerIconPack.icons
|
||||
.where((element) => element.issuer.any((element) => element == issuer));
|
||||
return matching.isNotEmpty
|
||||
? Image.file(
|
||||
File(
|
||||
'${_issuerIconPack.directory.path}${matching.first.filename}.png'),
|
||||
filterQuality: FilterQuality.medium)
|
||||
: null;
|
||||
}
|
||||
}
|
@ -25,6 +25,7 @@ import '../app/models.dart';
|
||||
import '../app/state.dart';
|
||||
import '../core/models.dart';
|
||||
import '../core/state.dart';
|
||||
import 'issuer_icon_provider.dart';
|
||||
import 'models.dart';
|
||||
|
||||
final searchProvider =
|
||||
@ -201,3 +202,6 @@ class FilteredCredentialsNotifier extends StateNotifier<List<OathPair>> {
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
final issuerIconProvider = Provider<IssuerIconProvider>(
|
||||
(ref) => IssuerIconProvider(FileSystemCache()));
|
@ -153,6 +153,20 @@ class _AccountViewState extends ConsumerState<AccountView> {
|
||||
final showAvatar = constraints.maxWidth >= 315;
|
||||
|
||||
final subtitle = helper.subtitle;
|
||||
|
||||
final circleAvatar = CircleAvatar(
|
||||
foregroundColor: darkMode ? Colors.black : Colors.white,
|
||||
backgroundColor: _iconColor(darkMode ? 300 : 400),
|
||||
child: Text(
|
||||
(credential.issuer ?? credential.name)
|
||||
.characters
|
||||
.first
|
||||
.toUpperCase(),
|
||||
style:
|
||||
const TextStyle(fontSize: 16, fontWeight: FontWeight.w300),
|
||||
),
|
||||
);
|
||||
|
||||
return Shortcuts(
|
||||
shortcuts: {
|
||||
LogicalKeySet(LogicalKeyboardKey.enter): const OpenIntent(),
|
||||
@ -186,20 +200,15 @@ class _AccountViewState extends ConsumerState<AccountView> {
|
||||
onLongPress: () {
|
||||
Actions.maybeInvoke(context, const CopyIntent());
|
||||
},
|
||||
leading: showAvatar
|
||||
? CircleAvatar(
|
||||
foregroundColor: darkMode ? Colors.black : Colors.white,
|
||||
backgroundColor: _iconColor(darkMode ? 300 : 400),
|
||||
child: Text(
|
||||
(credential.issuer ?? credential.name)
|
||||
.characters
|
||||
.first
|
||||
.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.w300),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
leading: SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: showAvatar
|
||||
? ref
|
||||
.read(issuerIconProvider)
|
||||
.issuerVectorGraphic(credential.issuer ?? '', circleAvatar) ??
|
||||
circleAvatar
|
||||
: null),
|
||||
title: Text(
|
||||
helper.title,
|
||||
overflow: TextOverflow.fade,
|
||||
|
@ -14,6 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
@ -39,6 +41,8 @@ class OathScreen extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
|
||||
ref.read(issuerIconProvider).readPack('issuer_icons');
|
||||
return ref.watch(oathStateProvider(devicePath)).when(
|
||||
loading: () => MessagePage(
|
||||
title: Text(AppLocalizations.of(context)!.oath_authenticator),
|
||||
|
@ -6,6 +6,7 @@ import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import desktop_drop
|
||||
import path_provider_foundation
|
||||
import screen_retriever
|
||||
import shared_preferences_foundation
|
||||
import url_launcher_macos
|
||||
@ -13,6 +14,7 @@ import window_manager
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
|
@ -2,6 +2,9 @@ PODS:
|
||||
- desktop_drop (0.0.1):
|
||||
- FlutterMacOS
|
||||
- FlutterMacOS (1.0.0)
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- screen_retriever (0.0.1):
|
||||
- FlutterMacOS
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
@ -15,6 +18,7 @@ PODS:
|
||||
DEPENDENCIES:
|
||||
- desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos`)
|
||||
- screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`)
|
||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/macos`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
@ -25,6 +29,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos
|
||||
FlutterMacOS:
|
||||
:path: Flutter/ephemeral
|
||||
path_provider_foundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos
|
||||
screen_retriever:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos
|
||||
shared_preferences_foundation:
|
||||
@ -37,6 +43,7 @@ EXTERNAL SOURCES:
|
||||
SPEC CHECKSUMS:
|
||||
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
|
||||
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
|
||||
shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca
|
||||
url_launcher_macos: c04e4fa86382d4f94f6b38f14625708be3ae52e2
|
||||
|
76
pubspec.lock
76
pubspec.lock
@ -429,13 +429,45 @@ packages:
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.2"
|
||||
path_parsing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_parsing
|
||||
sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.12"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.22"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -460,6 +492,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -776,6 +816,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
vector_graphics:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: vector_graphics
|
||||
sha256: "09562ef5f47aa84f6567495adb6b9cb2a3192b82c352623b8bd00b300d62603b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
vector_graphics_codec:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_codec
|
||||
sha256: "886e57742644ebed024dc3ade29712e37eea1b03d294fb314c0a3386243fe5a6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
vector_graphics_compiler:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: vector_graphics_compiler
|
||||
sha256: "5d9010c4a292766c55395b2288532579a85673f8148460d1e233d98ffe10d24e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -840,6 +904,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.2"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -850,4 +922,4 @@ packages:
|
||||
version: "3.1.1"
|
||||
sdks:
|
||||
dart: ">=2.19.0 <3.0.0"
|
||||
flutter: ">=3.3.0"
|
||||
flutter: ">=3.5.0-0"
|
||||
|
@ -53,6 +53,10 @@ dependencies:
|
||||
path: android/flutter_plugins/qrscanner_zxing
|
||||
desktop_drop: ^0.4.0
|
||||
url_launcher: ^6.1.7
|
||||
path_provider: ^2.0.12
|
||||
vector_graphics: ^1.0.1
|
||||
vector_graphics_compiler: ^1.0.1
|
||||
path: ^1.8.2
|
||||
|
||||
dev_dependencies:
|
||||
integration_test:
|
||||
|
Loading…
Reference in New Issue
Block a user