[mobile][locker] Add Drawer & Setting section in locker (#6895)

This commit is contained in:
Vishnu Mohandas
2025-08-19 15:35:41 +05:30
committed by GitHub
26 changed files with 1824 additions and 505 deletions

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

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

View File

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

View File

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

View File

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

View File

@@ -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';
}

View File

@@ -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<void> _initSystemTray() async {
}
Future<void> _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<void> _runInForeground() async {
locale: locale,
savedThemeMode: savedThemeMode,
supportedLocales: appSupportedLocales,
localizationsDelegates: AppLocalizations.localizationsDelegates,
localizationsDelegates: const [
...StringsLocalizations.localizationsDelegates,
...AppLocalizations.localizationsDelegates,
],
localeListResolutionCallback: localResolutionCallBack,
),
);

View File

@@ -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<ExpandableMenuItemWidget> createState() =>
_ExpandableMenuItemWidgetState();
}
class _ExpandableMenuItemWidgetState extends State<ExpandableMenuItemWidget> {
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,
),
),
),
);
}
}

View File

@@ -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<InformationAdditionDialog> createState() =>
_InformationAdditionDialogState();
}
class _InformationAdditionDialogState extends State<InformationAdditionDialog> {
void _onTypeSelected(InformationType type) {
final result = InformationAdditionResult(type: type);
Navigator.of(context).pop(result);
}
Future<void> _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<InformationAdditionResult?> showInformationAdditionDialog(
BuildContext context,
) async {
return showDialog<InformationAdditionResult>(
context: context,
barrierColor: getEnteColorScheme(context).backdropBase,
builder: (context) => const InformationAdditionDialog(),
);
}

View File

@@ -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<HomePage>
with TickerProviderStateMixin, SearchMixin {
late final _settingsPage = SettingsPage(
emailNotifier: UserService.instance.emailValueNotifier,
scaffoldKey: scaffoldKey,
);
final scaffoldKey = GlobalKey<ScaffoldState>();
bool _isLoading = true;
bool _isSettingsOpen = false;
List<Collection> _collections = [];
List<Collection> _filteredCollections = [];
List<EnteFile> _recentFiles = [];
List<EnteFile> _filteredFiles = [];
Map<int, int> _collectionFileCounts = {};
bool _isLoading = true;
String? _error;
final _logger = Logger('HomePage');
StreamSubscription? _mediaStreamSubscription;
@@ -64,8 +72,7 @@ class _HomePageState extends UploaderPageState<HomePage>
List<EnteFile> files,
) {
setState(() {
_filteredCollections =
CollectionSortUtil.filterAndSortCollections(collections);
_filteredCollections = _filterOutUncategorized(collections);
_filteredFiles = files;
});
}
@@ -74,8 +81,7 @@ class _HomePageState extends UploaderPageState<HomePage>
void onSearchStateChanged(bool isActive) {
if (!isActive) {
setState(() {
_filteredCollections =
CollectionSortUtil.filterAndSortCollections(_collections);
_filteredCollections = _filterOutUncategorized(_collections);
_filteredFiles = _recentFiles;
});
}
@@ -83,6 +89,10 @@ class _HomePageState extends UploaderPageState<HomePage>
List<Collection> get _displayedCollections {
final collections = isSearchActive ? _filteredCollections : _collections;
return _filterOutUncategorized(collections);
}
List<Collection> _filterOutUncategorized(List<Collection> collections) {
return CollectionSortUtil.filterAndSortCollections(collections);
}
@@ -260,8 +270,7 @@ class _HomePageState extends UploaderPageState<HomePage>
setState(() {
_collections = sortedCollections;
_filteredCollections =
CollectionSortUtil.filterAndSortCollections(sortedCollections);
_filteredCollections = _filterOutUncategorized(sortedCollections);
_filteredFiles = _recentFiles;
_isLoading = false;
});
@@ -276,18 +285,31 @@ class _HomePageState extends UploaderPageState<HomePage>
}
Future<void> _loadRecentFiles(List<Collection> collections) async {
final allFiles = await CollectionService.instance.getAllFiles();
final allFiles = <EnteFile>[];
final uniqueFilesMap = <String, EnteFile>{};
allFiles.addAll(await CollectionService.instance.getAllFiles());
final uniqueFiles = <EnteFile>[];
final seenHashes = <String>{};
final seenIds = <int>{};
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<HomePage>
@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<HomePage>
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<HomePage>
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<HomePage>
}
}
Future<void> _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<void> _addPhysicalDocument() async {
SnackBarUtils.showInfoSnackBar(
context,
"Soon",
);
}
Future<void> _addEmergencyContact() async {
SnackBarUtils.showInfoSnackBar(
context,
"Soon",
);
}
Future<void> _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<HomePage>
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<HomePage>
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),
),
],
),
),
),

View File

@@ -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<OnboardingPage> {
.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<OnboardingPage> {
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
),
),
),

View File

@@ -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<String?> emailNotifier;
final GlobalKey<ScaffoldState> 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<Widget> 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,
),
),
],
),
),
);
}
}

View File

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

View File

@@ -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<Widget> 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);
},
);
}
}

View File

@@ -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<AppVersionWidget> createState() => _AppVersionWidgetState();
}
class _AppVersionWidgetState extends State<AppVersionWidget> {
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<String>(
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<String> _getAppVersion() async {
final pkgInfo = await PackageInfo.fromPlatform();
return pkgInfo.version;
}
}

View File

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

View File

@@ -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<SecuritySectionWidget> createState() => _SecuritySectionWidgetState();
}
class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
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<Widget> 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<void> 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<void> 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);
}
}
}

View File

@@ -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<Widget> 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,
);
},
);
}
}

View File

@@ -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,
],
);
}
}

View File

@@ -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<ThemeSwitchWidget> createState() => _ThemeSwitchWidgetState();
}
class _ThemeSwitchWidgetState extends State<ThemeSwitchWidget> {
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(() {});
}
},
);
}
}

View File

@@ -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<ScaffoldState> 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),
],
),
),
);
}
}

View File

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

View File

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