From a5d78e5d158e1f0cea0e53c3a37ffb7d80b384ea Mon Sep 17 00:00:00 2001 From: wangym Date: Tue, 5 Nov 2024 16:07:31 +0800 Subject: [PATCH] add empty card and function improve (#10) * add empty card and function improve * add notes --- README.md | 17 ++++--- assets/translations/en.json | 7 ++- assets/translations/ja.json | 7 ++- assets/translations/zh.json | 9 +++- lib/screens/home_screen.dart | 43 ++++++++++++----- lib/screens/more_options_screen.dart | 7 ++- lib/services/language_service.dart | 4 +- lib/widgets/custom_about_dialog.dart | 71 ++++++++++++++++++++++++++++ lib/widgets/empty_state_widget.dart | 41 ++++++++++++++++ 9 files changed, 177 insertions(+), 29 deletions(-) create mode 100644 lib/widgets/custom_about_dialog.dart create mode 100644 lib/widgets/empty_state_widget.dart diff --git a/README.md b/README.md index 750d113..0e57c52 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,18 @@ # Flutter OTP Manager -A secure and user-friendly OTP (One-Time Password) manager application built with Flutter. +A secure and user-friendly OTP (One-Time Password) manager application built with Flutter, supporting multiple platforms and languages. + +[@Latest Release](https://github.com/Wangggym/two_factor_authentication/releases) ## Features -- Generate TOTP (Time-based One-Time Password) codes -- Add and manage multiple accounts +- Generate TOTP (Time-based One-Time Password) codes with automatic refresh +- Scan QR codes to add accounts (camera on mobile, screen capture on desktop) - Secure storage of account secrets -- Edit existing accounts -- Clean and intuitive user interface -- Automatic code refresh +- Copy OTP codes with one click +- Visual countdown timer with color indicators +- Multi-language support (English, 简体中文, 日本語) +- Cross-platform compatibility (Mobile & Desktop) ## Getting Started @@ -21,4 +24,4 @@ A secure and user-friendly OTP (One-Time Password) manager application built wit ### Installation -1. Clone the repository +1. Clone the repository \ No newline at end of file diff --git a/assets/translations/en.json b/assets/translations/en.json index fbed9ff..183801b 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -37,5 +37,10 @@ "capture_qr_code": "Capture QR Code", "scan_qr_code_desktop": "Capture Screen QR Code", "scan_qr_position_hint": "Position the QR code on your screen and click the button above to scan it.", - "failed_capture_qr": "Failed to capture or decode QR code: {error}" + "failed_capture_qr": "Failed to capture or decode QR code: {error}", + "no_accounts": "Welcome! Click the button below to add your first authenticator account ✨", + "version": "Version {version}", + "created_by": "Created by {author}", + "disclaimer": "Thank you for using Auth2! For your account security, please keep your keys safe.", + "close": "Close" } \ No newline at end of file diff --git a/assets/translations/ja.json b/assets/translations/ja.json index 2d68f5e..8f72ce8 100644 --- a/assets/translations/ja.json +++ b/assets/translations/ja.json @@ -37,5 +37,10 @@ "capture_qr_code": "QRコードをキャプチャ", "scan_qr_code_desktop": "画面のQRコードをキャプチャ", "scan_qr_position_hint": "QRコードを画面上に配置し、上のボタンをクリックしてスキャンしてください。", - "failed_capture_qr": "QRコードのキャプチャまたはデコードに失敗しました:{error}" + "failed_capture_qr": "QRコードのキャプチャまたはデコードに失敗しました:{error}", + "no_accounts": "ようこそ!下のボタンをクリックして最初の認証アカウントを追加しましょう ✨", + "version": "バージョン {version}", + "created_by": "開発者:{author}", + "disclaimer": "Auth2をご利用いただき、ありがとうございます!アカウントのセキュリティのため、キー情報を安全に保管してください。", + "close": "閉じる" } \ No newline at end of file diff --git a/assets/translations/zh.json b/assets/translations/zh.json index 995f19a..310df66 100644 --- a/assets/translations/zh.json +++ b/assets/translations/zh.json @@ -25,7 +25,7 @@ "has_been_pinned": "{name} 已置顶", "has_been_unpinned": "{name} 已取消置顶", "has_been_deleted": "{name} 已删除", - "edit_account": "编辑账户", + "edit_account": "编辑账", "save": "保存", "key_uri": "密钥URI", "note_changes": "注意:更改不会应用到密钥URI", @@ -37,5 +37,10 @@ "capture_qr_code": "捕获二维码", "scan_qr_code_desktop": "捕获屏幕二维码", "scan_qr_position_hint": "将二维码放置在屏幕上,然后点击上方按钮进行扫描。", - "failed_capture_qr": "捕获或解码二维码失败:{error}" + "failed_capture_qr": "捕获或解码二维码失败:{error}", + "no_accounts": "欢迎使用!点击下方按钮添加您的第一个验证器账户吧 ✨", + "version": "版本 {version}", + "created_by": "开发者:{author}", + "disclaimer": "感谢使用Auth2!为了您的账户安全,请妥善保管密钥信息。", + "close": "关闭" } \ No newline at end of file diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 78a1c91..7c42796 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -10,6 +10,7 @@ import '../widgets/otp_card.dart'; import 'add_account_screen.dart'; import 'edit_account_screen.dart'; import 'more_options_screen.dart'; +import '../widgets/empty_state_widget.dart'; class HomeScreen extends StatefulWidget { final Function(Locale) onLocaleChanged; @@ -208,18 +209,36 @@ class _HomeScreenState extends State { ), ], ), - body: ListView.builder( - itemCount: _accounts.length, - itemBuilder: (context, index) { - return OTPCard( - account: _accounts[index], - onDelete: _deleteAccount, - onEdit: _editAccount, - onPin: _pinAccount, - isPinned: _pinnedAccountNames.contains(_accounts[index].name), - ); - }, - ), + body: _accounts.isEmpty + ? EmptyStateWidget( + onAddPressed: () async { + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const AddAccountScreen(), + ), + ); + if (result != null && result is OTPAccount) { + setState(() { + _accounts.add(result); + _sortAccounts(); + }); + _saveAccounts(); + } + }, + ) + : ListView.builder( + itemCount: _accounts.length, + itemBuilder: (context, index) { + return OTPCard( + account: _accounts[index], + onDelete: _deleteAccount, + onEdit: _editAccount, + onPin: _pinAccount, + isPinned: _pinnedAccountNames.contains(_accounts[index].name), + ); + }, + ), ); } } diff --git a/lib/screens/more_options_screen.dart b/lib/screens/more_options_screen.dart index ff6c7e1..3a14981 100644 --- a/lib/screens/more_options_screen.dart +++ b/lib/screens/more_options_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'language_screen.dart'; import '../services/localization_service.dart'; +import '../widgets/custom_about_dialog.dart'; class MoreOptionsScreen extends StatelessWidget { final Function() onAddRandomAccount; @@ -53,11 +54,9 @@ class MoreOptionsScreen extends StatelessWidget { leading: const Icon(Icons.info_outline), title: Text(l10n.translate('about')), onTap: () { - showAboutDialog( + showDialog( context: context, - applicationName: l10n.translate('app_name'), - applicationVersion: '1.0.0', - applicationLegalese: '© 2024 Auth2', + builder: (context) => const CustomAboutDialog(), ); }, ), diff --git a/lib/services/language_service.dart b/lib/services/language_service.dart index e69a872..ac4d13f 100644 --- a/lib/services/language_service.dart +++ b/lib/services/language_service.dart @@ -5,8 +5,8 @@ class LanguageService { static const String _languageKey = 'selected_language'; static final Map supportedLocales = { - 'English': const Locale('en'), '简体中文': const Locale('zh'), + 'English': const Locale('en'), '日本語': const Locale('ja'), // 'Español': const Locale('es'), // 'Français': const Locale('fr'), @@ -14,7 +14,7 @@ class LanguageService { static Future getSelectedLocale() async { final prefs = await SharedPreferences.getInstance(); - final languageCode = prefs.getString(_languageKey) ?? 'en'; + final languageCode = prefs.getString(_languageKey) ?? 'zh'; return Locale(languageCode); } diff --git a/lib/widgets/custom_about_dialog.dart b/lib/widgets/custom_about_dialog.dart new file mode 100644 index 0000000..ac61fdd --- /dev/null +++ b/lib/widgets/custom_about_dialog.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import '../services/localization_service.dart'; + +class CustomAboutDialog extends StatelessWidget { + const CustomAboutDialog({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = LocalizationService.of(context); + final textTheme = Theme.of(context).textTheme; + + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + l10n.translate('app_name'), + style: textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Text( + l10n.translate('version', args: {'version': '1.0.0'}), + style: textTheme.bodyLarge, + ), + const SizedBox(height: 8), + Text( + '© 2024 Auth2', + style: textTheme.bodyMedium?.copyWith( + color: Colors.grey[600], + ), + ), + // const SizedBox(height: 16), + // Text( + // l10n.translate('created_by', args: {'author': 'Wang Yimin'}), + // style: textTheme.bodyMedium, + // ), + const SizedBox(height: 24), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(8), + ), + child: Text( + l10n.translate('disclaimer'), + style: textTheme.bodySmall?.copyWith( + color: Colors.grey[600], + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 24), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(l10n.translate('close')), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/empty_state_widget.dart b/lib/widgets/empty_state_widget.dart new file mode 100644 index 0000000..923d796 --- /dev/null +++ b/lib/widgets/empty_state_widget.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import '../services/localization_service.dart'; + +class EmptyStateWidget extends StatelessWidget { + final VoidCallback onAddPressed; + + const EmptyStateWidget({ + super.key, + required this.onAddPressed, + }); + + @override + Widget build(BuildContext context) { + final l10n = LocalizationService.of(context); + + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + l10n.translate('no_accounts'), + style: const TextStyle( + fontSize: 18, + color: Colors.grey, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: onAddPressed, + icon: const Icon(Icons.add), + label: Text(l10n.translate('add_account')), + ), + ], + ), + ), + ); + } +}