From 2b7aec8121319a4c132b70b4804ffe6d4a8e91f5 Mon Sep 17 00:00:00 2001 From: YeungKC Date: Tue, 4 Jul 2023 21:12:00 +0900 Subject: [PATCH 1/8] Feat: Screen lock --- assets/images/proxy.svg | 5 + assets/images/shield.svg | 5 + lib/account/security_key_value.dart | 32 ++++ lib/constants/resources.dart | 6 + .../route/responsive_navigator_cubit.dart | 11 ++ lib/ui/setting/appearance_page.dart | 6 +- lib/ui/setting/security_page.dart | 160 ++++++++++++++++++ lib/ui/setting/setting_page.dart | 13 +- lib/utils/file.dart | 5 +- lib/utils/hive_key_values.dart | 3 + lib/widgets/dialog.dart | 7 +- lib/widgets/toast.dart | 6 +- macos/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- pubspec.lock | 32 ++-- pubspec.yaml | 11 +- 16 files changed, 272 insertions(+), 34 deletions(-) create mode 100644 assets/images/proxy.svg create mode 100644 assets/images/shield.svg create mode 100644 lib/account/security_key_value.dart create mode 100644 lib/ui/setting/security_page.dart diff --git a/assets/images/proxy.svg b/assets/images/proxy.svg new file mode 100644 index 0000000000..4e67faa68f --- /dev/null +++ b/assets/images/proxy.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/shield.svg b/assets/images/shield.svg new file mode 100644 index 0000000000..8f5134acff --- /dev/null +++ b/assets/images/shield.svg @@ -0,0 +1,5 @@ + + + diff --git a/lib/account/security_key_value.dart b/lib/account/security_key_value.dart new file mode 100644 index 0000000000..a442271075 --- /dev/null +++ b/lib/account/security_key_value.dart @@ -0,0 +1,32 @@ +import '../utils/hive_key_values.dart'; + +class SecurityKeyValue extends HiveKeyValue { + SecurityKeyValue._() : super(_hiveSecurity); + + static SecurityKeyValue? _instance; + + static SecurityKeyValue get instance => _instance ??= SecurityKeyValue._(); + + static const _hiveSecurity = 'security_box'; + static const _passcode = 'passcode'; + static const _biometric = 'biometric'; + + String? get passcode => box.get(_passcode) as String?; + + set passcode(String? value) { + if (value != null && value.length != 6) { + throw ArgumentError('Passcode must be 6 digits'); + } + box.put(_passcode, value); + } + + bool get biometric => box.get(_biometric, defaultValue: false) as bool; + + set biometric(bool value) => box.put(_biometric, value); + + Stream watchHasPasscode() => + box.watch(key: _passcode).map((event) => event.value != null); + + Stream watchBiometric() => + box.watch(key: _biometric).map((event) => (event.value ?? false) as bool); +} diff --git a/lib/constants/resources.dart b/lib/constants/resources.dart index 5156e8320e..0262a1a925 100644 --- a/lib/constants/resources.dart +++ b/lib/constants/resources.dart @@ -493,6 +493,9 @@ class Resources { /// {@macro assets_generator.assetsImagesPrevSvg.preview} static const String assetsImagesPrevSvg = 'assets/images/prev.svg'; + /// {@macro assets_generator.assetsImagesProxySvg.preview} + static const String assetsImagesProxySvg = 'assets/images/proxy.svg'; + /// {@macro assets_generator.assetsImagesReadSvg.preview} static const String assetsImagesReadSvg = 'assets/images/read.svg'; @@ -534,6 +537,9 @@ class Resources { /// {@macro assets_generator.assetsImagesShareSvg.preview} static const String assetsImagesShareSvg = 'assets/images/share.svg'; + /// {@macro assets_generator.assetsImagesShieldSvg.preview} + static const String assetsImagesShieldSvg = 'assets/images/shield.svg'; + /// {@macro assets_generator.assetsImagesSlideContactsSvg.preview} static const String assetsImagesSlideContactsSvg = 'assets/images/slide_contacts.svg'; diff --git a/lib/ui/home/route/responsive_navigator_cubit.dart b/lib/ui/home/route/responsive_navigator_cubit.dart index 998be342e6..9aa8a112fd 100644 --- a/lib/ui/home/route/responsive_navigator_cubit.dart +++ b/lib/ui/home/route/responsive_navigator_cubit.dart @@ -9,6 +9,7 @@ import '../../setting/backup_page.dart'; import '../../setting/edit_profile_page.dart'; import '../../setting/notification_page.dart'; import '../../setting/proxy_page.dart'; +import '../../setting/security_page.dart'; import '../../setting/storage_page.dart'; import '../../setting/storage_usage_detail_page.dart'; import '../../setting/storage_usage_list_page.dart'; @@ -35,6 +36,7 @@ class ResponsiveNavigatorCubit extends AbstractResponsiveNavigatorCubit { static const storageUsage = 'storageUsage'; static const storageUsageDetail = 'storageUsageDetail'; static const proxyPage = 'proxyPage'; + static const securityPage = 'securityPage'; static const settingPageNameSet = { editProfilePage, @@ -48,6 +50,7 @@ class ResponsiveNavigatorCubit extends AbstractResponsiveNavigatorCubit { accountPage, accountDeletePage, proxyPage, + securityPage, }; @override @@ -154,6 +157,14 @@ class ResponsiveNavigatorCubit extends AbstractResponsiveNavigatorCubit { key: ValueKey(proxyPage), ), ); + case securityPage: + return const MaterialPage( + key: ValueKey(securityPage), + name: securityPage, + child: SecurityPage( + key: ValueKey(securityPage), + ), + ); default: throw ArgumentError('Invalid route'); } diff --git a/lib/ui/setting/appearance_page.dart b/lib/ui/setting/appearance_page.dart index 854457d38a..6c48167b94 100644 --- a/lib/ui/setting/appearance_page.dart +++ b/lib/ui/setting/appearance_page.dart @@ -41,7 +41,7 @@ class _Body extends StatelessWidget { @override Widget build(BuildContext context) => SingleChildScrollView( - child: Container( + child: Container( padding: const EdgeInsets.only(top: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -100,7 +100,9 @@ class _Body extends StatelessWidget { const _MessageAvatarSetting(), const _ChatTextSizeSetting(), ], - ))); + ), + ), + ); } class _MessageAvatarSetting extends HookWidget { diff --git a/lib/ui/setting/security_page.dart b/lib/ui/setting/security_page.dart new file mode 100644 index 0000000000..280681fad8 --- /dev/null +++ b/lib/ui/setting/security_page.dart @@ -0,0 +1,160 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:pin_input_text_field/pin_input_text_field.dart'; + +import '../../account/security_key_value.dart'; +import '../../utils/extension/extension.dart'; +import '../../widgets/app_bar.dart'; +import '../../widgets/buttons.dart'; +import '../../widgets/cell.dart'; +import '../../widgets/dialog.dart'; +import '../../widgets/toast.dart'; + +class SecurityPage extends StatelessWidget { + const SecurityPage({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + backgroundColor: context.theme.background, + appBar: const MixinAppBar( + // todo l10n + title: Text('Security'), + ), + body: ConstrainedBox( + constraints: const BoxConstraints.expand(), + child: const SingleChildScrollView( + child: Column( + children: [ + SizedBox(height: 40), + _Passcode(), + ], + ), + ), + ), + ); +} + +class _Passcode extends HookWidget { + const _Passcode(); + + @override + Widget build(BuildContext context) { + final hasPasscode = + useStream(SecurityKeyValue.instance.watchHasPasscode()).data ?? false; + + return CellGroup( + child: CellItem( + // todo l10n + title: const Text('Passcode'), + trailing: Transform.scale( + scale: 0.7, + child: CupertinoSwitch( + activeColor: context.theme.accent, + value: hasPasscode, + onChanged: (value) { + if (!value) { + SecurityKeyValue.instance.passcode = null; + return; + } + showMixinDialog( + context: context, + child: const _InputPasscode(), + ); + }, + ), + ), + ), + ); + } +} + +class _InputPasscode extends HookWidget { + const _InputPasscode(); + + @override + Widget build(BuildContext context) { + final textEditingController = useTextEditingController(); + + final passcode = useState(null); + final confirmPasscode = useState(null); + + useEffect(() { + if (passcode.value == null) return; + if (confirmPasscode.value == null) return; + + if (passcode.value != confirmPasscode.value) { + // todo l10n + showToastFailed('Passcode not match', context: context); + + passcode.value = null; + confirmPasscode.value = null; + textEditingController.text = ''; + } + + SecurityKeyValue.instance.passcode = passcode.value; + Navigator.maybePop(context); + }); + + return Padding( + padding: const EdgeInsets.only(top: 20, bottom: 80), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Row( + children: [ + Spacer(), + Padding( + padding: EdgeInsets.only(right: 12, top: 12), + child: MixinCloseButton(), + ), + ], + ), + Text( + // todo l10n + passcode.value != null + ? 'Enter again to confirm the passcode' + : 'Set Passcode to unlock Mixin Messenger', + textAlign: TextAlign.center, + style: TextStyle( + color: context.theme.text, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 40), + SizedBox( + width: 215, + child: PinInputTextField( + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + autoFocus: true, + decoration: UnderlineDecoration( + gapSpace: 25, + textStyle: TextStyle( + color: context.theme.text, + fontSize: 18, + fontWeight: FontWeight.w400, + ), + colorBuilder: FixedColorBuilder( + context.theme.listSelected, + ), + ), + controller: textEditingController, + onChanged: (value) { + if (value.length != 6) return; + + if (passcode.value != null) { + confirmPasscode.value = value; + } else { + passcode.value = value; + textEditingController.text = ''; + } + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/setting/setting_page.dart b/lib/ui/setting/setting_page.dart index b4b2858a05..f415c46f74 100644 --- a/lib/ui/setting/setting_page.dart +++ b/lib/ui/setting/setting_page.dart @@ -113,14 +113,16 @@ class SettingPage extends HookWidget { .dataAndStorageUsagePage, title: context.l10n.dataAndStorageUsage, ), + const _Item( + leadingAssetName: Resources.assetsImagesShieldSvg, + pageName: ResponsiveNavigatorCubit.securityPage, + // todo l10n + title: 'Security', + ), _Item( + leadingAssetName: Resources.assetsImagesProxySvg, pageName: ResponsiveNavigatorCubit.proxyPage, title: context.l10n.proxy, - leading: Icon( - Icons.shield_outlined, - size: 24, - color: context.theme.icon, - ), ), _Item( leadingAssetName: @@ -170,6 +172,7 @@ class _Item extends StatelessWidget { this.color, this.onTap, this.trailing = const Arrow(), + // ignore: unused_element this.leading, }); diff --git a/lib/utils/file.dart b/lib/utils/file.dart index fcd2c710b8..630b434e2b 100644 --- a/lib/utils/file.dart +++ b/lib/utils/file.dart @@ -45,7 +45,7 @@ Future saveFileToSystem( d('saveFileToSystem: $file, $targetName, $mineType, $extension'); - var path = await file_selector.getSavePath( + var path = (await file_selector.getSaveLocation( confirmButtonText: context.l10n.save, suggestedName: targetName, acceptedTypeGroups: [ @@ -55,7 +55,8 @@ Future saveFileToSystem( mimeTypes: [if (mineType != null) mineType], ), ], - ); + )) + ?.path; if (path == null || path.isEmpty) { return false; } diff --git a/lib/utils/hive_key_values.dart b/lib/utils/hive_key_values.dart index 453db09d1d..5d44e2eec3 100644 --- a/lib/utils/hive_key_values.dart +++ b/lib/utils/hive_key_values.dart @@ -7,6 +7,7 @@ import 'package:path/path.dart' as p; import '../account/account_key_value.dart'; import '../account/scam_warning_key_value.dart'; +import '../account/security_key_value.dart'; import '../account/session_key_value.dart'; import '../account/show_pin_message_key_value.dart'; import '../crypto/crypto_key_value.dart'; @@ -22,6 +23,7 @@ Future initKeyValues(String identityNumber) => Future.wait([ ScamWarningKeyValue.instance.init(identityNumber), DownloadKeyValue.instance.init(identityNumber), SessionKeyValue.instance.init(identityNumber), + SecurityKeyValue.instance.init(identityNumber), ]); Future clearKeyValues() => Future.wait([ @@ -32,6 +34,7 @@ Future clearKeyValues() => Future.wait([ ScamWarningKeyValue.instance.delete(), DownloadKeyValue.instance.delete(), SessionKeyValue.instance.delete(), + SecurityKeyValue.instance.delete(), ]); abstract class HiveKeyValue { diff --git a/lib/widgets/dialog.dart b/lib/widgets/dialog.dart index 90071e7c6b..2ebc9d490b 100644 --- a/lib/widgets/dialog.dart +++ b/lib/widgets/dialog.dart @@ -164,9 +164,6 @@ class _DialogPage extends StatelessWidget { child: DecoratedBox( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(11)), - border: Border.all( - color: const Color.fromRGBO(255, 255, 255, 0.08), - ), boxShadow: [ const BoxShadow( color: Color.fromRGBO(0, 0, 0, 0.15), @@ -179,9 +176,9 @@ class _DialogPage extends StatelessWidget { blurRadius: lerpDouble(16, 6, context.brightnessValue)!, ), ], - color: backgroundColor ?? context.theme.popUp, ), - child: ClipRRect( + child: Material( + color: backgroundColor ?? context.theme.popUp, borderRadius: const BorderRadius.all(Radius.circular(11)), child: child, ), diff --git a/lib/widgets/toast.dart b/lib/widgets/toast.dart index 37aa712f5d..bb0dc05176 100644 --- a/lib/widgets/toast.dart +++ b/lib/widgets/toast.dart @@ -23,9 +23,11 @@ class Toast { static void createView({ required WidgetBuilder builder, Duration? duration = Toast.shortDuration, + BuildContext? context, }) { dismiss(); _entry = showOverlay( + context: context, (context, progress) => Opacity( opacity: progress, child: builder(context), @@ -138,7 +140,9 @@ class ToastError extends Error { final String Function(BuildContext)? messageBuilder; } -void showToastFailed(Object? error) => Toast.createView( +void showToastFailed(Object? error, {BuildContext? context}) => + Toast.createView( + context: context, builder: (context) => ToastWidget( barrierColor: Colors.transparent, icon: const _Failed(), diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index a69d1b1a3c..4ab0e96d84 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -209,7 +209,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 33CC10EC2044A3C60003C045 = { diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 4a9fe9b8c7..07dcc4480a 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Date: Wed, 5 Jul 2023 15:42:38 +0900 Subject: [PATCH 2/8] update --- assets/images/lock.svg | 8 + lib/account/security_key_value.dart | 10 +- lib/app.dart | 5 +- lib/constants/resources.dart | 3 + lib/widgets/auth_guard/auth.dart | 195 ++++++++++++++++++ .../auth_guard/circle_pin_decoration.dart | 174 ++++++++++++++++ pubspec.yaml | 2 +- 7 files changed, 393 insertions(+), 4 deletions(-) create mode 100644 assets/images/lock.svg create mode 100644 lib/widgets/auth_guard/auth.dart create mode 100644 lib/widgets/auth_guard/circle_pin_decoration.dart diff --git a/assets/images/lock.svg b/assets/images/lock.svg new file mode 100644 index 0000000000..929c8d35f1 --- /dev/null +++ b/assets/images/lock.svg @@ -0,0 +1,8 @@ + + + + + diff --git a/lib/account/security_key_value.dart b/lib/account/security_key_value.dart index a442271075..b138826e6c 100644 --- a/lib/account/security_key_value.dart +++ b/lib/account/security_key_value.dart @@ -1,3 +1,5 @@ +import 'package:rxdart/rxdart.dart'; + import '../utils/hive_key_values.dart'; class SecurityKeyValue extends HiveKeyValue { @@ -24,8 +26,12 @@ class SecurityKeyValue extends HiveKeyValue { set biometric(bool value) => box.put(_biometric, value); - Stream watchHasPasscode() => - box.watch(key: _passcode).map((event) => event.value != null); + bool get hasPasscode => passcode != null; + + Stream watchHasPasscode() => box + .watch(key: _passcode) + .map((event) => event.value != null) + .startWith(passcode != null); Stream watchBiometric() => box.watch(key: _biometric).map((event) => (event.value ?? false) as bool); diff --git a/lib/app.dart b/lib/app.dart index 20e9782e1c..9113585071 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -34,6 +34,7 @@ import 'utils/platform.dart'; import 'utils/system/system_fonts.dart'; import 'utils/system/text_input.dart'; import 'utils/system/tray.dart'; +import 'widgets/auth_guard/auth.dart'; import 'widgets/brightness_observer.dart'; import 'widgets/focus_helper.dart'; import 'widgets/message/item/text/mention_builder.dart'; @@ -243,7 +244,9 @@ class _App extends StatelessWidget { ), child: SystemTrayWidget( child: TextInputActionHandler( - child: child!, + child: AuthGuard( + child: child!, + ), ), ), ), diff --git a/lib/constants/resources.dart b/lib/constants/resources.dart index 0262a1a925..57552c8fb3 100644 --- a/lib/constants/resources.dart +++ b/lib/constants/resources.dart @@ -425,6 +425,9 @@ class Resources { static const String assetsImagesLocationMarkSvg = 'assets/images/location_mark.svg'; + /// {@macro assets_generator.assetsImagesLockSvg.preview} + static const String assetsImagesLockSvg = 'assets/images/lock.svg'; + /// {@macro assets_generator.assetsImagesLogoPng.preview} static const String assetsImagesLogoPng = 'assets/images/logo.png'; diff --git a/lib/widgets/auth_guard/auth.dart b/lib/widgets/auth_guard/auth.dart new file mode 100644 index 0000000000..ea3fac9b47 --- /dev/null +++ b/lib/widgets/auth_guard/auth.dart @@ -0,0 +1,195 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:pin_input_text_field/pin_input_text_field.dart' + hide CirclePinDecoration; + +import '../../account/account_server.dart'; +import '../../account/security_key_value.dart'; +import '../../constants/resources.dart'; +import '../../ui/home/bloc/multi_auth_cubit.dart'; +import '../../utils/app_lifecycle.dart'; +import '../../utils/extension/extension.dart'; +import '../../utils/hook.dart'; +import 'circle_pin_decoration.dart'; + +const lockDuration = Duration(minutes: 1); + +class AuthGuard extends HookWidget { + const AuthGuard({ + super.key, + required this.child, + }); + + final Widget child; + + @override + Widget build(BuildContext context) { + final authAvailable = + useBlocState().current != null; + AccountServer? accountServer; + try { + accountServer = context.read(); + } catch (_) {} + final signed = authAvailable && accountServer != null; + + if (signed) return _AuthGuard(child: child); + + return child; + } +} + +class _AuthGuard extends HookWidget { + const _AuthGuard({required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + final focusNode = useFocusNode(); + final textEditingController = useTextEditingController(); + final hasPasscode = + useMemoizedStream(SecurityKeyValue.instance.watchHasPasscode).data ?? + SecurityKeyValue.instance.hasPasscode; + + final hasError = useState(false); + final lock = useState(false); + + useEffect(() { + Timer? timer; + void dispose() { + timer?.cancel(); + timer = null; + } + + void listener() { + if (lock.value) return; + + final needLock = !isAppActive; + + if (needLock) { + timer = Timer(lockDuration, () { + if (!hasPasscode) { + lock.value = false; + return; + } + + lock.value = !isAppActive; + }); + } else { + dispose(); + lock.value = needLock; + } + } + + listener(); + appActiveListener.addListener(listener); + return () { + dispose(); + appActiveListener.removeListener(listener); + }; + }, [hasPasscode]); + + useEffect(() { + focusNode.requestFocus(); + void listener() { + if (focusNode.hasFocus) return; + focusNode.requestFocus(); + } + + focusNode.addListener(listener); + return () { + focusNode.removeListener(listener); + }; + }, [lock.value]); + + return Stack( + children: [ + child, + if (lock.value) + BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 20, + sigmaY: 20, + ), + child: Center( + child: Material( + color: Colors.transparent, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + Resources.assetsImagesLockSvg, + width: 68, + height: 68, + colorFilter: + ColorFilter.mode(context.theme.icon, BlendMode.srcIn), + ), + const SizedBox(height: 24), + Text( + // todo l10n + 'Enter Passcode to unlock Mixin Messenger', + textAlign: TextAlign.center, + style: TextStyle( + color: context.theme.text, + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 40), + SizedBox( + width: 204, + height: 14, + child: PinInputTextField( + controller: textEditingController, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly + ], + autoFocus: true, + focusNode: focusNode, + decoration: CirclePinDecoration( + strokeColorBuilder: + FixedColorBuilder(context.theme.text), + ), + onChanged: (value) { + hasError.value = false; + if (value.length != 6) return; + textEditingController.text = ''; + if (SecurityKeyValue.instance.passcode == value) { + lock.value = false; + } else { + hasError.value = true; + } + }, + ), + ), + const SizedBox(height: 28), + Visibility( + visible: hasError.value, + maintainSize: true, + maintainAnimation: true, + maintainState: true, + child: Text( + // todo l10n + 'Passcode incorrect', + textAlign: TextAlign.center, + style: TextStyle( + color: context.theme.red, + fontSize: 16, + fontWeight: FontWeight.w400, + ), + ), + ), + ], + ), + ), + ), + ) + ], + ); + } +} diff --git a/lib/widgets/auth_guard/circle_pin_decoration.dart b/lib/widgets/auth_guard/circle_pin_decoration.dart new file mode 100644 index 0000000000..5bd3888cc8 --- /dev/null +++ b/lib/widgets/auth_guard/circle_pin_decoration.dart @@ -0,0 +1,174 @@ +import 'package:flutter/material.dart'; +import 'package:pin_input_text_field/pin_input_text_field.dart'; + +extension NumListExtension on Iterable { + /// Return the sum of the list even the list is empty. + T sumList() { + if (T == int) { + var sum = 0; + forEach((n) => sum += n as int); + return sum as T; + } else if (T == double) { + var sum = 0.0; + forEach((n) => sum += n); + return sum as T; + } + throw AssertionError('not support type:${T.runtimeType}'); + } +} + +@immutable +class CirclePinDecoration extends PinDecoration implements SupportGap { + const CirclePinDecoration({ + this.gapSpace = 16, + this.gapSpaces, + required this.strokeColorBuilder, + this.strokeWidth = 1, + }) : super( + baseBgColorBuilder: strokeColorBuilder, + ); + + /// The box border width. + final double strokeWidth; + + /// The adjacent box gap. + final double gapSpace; + + /// The gaps between every two adjacent box, higher priority than [gapSpace]. + final List? gapSpaces; + + /// The box border color of index character. + final ColorBuilder strokeColorBuilder; + + @override + PinDecoration copyWith({ + TextStyle? textStyle, + ObscureStyle? obscureStyle, + String? errorText, + TextStyle? errorTextStyle, + String? hintText, + TextStyle? hintTextStyle, + ColorBuilder? bgColorBuilder, + }) => + CirclePinDecoration( + strokeColorBuilder: strokeColorBuilder, + strokeWidth: strokeWidth, + gapSpace: gapSpace, + gapSpaces: gapSpaces, + ); + + @override + PinEntryType get pinEntryType => PinEntryType.circle; + + @override + void notifyChange(String? pin) { + strokeColorBuilder.notifyChange(pin!); + } + + @override + void drawPin( + Canvas canvas, + Size size, + String text, + int pinLength, + Cursor? cursor, + TextDirection textDirection, + ) { + /// Calculate the height of paint area for drawing the pin field. + /// it should honor the error text (if any) drawn by + /// the actual texfield behind. + /// but, since we can access the drawn textfield behind from here, + /// we use a simple logic to calculate it. + double mainHeight; + if (errorText != null && errorText!.isNotEmpty) { + mainHeight = size.height - (errorTextStyle?.fontSize ?? 0 + 8.0); + } else { + mainHeight = size.height; + } + + final borderPaint = Paint() + ..strokeWidth = strokeWidth + ..style = PaintingStyle.stroke + ..isAntiAlias = true; + + final insidePaint = Paint() + ..style = PaintingStyle.fill + ..isAntiAlias = true; + + final gapTotalLength = gapSpaces?.sumList() ?? (pinLength - 1) * gapSpace; + + /// Calculate the width of each digit include stroke. + final singleWidth = (size.width - strokeWidth - gapTotalLength) / pinLength; + + double radius; // include strokeWidth + List actualGapSpaces; + if (singleWidth / 2 < mainHeight / 2 - strokeWidth / 2) { + radius = singleWidth / 2; + actualGapSpaces = + gapSpaces == null ? List.filled(pinLength - 1, gapSpace) : gapSpaces!; + } else { + radius = mainHeight / 2 - strokeWidth / 2; + actualGapSpaces = List.filled( + pinLength - 1, + (size.width - strokeWidth - radius * 2 * pinLength) / + (pinLength - 1)); + } + + var startX = strokeWidth / 2; + final startY = mainHeight / 2; + + final centerPoints = List.filled(pinLength, 0); + + /// Draw the each shape of pin. + for (var i = 0; i < pinLength; i++) { + borderPaint.color = strokeColorBuilder.indexProperty(i); + insidePaint.color = borderPaint.color; + + centerPoints[i] = startX + radius; + + canvas.drawCircle( + Offset(centerPoints[i], startY), + radius, + borderPaint, + ); + + if (i < text.runes.length) { + canvas.drawCircle( + Offset(startX + radius, startY), + radius - strokeWidth / 2, + insidePaint, + ); + } + startX += radius * 2 + (i == pinLength - 1 ? 0 : actualGapSpaces[i]); + } + } + + @override + double get getGapWidth => gapSpace; + + @override + List? get getGapWidthList => gapSpaces; + + @override + bool operator ==(Object other) => + identical(this, other) || + super == other && + other is CirclePinDecoration && + runtimeType == other.runtimeType && + strokeWidth == other.strokeWidth && + gapSpace == other.gapSpace && + gapSpaces == other.gapSpaces && + strokeColorBuilder == other.strokeColorBuilder; + + @override + int get hashCode => + super.hashCode ^ + strokeWidth.hashCode ^ + gapSpace.hashCode ^ + gapSpaces.hashCode ^ + strokeColorBuilder.hashCode; + + @override + String toString() => + 'CirclePinDecoration{strokeWidth: $strokeWidth, gapSpace: $gapSpace, gapSpaces: $gapSpaces, strokeColorBuilder: $strokeColorBuilder}'; +} diff --git a/pubspec.yaml b/pubspec.yaml index dfe04d3e36..572c94346a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -229,4 +229,4 @@ msix_config: toast_activator: clsid: "94B64592-528D-48B4-B37B-C82D634F1BE7" arguments: "-ToastActivated" - display_name: "Mixin Messenger" + display_name: "Mixin Messenger" \ No newline at end of file From e799670627dd28e9e0cfd3aeb00122eb97facb0a Mon Sep 17 00:00:00 2001 From: YeungKC Date: Wed, 5 Jul 2023 15:46:59 +0900 Subject: [PATCH 3/8] update --- pubspec.lock | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index c1361779e8..cfa3d106e8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -238,10 +238,10 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.17.2" console: dependency: transitive description: @@ -899,10 +899,10 @@ packages: dependency: "direct main" description: name: intl - sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.18.0" + version: "0.18.1" intl_phone_number_input: dependency: "direct main" description: @@ -1043,18 +1043,18 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.5.0" meta: dependency: transitive description: @@ -1290,6 +1290,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.0.1" + pin_input_text_field: + dependency: "direct main" + description: + name: pin_input_text_field + sha256: "8d6fc670aa673a4df5976086f0e8039972a5b2bcb783c8db8dd3b9b4b072ca90" + url: "https://pub.dev" + source: hosted + version: "4.5.1" platform: dependency: transitive description: @@ -1546,10 +1554,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" sprintf: dependency: transitive description: @@ -1658,10 +1666,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.0" timezone: dependency: transitive description: From 23e743c7a13058db7903def39515f804b000485d Mon Sep 17 00:00:00 2001 From: YeungKC Date: Wed, 5 Jul 2023 16:03:04 +0900 Subject: [PATCH 4/8] l10n --- lib/generated/intl/messages_en.dart | 11 +++++ lib/generated/intl/messages_es.dart | 1 + lib/generated/intl/messages_in.dart | 1 + lib/generated/intl/messages_ja.dart | 1 + lib/generated/intl/messages_ms.dart | 1 + lib/generated/intl/messages_ru.dart | 1 + lib/generated/intl/messages_zh-HK.dart | 7 +++ lib/generated/intl/messages_zh-TW.dart | 7 +++ lib/generated/intl/messages_zh.dart | 7 +++ lib/generated/l10n.dart | 60 ++++++++++++++++++++++++++ lib/l10n/intl_en.arb | 6 +++ lib/l10n/intl_es.arb | 1 + lib/l10n/intl_in.arb | 1 + lib/l10n/intl_ja.arb | 1 + lib/l10n/intl_ms.arb | 1 + lib/l10n/intl_ru.arb | 1 + lib/l10n/intl_zh-HK.arb | 6 +++ lib/l10n/intl_zh-TW.arb | 6 +++ lib/l10n/intl_zh.arb | 6 +++ lib/ui/setting/security_page.dart | 17 +++----- lib/ui/setting/setting_page.dart | 5 +-- lib/widgets/auth_guard/auth.dart | 3 +- 22 files changed, 136 insertions(+), 15 deletions(-) diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 4c4d3cf15c..c44365eefd 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -243,6 +243,8 @@ class MessageLookup extends MessageLookupByLibrary { "combineAndForward": MessageLookupByLibrary.simpleMessage("Combine and forward"), "confirm": MessageLookupByLibrary.simpleMessage("Confirm"), + "confirmPasscodeDesc": MessageLookupByLibrary.simpleMessage( + "Enter again to confirm the passcode"), "confirmSyncChatsFromPhone": MessageLookupByLibrary.simpleMessage( "Are you sure to sync the chat history from the phone?"), "confirmSyncChatsToPhone": MessageLookupByLibrary.simpleMessage( @@ -563,6 +565,8 @@ class MessageLookup extends MessageLookupByLibrary { "originalImage": MessageLookupByLibrary.simpleMessage("Original"), "owner": MessageLookupByLibrary.simpleMessage("Owner"), "participantsCount": m35, + "passcodeIncorrect": + MessageLookupByLibrary.simpleMessage("Passcode incorrect"), "password": MessageLookupByLibrary.simpleMessage("Password"), "pendingConfirmation": m36, "phoneNumber": MessageLookupByLibrary.simpleMessage("Phone Number"), @@ -620,6 +624,8 @@ class MessageLookup extends MessageLookupByLibrary { "sayHi": MessageLookupByLibrary.simpleMessage("Say Hi"), "scamWarning": MessageLookupByLibrary.simpleMessage( "Warning: Many users reported this account as a scam. Please be careful, especially if it asks you for money"), + "screenPasscode": + MessageLookupByLibrary.simpleMessage("Screen Passcode"), "search": MessageLookupByLibrary.simpleMessage("Search"), "searchContact": MessageLookupByLibrary.simpleMessage("Search contact"), "searchConversation": @@ -632,6 +638,7 @@ class MessageLookup extends MessageLookupByLibrary { "searchUnread": MessageLookupByLibrary.simpleMessage("Search Unread"), "secretUrl": MessageLookupByLibrary.simpleMessage( "https://mixin.one/pages/1000007"), + "security": MessageLookupByLibrary.simpleMessage("Security"), "select": MessageLookupByLibrary.simpleMessage("Select"), "send": MessageLookupByLibrary.simpleMessage("Send"), "sendArchived": MessageLookupByLibrary.simpleMessage( @@ -645,6 +652,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Send Without Sound"), "set": MessageLookupByLibrary.simpleMessage("Set"), "setDisappearingMessageTimeTo": m40, + "setPasscodeDesc": MessageLookupByLibrary.simpleMessage( + "Set Passcode to unlock Mixin Messenger"), "settingAuthSearchHint": MessageLookupByLibrary.simpleMessage("Mixin ID, Name"), "settingBackupTips": MessageLookupByLibrary.simpleMessage( @@ -729,6 +738,8 @@ class MessageLookup extends MessageLookupByLibrary { "unitSecond": m48, "unitWeek": m49, "unknowError": MessageLookupByLibrary.simpleMessage("Unknow error"), + "unlockWithWasscode": MessageLookupByLibrary.simpleMessage( + "Enter Passcode to unlock Mixin Messenger"), "unmute": MessageLookupByLibrary.simpleMessage("Unmute"), "unpin": MessageLookupByLibrary.simpleMessage("Unpin"), "unpinAllMessages": diff --git a/lib/generated/intl/messages_es.dart b/lib/generated/intl/messages_es.dart index 8623c33a90..69bcccda15 100644 --- a/lib/generated/intl/messages_es.dart +++ b/lib/generated/intl/messages_es.dart @@ -634,6 +634,7 @@ class MessageLookup extends MessageLookupByLibrary { "searchUnread": MessageLookupByLibrary.simpleMessage("Buscar no leído"), "secretUrl": MessageLookupByLibrary.simpleMessage( "https://mixin.one/pages/1000007"), + "security": MessageLookupByLibrary.simpleMessage("Seguridad"), "select": MessageLookupByLibrary.simpleMessage("Seleccionar"), "send": MessageLookupByLibrary.simpleMessage("Enviar"), "sendArchived": MessageLookupByLibrary.simpleMessage( diff --git a/lib/generated/intl/messages_in.dart b/lib/generated/intl/messages_in.dart index 83b8327d7b..c26333704b 100644 --- a/lib/generated/intl/messages_in.dart +++ b/lib/generated/intl/messages_in.dart @@ -342,6 +342,7 @@ class MessageLookup extends MessageLookupByLibrary { "searchRelatedMessage": m39, "secretUrl": MessageLookupByLibrary.simpleMessage( "https://mixin.one/pages/1000007"), + "security": MessageLookupByLibrary.simpleMessage("Keamanan"), "select": MessageLookupByLibrary.simpleMessage("Pilih"), "send": MessageLookupByLibrary.simpleMessage("Kirim"), "settingAuthSearchHint": diff --git a/lib/generated/intl/messages_ja.dart b/lib/generated/intl/messages_ja.dart index bb488910a1..c2ff477b75 100644 --- a/lib/generated/intl/messages_ja.dart +++ b/lib/generated/intl/messages_ja.dart @@ -533,6 +533,7 @@ class MessageLookup extends MessageLookupByLibrary { "searchRelatedMessage": m39, "secretUrl": MessageLookupByLibrary.simpleMessage( "https://mixin.one/pages/1000007"), + "security": MessageLookupByLibrary.simpleMessage("セキュリティ"), "select": MessageLookupByLibrary.simpleMessage("選択"), "send": MessageLookupByLibrary.simpleMessage("送る"), "sendArchived": diff --git a/lib/generated/intl/messages_ms.dart b/lib/generated/intl/messages_ms.dart index c99af5bfdb..2822db1f9a 100644 --- a/lib/generated/intl/messages_ms.dart +++ b/lib/generated/intl/messages_ms.dart @@ -348,6 +348,7 @@ class MessageLookup extends MessageLookupByLibrary { "searchRelatedMessage": m39, "secretUrl": MessageLookupByLibrary.simpleMessage( "https://mixin.one/pages/1000007"), + "security": MessageLookupByLibrary.simpleMessage("Keselamatan"), "select": MessageLookupByLibrary.simpleMessage("Pilih"), "send": MessageLookupByLibrary.simpleMessage("Hantar"), "settingAuthSearchHint": diff --git a/lib/generated/intl/messages_ru.dart b/lib/generated/intl/messages_ru.dart index 562827cc80..b4b8a08463 100644 --- a/lib/generated/intl/messages_ru.dart +++ b/lib/generated/intl/messages_ru.dart @@ -597,6 +597,7 @@ class MessageLookup extends MessageLookupByLibrary { "searchRelatedMessage": m39, "secretUrl": MessageLookupByLibrary.simpleMessage( "https://mixin.one/pages/1000007"), + "security": MessageLookupByLibrary.simpleMessage("Безопасность"), "select": MessageLookupByLibrary.simpleMessage("Выбрать"), "send": MessageLookupByLibrary.simpleMessage("Отправить"), "sendArchived": MessageLookupByLibrary.simpleMessage( diff --git a/lib/generated/intl/messages_zh-HK.dart b/lib/generated/intl/messages_zh-HK.dart index 90140f1c36..e0f1d1e076 100644 --- a/lib/generated/intl/messages_zh-HK.dart +++ b/lib/generated/intl/messages_zh-HK.dart @@ -216,6 +216,7 @@ class MessageLookup extends MessageLookupByLibrary { "collapse": MessageLookupByLibrary.simpleMessage("摺疊"), "combineAndForward": MessageLookupByLibrary.simpleMessage("合併轉發"), "confirm": MessageLookupByLibrary.simpleMessage("確認"), + "confirmPasscodeDesc": MessageLookupByLibrary.simpleMessage("再次確認密碼"), "confirmSyncChatsFromPhone": MessageLookupByLibrary.simpleMessage("確認從手機端同步聊天記錄嗎?"), "confirmSyncChatsToPhone": @@ -494,6 +495,7 @@ class MessageLookup extends MessageLookupByLibrary { "originalImage": MessageLookupByLibrary.simpleMessage("原圖"), "owner": MessageLookupByLibrary.simpleMessage("羣主"), "participantsCount": m35, + "passcodeIncorrect": MessageLookupByLibrary.simpleMessage("密碼不正確"), "password": MessageLookupByLibrary.simpleMessage("密碼"), "pendingConfirmation": m36, "phoneNumber": MessageLookupByLibrary.simpleMessage("手機號碼"), @@ -543,6 +545,7 @@ class MessageLookup extends MessageLookupByLibrary { "sayHi": MessageLookupByLibrary.simpleMessage("打招呼"), "scamWarning": MessageLookupByLibrary.simpleMessage( "警告:此賬號被大量用户舉報,請謹防網絡詐騙,注意個人財產安全"), + "screenPasscode": MessageLookupByLibrary.simpleMessage("鎖屏密碼"), "search": MessageLookupByLibrary.simpleMessage("搜索"), "searchContact": MessageLookupByLibrary.simpleMessage("搜索用户"), "searchConversation": MessageLookupByLibrary.simpleMessage("搜索聊天記錄"), @@ -551,6 +554,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("搜索 Mixin ID 或手機號碼:"), "searchRelatedMessage": m39, "searchUnread": MessageLookupByLibrary.simpleMessage("搜索未讀會話"), + "security": MessageLookupByLibrary.simpleMessage("安全"), "select": MessageLookupByLibrary.simpleMessage("選擇"), "send": MessageLookupByLibrary.simpleMessage("發送"), "sendArchived": MessageLookupByLibrary.simpleMessage("打包成 zip 發送"), @@ -561,6 +565,7 @@ class MessageLookup extends MessageLookupByLibrary { "sendWithoutSound": MessageLookupByLibrary.simpleMessage("靜音發送"), "set": MessageLookupByLibrary.simpleMessage("設置"), "setDisappearingMessageTimeTo": m40, + "setPasscodeDesc": MessageLookupByLibrary.simpleMessage("設置解鎖密碼"), "settingAuthSearchHint": MessageLookupByLibrary.simpleMessage("Mixin ID, 暱稱"), "settingBackupTips": MessageLookupByLibrary.simpleMessage( @@ -627,6 +632,8 @@ class MessageLookup extends MessageLookupByLibrary { "unitSecond": m48, "unitWeek": m49, "unknowError": MessageLookupByLibrary.simpleMessage("未知錯誤"), + "unlockWithWasscode": + MessageLookupByLibrary.simpleMessage("輸入密碼解鎖 Mixin Messenger"), "unmute": MessageLookupByLibrary.simpleMessage("取消靜音"), "unpin": MessageLookupByLibrary.simpleMessage("取消置頂"), "unpinAllMessages": MessageLookupByLibrary.simpleMessage("取消所有置頂消息"), diff --git a/lib/generated/intl/messages_zh-TW.dart b/lib/generated/intl/messages_zh-TW.dart index 903b0daf88..74ba4a881c 100644 --- a/lib/generated/intl/messages_zh-TW.dart +++ b/lib/generated/intl/messages_zh-TW.dart @@ -216,6 +216,7 @@ class MessageLookup extends MessageLookupByLibrary { "collapse": MessageLookupByLibrary.simpleMessage("摺疊"), "combineAndForward": MessageLookupByLibrary.simpleMessage("合併轉發"), "confirm": MessageLookupByLibrary.simpleMessage("確認"), + "confirmPasscodeDesc": MessageLookupByLibrary.simpleMessage("再次確認密碼"), "confirmSyncChatsFromPhone": MessageLookupByLibrary.simpleMessage("確認從手機端同步聊天記錄嗎?"), "confirmSyncChatsToPhone": @@ -494,6 +495,7 @@ class MessageLookup extends MessageLookupByLibrary { "originalImage": MessageLookupByLibrary.simpleMessage("原圖"), "owner": MessageLookupByLibrary.simpleMessage("群主"), "participantsCount": m35, + "passcodeIncorrect": MessageLookupByLibrary.simpleMessage("密碼不正確"), "password": MessageLookupByLibrary.simpleMessage("密碼"), "pendingConfirmation": m36, "phoneNumber": MessageLookupByLibrary.simpleMessage("手機號碼"), @@ -543,6 +545,7 @@ class MessageLookup extends MessageLookupByLibrary { "sayHi": MessageLookupByLibrary.simpleMessage("打招呼"), "scamWarning": MessageLookupByLibrary.simpleMessage( "警告:此賬號被大量使用者舉報,請謹防網路詐騙,注意個人財產安全"), + "screenPasscode": MessageLookupByLibrary.simpleMessage("鎖屏密碼"), "search": MessageLookupByLibrary.simpleMessage("搜尋"), "searchContact": MessageLookupByLibrary.simpleMessage("搜尋使用者"), "searchConversation": MessageLookupByLibrary.simpleMessage("搜尋聊天記錄"), @@ -551,6 +554,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("搜尋 Mixin ID 或手機號碼:"), "searchRelatedMessage": m39, "searchUnread": MessageLookupByLibrary.simpleMessage("搜尋未讀會話"), + "security": MessageLookupByLibrary.simpleMessage("安全"), "select": MessageLookupByLibrary.simpleMessage("選擇"), "send": MessageLookupByLibrary.simpleMessage("傳送"), "sendArchived": MessageLookupByLibrary.simpleMessage("打包成 zip 傳送"), @@ -561,6 +565,7 @@ class MessageLookup extends MessageLookupByLibrary { "sendWithoutSound": MessageLookupByLibrary.simpleMessage("靜音傳送"), "set": MessageLookupByLibrary.simpleMessage("設定"), "setDisappearingMessageTimeTo": m40, + "setPasscodeDesc": MessageLookupByLibrary.simpleMessage("設定解鎖密碼"), "settingAuthSearchHint": MessageLookupByLibrary.simpleMessage("Mixin ID, 暱稱"), "settingBackupTips": MessageLookupByLibrary.simpleMessage( @@ -627,6 +632,8 @@ class MessageLookup extends MessageLookupByLibrary { "unitSecond": m48, "unitWeek": m49, "unknowError": MessageLookupByLibrary.simpleMessage("未知錯誤"), + "unlockWithWasscode": + MessageLookupByLibrary.simpleMessage("輸入密碼解鎖 Mixin Messenger"), "unmute": MessageLookupByLibrary.simpleMessage("取消靜音"), "unpin": MessageLookupByLibrary.simpleMessage("取消置頂"), "unpinAllMessages": MessageLookupByLibrary.simpleMessage("取消所有置頂訊息"), diff --git a/lib/generated/intl/messages_zh.dart b/lib/generated/intl/messages_zh.dart index 916e87bc38..78e1aeb694 100644 --- a/lib/generated/intl/messages_zh.dart +++ b/lib/generated/intl/messages_zh.dart @@ -216,6 +216,7 @@ class MessageLookup extends MessageLookupByLibrary { "collapse": MessageLookupByLibrary.simpleMessage("折叠"), "combineAndForward": MessageLookupByLibrary.simpleMessage("合并转发"), "confirm": MessageLookupByLibrary.simpleMessage("确认"), + "confirmPasscodeDesc": MessageLookupByLibrary.simpleMessage("再次确认密码"), "confirmSyncChatsFromPhone": MessageLookupByLibrary.simpleMessage("确认从手机端同步聊天记录吗?"), "confirmSyncChatsToPhone": @@ -494,6 +495,7 @@ class MessageLookup extends MessageLookupByLibrary { "originalImage": MessageLookupByLibrary.simpleMessage("原图"), "owner": MessageLookupByLibrary.simpleMessage("群主"), "participantsCount": m35, + "passcodeIncorrect": MessageLookupByLibrary.simpleMessage("密码不正确"), "password": MessageLookupByLibrary.simpleMessage("密码"), "pendingConfirmation": m36, "phoneNumber": MessageLookupByLibrary.simpleMessage("手机号码"), @@ -543,6 +545,7 @@ class MessageLookup extends MessageLookupByLibrary { "sayHi": MessageLookupByLibrary.simpleMessage("打招呼"), "scamWarning": MessageLookupByLibrary.simpleMessage( "警告:此账号被大量用户举报,请谨防网络诈骗,注意个人财产安全"), + "screenPasscode": MessageLookupByLibrary.simpleMessage("锁屏密码"), "search": MessageLookupByLibrary.simpleMessage("搜索"), "searchContact": MessageLookupByLibrary.simpleMessage("搜索用户"), "searchConversation": MessageLookupByLibrary.simpleMessage("搜索聊天记录"), @@ -551,6 +554,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("搜索 Mixin ID 或手机号码:"), "searchRelatedMessage": m39, "searchUnread": MessageLookupByLibrary.simpleMessage("搜索未读会话"), + "security": MessageLookupByLibrary.simpleMessage("安全"), "select": MessageLookupByLibrary.simpleMessage("选择"), "send": MessageLookupByLibrary.simpleMessage("发送"), "sendArchived": MessageLookupByLibrary.simpleMessage("打包成 zip 发送"), @@ -561,6 +565,7 @@ class MessageLookup extends MessageLookupByLibrary { "sendWithoutSound": MessageLookupByLibrary.simpleMessage("静音发送"), "set": MessageLookupByLibrary.simpleMessage("设置"), "setDisappearingMessageTimeTo": m40, + "setPasscodeDesc": MessageLookupByLibrary.simpleMessage("设置解锁密码"), "settingAuthSearchHint": MessageLookupByLibrary.simpleMessage("Mixin ID, 昵称"), "settingBackupTips": MessageLookupByLibrary.simpleMessage( @@ -627,6 +632,8 @@ class MessageLookup extends MessageLookupByLibrary { "unitSecond": m48, "unitWeek": m49, "unknowError": MessageLookupByLibrary.simpleMessage("未知错误"), + "unlockWithWasscode": + MessageLookupByLibrary.simpleMessage("输入密码解锁 Mixin Messenger"), "unmute": MessageLookupByLibrary.simpleMessage("取消静音"), "unpin": MessageLookupByLibrary.simpleMessage("取消置顶"), "unpinAllMessages": MessageLookupByLibrary.simpleMessage("取消所有置顶消息"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index a68ab6341b..724e80fa34 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -794,6 +794,16 @@ class Localization { ); } + /// `Enter again to confirm the passcode` + String get confirmPasscodeDesc { + return Intl.message( + 'Enter again to confirm the passcode', + name: 'confirmPasscodeDesc', + desc: '', + args: [], + ); + } + /// `Are you sure to sync the chat history from the phone?` String get confirmSyncChatsFromPhone { return Intl.message( @@ -2970,6 +2980,16 @@ class Localization { ); } + /// `Passcode incorrect` + String get passcodeIncorrect { + return Intl.message( + 'Passcode incorrect', + name: 'passcodeIncorrect', + desc: '', + args: [], + ); + } + /// `Password` String get password { return Intl.message( @@ -3424,6 +3444,16 @@ class Localization { ); } + /// `Screen Passcode` + String get screenPasscode { + return Intl.message( + 'Screen Passcode', + name: 'screenPasscode', + desc: '', + args: [], + ); + } + /// `Search` String get search { return Intl.message( @@ -3506,6 +3536,16 @@ class Localization { ); } + /// `Security` + String get security { + return Intl.message( + 'Security', + name: 'security', + desc: '', + args: [], + ); + } + /// `Select` String get select { return Intl.message( @@ -3596,6 +3636,16 @@ class Localization { ); } + /// `Set Passcode to unlock Mixin Messenger` + String get setPasscodeDesc { + return Intl.message( + 'Set Passcode to unlock Mixin Messenger', + name: 'setPasscodeDesc', + desc: '', + args: [], + ); + } + /// `Mixin ID, Name` String get settingAuthSearchHint { return Intl.message( @@ -4226,6 +4276,16 @@ class Localization { ); } + /// `Enter Passcode to unlock Mixin Messenger` + String get unlockWithWasscode { + return Intl.message( + 'Enter Passcode to unlock Mixin Messenger', + name: 'unlockWithWasscode', + desc: '', + args: [], + ); + } + /// `Unmute` String get unmute { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 1f5ff0567f..9b5ed2549a 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -73,6 +73,7 @@ "collapse" : "Collapse", "combineAndForward" : "Combine and forward", "confirm" : "Confirm", +"confirmPasscodeDesc" : "Enter again to confirm the passcode", "confirmSyncChatsFromPhone" : "Are you sure to sync the chat history from the phone?", "confirmSyncChatsToPhone" : "Are you sure to sync the chat history to the phone?", "contact" : "Contact", @@ -290,6 +291,7 @@ "originalImage" : "Original", "owner" : "Owner", "participantsCount" : "{arg0} PARTICIPANTS", +"passcodeIncorrect" : "Passcode incorrect", "password" : "Password", "pendingConfirmation" : "{count, plural, one{{arg0}/{arg1} confirmation} other{{arg0}/{arg1} confirmations}}", "phoneNumber" : "Phone Number", @@ -335,6 +337,7 @@ "saveToCameraRoll" : "Save to Camera Roll", "sayHi" : "Say Hi", "scamWarning" : "Warning: Many users reported this account as a scam. Please be careful, especially if it asks you for money", +"screenPasscode" : "Screen Passcode", "search" : "Search", "searchContact" : "Search contact", "searchConversation" : "Search Conversation", @@ -343,6 +346,7 @@ "searchRelatedMessage" : "{count, plural, one{{arg0} related message} other{{arg0} related messages}}", "searchUnread" : "Search Unread", "secretUrl" : "https://mixin.one/pages/1000007", +"security" : "Security", "select" : "Select", "send" : "Send", "sendArchived" : "Archived all files in one zip file", @@ -352,6 +356,7 @@ "sendWithoutSound" : "Send Without Sound", "set" : "Set", "setDisappearingMessageTimeTo" : "{arg0} set disappearing message time to {arg1}", +"setPasscodeDesc" : "Set Passcode to unlock Mixin Messenger", "settingAuthSearchHint" : "Mixin ID, Name", "settingBackupTips" : "Back up your chat history to iCloud. if you lose your iPhone or switch to a new one, you can restore your chat history when you reinstall Mixin Messenger. Messages you back up are not protected by Mixin Messenger end-to-end encryption while in iCloud.", "settingDeleteAccountPinContent" : "If you continue, your profile and account details will be delete on {arg0}. read our document to **learn more**.", @@ -414,6 +419,7 @@ "unitSecond" : "{count, plural, one{second} other{seconds}}", "unitWeek" : "{count, plural, one{week} other{weeks}}", "unknowError" : "Unknow error", +"unlockWithWasscode" : "Enter Passcode to unlock Mixin Messenger", "unmute" : "Unmute", "unpin" : "Unpin", "unpinAllMessages" : "Unpin All Messages", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 59ef41aa25..acaaeadfef 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -329,6 +329,7 @@ "searchRelatedMessage" : "{count, plural, one{{arg0} mensaje relacionado} other{{arg0} mensajes relacionados}}", "searchUnread" : "Buscar no leído", "secretUrl" : "https://mixin.one/pages/1000007", +"security" : "Seguridad", "select" : "Seleccionar", "send" : "Enviar", "sendArchived" : "Todos los archivos archivados en un archivo zip", diff --git a/lib/l10n/intl_in.arb b/lib/l10n/intl_in.arb index eb1fa4783a..dfe0e1679b 100644 --- a/lib/l10n/intl_in.arb +++ b/lib/l10n/intl_in.arb @@ -180,6 +180,7 @@ "searchConversation" : "Cari Percakapan", "searchRelatedMessage" : "{count, plural, one{null} other{{arg0} pesan terkait}}", "secretUrl" : "https://mixin.one/pages/1000007", +"security" : "Keamanan", "select" : "Pilih", "send" : "Kirim", "settingAuthSearchHint" : "Mixin ID, Nama", diff --git a/lib/l10n/intl_ja.arb b/lib/l10n/intl_ja.arb index d0a094338e..0472327986 100644 --- a/lib/l10n/intl_ja.arb +++ b/lib/l10n/intl_ja.arb @@ -309,6 +309,7 @@ "searchPlaceholderNumber" : "Mixin ID または電話番号を検索", "searchRelatedMessage" : "{count, plural, one{{arg0}個の関連するメッセージ} other{{arg0}個の関連するメッセージ}}", "secretUrl" : "https://mixin.one/pages/1000007", +"security" : "セキュリティ", "select" : "選択", "send" : "送る", "sendArchived" : "1つのZIPファイルにアーカイブ", diff --git a/lib/l10n/intl_ms.arb b/lib/l10n/intl_ms.arb index b2c3cd5a43..0138fd31e4 100644 --- a/lib/l10n/intl_ms.arb +++ b/lib/l10n/intl_ms.arb @@ -181,6 +181,7 @@ "searchConversation" : "Cari Perbualan", "searchRelatedMessage" : "{count, plural, one{null} other{{arg0} mesej berkaitan}}", "secretUrl" : "https://mixin.one/pages/1000007", +"security" : "Keselamatan", "select" : "Pilih", "send" : "Hantar", "settingAuthSearchHint" : "Mixin ID, Nama", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 18274ea512..68251e38e9 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -309,6 +309,7 @@ "searchPlaceholderNumber" : "Найдите Mixin ID или номер телефона:", "searchRelatedMessage" : "{count, plural, one{{arg0} связанное сообщение} other{{arg0} похожие сообщения}}", "secretUrl" : "https://mixin.one/pages/1000007", +"security" : "Безопасность", "select" : "Выбрать", "send" : "Отправить", "sendArchived" : "Заархивированы все файлы в один zip файл", diff --git a/lib/l10n/intl_zh-HK.arb b/lib/l10n/intl_zh-HK.arb index 76f6ae3e81..c74b221955 100644 --- a/lib/l10n/intl_zh-HK.arb +++ b/lib/l10n/intl_zh-HK.arb @@ -72,6 +72,7 @@ "collapse" : "摺疊", "combineAndForward" : "合併轉發", "confirm" : "確認", +"confirmPasscodeDesc" : "再次確認密碼", "confirmSyncChatsFromPhone" : "確認從手機端同步聊天記錄嗎?", "confirmSyncChatsToPhone" : "確認同步聊天記錄到手機端嗎?", "contact" : "聯繫人", @@ -289,6 +290,7 @@ "originalImage" : "原圖", "owner" : "羣主", "participantsCount" : "{arg0} 位羣組成員", +"passcodeIncorrect" : "密碼不正確", "password" : "密碼", "pendingConfirmation" : "{count, plural, one{{arg0}/{arg1} 區塊確認數} other{{arg0}/{arg1} 區塊確認數}}", "phoneNumber" : "手機號碼", @@ -334,6 +336,7 @@ "saveToCameraRoll" : "保存到相冊", "sayHi" : "打招呼", "scamWarning" : "警告:此賬號被大量用户舉報,請謹防網絡詐騙,注意個人財產安全", +"screenPasscode" : "鎖屏密碼", "search" : "搜索", "searchContact" : "搜索用户", "searchConversation" : "搜索聊天記錄", @@ -341,6 +344,7 @@ "searchPlaceholderNumber" : "搜索 Mixin ID 或手機號碼:", "searchRelatedMessage" : "{count, plural, one{{arg0} 條相關的消息} other{{arg0} 條相關的消息}}", "searchUnread" : "搜索未讀會話", +"security" : "安全", "select" : "選擇", "send" : "發送", "sendArchived" : "打包成 zip 發送", @@ -350,6 +354,7 @@ "sendWithoutSound" : "靜音發送", "set" : "設置", "setDisappearingMessageTimeTo" : "{arg0}將限時消息設置為 {arg1}", +"setPasscodeDesc" : "設置解鎖密碼", "settingAuthSearchHint" : "Mixin ID, 暱稱", "settingBackupTips" : "備份你的聊天記錄到 iCloud。如果你丟失或者更換手機,你可以在重新安裝 Mixin Messenger 時恢復你的聊天記錄。注意備份到 iCloud 中的聊天記錄不受端對端加密保護!", "settingDeleteAccountPinContent" : "如果您繼續,您的個人資料和賬户信息將在{arg0}被刪除。閲讀我們的文檔以**瞭解更多**。", @@ -410,6 +415,7 @@ "unitSecond" : "{count, plural, one{秒} other{秒}}", "unitWeek" : "{count, plural, one{週} other{週}}", "unknowError" : "未知錯誤", +"unlockWithWasscode" : "輸入密碼解鎖 Mixin Messenger", "unmute" : "取消靜音", "unpin" : "取消置頂", "unpinAllMessages" : "取消所有置頂消息", diff --git a/lib/l10n/intl_zh-TW.arb b/lib/l10n/intl_zh-TW.arb index a4f4e1f24d..20de01c4ce 100644 --- a/lib/l10n/intl_zh-TW.arb +++ b/lib/l10n/intl_zh-TW.arb @@ -72,6 +72,7 @@ "collapse" : "摺疊", "combineAndForward" : "合併轉發", "confirm" : "確認", +"confirmPasscodeDesc" : "再次確認密碼", "confirmSyncChatsFromPhone" : "確認從手機端同步聊天記錄嗎?", "confirmSyncChatsToPhone" : "確認同步聊天記錄到手機端嗎?", "contact" : "聯絡人", @@ -289,6 +290,7 @@ "originalImage" : "原圖", "owner" : "群主", "participantsCount" : "{arg0} 位群組成員", +"passcodeIncorrect" : "密碼不正確", "password" : "密碼", "pendingConfirmation" : "{count, plural, one{{arg0}/{arg1} 區塊確認數} other{{arg0}/{arg1} 區塊確認數}}", "phoneNumber" : "手機號碼", @@ -334,6 +336,7 @@ "saveToCameraRoll" : "儲存到相簿", "sayHi" : "打招呼", "scamWarning" : "警告:此賬號被大量使用者舉報,請謹防網路詐騙,注意個人財產安全", +"screenPasscode" : "鎖屏密碼", "search" : "搜尋", "searchContact" : "搜尋使用者", "searchConversation" : "搜尋聊天記錄", @@ -341,6 +344,7 @@ "searchPlaceholderNumber" : "搜尋 Mixin ID 或手機號碼:", "searchRelatedMessage" : "{count, plural, one{{arg0} 條相關的訊息} other{{arg0} 條相關的訊息}}", "searchUnread" : "搜尋未讀會話", +"security" : "安全", "select" : "選擇", "send" : "傳送", "sendArchived" : "打包成 zip 傳送", @@ -350,6 +354,7 @@ "sendWithoutSound" : "靜音傳送", "set" : "設定", "setDisappearingMessageTimeTo" : "{arg0}將限時訊息設定為 {arg1}", +"setPasscodeDesc" : "設定解鎖密碼", "settingAuthSearchHint" : "Mixin ID, 暱稱", "settingBackupTips" : "備份你的聊天記錄到 iCloud。如果你丟失或者更換手機,你可以在重新安裝 Mixin Messenger 時恢復你的聊天記錄。注意備份到 iCloud 中的聊天記錄不受端對端加密保護!", "settingDeleteAccountPinContent" : "如果您繼續,您的個人資料和賬戶資訊將在{arg0}被刪除。閱讀我們的檔案以**瞭解更多**。", @@ -410,6 +415,7 @@ "unitSecond" : "{count, plural, one{秒} other{秒}}", "unitWeek" : "{count, plural, one{週} other{週}}", "unknowError" : "未知錯誤", +"unlockWithWasscode" : "輸入密碼解鎖 Mixin Messenger", "unmute" : "取消靜音", "unpin" : "取消置頂", "unpinAllMessages" : "取消所有置頂訊息", diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index de1483733b..4f49b36719 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -72,6 +72,7 @@ "collapse" : "折叠", "combineAndForward" : "合并转发", "confirm" : "确认", +"confirmPasscodeDesc" : "再次确认密码", "confirmSyncChatsFromPhone" : "确认从手机端同步聊天记录吗?", "confirmSyncChatsToPhone" : "确认同步聊天记录到手机端吗?", "contact" : "联系人", @@ -289,6 +290,7 @@ "originalImage" : "原图", "owner" : "群主", "participantsCount" : "{arg0} 位群组成员", +"passcodeIncorrect" : "密码不正确", "password" : "密码", "pendingConfirmation" : "{count, plural, one{{arg0}/{arg1} 区块确认数} other{{arg0}/{arg1} 区块确认数}}", "phoneNumber" : "手机号码", @@ -334,6 +336,7 @@ "saveToCameraRoll" : "保存到相册", "sayHi" : "打招呼", "scamWarning" : "警告:此账号被大量用户举报,请谨防网络诈骗,注意个人财产安全", +"screenPasscode" : "锁屏密码", "search" : "搜索", "searchContact" : "搜索用户", "searchConversation" : "搜索聊天记录", @@ -341,6 +344,7 @@ "searchPlaceholderNumber" : "搜索 Mixin ID 或手机号码:", "searchRelatedMessage" : "{count, plural, one{{arg0} 条相关的消息} other{{arg0} 条相关的消息}}", "searchUnread" : "搜索未读会话", +"security" : "安全", "select" : "选择", "send" : "发送", "sendArchived" : "打包成 zip 发送", @@ -350,6 +354,7 @@ "sendWithoutSound" : "静音发送", "set" : "设置", "setDisappearingMessageTimeTo" : "{arg0}将限时消息设置为 {arg1}", +"setPasscodeDesc" : "设置解锁密码", "settingAuthSearchHint" : "Mixin ID, 昵称", "settingBackupTips" : "备份你的聊天记录到 iCloud。如果你丢失或者更换手机,你可以在重新安装 Mixin Messenger 时恢复你的聊天记录。注意备份到 iCloud 中的聊天记录不受端对端加密保护!", "settingDeleteAccountPinContent" : "如果您继续,您的个人资料和账户信息将在{arg0}被删除。阅读我们的文档以**了解更多**。", @@ -410,6 +415,7 @@ "unitSecond" : "{count, plural, one{秒} other{秒}}", "unitWeek" : "{count, plural, one{周} other{周}}", "unknowError" : "未知错误", +"unlockWithWasscode" : "输入密码解锁 Mixin Messenger", "unmute" : "取消静音", "unpin" : "取消置顶", "unpinAllMessages" : "取消所有置顶消息", diff --git a/lib/ui/setting/security_page.dart b/lib/ui/setting/security_page.dart index 280681fad8..742b6ae8d2 100644 --- a/lib/ui/setting/security_page.dart +++ b/lib/ui/setting/security_page.dart @@ -18,9 +18,8 @@ class SecurityPage extends StatelessWidget { @override Widget build(BuildContext context) => Scaffold( backgroundColor: context.theme.background, - appBar: const MixinAppBar( - // todo l10n - title: Text('Security'), + appBar: MixinAppBar( + title: Text(context.l10n.security), ), body: ConstrainedBox( constraints: const BoxConstraints.expand(), @@ -46,8 +45,7 @@ class _Passcode extends HookWidget { return CellGroup( child: CellItem( - // todo l10n - title: const Text('Passcode'), + title: Text(context.l10n.screenPasscode), trailing: Transform.scale( scale: 0.7, child: CupertinoSwitch( @@ -85,8 +83,8 @@ class _InputPasscode extends HookWidget { if (confirmPasscode.value == null) return; if (passcode.value != confirmPasscode.value) { - // todo l10n - showToastFailed('Passcode not match', context: context); + // todo fix toast + showToastFailed(context.l10n.passcodeIncorrect, context: context); passcode.value = null; confirmPasscode.value = null; @@ -112,10 +110,9 @@ class _InputPasscode extends HookWidget { ], ), Text( - // todo l10n passcode.value != null - ? 'Enter again to confirm the passcode' - : 'Set Passcode to unlock Mixin Messenger', + ? context.l10n.confirmPasscodeDesc + : context.l10n.setPasscodeDesc, textAlign: TextAlign.center, style: TextStyle( color: context.theme.text, diff --git a/lib/ui/setting/setting_page.dart b/lib/ui/setting/setting_page.dart index f415c46f74..c6e8f94ad6 100644 --- a/lib/ui/setting/setting_page.dart +++ b/lib/ui/setting/setting_page.dart @@ -113,11 +113,10 @@ class SettingPage extends HookWidget { .dataAndStorageUsagePage, title: context.l10n.dataAndStorageUsage, ), - const _Item( + _Item( leadingAssetName: Resources.assetsImagesShieldSvg, pageName: ResponsiveNavigatorCubit.securityPage, - // todo l10n - title: 'Security', + title: context.l10n.security, ), _Item( leadingAssetName: Resources.assetsImagesProxySvg, diff --git a/lib/widgets/auth_guard/auth.dart b/lib/widgets/auth_guard/auth.dart index ea3fac9b47..6fd41af1de 100644 --- a/lib/widgets/auth_guard/auth.dart +++ b/lib/widgets/auth_guard/auth.dart @@ -174,8 +174,7 @@ class _AuthGuard extends HookWidget { maintainAnimation: true, maintainState: true, child: Text( - // todo l10n - 'Passcode incorrect', + context.l10n.passcodeIncorrect, textAlign: TextAlign.center, style: TextStyle( color: context.theme.red, From 17dd1592404ede7067641eb4a6c545725a7dc115 Mon Sep 17 00:00:00 2001 From: YeungKC Date: Wed, 5 Jul 2023 17:04:35 +0900 Subject: [PATCH 5/8] update --- lib/app.dart | 2 +- lib/ui/setting/security_page.dart | 49 ++-- lib/widgets/auth.dart | 209 ++++++++++++++++++ lib/widgets/auth_guard/auth.dart | 194 ---------------- .../auth_guard/circle_pin_decoration.dart | 174 --------------- pubspec.lock | 8 - pubspec.yaml | 1 - 7 files changed, 244 insertions(+), 393 deletions(-) create mode 100644 lib/widgets/auth.dart delete mode 100644 lib/widgets/auth_guard/auth.dart delete mode 100644 lib/widgets/auth_guard/circle_pin_decoration.dart diff --git a/lib/app.dart b/lib/app.dart index 9113585071..ec8383f6fc 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -34,7 +34,7 @@ import 'utils/platform.dart'; import 'utils/system/system_fonts.dart'; import 'utils/system/text_input.dart'; import 'utils/system/tray.dart'; -import 'widgets/auth_guard/auth.dart'; +import 'widgets/auth.dart'; import 'widgets/brightness_observer.dart'; import 'widgets/focus_helper.dart'; import 'widgets/message/item/text/mention_builder.dart'; diff --git a/lib/ui/setting/security_page.dart b/lib/ui/setting/security_page.dart index 742b6ae8d2..cc6fb0af09 100644 --- a/lib/ui/setting/security_page.dart +++ b/lib/ui/setting/security_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:pin_input_text_field/pin_input_text_field.dart'; +import 'package:pin_code_fields/pin_code_fields.dart'; import '../../account/security_key_value.dart'; import '../../utils/extension/extension.dart'; @@ -73,6 +73,20 @@ class _InputPasscode extends HookWidget { @override Widget build(BuildContext context) { + final focusNode = useFocusNode(); + useEffect(() { + focusNode.requestFocus(); + void listener() { + if (focusNode.hasFocus) return; + focusNode.requestFocus(); + } + + focusNode.addListener(listener); + return () { + focusNode.removeListener(listener); + }; + }, []); + final textEditingController = useTextEditingController(); final passcode = useState(null); @@ -123,24 +137,29 @@ class _InputPasscode extends HookWidget { const SizedBox(height: 40), SizedBox( width: 215, - child: PinInputTextField( + child: PinCodeTextField( + appContext: context, + length: 6, inputFormatters: [FilteringTextInputFormatter.digitsOnly], autoFocus: true, - decoration: UnderlineDecoration( - gapSpace: 25, - textStyle: TextStyle( - color: context.theme.text, - fontSize: 18, - fontWeight: FontWeight.w400, - ), - colorBuilder: FixedColorBuilder( - context.theme.listSelected, - ), + keyboardType: TextInputType.number, + useHapticFeedback: true, + pinTheme: PinTheme( + activeColor: context.theme.text, + inactiveColor: context.theme.text, + selectedColor: context.theme.text, + fieldWidth: 15, + borderWidth: 2, + ), + textStyle: TextStyle( + fontSize: 18, + color: context.theme.text, ), + autoDisposeControllers: false, + focusNode: focusNode, controller: textEditingController, - onChanged: (value) { - if (value.length != 6) return; - + showCursor: false, + onCompleted: (value) { if (passcode.value != null) { confirmPasscode.value = value; } else { diff --git a/lib/widgets/auth.dart b/lib/widgets/auth.dart new file mode 100644 index 0000000000..17d5823087 --- /dev/null +++ b/lib/widgets/auth.dart @@ -0,0 +1,209 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:pin_code_fields/pin_code_fields.dart'; + +import '../account/account_server.dart'; +import '../account/security_key_value.dart'; +import '../constants/resources.dart'; +import '../ui/home/bloc/multi_auth_cubit.dart'; +import '../utils/app_lifecycle.dart'; +import '../utils/extension/extension.dart'; +import '../utils/hook.dart'; + +const lockDuration = Duration(minutes: 1); + +class AuthGuard extends HookWidget { + const AuthGuard({ + super.key, + required this.child, + }); + + final Widget child; + + @override + Widget build(BuildContext context) { + final authAvailable = + useBlocState().current != null; + AccountServer? accountServer; + try { + accountServer = context.read(); + } catch (_) {} + final signed = authAvailable && accountServer != null; + + if (signed) return _AuthGuard(child: child); + + return child; + } +} + +class _AuthGuard extends HookWidget { + const _AuthGuard({required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + final focusNode = useFocusNode(); + final textEditingController = useTextEditingController(); + final hasPasscode = + useMemoizedStream(SecurityKeyValue.instance.watchHasPasscode).data ?? + SecurityKeyValue.instance.hasPasscode; + + final hasError = useState(false); + final lock = useState(false); + + useEffect(() { + Timer? timer; + void dispose() { + timer?.cancel(); + timer = null; + } + + void listener() { + if (lock.value) return; + + final needLock = !isAppActive; + + if (needLock) { + timer = Timer(lockDuration, () { + if (!hasPasscode) { + lock.value = false; + return; + } + + lock.value = !isAppActive; + }); + } else { + dispose(); + lock.value = needLock; + } + } + + listener(); + appActiveListener.addListener(listener); + return () { + dispose(); + appActiveListener.removeListener(listener); + }; + }, [hasPasscode]); + + useEffect(() { + focusNode.requestFocus(); + void listener() { + if (focusNode.hasFocus) return; + focusNode.requestFocus(); + } + + focusNode.addListener(listener); + return () { + focusNode.removeListener(listener); + }; + }, [lock.value]); + + return Stack( + children: [ + child, + if (lock.value) + BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 20, + sigmaY: 20, + ), + child: MaterialApp( + color: Colors.transparent, + home: Material( + color: Colors.transparent, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + Resources.assetsImagesLockSvg, + width: 68, + height: 68, + colorFilter: ColorFilter.mode( + context.theme.icon, BlendMode.srcIn), + ), + const SizedBox(height: 24), + Text( + context.l10n.unlockWithWasscode, + textAlign: TextAlign.center, + style: TextStyle( + color: context.theme.text, + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 40), + SizedBox( + width: 204, + // height: 14, + child: PinCodeTextField( + appContext: context, + length: 6, + controller: textEditingController, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly + ], + pinTheme: PinTheme( + activeColor: context.theme.text, + inactiveColor: context.theme.text, + selectedColor: context.theme.text, + fieldWidth: 15, + borderWidth: 1, + shape: PinCodeFieldShape.circle, + ), + obscureText: true, + autoDisposeControllers: false, + obscuringWidget: Container( + decoration: BoxDecoration( + color: context.theme.text, + shape: BoxShape.circle, + )), + autoFocus: true, + focusNode: focusNode, + showCursor: false, + onCompleted: (value) { + textEditingController.text = ''; + if (SecurityKeyValue.instance.passcode == value) { + lock.value = false; + } else { + hasError.value = true; + } + }, + onChanged: (value) { + hasError.value = false; + }, + ), + ), + const SizedBox(height: 28), + Visibility( + visible: hasError.value, + maintainSize: true, + maintainAnimation: true, + maintainState: true, + child: Text( + context.l10n.passcodeIncorrect, + textAlign: TextAlign.center, + style: TextStyle( + color: context.theme.red, + fontSize: 16, + fontWeight: FontWeight.w400, + ), + ), + ), + ], + ), + ), + ), + ), + ) + ], + ); + } +} diff --git a/lib/widgets/auth_guard/auth.dart b/lib/widgets/auth_guard/auth.dart deleted file mode 100644 index 6fd41af1de..0000000000 --- a/lib/widgets/auth_guard/auth.dart +++ /dev/null @@ -1,194 +0,0 @@ -import 'dart:async'; -import 'dart:ui'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:pin_input_text_field/pin_input_text_field.dart' - hide CirclePinDecoration; - -import '../../account/account_server.dart'; -import '../../account/security_key_value.dart'; -import '../../constants/resources.dart'; -import '../../ui/home/bloc/multi_auth_cubit.dart'; -import '../../utils/app_lifecycle.dart'; -import '../../utils/extension/extension.dart'; -import '../../utils/hook.dart'; -import 'circle_pin_decoration.dart'; - -const lockDuration = Duration(minutes: 1); - -class AuthGuard extends HookWidget { - const AuthGuard({ - super.key, - required this.child, - }); - - final Widget child; - - @override - Widget build(BuildContext context) { - final authAvailable = - useBlocState().current != null; - AccountServer? accountServer; - try { - accountServer = context.read(); - } catch (_) {} - final signed = authAvailable && accountServer != null; - - if (signed) return _AuthGuard(child: child); - - return child; - } -} - -class _AuthGuard extends HookWidget { - const _AuthGuard({required this.child}); - - final Widget child; - - @override - Widget build(BuildContext context) { - final focusNode = useFocusNode(); - final textEditingController = useTextEditingController(); - final hasPasscode = - useMemoizedStream(SecurityKeyValue.instance.watchHasPasscode).data ?? - SecurityKeyValue.instance.hasPasscode; - - final hasError = useState(false); - final lock = useState(false); - - useEffect(() { - Timer? timer; - void dispose() { - timer?.cancel(); - timer = null; - } - - void listener() { - if (lock.value) return; - - final needLock = !isAppActive; - - if (needLock) { - timer = Timer(lockDuration, () { - if (!hasPasscode) { - lock.value = false; - return; - } - - lock.value = !isAppActive; - }); - } else { - dispose(); - lock.value = needLock; - } - } - - listener(); - appActiveListener.addListener(listener); - return () { - dispose(); - appActiveListener.removeListener(listener); - }; - }, [hasPasscode]); - - useEffect(() { - focusNode.requestFocus(); - void listener() { - if (focusNode.hasFocus) return; - focusNode.requestFocus(); - } - - focusNode.addListener(listener); - return () { - focusNode.removeListener(listener); - }; - }, [lock.value]); - - return Stack( - children: [ - child, - if (lock.value) - BackdropFilter( - filter: ImageFilter.blur( - sigmaX: 20, - sigmaY: 20, - ), - child: Center( - child: Material( - color: Colors.transparent, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SvgPicture.asset( - Resources.assetsImagesLockSvg, - width: 68, - height: 68, - colorFilter: - ColorFilter.mode(context.theme.icon, BlendMode.srcIn), - ), - const SizedBox(height: 24), - Text( - // todo l10n - 'Enter Passcode to unlock Mixin Messenger', - textAlign: TextAlign.center, - style: TextStyle( - color: context.theme.text, - fontSize: 20, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 40), - SizedBox( - width: 204, - height: 14, - child: PinInputTextField( - controller: textEditingController, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly - ], - autoFocus: true, - focusNode: focusNode, - decoration: CirclePinDecoration( - strokeColorBuilder: - FixedColorBuilder(context.theme.text), - ), - onChanged: (value) { - hasError.value = false; - if (value.length != 6) return; - textEditingController.text = ''; - if (SecurityKeyValue.instance.passcode == value) { - lock.value = false; - } else { - hasError.value = true; - } - }, - ), - ), - const SizedBox(height: 28), - Visibility( - visible: hasError.value, - maintainSize: true, - maintainAnimation: true, - maintainState: true, - child: Text( - context.l10n.passcodeIncorrect, - textAlign: TextAlign.center, - style: TextStyle( - color: context.theme.red, - fontSize: 16, - fontWeight: FontWeight.w400, - ), - ), - ), - ], - ), - ), - ), - ) - ], - ); - } -} diff --git a/lib/widgets/auth_guard/circle_pin_decoration.dart b/lib/widgets/auth_guard/circle_pin_decoration.dart deleted file mode 100644 index 5bd3888cc8..0000000000 --- a/lib/widgets/auth_guard/circle_pin_decoration.dart +++ /dev/null @@ -1,174 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:pin_input_text_field/pin_input_text_field.dart'; - -extension NumListExtension on Iterable { - /// Return the sum of the list even the list is empty. - T sumList() { - if (T == int) { - var sum = 0; - forEach((n) => sum += n as int); - return sum as T; - } else if (T == double) { - var sum = 0.0; - forEach((n) => sum += n); - return sum as T; - } - throw AssertionError('not support type:${T.runtimeType}'); - } -} - -@immutable -class CirclePinDecoration extends PinDecoration implements SupportGap { - const CirclePinDecoration({ - this.gapSpace = 16, - this.gapSpaces, - required this.strokeColorBuilder, - this.strokeWidth = 1, - }) : super( - baseBgColorBuilder: strokeColorBuilder, - ); - - /// The box border width. - final double strokeWidth; - - /// The adjacent box gap. - final double gapSpace; - - /// The gaps between every two adjacent box, higher priority than [gapSpace]. - final List? gapSpaces; - - /// The box border color of index character. - final ColorBuilder strokeColorBuilder; - - @override - PinDecoration copyWith({ - TextStyle? textStyle, - ObscureStyle? obscureStyle, - String? errorText, - TextStyle? errorTextStyle, - String? hintText, - TextStyle? hintTextStyle, - ColorBuilder? bgColorBuilder, - }) => - CirclePinDecoration( - strokeColorBuilder: strokeColorBuilder, - strokeWidth: strokeWidth, - gapSpace: gapSpace, - gapSpaces: gapSpaces, - ); - - @override - PinEntryType get pinEntryType => PinEntryType.circle; - - @override - void notifyChange(String? pin) { - strokeColorBuilder.notifyChange(pin!); - } - - @override - void drawPin( - Canvas canvas, - Size size, - String text, - int pinLength, - Cursor? cursor, - TextDirection textDirection, - ) { - /// Calculate the height of paint area for drawing the pin field. - /// it should honor the error text (if any) drawn by - /// the actual texfield behind. - /// but, since we can access the drawn textfield behind from here, - /// we use a simple logic to calculate it. - double mainHeight; - if (errorText != null && errorText!.isNotEmpty) { - mainHeight = size.height - (errorTextStyle?.fontSize ?? 0 + 8.0); - } else { - mainHeight = size.height; - } - - final borderPaint = Paint() - ..strokeWidth = strokeWidth - ..style = PaintingStyle.stroke - ..isAntiAlias = true; - - final insidePaint = Paint() - ..style = PaintingStyle.fill - ..isAntiAlias = true; - - final gapTotalLength = gapSpaces?.sumList() ?? (pinLength - 1) * gapSpace; - - /// Calculate the width of each digit include stroke. - final singleWidth = (size.width - strokeWidth - gapTotalLength) / pinLength; - - double radius; // include strokeWidth - List actualGapSpaces; - if (singleWidth / 2 < mainHeight / 2 - strokeWidth / 2) { - radius = singleWidth / 2; - actualGapSpaces = - gapSpaces == null ? List.filled(pinLength - 1, gapSpace) : gapSpaces!; - } else { - radius = mainHeight / 2 - strokeWidth / 2; - actualGapSpaces = List.filled( - pinLength - 1, - (size.width - strokeWidth - radius * 2 * pinLength) / - (pinLength - 1)); - } - - var startX = strokeWidth / 2; - final startY = mainHeight / 2; - - final centerPoints = List.filled(pinLength, 0); - - /// Draw the each shape of pin. - for (var i = 0; i < pinLength; i++) { - borderPaint.color = strokeColorBuilder.indexProperty(i); - insidePaint.color = borderPaint.color; - - centerPoints[i] = startX + radius; - - canvas.drawCircle( - Offset(centerPoints[i], startY), - radius, - borderPaint, - ); - - if (i < text.runes.length) { - canvas.drawCircle( - Offset(startX + radius, startY), - radius - strokeWidth / 2, - insidePaint, - ); - } - startX += radius * 2 + (i == pinLength - 1 ? 0 : actualGapSpaces[i]); - } - } - - @override - double get getGapWidth => gapSpace; - - @override - List? get getGapWidthList => gapSpaces; - - @override - bool operator ==(Object other) => - identical(this, other) || - super == other && - other is CirclePinDecoration && - runtimeType == other.runtimeType && - strokeWidth == other.strokeWidth && - gapSpace == other.gapSpace && - gapSpaces == other.gapSpaces && - strokeColorBuilder == other.strokeColorBuilder; - - @override - int get hashCode => - super.hashCode ^ - strokeWidth.hashCode ^ - gapSpace.hashCode ^ - gapSpaces.hashCode ^ - strokeColorBuilder.hashCode; - - @override - String toString() => - 'CirclePinDecoration{strokeWidth: $strokeWidth, gapSpace: $gapSpace, gapSpaces: $gapSpaces, strokeColorBuilder: $strokeColorBuilder}'; -} diff --git a/pubspec.lock b/pubspec.lock index cfa3d106e8..2ad6d86e1a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1290,14 +1290,6 @@ packages: url: "https://pub.dev" source: hosted version: "8.0.1" - pin_input_text_field: - dependency: "direct main" - description: - name: pin_input_text_field - sha256: "8d6fc670aa673a4df5976086f0e8039972a5b2bcb783c8db8dd3b9b4b072ca90" - url: "https://pub.dev" - source: hosted - version: "4.5.1" platform: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 20c0fb6748..0e16f15a86 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -100,7 +100,6 @@ dependencies: url: https://github.com/fireslime/photo_view.git ref: 52685ab7beaf8a9107904bdb8a1590924015d016 pin_code_fields: ^8.0.1 - pin_input_text_field: ^4.5.1 platform_device_id: ^1.0.1 protocol_handler: ^0.1.5 provider: ^6.0.4 From 23fadf9a96cd67fe18ffa9177e3bae5872ca565b Mon Sep 17 00:00:00 2001 From: YeungKC Date: Wed, 5 Jul 2023 17:15:09 +0900 Subject: [PATCH 6/8] update --- lib/ui/setting/security_page.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/ui/setting/security_page.dart b/lib/ui/setting/security_page.dart index cc6fb0af09..dac84823e3 100644 --- a/lib/ui/setting/security_page.dart +++ b/lib/ui/setting/security_page.dart @@ -97,12 +97,14 @@ class _InputPasscode extends HookWidget { if (confirmPasscode.value == null) return; if (passcode.value != confirmPasscode.value) { - // todo fix toast - showToastFailed(context.l10n.passcodeIncorrect, context: context); + WidgetsBinding.instance.addPostFrameCallback((_) { + showToastFailed(context.l10n.passcodeIncorrect); + }); passcode.value = null; confirmPasscode.value = null; textEditingController.text = ''; + return; } SecurityKeyValue.instance.passcode = passcode.value; From ed7340a3b4107cf9f5aaeabc7d1c1b7482627666 Mon Sep 17 00:00:00 2001 From: YeungKC Date: Thu, 6 Jul 2023 12:49:40 +0900 Subject: [PATCH 7/8] update --- lib/widgets/auth.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/auth.dart b/lib/widgets/auth.dart index 17d5823087..83332abcd4 100644 --- a/lib/widgets/auth.dart +++ b/lib/widgets/auth.dart @@ -55,7 +55,7 @@ class _AuthGuard extends HookWidget { SecurityKeyValue.instance.hasPasscode; final hasError = useState(false); - final lock = useState(false); + final lock = useState(SecurityKeyValue.instance.hasPasscode); useEffect(() { Timer? timer; From 6452a05523e5292a18099d4be7033d620a4873ca Mon Sep 17 00:00:00 2001 From: YeungKC Date: Thu, 6 Jul 2023 13:23:40 +0900 Subject: [PATCH 8/8] update --- lib/generated/intl/messages_en.dart | 1 + lib/generated/intl/messages_zh-HK.dart | 1 + lib/generated/intl/messages_zh-TW.dart | 1 + lib/generated/intl/messages_zh.dart | 1 + lib/generated/l10n.dart | 10 ++ lib/l10n/intl_en.arb | 1 + lib/l10n/intl_zh-HK.arb | 1 + lib/l10n/intl_zh-TW.arb | 1 + lib/l10n/intl_zh.arb | 1 + lib/widgets/auth.dart | 196 ++++++++++++++----------- lib/widgets/window/menus.dart | 22 +++ 11 files changed, 151 insertions(+), 85 deletions(-) diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index c44365eefd..a37e76923a 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -489,6 +489,7 @@ class MessageLookup extends MessageLookupByLibrary { "System time is unusual, please continue to use again after correction"), "locateToChat": MessageLookupByLibrary.simpleMessage("locate to chat"), "location": MessageLookupByLibrary.simpleMessage("Location"), + "lock": MessageLookupByLibrary.simpleMessage("Lock"), "logIn": MessageLookupByLibrary.simpleMessage("Log in"), "loginAndAbortAccountDeletion": MessageLookupByLibrary.simpleMessage( "Continue to log in and abort account deletion"), diff --git a/lib/generated/intl/messages_zh-HK.dart b/lib/generated/intl/messages_zh-HK.dart index e0f1d1e076..ff81ba3cbd 100644 --- a/lib/generated/intl/messages_zh-HK.dart +++ b/lib/generated/intl/messages_zh-HK.dart @@ -429,6 +429,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("檢測到系統時間異常,請校正後再繼續使用"), "locateToChat": MessageLookupByLibrary.simpleMessage("定位到聊天"), "location": MessageLookupByLibrary.simpleMessage("位置"), + "lock": MessageLookupByLibrary.simpleMessage("鎖定"), "logIn": MessageLookupByLibrary.simpleMessage("登錄"), "loginAndAbortAccountDeletion": MessageLookupByLibrary.simpleMessage("繼續登錄並放棄刪除賬户"), diff --git a/lib/generated/intl/messages_zh-TW.dart b/lib/generated/intl/messages_zh-TW.dart index 74ba4a881c..91d80276dd 100644 --- a/lib/generated/intl/messages_zh-TW.dart +++ b/lib/generated/intl/messages_zh-TW.dart @@ -429,6 +429,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("檢測到系統時間異常,請校正後再繼續使用"), "locateToChat": MessageLookupByLibrary.simpleMessage("定位到聊天"), "location": MessageLookupByLibrary.simpleMessage("位置"), + "lock": MessageLookupByLibrary.simpleMessage("鎖定"), "logIn": MessageLookupByLibrary.simpleMessage("登入"), "loginAndAbortAccountDeletion": MessageLookupByLibrary.simpleMessage("繼續登入並放棄刪除賬戶"), diff --git a/lib/generated/intl/messages_zh.dart b/lib/generated/intl/messages_zh.dart index 78e1aeb694..76dc8e2ef2 100644 --- a/lib/generated/intl/messages_zh.dart +++ b/lib/generated/intl/messages_zh.dart @@ -429,6 +429,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("检测到系统时间异常,请校正后再继续使用"), "locateToChat": MessageLookupByLibrary.simpleMessage("定位到聊天"), "location": MessageLookupByLibrary.simpleMessage("位置"), + "lock": MessageLookupByLibrary.simpleMessage("锁定"), "logIn": MessageLookupByLibrary.simpleMessage("登录"), "loginAndAbortAccountDeletion": MessageLookupByLibrary.simpleMessage("继续登录并放弃删除账户"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 724e80fa34..c95678e2cd 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -2430,6 +2430,16 @@ class Localization { ); } + /// `Lock` + String get lock { + return Intl.message( + 'Lock', + name: 'lock', + desc: '', + args: [], + ); + } + /// `Log in` String get logIn { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 9b5ed2549a..36f642db92 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -236,6 +236,7 @@ "loadingTime" : "System time is unusual, please continue to use again after correction", "locateToChat" : "locate to chat", "location" : "Location", +"lock" : "Lock", "logIn" : "Log in", "loginAndAbortAccountDeletion" : "Continue to log in and abort account deletion", "loginByQrcode" : "Login to Mixin Messenger by QR Code", diff --git a/lib/l10n/intl_zh-HK.arb b/lib/l10n/intl_zh-HK.arb index c74b221955..eba77747d2 100644 --- a/lib/l10n/intl_zh-HK.arb +++ b/lib/l10n/intl_zh-HK.arb @@ -235,6 +235,7 @@ "loadingTime" : "檢測到系統時間異常,請校正後再繼續使用", "locateToChat" : "定位到聊天", "location" : "位置", +"lock" : "鎖定", "logIn" : "登錄", "loginAndAbortAccountDeletion" : "繼續登錄並放棄刪除賬户", "loginByQrcode" : "通過二維碼登錄 Mixin Messenger", diff --git a/lib/l10n/intl_zh-TW.arb b/lib/l10n/intl_zh-TW.arb index 20de01c4ce..2f4d30a950 100644 --- a/lib/l10n/intl_zh-TW.arb +++ b/lib/l10n/intl_zh-TW.arb @@ -235,6 +235,7 @@ "loadingTime" : "檢測到系統時間異常,請校正後再繼續使用", "locateToChat" : "定位到聊天", "location" : "位置", +"lock" : "鎖定", "logIn" : "登入", "loginAndAbortAccountDeletion" : "繼續登入並放棄刪除賬戶", "loginByQrcode" : "透過二維碼登入 Mixin Messenger", diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 4f49b36719..77d9efe318 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -235,6 +235,7 @@ "loadingTime" : "检测到系统时间异常,请校正后再继续使用", "locateToChat" : "定位到聊天", "location" : "位置", +"lock" : "锁定", "logIn" : "登录", "loginAndAbortAccountDeletion" : "继续登录并放弃删除账户", "loginByQrcode" : "通过二维码登录 Mixin Messenger", diff --git a/lib/widgets/auth.dart b/lib/widgets/auth.dart index 83332abcd4..5bafca7387 100644 --- a/lib/widgets/auth.dart +++ b/lib/widgets/auth.dart @@ -6,17 +6,21 @@ import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_svg/svg.dart'; import 'package:pin_code_fields/pin_code_fields.dart'; +import 'package:rxdart/rxdart.dart'; import '../account/account_server.dart'; import '../account/security_key_value.dart'; import '../constants/resources.dart'; import '../ui/home/bloc/multi_auth_cubit.dart'; import '../utils/app_lifecycle.dart'; +import '../utils/event_bus.dart'; import '../utils/extension/extension.dart'; import '../utils/hook.dart'; const lockDuration = Duration(minutes: 1); +enum LockEvent { lock, unlock } + class AuthGuard extends HookWidget { const AuthGuard({ super.key, @@ -57,6 +61,15 @@ class _AuthGuard extends HookWidget { final hasError = useState(false); final lock = useState(SecurityKeyValue.instance.hasPasscode); + useEffect(() { + final listen = + EventBus.instance.on.whereType().listen((event) { + lock.value = event == LockEvent.lock; + }); + + return listen.cancel; + }, []); + useEffect(() { Timer? timer; void dispose() { @@ -99,9 +112,18 @@ class _AuthGuard extends HookWidget { focusNode.requestFocus(); } - focusNode.addListener(listener); + bool handler(KeyEvent _) { + listener(); + return false; + } + + FocusManager.instance.addListener(listener); + appActiveListener.addListener(listener); + ServicesBinding.instance.keyboard.addHandler(handler); return () { - focusNode.removeListener(listener); + appActiveListener.removeListener(listener); + FocusManager.instance.removeListener(listener); + ServicesBinding.instance.keyboard.removeHandler(handler); }; }, [lock.value]); @@ -109,95 +131,99 @@ class _AuthGuard extends HookWidget { children: [ child, if (lock.value) - BackdropFilter( - filter: ImageFilter.blur( - sigmaX: 20, - sigmaY: 20, - ), - child: MaterialApp( - color: Colors.transparent, - home: Material( + GestureDetector( + onTap: focusNode.requestFocus, + behavior: HitTestBehavior.translucent, + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 20, + sigmaY: 20, + ), + child: MaterialApp( color: Colors.transparent, - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SvgPicture.asset( - Resources.assetsImagesLockSvg, - width: 68, - height: 68, - colorFilter: ColorFilter.mode( - context.theme.icon, BlendMode.srcIn), - ), - const SizedBox(height: 24), - Text( - context.l10n.unlockWithWasscode, - textAlign: TextAlign.center, - style: TextStyle( - color: context.theme.text, - fontSize: 20, - fontWeight: FontWeight.w600, + home: Material( + color: Colors.transparent, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + Resources.assetsImagesLockSvg, + width: 68, + height: 68, + colorFilter: ColorFilter.mode( + context.theme.icon, BlendMode.srcIn), ), - ), - const SizedBox(height: 40), - SizedBox( - width: 204, - // height: 14, - child: PinCodeTextField( - appContext: context, - length: 6, - controller: textEditingController, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly - ], - pinTheme: PinTheme( - activeColor: context.theme.text, - inactiveColor: context.theme.text, - selectedColor: context.theme.text, - fieldWidth: 15, - borderWidth: 1, - shape: PinCodeFieldShape.circle, - ), - obscureText: true, - autoDisposeControllers: false, - obscuringWidget: Container( - decoration: BoxDecoration( - color: context.theme.text, - shape: BoxShape.circle, - )), - autoFocus: true, - focusNode: focusNode, - showCursor: false, - onCompleted: (value) { - textEditingController.text = ''; - if (SecurityKeyValue.instance.passcode == value) { - lock.value = false; - } else { - hasError.value = true; - } - }, - onChanged: (value) { - hasError.value = false; - }, - ), - ), - const SizedBox(height: 28), - Visibility( - visible: hasError.value, - maintainSize: true, - maintainAnimation: true, - maintainState: true, - child: Text( - context.l10n.passcodeIncorrect, + const SizedBox(height: 24), + Text( + context.l10n.unlockWithWasscode, textAlign: TextAlign.center, style: TextStyle( - color: context.theme.red, - fontSize: 16, - fontWeight: FontWeight.w400, + color: context.theme.text, + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 40), + SizedBox( + width: 204, + // height: 14, + child: PinCodeTextField( + appContext: context, + length: 6, + controller: textEditingController, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly + ], + pinTheme: PinTheme( + activeColor: context.theme.text, + inactiveColor: context.theme.text, + selectedColor: context.theme.text, + fieldWidth: 15, + borderWidth: 1, + shape: PinCodeFieldShape.circle, + ), + obscureText: true, + autoDisposeControllers: false, + obscuringWidget: Container( + decoration: BoxDecoration( + color: context.theme.text, + shape: BoxShape.circle, + )), + autoFocus: true, + focusNode: focusNode, + showCursor: false, + onCompleted: (value) { + textEditingController.text = ''; + if (SecurityKeyValue.instance.passcode == value) { + lock.value = false; + } else { + hasError.value = true; + } + }, + onChanged: (value) { + hasError.value = false; + }, + ), + ), + const SizedBox(height: 28), + Visibility( + visible: hasError.value, + maintainSize: true, + maintainAnimation: true, + maintainState: true, + child: Text( + context.l10n.passcodeIncorrect, + textAlign: TextAlign.center, + style: TextStyle( + color: context.theme.red, + fontSize: 16, + fontWeight: FontWeight.w400, + ), ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/widgets/window/menus.dart b/lib/widgets/window/menus.dart index a09c1bb85e..6de5e8170f 100644 --- a/lib/widgets/window/menus.dart +++ b/lib/widgets/window/menus.dart @@ -9,14 +9,17 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:window_manager/window_manager.dart'; import '../../account/account_server.dart'; +import '../../account/security_key_value.dart'; import '../../ui/home/bloc/multi_auth_cubit.dart'; import '../../ui/home/bloc/slide_category_cubit.dart'; import '../../ui/home/conversation/conversation_hotkey.dart'; import '../../utils/device_transfer/device_transfer_dialog.dart'; +import '../../utils/event_bus.dart'; import '../../utils/extension/extension.dart'; import '../../utils/hook.dart'; import '../../utils/uri_utils.dart'; import '../actions/actions.dart'; +import '../auth.dart'; abstract class ConversationMenuHandle { Stream get isMuted; @@ -115,6 +118,12 @@ class _Menus extends HookWidget { ).data ?? false; + final hasPasscode = useMemoizedStream(signed + ? SecurityKeyValue.instance.watchHasPasscode + : () => Stream.value(false)) + .data ?? + false; + PlatformMenu buildConversationMenu() => PlatformMenu( label: context.l10n.conversation, menus: [ @@ -182,6 +191,19 @@ class _Menus extends HookWidget { : null, ), ]), + PlatformMenuItemGroup(members: [ + PlatformMenuItem( + label: context.l10n.lock, + shortcut: const SingleActivator( + LogicalKeyboardKey.keyL, + meta: true, + shift: true, + ), + onSelected: hasPasscode + ? () => EventBus.instance.fire(LockEvent.lock) + : null, + ), + ]), PlatformMenuItemGroup( members: [ PlatformMenuItem(