Compare commits

..

34 Commits

Author SHA1 Message Date
Neeraj Gupta
31057cbe9e [server] Handle mobile flag 2025-08-20 14:51:30 +05:30
Neeraj
fc1096c985 [mob] Include mobile flag during ott (#6906)
## Description
Need this to identify which OTT template to send from backend.
Ref: https://github.com/ente-io/ente/pull/5654
## Tests
2025-08-20 14:25:22 +05:30
Neeraj Gupta
4b5f91a428 [mob] Send ismobile flag 2025-08-20 13:14:59 +05:30
Neeraj
29b12fc6b5 [docs] Lint (#6905)
## Description

## Tests
2025-08-20 11:01:44 +05:30
Neeraj Gupta
c3eec71d60 [docs] Lint 2025-08-20 11:01:25 +05:30
Neeraj
2fccdee0d6 [server] Add optional TLS/SSL encryption for SMTP (#6863)
## Description

Implement TLS/SSL encryption for sending emails via SMTP. When an SMTP
provider explicitly requires TLS/SSL communication the current
implementation runs in a timeout and fails. A new configuration
parameter for smtp was added to enable TLS/SSL communication.

This would solve #5958 

## Tests

I built a local docker image of my branch. The email provider I was
using is mailbox.org and using the tls configuration. Registering a new
user then resulted in a sent email containing the verification code.

I did not test a setup without TLS/SSL.
2025-08-20 11:00:36 +05:30
Neeraj
8f053e7a7b [m][photos] Fix for duplicate entries for local file (#6904)
## Description

## Tests
2025-08-20 10:42:41 +05:30
Neeraj
de7291f5d4 [mobile][photos] don't show lock screen on deep link (#6899)
## Description

Fixes the clicking on widget behavior

## Tests
2025-08-20 10:06:06 +05:30
Neeraj Gupta
e09c952198 Fix for duplicate enteries for file 2025-08-20 10:01:51 +05:30
Prateek Sunal
86a9dee49d fix: add note, revert old code, remove default redirect 2025-08-20 00:25:33 +05:30
Vishnu Mohandas
33b1a7e4f8 [mob] Remove generated code (#6901) 2025-08-19 20:22:51 +05:30
vishnukvmd
de783e91dc Ignore build 2025-08-19 20:22:22 +05:30
vishnukvmd
554c8f4b7a Remove noise 2025-08-19 20:22:11 +05:30
Vishnu Mohandas
f438142646 [locker] Icon (#6900) 2025-08-19 19:00:07 +05:30
vishnukvmd
aa6c010562 Update Android icon 2025-08-19 18:59:33 +05:30
vishnukvmd
2830c89bde Set icons 2025-08-19 18:58:21 +05:30
Prateek Sunal
4de530b882 fix: don't show lock sreen if not required 2025-08-19 18:27:33 +05:30
Vishnu Mohandas
62d7c87dc7 [locker] Update deps (#6898) 2025-08-19 18:23:51 +05:30
vishnukvmd
3a1b6dbf15 [locker] Update deps 2025-08-19 18:23:19 +05:30
Laurens Priem
5e0bba390b [mob][photos] Decoded image minor refactor (#6897)
## Description

Minor refactor (no functional change) in the way we're decoding and
returning images for indexing.
2025-08-19 16:42:24 +05:30
Vishnu Mohandas
df6a3b94db [mobile][locker] Add Drawer & Setting section in locker (#6895) 2025-08-19 15:35:41 +05:30
AmanRajSinghMourya
72b7e12768 Merge branch 'main' into drawer 2025-08-19 15:24:29 +05:30
AmanRajSinghMourya
3a7d82a799 Add setting section 2025-08-19 14:58:15 +05:30
Neeraj
5f1cfb9ba5 [server] Fail request on customDomain mismatch (#6893)
## Description

## Tests
2025-08-19 14:23:00 +05:30
Neeraj Gupta
298e3695c7 [server] Fail request on customDomain mismatch 2025-08-19 12:34:31 +05:30
Vishnu Mohandas
621713d0b4 [mob] Remove unused import (#6892) 2025-08-19 12:33:15 +05:30
vishnukvmd
34813d2fae [mob] Remove unused import 2025-08-19 12:32:58 +05:30
Vishnu Mohandas
d8e4418d78 Remove ignore (#6890) 2025-08-19 11:43:36 +05:30
vishnukvmd
9771a5bc5d Remove ignore 2025-08-19 11:41:44 +05:30
Neeraj
aa4207f878 [auth] New translations (#6877)
New translations from
[Crowdin](https://crowdin.com/project/ente-authenticator-app)
2025-08-19 10:13:12 +05:30
Crowdin Bot
f2049ac7fa New Crowdin translations by GitHub Action 2025-08-18 01:18:04 +00:00
Kilian Hohm
cf938eca91 Add CLI command to send a test email via admin API 2025-08-16 10:33:23 +02:00
Kilian Hohm
a3d3ee24f8 Document optional TLS/SSL encryption for sending emails via SMTP 2025-08-15 15:48:59 +02:00
Kilian Hohm
6b37cc46a5 Add optional TLS/SSL encryption for sending emails via SMTP 2025-08-15 15:48:41 +02:00
122 changed files with 2151 additions and 629 deletions

View File

@@ -142,6 +142,22 @@ var _updateFreeUserStorage = &cobra.Command{
},
}
var _sendMail = &cobra.Command{
Use: "send-mail <to-email> <from-email> <from-name>",
Args: cobra.ExactArgs(3),
Short: "Sends a test mail via the admin api",
RunE: func(cmd *cobra.Command, args []string) error {
recoverWithLog()
var flags = &model.AdminActionForUser{}
cmd.Flags().VisitAll(func(f *pflag.Flag) {
if f.Name == "admin-user" {
flags.AdminEmail = f.Value.String()
}
})
return ctrl.SendTestMail(context.Background(), *flags, args[0], args[1], args[2])
},
}
func init() {
rootCmd.AddCommand(_adminCmd)
_ = _userDetailsCmd.MarkFlagRequired("admin-user")
@@ -159,5 +175,6 @@ func init() {
_updateFreeUserStorage.Flags().StringP("user", "u", "", "The email of the user to update subscription for. (required)")
// add a flag with no value --no-limit
_updateFreeUserStorage.Flags().String("no-limit", "True", "When true, sets 100TB as storage limit, and expiry to current date + 100 years")
_adminCmd.AddCommand(_userDetailsCmd, _disable2faCmd, _disablePasskeyCmd, _updateFreeUserStorage, _listUsers, _deleteUser)
_sendMail.Flags().StringP("admin-user", "a", "", "The email of the admin user. ")
_adminCmd.AddCommand(_userDetailsCmd, _disable2faCmd, _disablePasskeyCmd, _updateFreeUserStorage, _listUsers, _deleteUser, _sendMail)
}

View File

@@ -139,5 +139,28 @@ func (c *Client) UpdateFreePlanSub(ctx context.Context, userDetails *models.User
}
}
return nil
}
func (c *Client) SendTestMail(ctx context.Context, toEmail, fromEmail, fromName string) error {
body := map[string]interface{}{
"to": []string{toEmail},
"fromName": fromName,
"fromEmail": fromEmail,
"subject": "Test mail from Ente",
"body": "This is a test mail from Ente",
}
r, err := c.restClient.R().
SetContext(ctx).
SetBody(body).
Post("/admin/mail")
if err != nil {
return err
}
if r.IsError() {
return &ApiError{
StatusCode: r.StatusCode(),
Message: r.String(),
}
}
return nil
}

View File

@@ -156,6 +156,23 @@ func (c *ClICtrl) UpdateFreeStorage(ctx context.Context, params model.AdminActio
return nil
}
func (c *ClICtrl) SendTestMail(ctx context.Context, params model.AdminActionForUser, to, from, fromName string) error {
accountCtx, err := c.buildAdminContext(ctx, params.AdminEmail)
if err != nil {
return err
}
err = c.Client.SendTestMail(accountCtx, to, from, fromName)
if err != nil {
if apiErr, ok := err.(*api.ApiError); ok && apiErr.StatusCode == 400 && strings.Contains(apiErr.Message, "Token is too old") {
fmt.Printf("Error: old admin token, please re-authenticate using `ente account add` \n")
return nil
}
return err
}
fmt.Printf("Successfully sent test email to %s\n", to)
return nil
}
func (c *ClICtrl) buildAdminContext(ctx context.Context, adminEmail string) (context.Context, error) {
accounts, err := c.GetAccounts(ctx)
if err != nil {

View File

@@ -171,6 +171,8 @@ smtp:
email:
# Optional name for sender
sender-name:
# Optional encryption
encryption:
```
| Variable | Description | Default |
@@ -181,6 +183,7 @@ smtp:
| `smtp.password` | SMTP auth password | |
| `smtp.email` | Sender email address | |
| `smtp.sender-name` | Custom name for email sender | |
| `smtp.encryption` | Encryption method (tls, ssl) | |
| `transmail.key` | Zeptomail API key | |
### WebAuthn Passkey Support

View File

@@ -1 +0,0 @@
surprise/

View File

@@ -88,6 +88,8 @@
"useRecoveryKey": "Χρήση κλειδιού ανάκτησης",
"incorrectPasswordTitle": "Λάθος κωδικός πρόσβασης",
"welcomeBack": "Καλωσορίσατε και πάλι!",
"emailAlreadyRegistered": "Το email είναι ήδη καταχωρημένο.",
"emailNotRegistered": "Το email δεν έχει καταχωρηθεί.",
"madeWithLoveAtPrefix": "φτιαγμένη με ❤️ στο ",
"supportDevs": "Εγγραφείτε στο <bold-green>ente</bold-green> για να μας υποστηρίξετε",
"supportDiscount": "Χρησιμοποιήστε τον κωδικό κουπονιού \"AUTH\" για να λάβετε 10% έκπτωση για τον πρώτο χρόνο",
@@ -171,6 +173,7 @@
"invalidQRCode": "Μη έγκυρος κωδικός QR",
"noRecoveryKeyTitle": "Χωρίς κλειδί ανάκτησης;",
"enterEmailHint": "Εισάγετε τη διεύθυνση email σας",
"enterNewEmailHint": "Εισάγετε την διεύθυνση ηλ. ταχυδρομείου σας",
"invalidEmailTitle": "Μη έγκυρη διεύθυνση email",
"invalidEmailMessage": "Παρακαλούμε εισάγετε μια έγκυρη διεύθυνση email.",
"deleteAccount": "Διαγραφή λογαριασμού",
@@ -258,6 +261,10 @@
"areYouSureYouWantToLogout": "Είστε σίγουροι ότι θέλετε να αποσυνδεθείτε;",
"yesLogout": "Ναι, αποσύνδεση",
"exit": "Εξοδος",
"theme": "Θέμα",
"lightTheme": "Φωτεινό",
"darkTheme": "Σκοτεινό",
"systemTheme": "Σύστημα",
"verifyingRecoveryKey": "Επαλήθευση κλειδιού ανάκτησης...",
"recoveryKeyVerified": "Το κλειδί ανάκτησης επαληθεύτηκε",
"recoveryKeySuccessBody": "Τέλεια! Το κλειδί ανάκτησης σας είναι έγκυρο. Σας ευχαριστούμε για την επαλήθευση.\n\nΠαρακαλώ θυμηθείτε να κρατήσετε το κλειδί ανάκτησης σας και σε αντίγραφο ασφαλείας.",
@@ -490,5 +497,24 @@
"appLockNotEnabled": "Το κλείδωμα εφαρμογής δεν είναι ενεργοποιημένο",
"appLockNotEnabledDescription": "Παρακαλώ ενεργοποιήστε το κλείδωμα εφαρμογής μέσω της επιλογής Ασφάλεια > Κλείδωμα εφαρμογής",
"authToViewPasskey": "Παρακαλώ πιστοποιηθείτε για να δείτε το κλειδί πρόσβασης",
"appLockOfflineModeWarning": "Έχετε επιλέξει να προχωρήσετε χωρίς αντίγραφα ασφαλείας. Αν ξεχάσετε τον κωδικό της εφαρμογής, θα κλειδωθείτε από την πρόσβαση στα δεδομένα σας."
"appLockOfflineModeWarning": "Έχετε επιλέξει να προχωρήσετε χωρίς αντίγραφα ασφαλείας. Αν ξεχάσετε τον κωδικό της εφαρμογής, θα κλειδωθείτε από την πρόσβαση στα δεδομένα σας.",
"duplicateCodes": "Διπλότυποι κωδικοί",
"noDuplicates": "✨ Δεν υπάρχουν διπλότυπα",
"youveNoDuplicateCodesThatCanBeCleared": "Δεν υπάρχουν διπλότυπα αρχεία που μπορούν να εκκαθαριστούν",
"deduplicateCodes": "Διπλότυποι κωδικοί",
"deselectAll": "Αποεπιλογή όλων",
"selectAll": "Επιλογή όλων",
"deleteDuplicates": "Διαγραφή διπλότυπων",
"plainHTML": "Απλό HTML",
"dropReviewiOS": "Αφήστε μια κριτική στο App Store",
"dropReviewAndroid": "Αφήστε μια κριτική στο Play Store",
"giveUsAStarOnGithub": "Δώστε μας ένα αστέρι στο Github",
"free5GB": "5GB δωρεάν στο <bold-green>ente</bold-green> Photos",
"freeStorageOffer": "10% έκπτωση στο <bold-green>ente</bold-green> photos",
"freeStorageOfferDescription": "Χρησιμοποιήστε τον κωδικό \"AUTH\" για να λάβετε 10% έκπτωση για τον πρώτο χρόνο",
"advanced": "Για προχωρημένους",
"algorithm": "Αλγόριθμος",
"type": "Τύπος",
"period": "Περίοδος",
"digits": "Ψηφία"
}

View File

@@ -45,7 +45,7 @@
"timeBasedKeyType": "Oparte na czasie (TOTP)",
"counterBasedKeyType": "Oparte na liczniku (HOTP)",
"saveAction": "Zapisz",
"nextTotpTitle": "następny",
"nextTotpTitle": "dalej",
"deleteCodeTitle": "Usunąć kod?",
"deleteCodeMessage": "Czy na pewno chcesz usunąć ten kod? Ta akcja jest nieodwracalna.",
"trashCode": "Przenieść kod do kosza?",

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

View File

@@ -8,7 +8,7 @@
<application
android:label="Ente Locker"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/icon_blue">
<activity
android:name=".MainActivity"
android:exported="true"

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground>
<inset
android:drawable="@drawable/ic_launcher_foreground"
android:inset="16%" />
</foreground>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#1071ff</color>
</resources>

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 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
@@ -209,7 +209,7 @@ SPEC CHECKSUMS:
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
SDWebImage: f29024626962457f3470184232766516dee8dfea
Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854
sentry_flutter: 2df8b0aab7e4aba81261c230cbea31c82a62dd1b
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f

View File

@@ -622,7 +622,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = IconBlue;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@@ -929,7 +929,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = IconBlue;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@@ -956,7 +956,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = IconBlue;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -0,0 +1 @@
{"images":[{"size":"20x20","idiom":"iphone","filename":"IconBlue-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"IconBlue-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"IconBlue-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"IconBlue-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"IconBlue-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"IconBlue-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"IconBlue-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"IconBlue-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"IconBlue-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"IconBlue-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"IconBlue-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"IconBlue-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"IconBlue-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"IconBlue-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"IconBlue-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"IconBlue-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"IconBlue-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"IconBlue-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"IconBlue-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"IconBlue-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"IconBlue-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"IconBlue-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"IconBlue-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"IconBlue-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"IconBlue-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

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

@@ -89,6 +89,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock:
dependency: transitive
description:
@@ -289,7 +305,7 @@ packages:
source: hosted
version: "2.0.1"
expandable:
dependency: transitive
dependency: "direct main"
description:
name: expandable
sha256: "9604d612d4d1146dafa96c6d8eec9c2ff0994658d6d09fed720ab788c7f5afc2"
@@ -445,6 +461,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.0"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
url: "https://pub.dev"
source: hosted
version: "0.14.4"
flutter_lints:
dependency: "direct dev"
description:
@@ -605,8 +629,16 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.1.0"
image:
dependency: transitive
description:
name: image
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
url: "https://pub.dev"
source: hosted
version: "4.5.4"
intl:
dependency: "direct main"
dependency: transitive
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
@@ -949,6 +981,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
url: "https://pub.dev"
source: hosted
version: "7.0.1"
pinput:
dependency: transitive
description:
@@ -985,10 +1025,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 +1105,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 +1222,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 +1363,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:
@@ -1458,6 +1498,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
url: "https://pub.dev"
source: hosted
version: "6.6.1"
xmlstream:
dependency: transitive
description:
@@ -1466,6 +1514,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.7.2 <4.0.0"
flutter: ">=3.24.0"
dart: ">=3.8.0 <4.0.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,10 +59,12 @@ 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
dev_dependencies:
flutter_launcher_icons: ^0.14.3
flutter_lints: ^4.0.0
flutter_test:
sdk: flutter
@@ -85,3 +87,10 @@ flutter:
- family: Montserrat
fonts:
- asset: assets/fonts/Montserrat-Bold.ttf
flutter_icons:
adaptive_icon_foreground: "assets/launcher-icons/icon-foreground.png"
adaptive_icon_background: "#1071ff"
android: "icon_blue"
ios: "IconBlue"
image_path: "assets/launcher-icons/icon-blue.png"

View File

@@ -1000,20 +1000,33 @@ class FilesDB with SqlDbBase {
final batch = localIDsList.sublist(i, endIndex);
final placeholders = List.filled(batch.length, '?').join(',');
final List<String> alreadyUploaded = [];
// find localIDs that are already uploaded
final result = await db.execute('''
SELECT DISTINCT $columnLocalID
FROM $filesTable
WHERE $columnLocalID IN ($placeholders)
AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID != -1)
''');
for (final row in result) {
alreadyUploaded.add(row[columnLocalID] as String);
}
final uploadedPlaceholders =
alreadyUploaded.map((id) => "'$id'").join(',');
final r = await db.execute(
'''
DELETE FROM $filesTable
WHERE $columnLocalID IN ($placeholders)
AND ($columnCollectionID IS NULL OR $columnCollectionID = -1)
WHERE $columnLocalID IN ($uploadedPlaceholders)
AND ($columnUploadedFileID IS NULL OR $columnUploadedFileID = -1)
''',
batch,
);
if (r.isNotEmpty) {
_logger
.fine("Batch ${(i ~/ batchSize) + 1}: Removed ${r.length} files");
_logger.warning(
"Batch ${(i ~/ batchSize) + 1}: Removed duplicate ${r.length} files",
);
totalRemoved += r.length;
}
}

View File

@@ -1,5 +1,6 @@
import 'dart:async';
import "dart:convert";
import "dart:io";
import "dart:math";
import 'package:bip39/bip39.dart' as bip39;
@@ -102,6 +103,7 @@ class UserService {
data: {
"email": email,
"purpose": isChangeEmail ? "change" : purpose ?? "",
"mobile": Platform.isIOS || Platform.isAndroid,
},
);
await dialog.hide();

View File

@@ -124,6 +124,10 @@ class _AppLockState extends State<AppLock> with WidgetsBindingObserver {
GlobalWidgetsLocalizations.delegate,
],
onGenerateRoute: (settings) {
// On Android disabling deep links doesn't work, so this function
// also gets triggered like /?generatedId=xyz&mainKey=abcd
// Related: https://github.com/flutter/flutter/issues/119938
switch (settings.name) {
case '/lock-screen':
return PageRouteBuilder(
@@ -135,7 +139,7 @@ class _AppLockState extends State<AppLock> with WidgetsBindingObserver {
this.widget.builder(settings.arguments),
);
}
return PageRouteBuilder(pageBuilder: (_, __, ___) => this._lockScreen);
return null;
},
);
}

View File

@@ -7,7 +7,6 @@ import "dart:ui";
import "package:exif_reader/exif_reader.dart";
import 'package:flutter/painting.dart' as paint show decodeImageFromList;
import "package:flutter_image_compress/flutter_image_compress.dart";
import 'package:image/image.dart' as img_pkg;
import "package:logging/logging.dart";
import 'package:ml_linalg/linalg.dart';
import "package:photos/models/ml/face/box.dart";
@@ -54,45 +53,7 @@ Future<DecodedImage> decodeImageFromPath(
String imagePath, {
required bool includeRgbaBytes,
required bool includeDartUiImage,
bool inBackground = false,
}) async {
if (inBackground) {
if (includeDartUiImage) {
_logger.severe(
"Decoding image in background with Dart UI Image is not possible!",
);
throw Exception(
"Decoding image in background with Dart UI Image is not possible!",
);
}
if (!includeRgbaBytes) {
_logger.severe(
"Decoding image in background but not returning anything",
);
throw Exception(
"Decoding image in background but not returning anything",
);
}
final image = await img_pkg.decodeImageFile(imagePath);
final imageData = image?.data;
if (imageData == null) {
_logger.severe(
"Failed to decode image from file: $imagePath using image package",
);
throw Exception(
"Failed to decode image from file: $imagePath using image package",
);
}
final bytes = imageData.getBytes(order: img_pkg.ChannelOrder.rgba);
final dimensions = Dimensions(
width: image!.width,
height: image.height,
);
return DecodedImage(
dimensions: dimensions,
rawRgbaBytes: bytes,
);
}
final imageData = await File(imagePath).readAsBytes();
final Map<String, IfdTag> exifData = await readExifFromBytes(imageData);

View File

@@ -1,3 +1,4 @@
- Neeraj: Fix for double enteries for local file
- (prtk) Fix widget initial launch on iOS
- (prtk) Upgrade Flutter version to 3.32.8
- (prtk) Run FFMpeg in an isolate

View File

@@ -1,5 +1,6 @@
import 'dart:async';
import "dart:convert";
import "dart:io";
import "dart:math";
import 'package:bip39/bip39.dart' as bip39;
@@ -88,6 +89,7 @@ class UserService {
data: {
"email": email,
"purpose": isChangeEmail ? "change" : purpose ?? "",
"mobile": Platform.isIOS || Platform.isAndroid,
},
);
await dialog.hide();

1
mobile/packages/strings/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
build/

View File

@@ -1 +0,0 @@
2edd7a931624573eff7b4280d22a7f4e

Some files were not shown because too many files have changed in this diff Show More