diff --git a/mobile/apps/locker/README.md b/mobile/apps/locker/README.md index 29c7d095bf..5bb58b4c62 100644 --- a/mobile/apps/locker/README.md +++ b/mobile/apps/locker/README.md @@ -1 +1,7 @@ -# soon. +# Ente Locker + +## TODOs + +Refactor and merge +- [ ] Verify correctness for `PackageInfoUtil.getPackageName()` on Linux and Windows +- [ ] Update `file_url.dart` to download only via CF worker when necessary diff --git a/mobile/apps/locker/assets/2.0x/broken_heart.png b/mobile/apps/locker/assets/2.0x/broken_heart.png new file mode 100644 index 0000000000..31c9014bc6 Binary files /dev/null and b/mobile/apps/locker/assets/2.0x/broken_heart.png differ diff --git a/mobile/apps/locker/assets/3.0x/broken_heart.png b/mobile/apps/locker/assets/3.0x/broken_heart.png new file mode 100644 index 0000000000..7be1e70ded Binary files /dev/null and b/mobile/apps/locker/assets/3.0x/broken_heart.png differ diff --git a/mobile/apps/locker/assets/broken_heart.png b/mobile/apps/locker/assets/broken_heart.png new file mode 100644 index 0000000000..b8288cee8e Binary files /dev/null and b/mobile/apps/locker/assets/broken_heart.png differ diff --git a/mobile/apps/locker/ios/Podfile.lock b/mobile/apps/locker/ios/Podfile.lock index a6199b577f..174a4f586e 100644 --- a/mobile/apps/locker/ios/Podfile.lock +++ b/mobile/apps/locker/ios/Podfile.lock @@ -75,9 +75,9 @@ PODS: - FlutterMacOS - privacy_screen (0.0.1): - Flutter - - SDWebImage (5.21.0): - - SDWebImage/Core (= 5.21.0) - - SDWebImage/Core (5.21.0) + - SDWebImage (5.21.1): + - SDWebImage/Core (= 5.21.1) + - SDWebImage/Core (5.21.1) - Sentry/HybridSDK (8.46.0) - sentry_flutter (8.14.2): - Flutter @@ -188,37 +188,37 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: - app_links: f3e17e4ee5e357b39d8b95290a9b2c299fca71c6 - cupertino_http: 947a233f40cfea55167a49f2facc18434ea117ba - device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 + app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7 + cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c + device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89 DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 - file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49 - file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 + file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be + file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_email_sender: e03bdda7637bcd3539bfe718fddd980e9508efaa - flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4 - flutter_local_authentication: 1172a4dd88f6306dadce067454e2c4caf07977bb - flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 - fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f - listen_sharing_intent: 74a842adcbcf7bedf7bbc938c749da9155141b9a - local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3 - objective_c: 77e887b5ba1827970907e10e832eec1683f3431d - open_file_ios: 461db5853723763573e140de3193656f91990d9e + flutter_email_sender: aa1e9772696691d02cd91fea829856c11efb8e58 + flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 + flutter_local_authentication: 989278c681612f1ee0e36019e149137f114b9d7f + flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 + fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 + listen_sharing_intent: fe0b9a59913cc124dd6cbd55cd9f881de5f75759 + local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 + objective_c: 89e720c30d716b036faf9c9684022048eee1eee2 + open_file_ios: 5ff7526df64e4394b4fe207636b67a95e83078bb OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 - package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e - SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + privacy_screen: 3159a541f5d3a31bea916cfd4e58f9dc722b3fd4 + SDWebImage: f29024626962457f3470184232766516dee8dfea Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854 - sentry_flutter: 2df8b0aab7e4aba81261c230cbea31c82a62dd1b - share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sodium_libs: 1faae17af662384acbd13e41867a0008cd2e2318 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + sentry_flutter: 27892878729f42701297c628eb90e7c6529f3684 + share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sodium_libs: 6c6d0e83f4ee427c6464caa1f1bdc2abf3ca0b7f + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 - ua_client_hints: aeabd123262c087f0ce151ef96fa3ab77bfc8b38 - url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + ua_client_hints: 92fe0d139619b73ec9fcb46cc7e079a26178f586 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d PODFILE CHECKSUM: d2d3220ea22664a259778d9e314054751db31361 diff --git a/mobile/apps/locker/lib/core/constants.dart b/mobile/apps/locker/lib/core/constants.dart index b1ff31d0cb..c5b9c6f8a5 100644 --- a/mobile/apps/locker/lib/core/constants.dart +++ b/mobile/apps/locker/lib/core/constants.dart @@ -9,6 +9,10 @@ const int android11SDKINT = 30; const mnemonicKeyWordCount = 24; const kDefaultProductionEndpoint = 'https://api.ente.io'; +const String githubDiscussionsUrl = + "https://github.com/ente-io/ente/discussions"; + +const supportEmail = 'support@ente.io'; final tempDirCleanUpInterval = kDebugMode ? const Duration(hours: 1).inMicroseconds diff --git a/mobile/apps/locker/lib/l10n/app_en.arb b/mobile/apps/locker/lib/l10n/app_en.arb index 0d05f8ec45..6c9b5580dc 100644 --- a/mobile/apps/locker/lib/l10n/app_en.arb +++ b/mobile/apps/locker/lib/l10n/app_en.arb @@ -301,12 +301,53 @@ "failedToCreateShareLink": "Failed to create link", "failedToDeleteShareLink": "Failed to delete link", "deletingFile": "Deleting file...", - "addInformation": "Add information", - "addInformationDialogSubtitle": "Choose the type of information you want to add", - "physicalDocument": "Physical document", - "physicalDocumentDescription": "Save information about documents and items in the real world.", - "emergencyContact": "Emergency contact", - "emergencyContactDescription": "Save information about important contacts.", - "accountCredential": "Account credential", - "accountCredentialDescription": "Save information about your important account credentials." + "changeEmail": "Change email", + "authToChangeYourEmail": "Please authenticate to change your email", + "changePasswordTitle": "Change password", + "authToChangeYourPassword": "Please authenticate to change your password", + "recoveryKey": "Recovery key", + "ok": "Ok", + "logout": "Logout", + "deleteAccount": "Delete account", + "areYouSureYouWantToLogout": "Are you sure you want to logout?", + "yesLogout": "Yes, logout", + "changePassword": "Change password", + "authToViewYourRecoveryKey": "Please authenticate to view your recovery key", + "account": "Account", + "security": "Security", + "emailVerificationToggle": "Email verification", + "authToChangeEmailVerificationSetting": "Please authenticate to change email verification", + "passkey": "Passkey", + "authenticateGeneric": "Please authenticate", + "somethingWentWrong": "Something went wrong", + "appLock": "App lock", + "warning": "Warning", + "appLockOfflineModeWarning": "You have chosen to proceed without backups. If you forget your applock, you will be locked out from accessing your data.", + "authToChangeLockscreenSetting": "Please authenticate to change lockscreen setting", + "authToViewPasskey": "Please authenticate to view passkey", + "theme": "Theme", + "lightTheme": "Light", + "darkTheme": "Dark", + "systemTheme": "System", + "settings": "Settings", + "about": "About", + "weAreOpenSource": "We are open source!", + "privacy": "Privacy", + "terms": "Terms", + "termsOfServicesTitle": "Terms", + "support": "Support", + "contactSupport": "Contact support", + "help": "Help", + "suggestFeatures": "Suggest features", + "reportABug": "Report a bug", + "reportBug": "Report bug", + "social": "Social", + "rateUsOnStore": "Rate us on {storeName}", + "blog": "Blog", + "merchandise": "Merchandise", + "twitter": "Twitter", + "mastodon": "Mastodon", + "matrix": "Matrix", + "discord": "Discord", + "reddit": "Reddit" } diff --git a/mobile/apps/locker/lib/l10n/app_localizations.dart b/mobile/apps/locker/lib/l10n/app_localizations.dart index 5b6d76b735..01e10b9054 100644 --- a/mobile/apps/locker/lib/l10n/app_localizations.dart +++ b/mobile/apps/locker/lib/l10n/app_localizations.dart @@ -724,53 +724,299 @@ abstract class AppLocalizations { /// **'Deleting file...'** String get deletingFile; - /// No description provided for @addInformation. + /// No description provided for @changeEmail. /// /// In en, this message translates to: - /// **'Add information'** - String get addInformation; + /// **'Change email'** + String get changeEmail; - /// No description provided for @addInformationDialogSubtitle. + /// No description provided for @authToChangeYourEmail. /// /// In en, this message translates to: - /// **'Choose the type of information you want to add'** - String get addInformationDialogSubtitle; + /// **'Please authenticate to change your email'** + String get authToChangeYourEmail; - /// No description provided for @physicalDocument. + /// No description provided for @changePasswordTitle. /// /// In en, this message translates to: - /// **'Physical document'** - String get physicalDocument; + /// **'Change password'** + String get changePasswordTitle; - /// No description provided for @physicalDocumentDescription. + /// No description provided for @authToChangeYourPassword. /// /// In en, this message translates to: - /// **'Save information about documents and items in the real world.'** - String get physicalDocumentDescription; + /// **'Please authenticate to change your password'** + String get authToChangeYourPassword; - /// No description provided for @emergencyContact. + /// No description provided for @recoveryKey. /// /// In en, this message translates to: - /// **'Emergency contact'** - String get emergencyContact; + /// **'Recovery key'** + String get recoveryKey; - /// No description provided for @emergencyContactDescription. + /// No description provided for @ok. /// /// In en, this message translates to: - /// **'Save information about important contacts.'** - String get emergencyContactDescription; + /// **'Ok'** + String get ok; - /// No description provided for @accountCredential. + /// No description provided for @logout. /// /// In en, this message translates to: - /// **'Account credential'** - String get accountCredential; + /// **'Logout'** + String get logout; - /// No description provided for @accountCredentialDescription. + /// No description provided for @deleteAccount. /// /// In en, this message translates to: - /// **'Save information about your important account credentials.'** - String get accountCredentialDescription; + /// **'Delete account'** + String get deleteAccount; + + /// No description provided for @areYouSureYouWantToLogout. + /// + /// In en, this message translates to: + /// **'Are you sure you want to logout?'** + String get areYouSureYouWantToLogout; + + /// No description provided for @yesLogout. + /// + /// In en, this message translates to: + /// **'Yes, logout'** + String get yesLogout; + + /// No description provided for @changePassword. + /// + /// In en, this message translates to: + /// **'Change password'** + String get changePassword; + + /// No description provided for @authToViewYourRecoveryKey. + /// + /// In en, this message translates to: + /// **'Please authenticate to view your recovery key'** + String get authToViewYourRecoveryKey; + + /// No description provided for @account. + /// + /// In en, this message translates to: + /// **'Account'** + String get account; + + /// No description provided for @security. + /// + /// In en, this message translates to: + /// **'Security'** + String get security; + + /// No description provided for @emailVerificationToggle. + /// + /// In en, this message translates to: + /// **'Email verification'** + String get emailVerificationToggle; + + /// No description provided for @authToChangeEmailVerificationSetting. + /// + /// In en, this message translates to: + /// **'Please authenticate to change email verification'** + String get authToChangeEmailVerificationSetting; + + /// No description provided for @passkey. + /// + /// In en, this message translates to: + /// **'Passkey'** + String get passkey; + + /// No description provided for @authenticateGeneric. + /// + /// In en, this message translates to: + /// **'Please authenticate'** + String get authenticateGeneric; + + /// No description provided for @somethingWentWrong. + /// + /// In en, this message translates to: + /// **'Something went wrong'** + String get somethingWentWrong; + + /// No description provided for @appLock. + /// + /// In en, this message translates to: + /// **'App lock'** + String get appLock; + + /// No description provided for @warning. + /// + /// In en, this message translates to: + /// **'Warning'** + String get warning; + + /// No description provided for @appLockOfflineModeWarning. + /// + /// In en, this message translates to: + /// **'You have chosen to proceed without backups. If you forget your applock, you will be locked out from accessing your data.'** + String get appLockOfflineModeWarning; + + /// No description provided for @authToChangeLockscreenSetting. + /// + /// In en, this message translates to: + /// **'Please authenticate to change lockscreen setting'** + String get authToChangeLockscreenSetting; + + /// No description provided for @authToViewPasskey. + /// + /// In en, this message translates to: + /// **'Please authenticate to view passkey'** + String get authToViewPasskey; + + /// No description provided for @theme. + /// + /// In en, this message translates to: + /// **'Theme'** + String get theme; + + /// No description provided for @lightTheme. + /// + /// In en, this message translates to: + /// **'Light'** + String get lightTheme; + + /// No description provided for @darkTheme. + /// + /// In en, this message translates to: + /// **'Dark'** + String get darkTheme; + + /// No description provided for @systemTheme. + /// + /// In en, this message translates to: + /// **'System'** + String get systemTheme; + + /// No description provided for @settings. + /// + /// In en, this message translates to: + /// **'Settings'** + String get settings; + + /// No description provided for @about. + /// + /// In en, this message translates to: + /// **'About'** + String get about; + + /// No description provided for @weAreOpenSource. + /// + /// In en, this message translates to: + /// **'We are open source!'** + String get weAreOpenSource; + + /// No description provided for @privacy. + /// + /// In en, this message translates to: + /// **'Privacy'** + String get privacy; + + /// No description provided for @terms. + /// + /// In en, this message translates to: + /// **'Terms'** + String get terms; + + /// No description provided for @termsOfServicesTitle. + /// + /// In en, this message translates to: + /// **'Terms'** + String get termsOfServicesTitle; + + /// No description provided for @support. + /// + /// In en, this message translates to: + /// **'Support'** + String get support; + + /// No description provided for @contactSupport. + /// + /// In en, this message translates to: + /// **'Contact support'** + String get contactSupport; + + /// No description provided for @help. + /// + /// In en, this message translates to: + /// **'Help'** + String get help; + + /// No description provided for @suggestFeatures. + /// + /// In en, this message translates to: + /// **'Suggest features'** + String get suggestFeatures; + + /// No description provided for @reportABug. + /// + /// In en, this message translates to: + /// **'Report a bug'** + String get reportABug; + + /// No description provided for @reportBug. + /// + /// In en, this message translates to: + /// **'Report bug'** + String get reportBug; + + /// No description provided for @social. + /// + /// In en, this message translates to: + /// **'Social'** + String get social; + + /// No description provided for @rateUsOnStore. + /// + /// In en, this message translates to: + /// **'Rate us on {storeName}'** + String rateUsOnStore(Object storeName); + + /// No description provided for @blog. + /// + /// In en, this message translates to: + /// **'Blog'** + String get blog; + + /// No description provided for @merchandise. + /// + /// In en, this message translates to: + /// **'Merchandise'** + String get merchandise; + + /// No description provided for @twitter. + /// + /// In en, this message translates to: + /// **'Twitter'** + String get twitter; + + /// No description provided for @mastodon. + /// + /// In en, this message translates to: + /// **'Mastodon'** + String get mastodon; + + /// No description provided for @matrix. + /// + /// In en, this message translates to: + /// **'Matrix'** + String get matrix; + + /// No description provided for @discord. + /// + /// In en, this message translates to: + /// **'Discord'** + String get discord; + + /// No description provided for @reddit. + /// + /// In en, this message translates to: + /// **'Reddit'** + String get reddit; } class _AppLocalizationsDelegate diff --git a/mobile/apps/locker/lib/l10n/app_localizations_en.dart b/mobile/apps/locker/lib/l10n/app_localizations_en.dart index 5fd46a81d3..b9110908c7 100644 --- a/mobile/apps/locker/lib/l10n/app_localizations_en.dart +++ b/mobile/apps/locker/lib/l10n/app_localizations_en.dart @@ -381,30 +381,157 @@ class AppLocalizationsEn extends AppLocalizations { String get deletingFile => 'Deleting file...'; @override - String get addInformation => 'Add information'; + String get changeEmail => 'Change email'; @override - String get addInformationDialogSubtitle => - 'Choose the type of information you want to add'; + String get authToChangeYourEmail => + 'Please authenticate to change your email'; @override - String get physicalDocument => 'Physical document'; + String get changePasswordTitle => 'Change password'; @override - String get physicalDocumentDescription => - 'Save information about documents and items in the real world.'; + String get authToChangeYourPassword => + 'Please authenticate to change your password'; @override - String get emergencyContact => 'Emergency contact'; + String get recoveryKey => 'Recovery key'; @override - String get emergencyContactDescription => - 'Save information about important contacts.'; + String get ok => 'Ok'; @override - String get accountCredential => 'Account credential'; + String get logout => 'Logout'; @override - String get accountCredentialDescription => - 'Save information about your important account credentials.'; + String get deleteAccount => 'Delete account'; + + @override + String get areYouSureYouWantToLogout => 'Are you sure you want to logout?'; + + @override + String get yesLogout => 'Yes, logout'; + + @override + String get changePassword => 'Change password'; + + @override + String get authToViewYourRecoveryKey => + 'Please authenticate to view your recovery key'; + + @override + String get account => 'Account'; + + @override + String get security => 'Security'; + + @override + String get emailVerificationToggle => 'Email verification'; + + @override + String get authToChangeEmailVerificationSetting => + 'Please authenticate to change email verification'; + + @override + String get passkey => 'Passkey'; + + @override + String get authenticateGeneric => 'Please authenticate'; + + @override + String get somethingWentWrong => 'Something went wrong'; + + @override + String get appLock => 'App lock'; + + @override + String get warning => 'Warning'; + + @override + String get appLockOfflineModeWarning => + 'You have chosen to proceed without backups. If you forget your applock, you will be locked out from accessing your data.'; + + @override + String get authToChangeLockscreenSetting => + 'Please authenticate to change lockscreen setting'; + + @override + String get authToViewPasskey => 'Please authenticate to view passkey'; + + @override + String get theme => 'Theme'; + + @override + String get lightTheme => 'Light'; + + @override + String get darkTheme => 'Dark'; + + @override + String get systemTheme => 'System'; + + @override + String get settings => 'Settings'; + + @override + String get about => 'About'; + + @override + String get weAreOpenSource => 'We are open source!'; + + @override + String get privacy => 'Privacy'; + + @override + String get terms => 'Terms'; + + @override + String get termsOfServicesTitle => 'Terms'; + + @override + String get support => 'Support'; + + @override + String get contactSupport => 'Contact support'; + + @override + String get help => 'Help'; + + @override + String get suggestFeatures => 'Suggest features'; + + @override + String get reportABug => 'Report a bug'; + + @override + String get reportBug => 'Report bug'; + + @override + String get social => 'Social'; + + @override + String rateUsOnStore(Object storeName) { + return 'Rate us on $storeName'; + } + + @override + String get blog => 'Blog'; + + @override + String get merchandise => 'Merchandise'; + + @override + String get twitter => 'Twitter'; + + @override + String get mastodon => 'Mastodon'; + + @override + String get matrix => 'Matrix'; + + @override + String get discord => 'Discord'; + + @override + String get reddit => 'Reddit'; } diff --git a/mobile/apps/locker/lib/main.dart b/mobile/apps/locker/lib/main.dart index b0e7d68eb9..da25ae5c72 100644 --- a/mobile/apps/locker/lib/main.dart +++ b/mobile/apps/locker/lib/main.dart @@ -9,6 +9,8 @@ import 'package:ente_lock_screen/ui/app_lock.dart'; import 'package:ente_lock_screen/ui/lock_screen.dart'; import 'package:ente_logging/logging.dart'; import 'package:ente_network/network.dart'; +import "package:ente_strings/l10n/strings_localizations.dart"; +import "package:ente_ui/theme/theme_config.dart"; import 'package:ente_ui/utils/window_listener_service.dart'; import 'package:ente_utils/platform_util.dart'; import "package:flutter/material.dart"; @@ -84,6 +86,7 @@ Future _initSystemTray() async { } Future _runInForeground() async { + AppThemeConfig.initialize(EnteApp.locker); final savedThemeMode = _themeMode(await AdaptiveTheme.getThemeMode()); return await _runWithLogs(() async { _logger.info("Starting app in foreground"); @@ -102,7 +105,10 @@ Future _runInForeground() async { locale: locale, savedThemeMode: savedThemeMode, supportedLocales: appSupportedLocales, - localizationsDelegates: AppLocalizations.localizationsDelegates, + localizationsDelegates: const [ + ...StringsLocalizations.localizationsDelegates, + ...AppLocalizations.localizationsDelegates, + ], localeListResolutionCallback: localResolutionCallBack, ), ); diff --git a/mobile/apps/locker/lib/ui/components/expandable_menu_item_widget.dart b/mobile/apps/locker/lib/ui/components/expandable_menu_item_widget.dart new file mode 100644 index 0000000000..8327d55749 --- /dev/null +++ b/mobile/apps/locker/lib/ui/components/expandable_menu_item_widget.dart @@ -0,0 +1,78 @@ +import "package:ente_ui/components/captioned_text_widget.dart"; +import "package:ente_ui/components/menu_item_widget.dart"; +import "package:ente_ui/theme/ente_theme_data.dart"; +import "package:expandable/expandable.dart"; +import 'package:flutter/material.dart'; +import "package:locker/ui/settings/common_settings.dart"; + +class ExpandableMenuItemWidget extends StatefulWidget { + final String title; + final Widget selectionOptionsWidget; + final IconData leadingIcon; + const ExpandableMenuItemWidget({ + required this.title, + required this.selectionOptionsWidget, + required this.leadingIcon, + super.key, + }); + + @override + State createState() => + _ExpandableMenuItemWidgetState(); +} + +class _ExpandableMenuItemWidgetState extends State { + final expandableController = ExpandableController(initialExpanded: false); + @override + void initState() { + expandableController.addListener(() { + setState(() {}); + }); + super.initState(); + } + + @override + void dispose() { + expandableController.removeListener(() {}); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final enteColorScheme = Theme.of(context).colorScheme.enteTheme.colorScheme; + final backgroundColor = + MediaQuery.of(context).platformBrightness == Brightness.light + ? enteColorScheme.backgroundElevated2 + : enteColorScheme.backgroundElevated; + return AnimatedContainer( + curve: Curves.ease, + duration: const Duration(milliseconds: 200), + decoration: BoxDecoration( + color: expandableController.value ? backgroundColor : null, + borderRadius: BorderRadius.circular(4), + ), + child: ExpandableNotifier( + controller: expandableController, + child: ScrollOnExpand( + child: ExpandablePanel( + header: MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: widget.title, + makeTextBold: true, + ), + isExpandable: true, + leadingIcon: widget.leadingIcon, + trailingIcon: Icons.expand_more, + menuItemColor: enteColorScheme.fillFaint, + expandableController: expandableController, + ), + collapsed: const SizedBox.shrink(), + expanded: widget.selectionOptionsWidget, + theme: getExpandableTheme(), + controller: expandableController, + ), + ), + ), + ); + } +} diff --git a/mobile/apps/locker/lib/ui/components/information_addition_dialog.dart b/mobile/apps/locker/lib/ui/components/information_addition_dialog.dart deleted file mode 100644 index 63021a39a7..0000000000 --- a/mobile/apps/locker/lib/ui/components/information_addition_dialog.dart +++ /dev/null @@ -1,208 +0,0 @@ -import 'package:ente_ui/components/buttons/button_widget.dart'; -import 'package:ente_ui/components/buttons/models/button_type.dart'; -import 'package:ente_ui/theme/ente_theme.dart'; -import 'package:flutter/material.dart'; -import 'package:locker/l10n/l10n.dart'; - -enum InformationType { - physicalDocument, - emergencyContact, - accountCredential, -} - -class InformationAdditionResult { - final InformationType type; - - InformationAdditionResult({ - required this.type, - }); -} - -class InformationAdditionDialog extends StatefulWidget { - const InformationAdditionDialog({super.key}); - - @override - State createState() => - _InformationAdditionDialogState(); -} - -class _InformationAdditionDialogState extends State { - void _onTypeSelected(InformationType type) { - final result = InformationAdditionResult(type: type); - Navigator.of(context).pop(result); - } - - Future _onCancel() async { - Navigator.of(context).pop(); - } - - @override - Widget build(BuildContext context) { - final colorScheme = getEnteColorScheme(context); - final textTheme = getEnteTextTheme(context); - - return Dialog( - backgroundColor: colorScheme.backgroundElevated, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - child: Container( - width: 400, - constraints: const BoxConstraints(maxHeight: 600), - padding: const EdgeInsets.all(20), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Icon( - Icons.post_add, - color: Colors.blue, - size: 24, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - context.l10n.addInformation, - style: textTheme.largeBold, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - const SizedBox(height: 8), - Text( - context.l10n.addInformationDialogSubtitle, - style: textTheme.body.copyWith( - color: colorScheme.textMuted, - ), - ), - const SizedBox(height: 20), - Flexible( - child: SingleChildScrollView( - child: Column( - children: [ - _buildOptionTile( - type: InformationType.physicalDocument, - icon: Icons.description, - title: context.l10n.physicalDocument, - subtitle: context.l10n.physicalDocumentDescription, - colorScheme: colorScheme, - textTheme: textTheme, - ), - const SizedBox(height: 12), - _buildOptionTile( - type: InformationType.emergencyContact, - icon: Icons.emergency, - title: context.l10n.emergencyContact, - subtitle: context.l10n.emergencyContactDescription, - colorScheme: colorScheme, - textTheme: textTheme, - ), - const SizedBox(height: 12), - _buildOptionTile( - type: InformationType.accountCredential, - icon: Icons.key, - title: context.l10n.accountCredential, - subtitle: context.l10n.accountCredentialDescription, - colorScheme: colorScheme, - textTheme: textTheme, - ), - ], - ), - ), - ), - const SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Flexible( - child: ButtonWidget( - buttonType: ButtonType.secondary, - labelText: context.l10n.cancel, - onTap: _onCancel, - ), - ), - ], - ), - ], - ), - ), - ); - } - - Widget _buildOptionTile({ - required InformationType type, - required IconData icon, - required String title, - required String subtitle, - required colorScheme, - required textTheme, - }) { - return InkWell( - onTap: () => _onTypeSelected(type), - borderRadius: BorderRadius.circular(8), - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: colorScheme.fillFaint, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: colorScheme.strokeFaint, - width: 1, - ), - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: colorScheme.fillMuted, - borderRadius: BorderRadius.circular(6), - ), - child: Icon( - icon, - color: colorScheme.textMuted, - size: 20, - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: textTheme.body.copyWith( - fontWeight: FontWeight.w600, - color: colorScheme.textBase, - ), - ), - const SizedBox(height: 4), - Text( - subtitle, - style: textTheme.small.copyWith( - color: colorScheme.textMuted, - ), - ), - ], - ), - ), - ], - ), - ), - ); - } -} - -Future showInformationAdditionDialog( - BuildContext context, -) async { - return showDialog( - context: context, - barrierColor: getEnteColorScheme(context).backdropBase, - builder: (context) => const InformationAdditionDialog(), - ); -} diff --git a/mobile/apps/locker/lib/ui/pages/home_page.dart b/mobile/apps/locker/lib/ui/pages/home_page.dart index 86aa5b708e..90ad706866 100644 --- a/mobile/apps/locker/lib/ui/pages/home_page.dart +++ b/mobile/apps/locker/lib/ui/pages/home_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; +import "package:ente_accounts/services/user_service.dart"; import 'package:ente_events/event_bus.dart'; import 'package:ente_ui/components/buttons/gradient_button.dart'; import 'package:ente_ui/theme/ente_theme.dart'; @@ -14,12 +15,12 @@ import 'package:locker/l10n/l10n.dart'; import 'package:locker/services/collections/collections_service.dart'; import 'package:locker/services/collections/models/collection.dart'; import 'package:locker/services/files/sync/models/file.dart'; -import 'package:locker/ui/components/information_addition_dialog.dart'; import 'package:locker/ui/components/recents_section_widget.dart'; import 'package:locker/ui/components/search_result_view.dart'; import 'package:locker/ui/mixins/search_mixin.dart'; import 'package:locker/ui/pages/all_collections_page.dart'; import 'package:locker/ui/pages/collection_page.dart'; +import "package:locker/ui/pages/settings_page.dart"; import 'package:locker/ui/pages/uploader_page.dart'; import 'package:locker/utils/collection_actions.dart'; import 'package:locker/utils/collection_sort_util.dart'; @@ -37,12 +38,19 @@ class HomePage extends UploaderPage { class _HomePageState extends UploaderPageState with TickerProviderStateMixin, SearchMixin { + late final _settingsPage = SettingsPage( + emailNotifier: UserService.instance.emailValueNotifier, + scaffoldKey: scaffoldKey, + ); + final scaffoldKey = GlobalKey(); + bool _isLoading = true; + bool _isSettingsOpen = false; + List _collections = []; List _filteredCollections = []; List _recentFiles = []; List _filteredFiles = []; Map _collectionFileCounts = {}; - bool _isLoading = true; String? _error; final _logger = Logger('HomePage'); StreamSubscription? _mediaStreamSubscription; @@ -64,8 +72,7 @@ class _HomePageState extends UploaderPageState List files, ) { setState(() { - _filteredCollections = - CollectionSortUtil.filterAndSortCollections(collections); + _filteredCollections = _filterOutUncategorized(collections); _filteredFiles = files; }); } @@ -74,8 +81,7 @@ class _HomePageState extends UploaderPageState void onSearchStateChanged(bool isActive) { if (!isActive) { setState(() { - _filteredCollections = - CollectionSortUtil.filterAndSortCollections(_collections); + _filteredCollections = _filterOutUncategorized(_collections); _filteredFiles = _recentFiles; }); } @@ -83,6 +89,10 @@ class _HomePageState extends UploaderPageState List get _displayedCollections { final collections = isSearchActive ? _filteredCollections : _collections; + return _filterOutUncategorized(collections); + } + + List _filterOutUncategorized(List collections) { return CollectionSortUtil.filterAndSortCollections(collections); } @@ -260,8 +270,7 @@ class _HomePageState extends UploaderPageState setState(() { _collections = sortedCollections; - _filteredCollections = - CollectionSortUtil.filterAndSortCollections(sortedCollections); + _filteredCollections = _filterOutUncategorized(sortedCollections); _filteredFiles = _recentFiles; _isLoading = false; }); @@ -276,18 +285,31 @@ class _HomePageState extends UploaderPageState } Future _loadRecentFiles(List collections) async { - final allFiles = await CollectionService.instance.getAllFiles(); + final allFiles = []; - final uniqueFilesMap = {}; + allFiles.addAll(await CollectionService.instance.getAllFiles()); + + final uniqueFiles = []; + final seenHashes = {}; + final seenIds = {}; for (final file in allFiles) { - final key = file.uploadedFileID?.toString() ?? file.toString(); - if (!uniqueFilesMap.containsKey(key)) { - uniqueFilesMap[key] = file; + bool isDuplicate = false; + + if (file.hash != null && seenHashes.contains(file.hash)) { + isDuplicate = true; + } else if (file.uploadedFileID != null && + seenIds.contains(file.uploadedFileID)) { + isDuplicate = true; + } + + if (!isDuplicate) { + uniqueFiles.add(file); + if (file.hash != null) seenHashes.add(file.hash!); + if (file.uploadedFileID != null) seenIds.add(file.uploadedFileID!); } } - final uniqueFiles = uniqueFilesMap.values.toList(); uniqueFiles.sort((a, b) { final timeA = a.updationTime ?? a.modificationTime ?? 0; final timeB = b.updationTime ?? b.modificationTime ?? 0; @@ -307,38 +329,55 @@ class _HomePageState extends UploaderPageState @override Widget build(BuildContext context) { - return KeyboardListener( - focusNode: FocusNode(), - onKeyEvent: handleKeyEvent, - child: Scaffold( - appBar: AppBar( - automaticallyImplyLeading: false, - leading: buildSearchLeading(), - title: GestureDetector( - onLongPress: () { - sendLogs( - context, - 'vishnu@ente.io', - subject: 'Locker logs', - body: 'Debug logs for Locker app.\n\n', - ); - }, - child: const Text( - 'Locker', - style: TextStyle(fontWeight: FontWeight.bold), - ), + return PopScope( + onPopInvokedWithResult: (_, result) async { + if (_isSettingsOpen) { + scaffoldKey.currentState!.closeDrawer(); + return; + } else if (!Platform.isAndroid) { + Navigator.of(context).pop(); + return; + } + }, + child: KeyboardListener( + focusNode: FocusNode(), + onKeyEvent: handleKeyEvent, + child: Scaffold( + key: scaffoldKey, + drawer: Drawer( + width: 428, + child: _settingsPage, ), - elevation: 0, - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - foregroundColor: Theme.of(context).textTheme.bodyLarge?.color, - actions: [ - buildSearchAction(), - ...buildSearchActions(), - ], + drawerEnableOpenDragGesture: !Platform.isAndroid, + onDrawerChanged: (isOpened) => _isSettingsOpen = isOpened, + appBar: AppBar( + leading: buildSearchLeading(), + title: GestureDetector( + onLongPress: () { + sendLogs( + context, + 'vishnu@ente.io', + subject: 'Locker logs', + body: 'Debug logs for Locker app.\n\n', + ); + }, + child: const Text( + 'Locker', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + elevation: 0, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + foregroundColor: Theme.of(context).textTheme.bodyLarge?.color, + actions: [ + buildSearchAction(), + ...buildSearchActions(), + ], + ), + body: _buildBody(), + floatingActionButton: + isSearchActive ? const SizedBox.shrink() : _buildMultiOptionFab(), ), - body: _buildBody(), - floatingActionButton: - isSearchActive ? const SizedBox.shrink() : _buildMultiOptionFab(), ), ); } @@ -400,18 +439,40 @@ class _HomePageState extends UploaderPageState physics: const AlwaysScrollableScrollPhysics(), child: SizedBox( height: MediaQuery.of(context).size.height - 200, - child: _buildEmptyState( - icon: Icons.folder_outlined, - title: context.l10n.noCollectionsFound, - subtitle: context.l10n.createYourFirstCollection, - action: Padding( - padding: const EdgeInsets.symmetric(horizontal: 40), - child: GradientButton( - onTap: _createCollection, - text: context.l10n.createCollection, - iconData: Icons.add, - paddingValue: 8.0, - ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.folder_outlined, + size: 64, + color: Colors.grey, + ), + const SizedBox(height: 16), + Text( + context.l10n.noCollectionsFound, + style: getEnteTextTheme(context).large.copyWith( + color: Colors.grey, + ), + ), + const SizedBox(height: 8), + Text( + context.l10n.createYourFirstCollection, + style: getEnteTextTheme(context).body.copyWith( + color: Colors.grey, + ), + ), + const SizedBox(height: 24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: GradientButton( + onTap: _createCollection, + text: context.l10n.createCollection, + iconData: Icons.add, + paddingValue: 8.0, + ), + ), + ], ), ), ), @@ -447,21 +508,42 @@ class _HomePageState extends UploaderPageState if (_recentFiles.isEmpty) { return Container( padding: const EdgeInsets.symmetric(vertical: 40), - child: _buildEmptyState( - icon: Icons.description_outlined, - title: context.l10n.nothingYet, - subtitle: context.l10n.uploadYourFirstDocument, - action: GradientButton( - onTap: addFile, - text: context.l10n.uploadDocument, - iconData: Icons.file_upload, - paddingValue: 8.0, + child: Center( + child: Column( + children: [ + Icon( + Icons.description_outlined, + size: 48, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + Text( + context.l10n.nothingYet, + style: getEnteTextTheme(context).body.copyWith( + color: Colors.grey[600], + ), + ), + const SizedBox(height: 8), + Text( + context.l10n.uploadYourFirstDocument, + style: getEnteTextTheme(context).small.copyWith( + color: Colors.grey[500], + ), + ), + const SizedBox(height: 24), + GradientButton( + onTap: addFile, + text: context.l10n.uploadDocument, + iconData: Icons.file_upload, + paddingValue: 8.0, + ), + ], ), ), ); } return RecentsSectionWidget( - collections: CollectionSortUtil.filterAndSortCollections(_collections), + collections: _filterOutUncategorized(_collections), recentFiles: _recentFiles, ); } @@ -475,113 +557,6 @@ class _HomePageState extends UploaderPageState } } - Future _addInformation() async { - final result = await showInformationAdditionDialog(context); - - if (result != null && mounted) { - switch (result.type) { - case InformationType.physicalDocument: - await _addPhysicalDocument(); - break; - case InformationType.emergencyContact: - await _addEmergencyContact(); - break; - case InformationType.accountCredential: - await _addAccountCredential(); - break; - } - } - } - - Future _addPhysicalDocument() async { - SnackBarUtils.showInfoSnackBar( - context, - "Soon", - ); - } - - Future _addEmergencyContact() async { - SnackBarUtils.showInfoSnackBar( - context, - "Soon", - ); - } - - Future _addAccountCredential() async { - SnackBarUtils.showInfoSnackBar( - context, - "Soon", - ); - } - - Widget _buildEmptyState({ - required IconData icon, - required String title, - required String subtitle, - Widget? action, - }) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(icon, size: 64, color: Colors.grey), - const SizedBox(height: 16), - Text( - title, - style: getEnteTextTheme(context).large.copyWith(color: Colors.grey), - ), - const SizedBox(height: 8), - Text( - subtitle, - style: getEnteTextTheme(context).body.copyWith(color: Colors.grey), - ), - if (action != null) ...[ - const SizedBox(height: 24), - action, - ], - ], - ), - ); - } - - Widget _buildFabLabel(String text) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: getEnteColorScheme(context).fillBase, - borderRadius: BorderRadius.circular(16), - ), - child: Text( - text, - style: getEnteTextTheme(context).small.copyWith( - color: getEnteColorScheme(context).backgroundBase, - ), - ), - ); - } - - Widget _buildFabOption({ - required String label, - required IconData icon, - required VoidCallback onPressed, - required String heroTag, - }) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - _buildFabLabel(label), - const SizedBox(width: 8), - FloatingActionButton( - heroTag: heroTag, - mini: true, - onPressed: onPressed, - backgroundColor: getEnteColorScheme(context).fillBase, - child: Icon(icon), - ), - ], - ); - } - Widget _buildCollectionsHeader() { return GestureDetector( behavior: HitTestBehavior.opaque, @@ -708,14 +683,39 @@ class _HomePageState extends UploaderPageState scale: _animation, child: Container( margin: const EdgeInsets.only(bottom: 16), - child: _buildFabOption( - label: context.l10n.addInformation, - icon: Icons.post_add, - onPressed: () { - _toggleFab(); - _addInformation(); - }, - heroTag: "addInformation", + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: getEnteColorScheme(context).fillBase, + borderRadius: BorderRadius.circular(16), + ), + child: Text( + context.l10n.createCollectionTooltip, + style: getEnteTextTheme(context).small.copyWith( + color: getEnteColorScheme(context) + .backgroundBase, + ), + ), + ), + const SizedBox(width: 8), + FloatingActionButton( + heroTag: "createCollection", + mini: true, + onPressed: () { + _toggleFab(); + _createCollection(); + }, + backgroundColor: + getEnteColorScheme(context).fillBase, + child: const Icon(Icons.create_new_folder), + ), + ], ), ), ), @@ -724,14 +724,40 @@ class _HomePageState extends UploaderPageState scale: _animation, child: Container( margin: const EdgeInsets.only(bottom: 8), - child: _buildFabOption( - label: context.l10n.uploadDocumentTooltip, - icon: Icons.file_upload, - onPressed: () { - _toggleFab(); - addFile(); - }, - heroTag: "addFile", + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: getEnteColorScheme(context).fillBase, + borderRadius: BorderRadius.circular(16), + ), + child: Text( + context.l10n.uploadDocumentTooltip, + style: + getEnteTextTheme(context).small.copyWith( + color: getEnteColorScheme(context) + .backgroundBase, + ), + ), + ), + const SizedBox(width: 8), + FloatingActionButton( + heroTag: "addFile", + mini: true, + onPressed: () { + _toggleFab(); + addFile(); + }, + backgroundColor: + getEnteColorScheme(context).fillBase, + child: const Icon(Icons.file_upload), + ), + ], ), ), ), diff --git a/mobile/apps/locker/lib/ui/pages/onboarding_page.dart b/mobile/apps/locker/lib/ui/pages/onboarding_page.dart index a5654f0e3a..f135b4ed9d 100644 --- a/mobile/apps/locker/lib/ui/pages/onboarding_page.dart +++ b/mobile/apps/locker/lib/ui/pages/onboarding_page.dart @@ -7,6 +7,7 @@ import 'package:ente_ui/components/buttons/gradient_button.dart'; import 'package:ente_ui/components/developer_settings_widget.dart'; import "package:ente_ui/pages/developer_settings_page.dart"; import 'package:ente_ui/theme/ente_theme.dart'; +import "package:ente_ui/theme/ente_theme_data.dart"; import 'package:ente_ui/utils/dialog_util.dart'; import 'package:flutter/material.dart'; import 'package:locker/l10n/l10n.dart'; @@ -103,12 +104,15 @@ class _OnboardingPageState extends State { .textTheme .titleLarge! .copyWith( - color: Colors.white38, + color: Theme.of(context) + .colorScheme + .onBoardingBodyColor, ), ), ], ), ), + const SizedBox(height: 100), Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 20), @@ -125,19 +129,14 @@ class _OnboardingPageState extends State { child: Hero( tag: "log_in", child: ElevatedButton( - style: ElevatedButton.styleFrom().copyWith( - shape: WidgetStateProperty.all< - RoundedRectangleBorder>( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(32.0), - ), - ), - ), + style: Theme.of(context) + .colorScheme + .optionalActionButtonStyle, onPressed: _navigateToSignInPage, child: Text( l10n.existingUser, style: const TextStyle( - color: Colors.white, // same for both themes + color: Colors.black, // same for both themes ), ), ), diff --git a/mobile/apps/locker/lib/ui/pages/settings_page.dart b/mobile/apps/locker/lib/ui/pages/settings_page.dart new file mode 100644 index 0000000000..4952b4a23c --- /dev/null +++ b/mobile/apps/locker/lib/ui/pages/settings_page.dart @@ -0,0 +1,125 @@ +import "dart:io"; + +import "package:ente_accounts/services/user_service.dart"; +import "package:ente_ui/theme/colors.dart"; +import "package:ente_ui/theme/ente_theme.dart"; +import "package:flutter/foundation.dart"; +import "package:flutter/material.dart"; +import "package:locker/services/configuration.dart"; +import "package:locker/ui/settings/about_section_widget.dart"; +import 'package:locker/ui/settings/account_section_widget.dart'; +import "package:locker/ui/settings/app_version_widget.dart"; +import "package:locker/ui/settings/security_section_widget.dart"; +import "package:locker/ui/settings/social_section_widget.dart"; +import "package:locker/ui/settings/support_section_widget.dart"; +import "package:locker/ui/settings/theme_switch_widget.dart"; +import "package:locker/ui/settings/title_bar_widget.dart"; + +class SettingsPage extends StatelessWidget { + final ValueNotifier emailNotifier; + final GlobalKey scaffoldKey; + + const SettingsPage({ + super.key, + required this.emailNotifier, + required this.scaffoldKey, + }); + + @override + Widget build(BuildContext context) { + final hasLoggedIn = Configuration.instance.hasConfiguredAccount(); + if (hasLoggedIn) { + UserService.instance.getUserDetailsV2().ignore(); + } + final enteColorScheme = getEnteColorScheme(context); + return Scaffold( + body: Container( + color: enteColorScheme.backdropBase, + child: _getBody(context, enteColorScheme), + ), + ); + } + + Widget _getBody(BuildContext context, EnteColorScheme colorScheme) { + final hasLoggedIn = Configuration.instance.hasConfiguredAccount(); + final enteTextTheme = getEnteTextTheme(context); + const sectionSpacing = SizedBox(height: 8); + final List contents = []; + + if (hasLoggedIn) { + contents.add( + Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Align( + alignment: Alignment.centerLeft, + child: AnimatedBuilder( + // [AnimatedBuilder] accepts any [Listenable] subtype. + animation: emailNotifier, + builder: (BuildContext context, Widget? child) { + return Text( + emailNotifier.value!, + style: enteTextTheme.body.copyWith( + color: colorScheme.textMuted, + overflow: TextOverflow.ellipsis, + ), + ); + }, + ), + ), + ), + ); + contents.addAll([ + const SizedBox(height: 12), + const AccountSectionWidget(), + sectionSpacing, + ]); + + contents.addAll([ + const SecuritySectionWidget(), + sectionSpacing, + ]); + + if (Platform.isAndroid || + Platform.isWindows || + Platform.isLinux || + kDebugMode) { + contents.addAll([ + const ThemeSwitchWidget(), + sectionSpacing, + ]); + } + } + + contents.addAll([ + const SupportSectionWidget(), + sectionSpacing, + const SocialSectionWidget(), + sectionSpacing, + const AboutSectionWidget(), + const AppVersionWidget(), + const Padding( + padding: EdgeInsets.only(bottom: 60), + ), + ]); + + return SafeArea( + bottom: false, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SettingsTitleBarWidget( + scaffoldKey: scaffoldKey, + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), + child: Column( + children: contents, + ), + ), + ], + ), + ), + ); + } +} diff --git a/mobile/apps/locker/lib/ui/settings/about_section_widget.dart b/mobile/apps/locker/lib/ui/settings/about_section_widget.dart new file mode 100644 index 0000000000..69b8002098 --- /dev/null +++ b/mobile/apps/locker/lib/ui/settings/about_section_widget.dart @@ -0,0 +1,84 @@ +import "package:ente_ui/components/captioned_text_widget.dart"; +import "package:ente_ui/components/menu_item_widget.dart"; +import "package:ente_ui/theme/ente_theme.dart"; +import "package:ente_utils/platform_util.dart"; +import "package:flutter/material.dart"; +import "package:locker/l10n/l10n.dart"; +import "package:locker/ui/components/expandable_menu_item_widget.dart"; +import "package:locker/ui/settings/common_settings.dart"; +import "package:url_launcher/url_launcher.dart"; + +class AboutSectionWidget extends StatelessWidget { + const AboutSectionWidget({super.key}); + + @override + Widget build(BuildContext context) { + return ExpandableMenuItemWidget( + title: context.l10n.about, + selectionOptionsWidget: _getSectionOptions(context), + leadingIcon: Icons.info_outline, + ); + } + + Widget _getSectionOptions(BuildContext context) { + return Column( + children: [ + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: context.l10n.weAreOpenSource, + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + // ignore: unawaited_futures + launchUrl(Uri.parse("https://github.com/ente-io/ente")); + }, + ), + sectionOptionSpacing, + AboutMenuItemWidget( + title: context.l10n.privacy, + url: "https://ente.io/privacy", + ), + sectionOptionSpacing, + AboutMenuItemWidget( + title: context.l10n.termsOfServicesTitle, + url: "https://ente.io/terms", + ), + sectionOptionSpacing, + ], + ); + } +} + +class AboutMenuItemWidget extends StatelessWidget { + final String title; + final String url; + final String? webPageTitle; + const AboutMenuItemWidget({ + required this.title, + required this.url, + this.webPageTitle, + super.key, + }); + + @override + Widget build(BuildContext context) { + return MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: title, + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + await PlatformUtil.openWebView( + context, + webPageTitle ?? title, + url, + ); + }, + ); + } +} diff --git a/mobile/apps/locker/lib/ui/settings/account_section_widget.dart b/mobile/apps/locker/lib/ui/settings/account_section_widget.dart new file mode 100644 index 0000000000..ff4a573f54 --- /dev/null +++ b/mobile/apps/locker/lib/ui/settings/account_section_widget.dart @@ -0,0 +1,181 @@ +import "package:ente_accounts/pages/change_email_dialog.dart"; +import "package:ente_accounts/pages/delete_account_page.dart"; +import "package:ente_accounts/pages/password_entry_page.dart"; +import "package:ente_accounts/pages/recovery_key_page.dart"; +import "package:ente_accounts/services/user_service.dart"; +import "package:ente_crypto_dart/ente_crypto_dart.dart"; +import "package:ente_lock_screen/local_authentication_service.dart"; +import "package:ente_ui/components/captioned_text_widget.dart"; +import "package:ente_ui/components/menu_item_widget.dart"; +import "package:ente_ui/theme/ente_theme.dart"; +import "package:ente_ui/utils/dialog_util.dart"; +import "package:ente_utils/navigation_util.dart"; +import "package:ente_utils/platform_util.dart"; +import "package:flutter/material.dart"; +import "package:locker/l10n/l10n.dart"; +import "package:locker/services/configuration.dart"; +import "package:locker/ui/components/expandable_menu_item_widget.dart"; +import "package:locker/ui/pages/home_page.dart"; +import "package:locker/ui/settings/common_settings.dart"; + +class AccountSectionWidget extends StatelessWidget { + const AccountSectionWidget({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return ExpandableMenuItemWidget( + title: l10n.account, + selectionOptionsWidget: _getSectionOptions(context), + leadingIcon: Icons.account_circle_outlined, + ); + } + + Column _getSectionOptions(BuildContext context) { + final l10n = context.l10n; + final List children = []; + children.addAll([ + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: l10n.changeEmail, + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + final hasAuthenticated = await LocalAuthenticationService.instance + .requestLocalAuthentication( + context, + l10n.authToChangeYourEmail, + ); + await PlatformUtil.refocusWindows(); + if (hasAuthenticated) { + // ignore: unawaited_futures + showDialog( + context: context, + builder: (BuildContext context) { + return const ChangeEmailDialog(); + }, + barrierColor: Colors.black.withValues(alpha: 0.85), + barrierDismissible: false, + ); + } + }, + ), + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: l10n.changePassword, + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + final hasAuthenticated = await LocalAuthenticationService.instance + .requestLocalAuthentication( + context, + l10n.authToChangeYourPassword, + ); + if (hasAuthenticated) { + // ignore: unawaited_futures + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return PasswordEntryPage( + Configuration.instance, + PasswordEntryMode.update, + const HomePage(), + ); + }, + ), + ); + } + }, + ), + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: l10n.recoveryKey, + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + final hasAuthenticated = await LocalAuthenticationService.instance + .requestLocalAuthentication( + context, + l10n.authToViewYourRecoveryKey, + ); + if (hasAuthenticated) { + String recoveryKey; + try { + recoveryKey = + CryptoUtil.bin2hex(Configuration.instance.getRecoveryKey()); + } catch (e) { + // ignore: unawaited_futures + showGenericErrorDialog( + context: context, + error: e, + ); + return; + } + // ignore: unawaited_futures + routeToPage( + context, + RecoveryKeyPage( + Configuration.instance, + recoveryKey, + l10n.ok, + showAppBar: true, + onDone: () {}, + ), + ); + } + }, + ), + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: context.l10n.logout, + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + _onLogoutTapped(context); + }, + ), + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: context.l10n.deleteAccount, + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + final config = Configuration.instance; + // ignore: unawaited_futures + routeToPage(context, DeleteAccountPage(config)); + }, + ), + sectionOptionSpacing, + ]); + return Column( + children: children, + ); + } + + void _onLogoutTapped(BuildContext context) { + showChoiceActionSheet( + context, + title: context.l10n.areYouSureYouWantToLogout, + firstButtonLabel: context.l10n.yesLogout, + isCritical: true, + firstButtonOnTap: () async { + await UserService.instance.logout(context); + }, + ); + } +} diff --git a/mobile/apps/locker/lib/ui/settings/app_version_widget.dart b/mobile/apps/locker/lib/ui/settings/app_version_widget.dart new file mode 100644 index 0000000000..ae78268e40 --- /dev/null +++ b/mobile/apps/locker/lib/ui/settings/app_version_widget.dart @@ -0,0 +1,66 @@ +import "package:ente_ui/utils/dialog_util.dart"; +import 'package:flutter/material.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +class AppVersionWidget extends StatefulWidget { + const AppVersionWidget({ + super.key, + }); + + @override + State createState() => _AppVersionWidgetState(); +} + +class _AppVersionWidgetState extends State { + static const kTapThresholdForInspector = 5; + static const kConsecutiveTapTimeWindowInMilliseconds = 2000; + static const kDummyDelayDurationInMilliseconds = 1500; + + int? _lastTap; + int _consecutiveTaps = 0; + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () async { + final int now = DateTime.now().millisecondsSinceEpoch; + if (now - (_lastTap ?? now) < kConsecutiveTapTimeWindowInMilliseconds) { + _consecutiveTaps++; + if (_consecutiveTaps == kTapThresholdForInspector) { + final dialog = + createProgressDialog(context, "Starting network inspector..."); + await dialog.show(); + await Future.delayed( + const Duration(milliseconds: kDummyDelayDurationInMilliseconds), + ); + await dialog.hide(); + } + } else { + _consecutiveTaps = 1; + } + _lastTap = now; + }, + child: FutureBuilder( + future: _getAppVersion(), + builder: (context, snapshot) { + if (snapshot.hasData) { + return Padding( + padding: const EdgeInsets.all(20), + child: Text( + "Version: ${snapshot.data!}", + style: Theme.of(context).textTheme.bodySmall, + ), + ); + } + return Container(); + }, + ), + ); + } + + Future _getAppVersion() async { + final pkgInfo = await PackageInfo.fromPlatform(); + return pkgInfo.version; + } +} diff --git a/mobile/apps/locker/lib/ui/settings/common_settings.dart b/mobile/apps/locker/lib/ui/settings/common_settings.dart new file mode 100644 index 0000000000..d922cdee7b --- /dev/null +++ b/mobile/apps/locker/lib/ui/settings/common_settings.dart @@ -0,0 +1,14 @@ +import "package:expandable/expandable.dart"; +import "package:flutter/material.dart"; + +Widget sectionOptionSpacing = const SizedBox(height: 6); + +ExpandableThemeData getExpandableTheme() { + return const ExpandableThemeData( + hasIcon: false, + useInkWell: false, + tapBodyToCollapse: true, + tapBodyToExpand: true, + animationDuration: Duration(milliseconds: 400), + ); +} diff --git a/mobile/apps/locker/lib/ui/settings/security_section_widget.dart b/mobile/apps/locker/lib/ui/settings/security_section_widget.dart new file mode 100644 index 0000000000..6d25cb3004 --- /dev/null +++ b/mobile/apps/locker/lib/ui/settings/security_section_widget.dart @@ -0,0 +1,217 @@ +import "dart:async"; +import "dart:typed_data"; + +import "package:ente_accounts/models/user_details.dart"; +import "package:ente_accounts/pages/request_pwd_verification_page.dart"; +import "package:ente_accounts/services/passkey_service.dart"; +import "package:ente_accounts/services/user_service.dart"; +import "package:ente_crypto_dart/ente_crypto_dart.dart"; +import "package:ente_lock_screen/auth_util.dart"; +import "package:ente_lock_screen/local_authentication_service.dart"; +import "package:ente_lock_screen/lock_screen_settings.dart"; +import "package:ente_lock_screen/ui/lock_screen_options.dart"; +import "package:ente_ui/components/captioned_text_widget.dart"; +import "package:ente_ui/components/menu_item_widget.dart"; +import "package:ente_ui/components/toggle_switch_widget.dart"; +import "package:ente_ui/theme/ente_theme.dart"; +import "package:ente_ui/utils/dialog_util.dart"; +import "package:ente_ui/utils/toast_util.dart"; +import "package:ente_utils/navigation_util.dart"; +import "package:ente_utils/platform_util.dart"; +import "package:flutter/foundation.dart"; +import "package:flutter/material.dart"; +import "package:locker/l10n/l10n.dart"; +import "package:locker/services/configuration.dart"; +import "package:locker/ui/components/expandable_menu_item_widget.dart"; +import "package:locker/ui/settings/common_settings.dart"; +import "package:logging/logging.dart"; + +class SecuritySectionWidget extends StatefulWidget { + const SecuritySectionWidget({super.key}); + + @override + State createState() => _SecuritySectionWidgetState(); +} + +class _SecuritySectionWidgetState extends State { + final _config = Configuration.instance; + late bool _hasLoggedIn; + final Logger _logger = Logger('SecuritySectionWidget'); + + @override + void initState() { + _hasLoggedIn = _config.hasConfiguredAccount(); + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return ExpandableMenuItemWidget( + title: l10n.security, + selectionOptionsWidget: _getSectionOptions(context), + leadingIcon: Icons.local_police_outlined, + ); + } + + Widget _getSectionOptions(BuildContext context) { + final l10n = context.l10n; + final List children = []; + if (_hasLoggedIn) { + children.addAll( + [ + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: l10n.emailVerificationToggle, + ), + trailingWidget: ToggleSwitchWidget( + value: () => UserService.instance.hasEmailMFAEnabled(), + onChanged: () async { + final hasAuthenticated = await LocalAuthenticationService + .instance + .requestLocalAuthentication( + context, + l10n.authToChangeEmailVerificationSetting, + ); + final isEmailMFAEnabled = + UserService.instance.hasEmailMFAEnabled(); + if (hasAuthenticated) { + await updateEmailMFA(!isEmailMFAEnabled); + } + }, + ), + ), + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: context.l10n.passkey, + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + final hasAuthenticated = await LocalAuthenticationService.instance + .requestLocalAuthentication( + context, + l10n.authToViewPasskey, + ); + if (hasAuthenticated) { + await onPasskeyClick(context); + } + }, + ), + sectionOptionSpacing, + ], + ); + } else { + children.add(sectionOptionSpacing); + } + children.addAll([ + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: context.l10n.appLock, + ), + surfaceExecutionStates: false, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + if (await LockScreenSettings.instance.shouldShowLockScreen()) { + final bool result = await requestAuthentication( + context, + context.l10n.authToChangeLockscreenSetting, + ); + if (result) { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return const LockScreenOptions(); + }, + ), + ); + } + } else { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return const LockScreenOptions(); + }, + ), + ); + } + }, + ), + sectionOptionSpacing, + ]); + + return Column( + children: children, + ); + } + + Future onPasskeyClick(BuildContext buildContext) async { + try { + final hasAuthenticated = + await LocalAuthenticationService.instance.requestLocalAuthentication( + context, + context.l10n.authenticateGeneric, + ); + await PlatformUtil.refocusWindows(); + if (!hasAuthenticated) { + return; + } + final isPassKeyResetEnabled = + await PasskeyService.instance.isPasskeyRecoveryEnabled(); + if (!isPassKeyResetEnabled) { + final Uint8List recoveryKey = Configuration.instance.getRecoveryKey(); + final resetKey = CryptoUtil.generateKey(); + final resetKeyBase64 = CryptoUtil.bin2base64(resetKey); + final encryptionResult = CryptoUtil.encryptSync( + resetKey, + recoveryKey, + ); + await PasskeyService.instance.configurePasskeyRecovery( + resetKeyBase64, + CryptoUtil.bin2base64(encryptionResult.encryptedData!), + CryptoUtil.bin2base64(encryptionResult.nonce!), + ); + } + PasskeyService.instance.openPasskeyPage(buildContext).ignore(); + } catch (e, s) { + _logger.severe("failed to open passkey page", e, s); + await showGenericErrorDialog( + context: context, + error: e, + ); + } + } + + Future updateEmailMFA(bool isEnabled) async { + try { + final UserDetails details = + await UserService.instance.getUserDetailsV2(memoryCount: false); + if ((details.profileData?.canDisableEmailMFA ?? false) == false) { + await routeToPage( + context, + RequestPasswordVerificationPage( + Configuration.instance, + onPasswordVerified: (Uint8List keyEncryptionKey) async { + final Uint8List loginKey = + await CryptoUtil.deriveLoginKey(keyEncryptionKey); + await UserService.instance.registerOrUpdateSrp(loginKey); + }, + ), + ); + } + await UserService.instance.updateEmailMFA(isEnabled); + } catch (e) { + showToast(context, context.l10n.somethingWentWrong); + } + } +} diff --git a/mobile/apps/locker/lib/ui/settings/social_section_widget.dart b/mobile/apps/locker/lib/ui/settings/social_section_widget.dart new file mode 100644 index 0000000000..39b5cb0048 --- /dev/null +++ b/mobile/apps/locker/lib/ui/settings/social_section_widget.dart @@ -0,0 +1,88 @@ +import 'dart:io'; + +import "package:ente_ui/components/captioned_text_widget.dart"; +import "package:ente_ui/components/menu_item_widget.dart"; +import "package:ente_ui/theme/ente_theme.dart"; +import 'package:flutter/material.dart'; +import "package:locker/l10n/l10n.dart"; +import "package:locker/ui/components/expandable_menu_item_widget.dart"; +import "package:locker/ui/settings/common_settings.dart"; +import 'package:url_launcher/url_launcher_string.dart'; + +class SocialSectionWidget extends StatelessWidget { + const SocialSectionWidget({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return ExpandableMenuItemWidget( + title: l10n.social, + selectionOptionsWidget: _getSectionOptions(context), + leadingIcon: Icons.interests_outlined, + ); + } + + Widget _getSectionOptions(BuildContext context) { + final l10n = context.l10n; + + final List options = [ + sectionOptionSpacing, + SocialsMenuItemWidget( + l10n.blog, + "https://ente.io/blog", + launchInExternalApp: !Platform.isAndroid, + ), + sectionOptionSpacing, + SocialsMenuItemWidget( + l10n.merchandise, + "https://shop.ente.io", + launchInExternalApp: !Platform.isAndroid, + ), + const SocialsMenuItemWidget("Twitter", "https://twitter.com/enteio"), + sectionOptionSpacing, + const SocialsMenuItemWidget("Mastodon", "https://fosstodon.org/@ente"), + sectionOptionSpacing, + const SocialsMenuItemWidget("Matrix", "https://ente.io/matrix"), + sectionOptionSpacing, + const SocialsMenuItemWidget("Discord", "https://ente.io/discord"), + sectionOptionSpacing, + const SocialsMenuItemWidget("Reddit", "https://reddit.com/r/enteio"), + sectionOptionSpacing, + ]; + return Column(children: options); + } +} + +class SocialsMenuItemWidget extends StatelessWidget { + final String text; + final String url; + final bool launchInExternalApp; + + const SocialsMenuItemWidget( + this.text, + this.url, { + super.key, + this.launchInExternalApp = true, + }); + + @override + Widget build(BuildContext context) { + return MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: text, + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + // ignore: unawaited_futures + launchUrlString( + url, + mode: launchInExternalApp + ? LaunchMode.externalApplication + : LaunchMode.platformDefault, + ); + }, + ); + } +} diff --git a/mobile/apps/locker/lib/ui/settings/support_section_widget.dart b/mobile/apps/locker/lib/ui/settings/support_section_widget.dart new file mode 100644 index 0000000000..b412449c2d --- /dev/null +++ b/mobile/apps/locker/lib/ui/settings/support_section_widget.dart @@ -0,0 +1,87 @@ +import 'dart:io'; + +import "package:ente_ui/components/captioned_text_widget.dart"; +import "package:ente_ui/components/menu_item_widget.dart"; +import "package:ente_ui/theme/ente_theme.dart"; +import "package:ente_utils/email_util.dart"; +import 'package:flutter/material.dart'; +import "package:locker/core/constants.dart"; +import "package:locker/l10n/l10n.dart"; +import "package:locker/ui/components/expandable_menu_item_widget.dart"; +import "package:locker/ui/settings/about_section_widget.dart"; +import "package:locker/ui/settings/common_settings.dart"; +import "package:url_launcher/url_launcher_string.dart"; + +class SupportSectionWidget extends StatelessWidget { + const SupportSectionWidget({super.key}); + + get supportEmail => null; + + @override + Widget build(BuildContext context) { + return ExpandableMenuItemWidget( + title: context.l10n.support, + selectionOptionsWidget: _getSectionOptions(context), + leadingIcon: Icons.help_outline_outlined, + ); + } + + Widget _getSectionOptions(BuildContext context) { + final String bugsEmail = + Platform.isAndroid ? "android-bugs@ente.io" : "ios-bugs@ente.io"; + return Column( + children: [ + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: context.l10n.contactSupport, + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + await sendEmail(context, to: supportEmail); + }, + ), + sectionOptionSpacing, + AboutMenuItemWidget( + title: context.l10n.help, + url: "https://help.ente.io", + ), + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: context.l10n.suggestFeatures, + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + // ignore: unawaited_futures + launchUrlString( + githubDiscussionsUrl, + mode: LaunchMode.externalApplication, + ); + }, + ), + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: context.l10n.reportABug, + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + await sendLogs(context, context.l10n.reportBug); + }, + onLongPress: () async { + final zipFilePath = await getZippedLogsFile(); + await shareLogs(context, bugsEmail, zipFilePath); + }, + ), + sectionOptionSpacing, + ], + ); + } +} diff --git a/mobile/apps/locker/lib/ui/settings/theme_switch_widget.dart b/mobile/apps/locker/lib/ui/settings/theme_switch_widget.dart new file mode 100644 index 0000000000..ee73a52b84 --- /dev/null +++ b/mobile/apps/locker/lib/ui/settings/theme_switch_widget.dart @@ -0,0 +1,95 @@ +import "package:adaptive_theme/adaptive_theme.dart"; +import "package:ente_ui/components/captioned_text_widget.dart"; +import "package:ente_ui/components/menu_item_widget.dart"; +import "package:ente_ui/theme/ente_theme.dart"; +import "package:ente_ui/theme/ente_theme_data.dart"; +import "package:flutter/material.dart"; +import "package:locker/l10n/l10n.dart"; +import "package:locker/ui/components/expandable_menu_item_widget.dart"; +import "package:locker/ui/settings/common_settings.dart"; + +class ThemeSwitchWidget extends StatefulWidget { + const ThemeSwitchWidget({super.key}); + + @override + State createState() => _ThemeSwitchWidgetState(); +} + +class _ThemeSwitchWidgetState extends State { + AdaptiveThemeMode? currentThemeMode; + + @override + void initState() { + super.initState(); + AdaptiveTheme.getThemeMode().then( + (value) { + currentThemeMode = value ?? AdaptiveThemeMode.system; + debugPrint('theme value $value'); + if (mounted) { + setState(() => {}); + } + }, + ); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ExpandableMenuItemWidget( + title: context.l10n.theme, + selectionOptionsWidget: _getSectionOptions(context), + leadingIcon: Theme.of(context).brightness == Brightness.light + ? Icons.light_mode_outlined + : Icons.dark_mode_outlined, + ); + } + + Widget _getSectionOptions(BuildContext context) { + return Column( + children: [ + sectionOptionSpacing, + _menuItem(context, AdaptiveThemeMode.light), + sectionOptionSpacing, + _menuItem(context, AdaptiveThemeMode.dark), + sectionOptionSpacing, + _menuItem(context, AdaptiveThemeMode.system), + sectionOptionSpacing, + ], + ); + } + + String _name(BuildContext ctx, AdaptiveThemeMode mode) { + switch (mode) { + case AdaptiveThemeMode.light: + return ctx.l10n.lightTheme; + case AdaptiveThemeMode.dark: + return ctx.l10n.darkTheme; + case AdaptiveThemeMode.system: + return ctx.l10n.systemTheme; + } + } + + Widget _menuItem(BuildContext context, AdaptiveThemeMode themeMode) { + return MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: _name(context, themeMode), + textStyle: Theme.of(context).colorScheme.enteTheme.textTheme.body, + ), + pressedColor: getEnteColorScheme(context).fillFaint, + isExpandable: false, + trailingIcon: currentThemeMode == themeMode ? Icons.check : null, + trailingExtraMargin: 4, + onTap: () async { + AdaptiveTheme.of(context).setThemeMode(themeMode); + currentThemeMode = themeMode; + if (mounted) { + setState(() {}); + } + }, + ); + } +} diff --git a/mobile/apps/locker/lib/ui/settings/title_bar_widget.dart b/mobile/apps/locker/lib/ui/settings/title_bar_widget.dart new file mode 100644 index 0000000000..ac44124f68 --- /dev/null +++ b/mobile/apps/locker/lib/ui/settings/title_bar_widget.dart @@ -0,0 +1,36 @@ + +import 'package:flutter/material.dart'; +import "package:locker/l10n/l10n.dart"; + +class SettingsTitleBarWidget extends StatelessWidget { + const SettingsTitleBarWidget({ + super.key, + required this.scaffoldKey, + }); + + final GlobalKey scaffoldKey; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return Container( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 20, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + visualDensity: const VisualDensity(horizontal: -2, vertical: -2), + onPressed: () { + scaffoldKey.currentState?.closeDrawer(); + }, + icon: const Icon(Icons.keyboard_double_arrow_left_outlined), + ), + Text(l10n.settings), + ], + ), + ), + ); + } +} diff --git a/mobile/apps/locker/pubspec.lock b/mobile/apps/locker/pubspec.lock index a69b237c67..6f01235ebb 100644 --- a/mobile/apps/locker/pubspec.lock +++ b/mobile/apps/locker/pubspec.lock @@ -289,7 +289,7 @@ packages: source: hosted version: "2.0.1" expandable: - dependency: transitive + dependency: "direct main" description: name: expandable sha256: "9604d612d4d1146dafa96c6d8eec9c2ff0994658d6d09fed720ab788c7f5afc2" @@ -340,10 +340,10 @@ packages: dependency: transitive description: name: file_saver - sha256: "9d93db09bd4da9e43238f9dd485360fc51a5c138eea5ef5f407ec56e58079ac0" + sha256: "448b1e30142cffe52f37ee085ea9ca50670d5425bb09b649d193549b2dcf6e26" url: "https://pub.dev" source: hosted - version: "0.3.1" + version: "0.3.0" fixnum: dependency: transitive description: @@ -606,7 +606,7 @@ packages: source: hosted version: "0.1.0" intl: - dependency: "direct main" + dependency: transitive description: name: intl sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" @@ -985,10 +985,10 @@ packages: dependency: transitive description: name: posix - sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62 + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.0.3" privacy_screen: dependency: transitive description: @@ -1065,18 +1065,18 @@ packages: dependency: transitive description: name: share_plus - sha256: b2961506569e28948d75ec346c28775bb111986bb69dc6a20754a457e3d97fa0 + sha256: d7dc0630a923883c6328ca31b89aa682bacbf2f8304162d29f7c6aaff03a27a1 url: "https://pub.dev" source: hosted - version: "11.0.0" + version: "11.1.0" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "1032d392bc5d2095a77447a805aa3f804d2ae6a4d5eef5e6ebb3bd94c1bc19ef" + sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "6.1.0" shared_preferences: dependency: "direct main" description: @@ -1182,34 +1182,34 @@ packages: dependency: "direct main" description: name: sqflite - sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.1" sqflite_android: dependency: transitive description: name: sqflite_android - sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b" + sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.0" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b" + sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" url: "https://pub.dev" source: hosted - version: "2.5.5" + version: "2.5.4+6" sqflite_darwin: dependency: transitive description: name: sqflite_darwin - sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.1+1" sqflite_platform_interface: dependency: transitive description: @@ -1323,13 +1323,13 @@ packages: source: hosted version: "1.1.0" url_launcher: - dependency: transitive + dependency: "direct main" description: name: url_launcher - sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.2" url_launcher_android: dependency: transitive description: @@ -1468,4 +1468,4 @@ packages: version: "1.1.1" sdks: dart: ">=3.7.2 <4.0.0" - flutter: ">=3.24.0" + flutter: ">=3.27.0" diff --git a/mobile/apps/locker/pubspec.yaml b/mobile/apps/locker/pubspec.yaml index 1e1197b001..1905c48cbf 100644 --- a/mobile/apps/locker/pubspec.yaml +++ b/mobile/apps/locker/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: "none" version: 0.1.0 environment: - sdk: ^3.6.0 + sdk: ">=3.0.0 <4.0.0" dependencies: adaptive_theme: ^3.6.0 @@ -35,6 +35,7 @@ dependencies: ente_utils: path: ../../packages/utils event_bus: ^2.0.1 + expandable: ^5.0.1 fast_base58: ^0.2.1 file_picker: ^10.2.0 flutter: @@ -47,7 +48,6 @@ dependencies: flutter_localizations: sdk: flutter http: ^1.4.0 - intl: ^0.20.2 io: ^1.0.5 listen_sharing_intent: ^1.9.2 logging: ^1.3.0 @@ -59,6 +59,7 @@ dependencies: sqflite: ^2.4.1 styled_text: ^8.1.0 tray_manager: ^0.5.0 + url_launcher: ^6.3.2 uuid: ^4.5.1 window_manager: ^0.5.0