use icon pack

This commit is contained in:
Adam Velebil 2023-02-17 15:15:20 +01:00
parent 47e577c947
commit a456d813ac
No known key found for this signature in database
GPG Key ID: C9B1E4A3CBBD2E10
8 changed files with 297 additions and 16 deletions

View 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;
}
}

View File

@ -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()));

View File

@ -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,

View File

@ -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),

View File

@ -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"))

View File

@ -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

View File

@ -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"

View File

@ -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: