mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 02:01:36 +03:00
Implement theming and re-arrange views.
This commit is contained in:
parent
4c875865a8
commit
9236701f02
@ -37,7 +37,7 @@ class AboutPage extends ConsumerWidget {
|
|||||||
ref.read(logLevelProvider.notifier).setLevel(Level.INFO);
|
ref.read(logLevelProvider.notifier).setLevel(Level.INFO);
|
||||||
log.info('Log level changed to INFO');
|
log.info('Log level changed to INFO');
|
||||||
},
|
},
|
||||||
child: const Text('INFO'),
|
child: const Text('Info'),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@ -46,10 +46,18 @@ class AboutPage extends ConsumerWidget {
|
|||||||
.setLevel(Level.CONFIG);
|
.setLevel(Level.CONFIG);
|
||||||
log.config('Log level changed to CONFIG');
|
log.config('Log level changed to CONFIG');
|
||||||
},
|
},
|
||||||
child: const Text('DEBUG'),
|
child: const Text('Config'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
ref.read(logLevelProvider.notifier).setLevel(Level.FINE);
|
||||||
|
log.fine('Log level changed to FINE');
|
||||||
|
},
|
||||||
|
child: const Text('Fine'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const Divider(),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
log.info('Running diagnostics...');
|
log.info('Running diagnostics...');
|
||||||
|
18
lib/app.dart
18
lib/app.dart
@ -1,18 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class YubicoAuthenticatorApp extends StatelessWidget {
|
|
||||||
final Widget page;
|
|
||||||
const YubicoAuthenticatorApp({required this.page, Key? key})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return MaterialApp(
|
|
||||||
title: 'Yubico Authenticator',
|
|
||||||
theme: ThemeData(
|
|
||||||
primarySwatch: Colors.blue,
|
|
||||||
),
|
|
||||||
home: page,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
23
lib/app/app.dart
Executable file
23
lib/app/app.dart
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'state.dart';
|
||||||
|
import '../theme.dart';
|
||||||
|
|
||||||
|
class YubicoAuthenticatorApp extends ConsumerWidget {
|
||||||
|
final Widget page;
|
||||||
|
const YubicoAuthenticatorApp({required this.page, Key? key})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return MaterialApp(
|
||||||
|
title: 'Yubico Authenticator',
|
||||||
|
theme: AppTheme.lightTheme,
|
||||||
|
darkTheme: AppTheme.darkTheme,
|
||||||
|
themeMode: ref.watch(themeModeProvider),
|
||||||
|
home: page,
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import '../../management/models.dart';
|
import '../../management/models.dart';
|
||||||
|
|
||||||
@ -19,3 +20,11 @@ class DeviceNode with _$DeviceNode {
|
|||||||
factory DeviceNode.fromJson(Map<String, dynamic> json) =>
|
factory DeviceNode.fromJson(Map<String, dynamic> json) =>
|
||||||
_$DeviceNodeFromJson(json);
|
_$DeviceNodeFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class MenuAction with _$MenuAction {
|
||||||
|
factory MenuAction(
|
||||||
|
{required String text,
|
||||||
|
required Icon icon,
|
||||||
|
void Function()? action}) = _MenuAction;
|
||||||
|
}
|
||||||
|
@ -251,3 +251,166 @@ abstract class _DeviceNode implements DeviceNode {
|
|||||||
_$DeviceNodeCopyWith<_DeviceNode> get copyWith =>
|
_$DeviceNodeCopyWith<_DeviceNode> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$MenuActionTearOff {
|
||||||
|
const _$MenuActionTearOff();
|
||||||
|
|
||||||
|
_MenuAction call(
|
||||||
|
{required String text, required Icon icon, void Function()? action}) {
|
||||||
|
return _MenuAction(
|
||||||
|
text: text,
|
||||||
|
icon: icon,
|
||||||
|
action: action,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
const $MenuAction = _$MenuActionTearOff();
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$MenuAction {
|
||||||
|
String get text => throw _privateConstructorUsedError;
|
||||||
|
Icon get icon => throw _privateConstructorUsedError;
|
||||||
|
void Function()? get action => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
$MenuActionCopyWith<MenuAction> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $MenuActionCopyWith<$Res> {
|
||||||
|
factory $MenuActionCopyWith(
|
||||||
|
MenuAction value, $Res Function(MenuAction) then) =
|
||||||
|
_$MenuActionCopyWithImpl<$Res>;
|
||||||
|
$Res call({String text, Icon icon, void Function()? action});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$MenuActionCopyWithImpl<$Res> implements $MenuActionCopyWith<$Res> {
|
||||||
|
_$MenuActionCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
final MenuAction _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function(MenuAction) _then;
|
||||||
|
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? text = freezed,
|
||||||
|
Object? icon = freezed,
|
||||||
|
Object? action = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
text: text == freezed
|
||||||
|
? _value.text
|
||||||
|
: text // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
icon: icon == freezed
|
||||||
|
? _value.icon
|
||||||
|
: icon // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Icon,
|
||||||
|
action: action == freezed
|
||||||
|
? _value.action
|
||||||
|
: action // ignore: cast_nullable_to_non_nullable
|
||||||
|
as void Function()?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$MenuActionCopyWith<$Res> implements $MenuActionCopyWith<$Res> {
|
||||||
|
factory _$MenuActionCopyWith(
|
||||||
|
_MenuAction value, $Res Function(_MenuAction) then) =
|
||||||
|
__$MenuActionCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
$Res call({String text, Icon icon, void Function()? action});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$MenuActionCopyWithImpl<$Res> extends _$MenuActionCopyWithImpl<$Res>
|
||||||
|
implements _$MenuActionCopyWith<$Res> {
|
||||||
|
__$MenuActionCopyWithImpl(
|
||||||
|
_MenuAction _value, $Res Function(_MenuAction) _then)
|
||||||
|
: super(_value, (v) => _then(v as _MenuAction));
|
||||||
|
|
||||||
|
@override
|
||||||
|
_MenuAction get _value => super._value as _MenuAction;
|
||||||
|
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? text = freezed,
|
||||||
|
Object? icon = freezed,
|
||||||
|
Object? action = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_MenuAction(
|
||||||
|
text: text == freezed
|
||||||
|
? _value.text
|
||||||
|
: text // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
icon: icon == freezed
|
||||||
|
? _value.icon
|
||||||
|
: icon // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Icon,
|
||||||
|
action: action == freezed
|
||||||
|
? _value.action
|
||||||
|
: action // ignore: cast_nullable_to_non_nullable
|
||||||
|
as void Function()?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
|
||||||
|
class _$_MenuAction implements _MenuAction {
|
||||||
|
_$_MenuAction({required this.text, required this.icon, this.action});
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String text;
|
||||||
|
@override
|
||||||
|
final Icon icon;
|
||||||
|
@override
|
||||||
|
final void Function()? action;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'MenuAction(text: $text, icon: $icon, action: $action)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(dynamic other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _MenuAction &&
|
||||||
|
(identical(other.text, text) || other.text == text) &&
|
||||||
|
(identical(other.icon, icon) || other.icon == icon) &&
|
||||||
|
(identical(other.action, action) || other.action == action));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType, text, icon, action);
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
_$MenuActionCopyWith<_MenuAction> get copyWith =>
|
||||||
|
__$MenuActionCopyWithImpl<_MenuAction>(this, _$identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _MenuAction implements MenuAction {
|
||||||
|
factory _MenuAction(
|
||||||
|
{required String text,
|
||||||
|
required Icon icon,
|
||||||
|
void Function()? action}) = _$_MenuAction;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get text;
|
||||||
|
@override
|
||||||
|
Icon get icon;
|
||||||
|
@override
|
||||||
|
void Function()? get action;
|
||||||
|
@override
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
_$MenuActionCopyWith<_MenuAction> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
@ -1,17 +1,55 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../../core/rpc.dart';
|
import '../core/models.dart';
|
||||||
import '../../core/state.dart';
|
import '../core/state.dart';
|
||||||
|
import '../core/rpc.dart';
|
||||||
|
import '../oath/menu_actions.dart';
|
||||||
import 'models.dart';
|
import 'models.dart';
|
||||||
|
|
||||||
final log = Logger('app.state');
|
final log = Logger('app.state');
|
||||||
|
|
||||||
|
final themeModeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>(
|
||||||
|
(ref) => ThemeModeNotifier(ref.watch(prefProvider)));
|
||||||
|
|
||||||
|
class ThemeModeNotifier extends StateNotifier<ThemeMode> {
|
||||||
|
static const String _key = 'APP_STATE_THEME';
|
||||||
|
final SharedPreferences _prefs;
|
||||||
|
ThemeModeNotifier(this._prefs) : super(_fromName(_prefs.getString(_key)));
|
||||||
|
|
||||||
|
void setThemeMode(ThemeMode mode) {
|
||||||
|
state = mode;
|
||||||
|
_prefs.setString(_key, mode.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ThemeMode _fromName(String? name) {
|
||||||
|
switch (name) {
|
||||||
|
case 'light':
|
||||||
|
return ThemeMode.light;
|
||||||
|
case 'dark':
|
||||||
|
return ThemeMode.dark;
|
||||||
|
default:
|
||||||
|
return ThemeMode.system;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final searchProvider =
|
||||||
|
StateNotifierProvider<SearchNotifier, String>((ref) => SearchNotifier());
|
||||||
|
|
||||||
|
class SearchNotifier extends StateNotifier<String> {
|
||||||
|
SearchNotifier() : super('');
|
||||||
|
|
||||||
|
setFilter(String value) {
|
||||||
|
state = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final attachedDevicesProvider =
|
final attachedDevicesProvider =
|
||||||
StateNotifierProvider<AttachedDeviceNotifier, List<DeviceNode>>(
|
StateNotifierProvider<AttachedDeviceNotifier, List<DeviceNode>>(
|
||||||
(ref) => AttachedDeviceNotifier(ref.watch(rpcProvider)));
|
(ref) => AttachedDeviceNotifier(ref.watch(rpcProvider)));
|
||||||
@ -31,25 +69,29 @@ class AttachedDeviceNotifier extends StateNotifier<List<DeviceNode>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _pollUsb() async {
|
void _pollUsb() async {
|
||||||
|
try {
|
||||||
var scan = await _rpc.command('scan', ['usb']);
|
var scan = await _rpc.command('scan', ['usb']);
|
||||||
|
|
||||||
if (_usbState != scan['state']) {
|
if (_usbState != scan['state'] || state.length != scan['pids'].length) {
|
||||||
var usbResult = await _rpc.command('get', ['usb']);
|
var usbResult = await _rpc.command('get', ['usb']);
|
||||||
log.info('USB state change', jsonEncode(usbResult));
|
log.info('USB state change', jsonEncode(usbResult));
|
||||||
|
|
||||||
_usbState = usbResult['data']['state'];
|
|
||||||
|
|
||||||
List<DeviceNode> devices = [];
|
List<DeviceNode> devices = [];
|
||||||
for (String id in (usbResult['children'] as Map).keys) {
|
for (String id in (usbResult['children'] as Map).keys) {
|
||||||
var path = ['usb', id];
|
var path = ['usb', id];
|
||||||
var deviceResult = await _rpc.command('get', path);
|
var deviceResult = await _rpc.command('get', path);
|
||||||
devices
|
devices.add(
|
||||||
.add(DeviceNode.fromJson({'path': path, ...deviceResult['data']}));
|
DeviceNode.fromJson({'path': path, ...deviceResult['data']}));
|
||||||
}
|
}
|
||||||
|
_usbState = usbResult['data']['state'];
|
||||||
|
log.info('USB state updated');
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
state = devices;
|
state = devices;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} on RpcError catch (e) {
|
||||||
|
log.severe('Error polling USB', jsonEncode(e));
|
||||||
|
}
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
_pollTimer = Timer(const Duration(milliseconds: 500), _pollUsb);
|
_pollTimer = Timer(const Duration(milliseconds: 500), _pollUsb);
|
||||||
}
|
}
|
||||||
@ -90,6 +132,16 @@ class CurrentDeviceNotifier extends StateNotifier<DeviceNode?> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final sortedDevicesProvider = Provider<List<DeviceNode>>((ref) {
|
||||||
|
final devices = ref.watch(attachedDevicesProvider).toList();
|
||||||
|
devices.sort((a, b) => a.name.compareTo(b.name));
|
||||||
|
final device = ref.watch(currentDeviceProvider);
|
||||||
|
if (device != null) {
|
||||||
|
return [device, ...devices.where((e) => e != device)];
|
||||||
|
}
|
||||||
|
return devices;
|
||||||
|
});
|
||||||
|
|
||||||
final subPageProvider = StateNotifierProvider<SubPageNotifier, SubPage>(
|
final subPageProvider = StateNotifierProvider<SubPageNotifier, SubPage>(
|
||||||
(ref) => SubPageNotifier(SubPage.authenticator));
|
(ref) => SubPageNotifier(SubPage.authenticator));
|
||||||
|
|
||||||
@ -100,3 +152,16 @@ class SubPageNotifier extends StateNotifier<SubPage> {
|
|||||||
state = page;
|
state = page;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef BuildActions = List<MenuAction> Function(BuildContext);
|
||||||
|
|
||||||
|
final menuActionsProvider = Provider.autoDispose<BuildActions>((ref) {
|
||||||
|
switch (ref.watch(subPageProvider)) {
|
||||||
|
case SubPage.authenticator:
|
||||||
|
return (context) => buildOathMenuActions(context, ref);
|
||||||
|
case SubPage.yubikey:
|
||||||
|
// TODO: Handle this case.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return (_) => [];
|
||||||
|
});
|
||||||
|
26
lib/app/views/device_avatar.dart
Executable file
26
lib/app/views/device_avatar.dart
Executable file
@ -0,0 +1,26 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../models.dart';
|
||||||
|
import 'device_images.dart';
|
||||||
|
|
||||||
|
class DeviceAvatar extends StatelessWidget {
|
||||||
|
final DeviceNode device;
|
||||||
|
final bool selected;
|
||||||
|
|
||||||
|
const DeviceAvatar(this.device, {this.selected = false, Key? key})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CircleAvatar(
|
||||||
|
child: CircleAvatar(
|
||||||
|
child: getProductImage(device),
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.background,
|
||||||
|
),
|
||||||
|
radius: 22,
|
||||||
|
backgroundColor: selected
|
||||||
|
? Theme.of(context).colorScheme.secondary
|
||||||
|
: Colors.transparent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,67 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:yubico_authenticator/app/views/device_images.dart';
|
|
||||||
|
|
||||||
import '../../about_page.dart';
|
|
||||||
import '../models.dart';
|
|
||||||
import '../state.dart';
|
|
||||||
|
|
||||||
class DevicePickerDialog extends ConsumerWidget {
|
|
||||||
const DevicePickerDialog({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final device = ref.watch(currentDeviceProvider);
|
|
||||||
final devices = ref.watch(attachedDevicesProvider);
|
|
||||||
|
|
||||||
Widget _buildDeviceInfo(DeviceNode device) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
CircleAvatar(
|
|
||||||
child: getProductImage(device),
|
|
||||||
radius: 40.0,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16.0),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(device.name),
|
|
||||||
Text('Version: ${device.info.version}'),
|
|
||||||
Text('Serial: ${device.info.serial}'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SimpleDialog(
|
|
||||||
//title: Text(device?.name ?? 'No YubiKey'),
|
|
||||||
children: [
|
|
||||||
if (device != null) _buildDeviceInfo(device),
|
|
||||||
...devices.where((e) => e != device).map((e) => TextButton(
|
|
||||||
child: Text('${e.name} (${e.info.serial})'),
|
|
||||||
onPressed: () {
|
|
||||||
ref.read(currentDeviceProvider.notifier).setCurrentDevice(e);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
const Divider(),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(builder: (context) => const AboutPage()),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: const Text('About Yubico Authenticator...'))
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
85
lib/app/views/main_actions_dialog.dart
Executable file
85
lib/app/views/main_actions_dialog.dart
Executable file
@ -0,0 +1,85 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../models.dart';
|
||||||
|
import '../state.dart';
|
||||||
|
import 'device_avatar.dart';
|
||||||
|
|
||||||
|
class MainActionsDialog extends ConsumerWidget {
|
||||||
|
const MainActionsDialog({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final devices = ref.watch(sortedDevicesProvider);
|
||||||
|
final device = ref.watch(currentDeviceProvider);
|
||||||
|
final actions = ref.watch(menuActionsProvider)(context);
|
||||||
|
|
||||||
|
return SimpleDialog(
|
||||||
|
//title: Text(device?.name ?? 'No YubiKey'),
|
||||||
|
children: [
|
||||||
|
...devices.map((e) => Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: DeviceRow(
|
||||||
|
e,
|
||||||
|
selected: e == device,
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
ref.read(currentDeviceProvider.notifier).setCurrentDevice(e);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
if (actions.isNotEmpty) const Divider(),
|
||||||
|
...actions.map((a) => ListTile(
|
||||||
|
dense: true,
|
||||||
|
leading: a.icon,
|
||||||
|
title: Text(a.text),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
a.action?.call();
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DeviceRow extends StatelessWidget {
|
||||||
|
final DeviceNode device;
|
||||||
|
final bool selected;
|
||||||
|
final Function() onPressed;
|
||||||
|
const DeviceRow(
|
||||||
|
this.device, {
|
||||||
|
this.selected = false,
|
||||||
|
required this.onPressed,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TextButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
DeviceAvatar(
|
||||||
|
device,
|
||||||
|
selected: selected,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16.0),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
device.name,
|
||||||
|
style: Theme.of(context).textTheme.headline6,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'S/N: ${device.info.serial} F/W: ${device.info.version}',
|
||||||
|
style: Theme.of(context).textTheme.bodyText1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../about_page.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
|
|
||||||
@ -25,26 +26,103 @@ class MainPageDrawer extends ConsumerWidget {
|
|||||||
return Drawer(
|
return Drawer(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
const DrawerHeader(
|
Padding(
|
||||||
decoration: BoxDecoration(
|
padding: const EdgeInsets.all(12.0),
|
||||||
color: Colors.blue,
|
child: Text(
|
||||||
),
|
'Yubico Authenticator',
|
||||||
child: Text('Hello'),
|
|
||||||
),
|
|
||||||
...SubPage.values.map((value) => ListTile(
|
|
||||||
title: Text(
|
|
||||||
value.displayName,
|
|
||||||
style: Theme.of(context).textTheme.headline6,
|
style: Theme.of(context).textTheme.headline6,
|
||||||
),
|
),
|
||||||
tileColor: value == currentSubPage ? Colors.blueGrey : null,
|
),
|
||||||
enabled: value != currentSubPage,
|
const Divider(),
|
||||||
|
...SubPage.values.map((page) => DrawerItem(
|
||||||
|
titleText: page.displayName,
|
||||||
|
icon: const Icon(Icons.miscellaneous_services),
|
||||||
|
selected: page == currentSubPage,
|
||||||
|
onTap: page != currentSubPage
|
||||||
|
? () {
|
||||||
|
ref.read(subPageProvider.notifier).setSubPage(page);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
)),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Text(
|
||||||
|
'CONFIGURATION',
|
||||||
|
style: Theme.of(context).textTheme.bodyText2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DrawerItem(
|
||||||
|
titleText: 'Placeholder Light mode',
|
||||||
|
icon: const Icon(Icons.alarm),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ref.read(subPageProvider.notifier).setSubPage(value);
|
ref
|
||||||
|
.read(themeModeProvider.notifier)
|
||||||
|
.setThemeMode(ThemeMode.light);
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
)),
|
),
|
||||||
|
DrawerItem(
|
||||||
|
titleText: 'Placeholder Dark mode',
|
||||||
|
icon: const Icon(Icons.house),
|
||||||
|
onTap: () {
|
||||||
|
ref.read(themeModeProvider.notifier).setThemeMode(ThemeMode.dark);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
DrawerItem(
|
||||||
|
titleText: 'About Yubico Authenticator',
|
||||||
|
icon: const Icon(Icons.settings_applications),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context)
|
||||||
|
..pop()
|
||||||
|
..push(
|
||||||
|
MaterialPageRoute(builder: (context) => const AboutPage()),
|
||||||
|
);
|
||||||
|
//Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DrawerItem extends StatelessWidget {
|
||||||
|
final bool selected;
|
||||||
|
final String titleText;
|
||||||
|
final Icon icon;
|
||||||
|
final void Function()? onTap;
|
||||||
|
|
||||||
|
const DrawerItem({
|
||||||
|
required this.titleText,
|
||||||
|
required this.icon,
|
||||||
|
this.onTap,
|
||||||
|
this.selected = false,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8),
|
||||||
|
child: ListTile(
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.horizontal(right: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
dense: true,
|
||||||
|
selected: selected,
|
||||||
|
selectedColor: Theme.of(context).backgroundColor,
|
||||||
|
selectedTileColor: Theme.of(context).colorScheme.secondary,
|
||||||
|
leading: icon,
|
||||||
|
title: Text(
|
||||||
|
titleText,
|
||||||
|
//style: Theme.of(context).textTheme.headline6,
|
||||||
|
),
|
||||||
|
//enabled: value != currentSubPage,
|
||||||
|
onTap: onTap,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:yubico_authenticator/app/views/device_images.dart';
|
|
||||||
|
|
||||||
import 'device_picker_dialog.dart';
|
import 'device_avatar.dart';
|
||||||
|
import 'main_actions_dialog.dart';
|
||||||
import 'main_drawer.dart';
|
import 'main_drawer.dart';
|
||||||
import 'no_device_screen.dart';
|
import 'no_device_screen.dart';
|
||||||
import 'device_info_screen.dart';
|
import 'device_info_screen.dart';
|
||||||
@ -13,7 +13,10 @@ import '../../oath/views/oath_screen.dart';
|
|||||||
class MainPage extends ConsumerWidget {
|
class MainPage extends ConsumerWidget {
|
||||||
const MainPage({Key? key}) : super(key: key);
|
const MainPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
Widget _buildSubPage(SubPage subPage, DeviceNode device) {
|
Widget _buildSubPage(SubPage subPage, DeviceNode? device) {
|
||||||
|
if (device == null) {
|
||||||
|
return const NoDeviceScreen();
|
||||||
|
}
|
||||||
// TODO: If page not supported by device, do something?
|
// TODO: If page not supported by device, do something?
|
||||||
switch (subPage) {
|
switch (subPage) {
|
||||||
case SubPage.authenticator:
|
case SubPage.authenticator:
|
||||||
@ -30,25 +33,42 @@ class MainPage extends ConsumerWidget {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Yubico Authenticator'),
|
//title: const Text('Yubico Authenticator'),
|
||||||
|
/*
|
||||||
|
backgroundColor: Colors.grey.shade900,
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(40)),
|
||||||
|
side: BorderSide(
|
||||||
|
width: 8, color: Theme.of(context).scaffoldBackgroundColor),
|
||||||
|
),
|
||||||
|
*/
|
||||||
|
title: TextField(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: 'Search...',
|
||||||
|
border: InputBorder.none,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
ref.read(searchProvider.notifier).setFilter(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
InkWell(
|
||||||
icon: currentDevice == null
|
//iconSize: 32,
|
||||||
|
child: currentDevice == null
|
||||||
? const Icon(Icons.info)
|
? const Icon(Icons.info)
|
||||||
: getProductImage(currentDevice),
|
: DeviceAvatar(currentDevice, selected: true),
|
||||||
onPressed: () {
|
onTap: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => const DevicePickerDialog(),
|
builder: (context) => const MainActionsDialog(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
drawer: const MainPageDrawer(),
|
drawer: const MainPageDrawer(),
|
||||||
body: currentDevice == null
|
body: _buildSubPage(subPage, currentDevice),
|
||||||
? const NoDeviceScreen()
|
|
||||||
: _buildSubPage(subPage, currentDevice),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
import 'app.dart';
|
import 'app/app.dart';
|
||||||
import 'app/views/main_page.dart';
|
import 'app/views/main_page.dart';
|
||||||
import 'core/rpc.dart';
|
import 'core/rpc.dart';
|
||||||
import 'core/state.dart';
|
import 'core/state.dart';
|
||||||
|
46
lib/oath/menu_actions.dart
Executable file
46
lib/oath/menu_actions.dart
Executable file
@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../app/models.dart';
|
||||||
|
import '../app/state.dart';
|
||||||
|
import 'state.dart';
|
||||||
|
import 'views/add_account_page.dart';
|
||||||
|
|
||||||
|
List<MenuAction> buildOathMenuActions(
|
||||||
|
BuildContext context, AutoDisposeProviderRef ref) {
|
||||||
|
final device = ref.watch(currentDeviceProvider);
|
||||||
|
if (device != null) {
|
||||||
|
final state = ref.watch(oathStateProvider(device.path));
|
||||||
|
if (state != null) {
|
||||||
|
return [
|
||||||
|
if (!state.locked)
|
||||||
|
MenuAction(
|
||||||
|
text: 'Add credential',
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
action: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => OathAddAccountPage(device: device),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MenuAction(
|
||||||
|
text: 'Factory reset',
|
||||||
|
icon: const Icon(Icons.delete_forever),
|
||||||
|
action: () {
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..clearSnackBars()
|
||||||
|
..showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Not implemented'),
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
import '../app/state.dart';
|
||||||
import '../core/state.dart';
|
import '../core/state.dart';
|
||||||
import 'models.dart';
|
import 'models.dart';
|
||||||
|
|
||||||
@ -241,6 +242,7 @@ class FavoriteNotifier extends StateNotifier<bool> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
final searchFilterProvider =
|
final searchFilterProvider =
|
||||||
StateNotifierProvider<SearchFilterNotifier, String>(
|
StateNotifierProvider<SearchFilterNotifier, String>(
|
||||||
(ref) => SearchFilterNotifier());
|
(ref) => SearchFilterNotifier());
|
||||||
@ -251,7 +253,7 @@ class SearchFilterNotifier extends StateNotifier<String> {
|
|||||||
setFilter(String value) {
|
setFilter(String value) {
|
||||||
state = value;
|
state = value;
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
final filteredCredentialsProvider = StateNotifierProvider.autoDispose
|
final filteredCredentialsProvider = StateNotifierProvider.autoDispose
|
||||||
.family<FilteredCredentialsNotifier, List<OathPair>, List<OathPair>>(
|
.family<FilteredCredentialsNotifier, List<OathPair>, List<OathPair>>(
|
||||||
@ -261,7 +263,7 @@ final filteredCredentialsProvider = StateNotifierProvider.autoDispose
|
|||||||
credential: ref.watch(favoriteProvider(credential.id))
|
credential: ref.watch(favoriteProvider(credential.id))
|
||||||
};
|
};
|
||||||
return FilteredCredentialsNotifier(
|
return FilteredCredentialsNotifier(
|
||||||
full, favorites, ref.watch(searchFilterProvider));
|
full, favorites, ref.watch(searchProvider));
|
||||||
});
|
});
|
||||||
|
|
||||||
class FilteredCredentialsNotifier extends StateNotifier<List<OathPair>> {
|
class FilteredCredentialsNotifier extends StateNotifier<List<OathPair>> {
|
||||||
@ -271,7 +273,8 @@ class FilteredCredentialsNotifier extends StateNotifier<List<OathPair>> {
|
|||||||
List<OathPair> full,
|
List<OathPair> full,
|
||||||
this.favorites,
|
this.favorites,
|
||||||
this.query,
|
this.query,
|
||||||
) : super(full
|
) : super(
|
||||||
|
full
|
||||||
.where((pair) =>
|
.where((pair) =>
|
||||||
"${pair.credential.issuer ?? ''}:${pair.credential.name}"
|
"${pair.credential.issuer ?? ''}:${pair.credential.name}"
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
@ -279,7 +282,10 @@ class FilteredCredentialsNotifier extends StateNotifier<List<OathPair>> {
|
|||||||
.toList()
|
.toList()
|
||||||
..sort((a, b) {
|
..sort((a, b) {
|
||||||
String searchKey(OathCredential c) =>
|
String searchKey(OathCredential c) =>
|
||||||
(favorites[c] == true ? '0' : '1') + (c.issuer ?? '') + c.name;
|
(favorites[c] == true ? '0' : '1') +
|
||||||
|
(c.issuer ?? '') +
|
||||||
|
c.name;
|
||||||
return searchKey(a.credential).compareTo(searchKey(b.credential));
|
return searchKey(a.credential).compareTo(searchKey(b.credential));
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import 'account_list.dart';
|
import 'account_list.dart';
|
||||||
import 'add_account_page.dart';
|
|
||||||
|
|
||||||
class OathScreen extends ConsumerWidget {
|
class OathScreen extends ConsumerWidget {
|
||||||
final DeviceNode device;
|
final DeviceNode device;
|
||||||
@ -43,25 +42,9 @@ class OathScreen extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Column(
|
return AccountList(
|
||||||
children: [
|
device,
|
||||||
TextField(
|
ref.watch(filteredCredentialsProvider(accounts)),
|
||||||
onChanged: (value) {
|
|
||||||
ref.read(searchFilterProvider.notifier).setFilter(value);
|
|
||||||
},
|
|
||||||
decoration: const InputDecoration(labelText: 'Search'),
|
|
||||||
),
|
|
||||||
AccountList(device, ref.watch(filteredCredentialsProvider(accounts))),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => OathAddAccountPage(device: device)),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: const Text('Add'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
23
lib/theme.dart
Executable file
23
lib/theme.dart
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AppTheme {
|
||||||
|
static ThemeData get lightTheme => ThemeData(
|
||||||
|
brightness: Brightness.light,
|
||||||
|
);
|
||||||
|
|
||||||
|
static ThemeData get darkTheme => ThemeData(
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
colorScheme:
|
||||||
|
ColorScheme.fromSwatch(brightness: Brightness.dark).copyWith(
|
||||||
|
secondary: const Color(0xffa8c86c),
|
||||||
|
),
|
||||||
|
textTheme: TextTheme(
|
||||||
|
bodyText1: TextStyle(
|
||||||
|
color: Colors.grey.shade400,
|
||||||
|
),
|
||||||
|
bodyText2: TextStyle(
|
||||||
|
color: Colors.grey.shade500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
14
pubspec.lock
14
pubspec.lock
@ -7,14 +7,14 @@ packages:
|
|||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "30.0.0"
|
version: "31.0.0"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.7.0"
|
version: "2.8.0"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -208,7 +208,7 @@ packages:
|
|||||||
name: flutter_riverpod
|
name: flutter_riverpod
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.1"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -288,14 +288,14 @@ packages:
|
|||||||
name: json_annotation
|
name: json_annotation
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.3.0"
|
version: "4.4.0"
|
||||||
json_serializable:
|
json_serializable:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: json_serializable
|
name: json_serializable
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.1"
|
version: "6.1.0"
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -414,7 +414,7 @@ packages:
|
|||||||
name: riverpod
|
name: riverpod
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.1"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -496,7 +496,7 @@ packages:
|
|||||||
name: source_gen
|
name: source_gen
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.2.0"
|
||||||
source_helper:
|
source_helper:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit 1088e1adeed9ab7b8e7abfa57855b48558fd4c64
|
Subproject commit 2ff1dcd6e0a533414add4c5d171387b1f1837790
|
Loading…
Reference in New Issue
Block a user