Browse Source

Merge remote-tracking branch 'origin' into bg_sync

vishnukvmd 3 years ago
parent
commit
6185579656

+ 1 - 1
android/app/src/main/AndroidManifest.xml

@@ -48,7 +48,7 @@
         <!-- Don't delete the meta-data below.
              This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
         <meta-data android:name="flutterEmbedding" android:value="2" />
-
+        <meta-data android:name="asset_statements" android:resource="@string/asset_statements" />
         <meta-data android:name="io.sentry.dsn" android:value="https://2235e5c99219488ea93da34b9ac1cb68@sentry.ente.io/4" />
     </application>
 

+ 6 - 0
android/app/src/main/res/values/strings.xml

@@ -1,4 +1,10 @@
 <resources>
     <string name="app_name">ente</string>
     <string name="backup">backup</string>
+    <string name="asset_statements" translatable="false">
+        [{
+        \"include\": \"https://web.ente.io/.well-known/assetlinks.json\"
+        }]
+    </string>
+
 </resources>

+ 2 - 0
ios/Runner.xcodeproj/project.pbxproj

@@ -52,6 +52,7 @@
 		97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		A78E51A260432466D4C456A9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
 		BB097BB5EB0EEB41344338D2 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
+		DA8D6672273BBB59007651D4 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
 		F82DAEEB9A7D9FD00E0FFA1E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 /* End PBXFileReference section */
 
@@ -103,6 +104,7 @@
 			isa = PBXGroup;
 			children = (
 				271BD99F270EE74F00D86E6F /* Runner.entitlements */,
+				DA8D6672273BBB59007651D4 /* Runner.entitlements */,
 				97C146FA1CF9000F007C117D /* Main.storyboard */,
 				97C146FD1CF9000F007C117D /* Assets.xcassets */,
 				97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,

+ 1 - 1
ios/Runner/Info.plist

@@ -49,7 +49,7 @@
 	<key>NSFaceIDUsageDescription</key>
 	<string>Please allow ente to lock itself with FaceID or TouchID</string>
 	<key>NSPhotoLibraryUsageDescription</key>
-	<string>Please allow access to your photos so that ente can encrypt and backup them up.</string>
+	<string>Please allow access to your photos so that ente can encrypt and back them up.</string>
 	<key>UIBackgroundModes</key>
 	<array>
 		<string>fetch</string>

+ 4 - 0
ios/Runner/Runner.entitlements

@@ -4,5 +4,9 @@
 <dict>
 	<key>aps-environment</key>
 	<string>development</string>
+	<key>com.apple.developer.associated-domains</key>
+	<array>
+		<string>webcredentials:web.ente.io</string>
+	</array>
 </dict>
 </plist>

+ 11 - 0
lib/core/configuration.dart

@@ -2,6 +2,7 @@ import 'dart:convert';
 import 'dart:io' as io;
 import 'dart:typed_data';
 
+import 'package:bip39/bip39.dart' as bip39;
 import 'package:flutter_secure_storage/flutter_secure_storage.dart';
 import 'package:flutter_sodium/flutter_sodium.dart';
 import 'package:logging/logging.dart';
@@ -29,6 +30,8 @@ import 'package:shared_preferences/shared_preferences.dart';
 import 'package:super_logging/super_logging.dart';
 import 'package:uuid/uuid.dart';
 
+import 'constants.dart';
+
 class Configuration {
   Configuration._privateConstructor();
 
@@ -266,6 +269,14 @@ class Configuration {
   }
 
   Future<void> recover(String recoveryKey) async {
+    // check if user has entered mnemonic code
+    if (recoveryKey.contains(' ')) {
+      if (recoveryKey.split(' ').length != kMnemonicKeyWordCount) {
+        throw AssertionError(
+            'recovery code should have $kMnemonicKeyWordCount words');
+      }
+      recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
+    }
     final attributes = getKeyAttributes();
     Uint8List masterKey;
     try {

+ 4 - 0
lib/core/constants.dart

@@ -20,3 +20,7 @@ const String kLivePhotoToastCounterKey = "show_live_photo_toast";
 
 const kThumbnailDiskLoadDeferDuration = Duration(milliseconds: 40);
 const kThumbnailServerLoadDeferDuration = Duration(milliseconds: 80);
+
+// 256 bit key maps to 24 words
+// https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#Generating_the_mnemonic
+const kMnemonicKeyWordCount = 24;

+ 7 - 0
lib/models/file.dart

@@ -208,6 +208,13 @@ class File {
     }
   }
 
+  String getDisplayName() {
+    if (pubMagicMetadata != null && pubMagicMetadata.editedName != null) {
+      return pubMagicMetadata.editedName;
+    }
+    return title;
+  }
+
   // returns true if the file isn't available in the user's gallery
   bool isRemoteFile() {
     return localID == null && uploadedFileID != null;

+ 5 - 1
lib/models/magic_metadata.dart

@@ -6,6 +6,7 @@ const kVisibilityArchive = 1;
 const kMagicKeyVisibility = 'visibility';
 
 const kPubMagicKeyEditedTime = 'editedTime';
+const kPubMagicKeyEditedName = 'editedName';
 
 class MagicMetadata {
   // 0 -> visible
@@ -36,8 +37,9 @@ class MagicMetadata {
 
 class PubMagicMetadata {
   int editedTime;
+  String editedName;
 
-  PubMagicMetadata({this.editedTime});
+  PubMagicMetadata({this.editedTime, this.editedName});
 
   factory PubMagicMetadata.fromEncodedJson(String encodedJson) =>
       PubMagicMetadata.fromJson(jsonDecode(encodedJson));
@@ -48,6 +50,7 @@ class PubMagicMetadata {
   Map<String, dynamic> toJson() {
     final map = <String, dynamic>{};
     map[kPubMagicKeyEditedTime] = editedTime;
+    map[kPubMagicKeyEditedName] = editedName;
     return map;
   }
 
@@ -55,6 +58,7 @@ class PubMagicMetadata {
     if (map == null) return null;
     return PubMagicMetadata(
       editedTime: map[kPubMagicKeyEditedTime],
+      editedName: map[kPubMagicKeyEditedName],
     );
   }
 }

+ 131 - 122
lib/ui/email_entry_page.dart

@@ -3,6 +3,7 @@ import 'dart:io';
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
 import 'package:flutter/widgets.dart';
 import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 import 'package:flutter_password_strength/flutter_password_strength.dart';
@@ -94,139 +95,147 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
           strengthColors: passwordStrengthColors,
         ),
         Expanded(
-          child: ListView(
-            children: [
-              Padding(padding: EdgeInsets.all(40)),
-              Padding(
-                padding: const EdgeInsets.fromLTRB(32, 0, 32, 0),
-                child: TextFormField(
-                  decoration: InputDecoration(
-                    hintText: 'email',
-                    hintStyle: TextStyle(
-                      color: Colors.white30,
+          child: AutofillGroup(
+            child: ListView(
+              children: [
+                Padding(padding: EdgeInsets.all(40)),
+                Padding(
+                  padding: const EdgeInsets.fromLTRB(32, 0, 32, 0),
+                  child: TextFormField(
+                    autofillHints: [AutofillHints.email],
+                    decoration: InputDecoration(
+                      hintText: 'email',
+                      hintStyle: TextStyle(
+                        color: Colors.white30,
+                      ),
+                      contentPadding: EdgeInsets.all(12),
                     ),
-                    contentPadding: EdgeInsets.all(12),
+                    onChanged: (value) {
+                      setState(() {
+                        _email = value.trim();
+                      });
+                    },
+                    autocorrect: false,
+                    keyboardType: TextInputType.emailAddress,
+                    initialValue: _email,
+                    textInputAction: TextInputAction.next,
                   ),
-                  onChanged: (value) {
-                    setState(() {
-                      _email = value.trim();
-                    });
-                  },
-                  autocorrect: false,
-                  keyboardType: TextInputType.emailAddress,
-                  initialValue: _email,
-                  textInputAction: TextInputAction.next,
                 ),
-              ),
-              Padding(padding: EdgeInsets.all(8)),
-              Padding(
-                padding: const EdgeInsets.fromLTRB(32, 0, 32, 0),
-                child: TextFormField(
-                  keyboardType: TextInputType.text,
-                  controller: _passwordController1,
-                  obscureText: !_password1Visible,
-                  decoration: InputDecoration(
-                    hintText: "password",
-                    hintStyle: TextStyle(
-                      color: Colors.white30,
+                Padding(padding: EdgeInsets.all(8)),
+                Padding(
+                  padding: const EdgeInsets.fromLTRB(32, 0, 32, 0),
+                  child: TextFormField(
+                    keyboardType: TextInputType.text,
+                    controller: _passwordController1,
+                    obscureText: !_password1Visible,
+                    enableSuggestions: true,
+                    autofillHints: [AutofillHints.newPassword],
+                    decoration: InputDecoration(
+                      hintText: "password",
+                      hintStyle: TextStyle(
+                        color: Colors.white30,
+                      ),
+                      contentPadding: EdgeInsets.all(12),
+                      suffixIcon: _password1InFocus
+                          ? IconButton(
+                              icon: Icon(
+                                _password1Visible
+                                    ? Icons.visibility
+                                    : Icons.visibility_off,
+                                color: Colors.white.withOpacity(0.5),
+                                size: 20,
+                              ),
+                              onPressed: () {
+                                setState(() {
+                                  _password1Visible = !_password1Visible;
+                                });
+                              },
+                            )
+                          : null,
                     ),
-                    contentPadding: EdgeInsets.all(12),
-                    suffixIcon: _password1InFocus
-                        ? IconButton(
-                            icon: Icon(
-                              _password1Visible
-                                  ? Icons.visibility
-                                  : Icons.visibility_off,
-                              color: Colors.white.withOpacity(0.5),
-                              size: 20,
-                            ),
-                            onPressed: () {
-                              setState(() {
-                                _password1Visible = !_password1Visible;
-                              });
-                            },
-                          )
-                        : null,
+                    focusNode: _password1FocusNode,
+                    onChanged: (_) {
+                      setState(() {});
+                    },
+                    onEditingComplete: () {
+                      _password1FocusNode.unfocus();
+                      _password2FocusNode.requestFocus();
+                      TextInput.finishAutofillContext();
+                    },
                   ),
-                  focusNode: _password1FocusNode,
-                  onChanged: (_) {
-                    setState(() {});
-                  },
-                  onEditingComplete: () {
-                    _password1FocusNode.unfocus();
-                    _password2FocusNode.requestFocus();
-                  },
                 ),
-              ),
-              Padding(padding: EdgeInsets.all(8)),
-              Padding(
-                padding: const EdgeInsets.fromLTRB(32, 0, 32, 0),
-                child: TextFormField(
-                  keyboardType: TextInputType.text,
-                  controller: _passwordController2,
-                  obscureText: !_password2Visible,
-                  decoration: InputDecoration(
-                    hintText: "confirm password",
-                    hintStyle: TextStyle(
-                      color: Colors.white30,
+                Padding(padding: EdgeInsets.all(8)),
+                Padding(
+                  padding: const EdgeInsets.fromLTRB(32, 0, 32, 0),
+                  child: TextFormField(
+                    keyboardType: TextInputType.visiblePassword,
+                    controller: _passwordController2,
+                    obscureText: !_password2Visible,
+                    autofillHints: [AutofillHints.newPassword],
+                    onEditingComplete: () => TextInput.finishAutofillContext(),
+                    decoration: InputDecoration(
+                      hintText: "confirm password",
+                      hintStyle: TextStyle(
+                        color: Colors.white30,
+                      ),
+                      contentPadding: EdgeInsets.all(12),
+                      suffixIcon: _password2InFocus
+                          ? IconButton(
+                              icon: Icon(
+                                _password2Visible
+                                    ? Icons.visibility
+                                    : Icons.visibility_off,
+                                color: Colors.white.withOpacity(0.5),
+                                size: 20,
+                              ),
+                              onPressed: () {
+                                setState(() {
+                                  _password2Visible = !_password2Visible;
+                                });
+                              },
+                            )
+                          : null,
                     ),
-                    contentPadding: EdgeInsets.all(12),
-                    suffixIcon: _password2InFocus
-                        ? IconButton(
-                            icon: Icon(
-                              _password2Visible
-                                  ? Icons.visibility
-                                  : Icons.visibility_off,
-                              color: Colors.white.withOpacity(0.5),
-                              size: 20,
-                            ),
-                            onPressed: () {
-                              setState(() {
-                                _password2Visible = !_password2Visible;
-                              });
-                            },
-                          )
-                        : null,
+                    focusNode: _password2FocusNode,
                   ),
-                  focusNode: _password2FocusNode,
                 ),
-              ),
-              Padding(
-                padding: EdgeInsets.all(20),
-              ),
-              _getAgreement(),
-              Padding(padding: EdgeInsets.all(20)),
-              Container(
-                width: double.infinity,
-                height: 64,
-                padding: const EdgeInsets.fromLTRB(80, 0, 80, 0),
-                child: button(
-                  AppLocalizations.of(context).sign_up,
-                  onPressed: _isFormValid()
-                      ? () {
-                          if (!isValidEmail(_email)) {
-                            showErrorDialog(context, "invalid email",
-                                "please enter a valid email address.");
-                          } else if (_passwordController1.text !=
-                              _passwordController2.text) {
-                            showErrorDialog(context, "uhm...",
-                                "the passwords you entered don't match");
-                          } else if (_passwordStrength <
-                              kPasswordStrengthThreshold) {
-                            showErrorDialog(context, "weak password",
-                                "the password you have chosen is too simple, please choose another one");
-                          } else {
-                            _config
-                                .setVolatilePassword(_passwordController1.text);
-                            _config.setEmail(_email);
-                            UserService.instance.getOtt(context, _email);
+                Padding(
+                  padding: EdgeInsets.all(20),
+                ),
+                _getAgreement(),
+                Padding(padding: EdgeInsets.all(20)),
+                Container(
+                  width: double.infinity,
+                  height: 64,
+                  padding: const EdgeInsets.fromLTRB(80, 0, 80, 0),
+                  child: button(
+                    AppLocalizations.of(context).sign_up,
+                    onPressed: _isFormValid()
+                        ? () {
+                            if (!isValidEmail(_email)) {
+                              showErrorDialog(context, "invalid email",
+                                  "please enter a valid email address.");
+                            } else if (_passwordController1.text !=
+                                _passwordController2.text) {
+                              showErrorDialog(context, "uhm...",
+                                  "the passwords you entered don't match");
+                            } else if (_passwordStrength <
+                                kPasswordStrengthThreshold) {
+                              showErrorDialog(context, "weak password",
+                                  "the password you have chosen is too simple, please choose another one");
+                            } else {
+                              _config.setVolatilePassword(
+                                  _passwordController1.text);
+                              _config.setEmail(_email);
+                              UserService.instance.getOtt(context, _email);
+                            }
                           }
-                        }
-                      : null,
-                  fontSize: 18,
+                        : null,
+                    fontSize: 18,
+                  ),
                 ),
-              ),
-            ],
+              ],
+            ),
           ),
         ),
       ],

+ 38 - 9
lib/ui/file_info_dialog.dart

@@ -1,6 +1,7 @@
 import 'package:exif/exif.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
+import 'package:photos/core/configuration.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/models/file_type.dart';
 import 'package:photos/services/collections_service.dart';
@@ -8,6 +9,7 @@ import 'package:photos/ui/exif_info_dialog.dart';
 import 'package:photos/utils/date_time_util.dart';
 import 'package:photos/utils/exif_util.dart';
 import 'package:photos/utils/file_util.dart';
+import 'package:photos/utils/magic_util.dart';
 import 'package:photos/utils/toast_util.dart';
 
 class FileInfoWidget extends StatefulWidget {
@@ -42,6 +44,7 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
 
   @override
   Widget build(BuildContext context) {
+    final file = widget.file;
     var items = <Widget>[
       Row(
         children: [
@@ -52,7 +55,7 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
           Padding(padding: EdgeInsets.all(4)),
           Text(
             getFormattedTime(
-              DateTime.fromMicrosecondsSinceEpoch(widget.file.creationTime),
+              DateTime.fromMicrosecondsSinceEpoch(file.creationTime),
             ),
             style: TextStyle(
               color: Colors.white.withOpacity(0.85),
@@ -69,9 +72,9 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
           ),
           Padding(padding: EdgeInsets.all(4)),
           Text(
-            widget.file.deviceFolder ??
+            file.deviceFolder ??
                 CollectionsService.instance
-                    .getCollectionByID(widget.file.collectionID)
+                    .getCollectionByID(file.collectionID)
                     .name,
             style: TextStyle(
               color: Colors.white.withOpacity(0.85),
@@ -96,7 +99,7 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
         Padding(padding: EdgeInsets.all(6)),
       ],
     );
-    if (widget.file.localID != null && !_isImage) {
+    if (file.localID != null && !_isImage) {
       items.addAll(
         [
           Row(
@@ -107,7 +110,7 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
               ),
               Padding(padding: EdgeInsets.all(4)),
               FutureBuilder(
-                future: widget.file.getAsset(),
+                future: file.getAsset(),
                 builder: (context, snapshot) {
                   if (snapshot.hasData) {
                     return Text(
@@ -137,7 +140,7 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
     if (_isImage && _exif != null) {
       items.add(_getExifWidgets(_exif));
     }
-    if (widget.file.uploadedFileID != null && widget.file.updationTime != null) {
+    if (file.uploadedFileID != null && file.updationTime != null) {
       items.addAll(
         [
           Row(
@@ -148,8 +151,8 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
               ),
               Padding(padding: EdgeInsets.all(4)),
               Text(
-                getFormattedTime(DateTime.fromMicrosecondsSinceEpoch(
-                    widget.file.updationTime)),
+                getFormattedTime(
+                    DateTime.fromMicrosecondsSinceEpoch(file.updationTime)),
                 style: TextStyle(
                   color: Colors.white.withOpacity(0.85),
                 ),
@@ -169,8 +172,34 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
         children: _getActions(),
       ),
     );
+
+    Widget titleContent;
+    if (file.uploadedFileID == null ||
+        file.ownerID != Configuration.instance.getUserID()) {
+      titleContent = Text(file.getDisplayName());
+    } else {
+      titleContent = InkWell(
+        child: Row(
+          mainAxisAlignment: MainAxisAlignment.spaceBetween,
+          children: [
+            Text(
+              file.getDisplayName(),
+            ),
+            Icon(
+              Icons.edit,
+              color: Colors.white.withOpacity(0.85),
+            ),
+          ],
+        ),
+        onTap: () async {
+          await editFilename(context, file);
+          setState(() {});
+        },
+      );
+    }
+
     return AlertDialog(
-      title: Text(widget.file.title),
+      title: titleContent,
       content: SingleChildScrollView(
         child: ListBody(
           children: items,

+ 4 - 4
lib/ui/gallery_app_bar_widget.dart

@@ -12,8 +12,8 @@ import 'package:photos/models/collection.dart';
 import 'package:photos/models/magic_metadata.dart';
 import 'package:photos/models/selected_files.dart';
 import 'package:photos/services/collections_service.dart';
-import 'package:photos/ui/change_collection_name_dialog.dart';
 import 'package:photos/ui/create_collection_page.dart';
+import 'package:photos/ui/rename_dialog.dart';
 import 'package:photos/ui/share_collection_widget.dart';
 import 'package:photos/utils/delete_file_util.dart';
 import 'package:photos/utils/dialog_util.dart';
@@ -122,15 +122,15 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
     if (widget.type != GalleryAppBarType.owned_collection) {
       return;
     }
-    final result = await showDialog(
+    final result = await showDialog<String>(
       context: context,
       builder: (BuildContext context) {
-        return ChangeCollectionNameDialog(name: _appBarTitle);
+        return RenameDialog(_appBarTitle, 'album');
       },
       barrierColor: Colors.black.withOpacity(0.85),
     );
     // indicates user cancelled the rename request
-    if (result == null) {
+    if (result == null || result.trim() == _appBarTitle.trim()) {
       return;
     }
 

+ 1 - 0
lib/ui/login_page.dart

@@ -58,6 +58,7 @@ class _LoginPageState extends State<LoginPage> {
         Padding(
           padding: const EdgeInsets.fromLTRB(40, 40, 40, 0),
           child: TextFormField(
+            autofillHints: [AutofillHints.email],
             decoration: InputDecoration(
               hintText: 'email',
               hintStyle: TextStyle(

+ 127 - 106
lib/ui/password_entry_page.dart

@@ -92,6 +92,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
     if (_password != null) {
       return Container();
     }
+    final String email = Configuration.instance.getEmail() ?? '';
     return Column(
       children: [
         FlutterPasswordStrength(
@@ -105,118 +106,137 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
         SingleChildScrollView(
           child: Container(
             padding: EdgeInsets.fromLTRB(16, 36, 16, 16),
-            child: Column(
-              children: [
-                Padding(padding: EdgeInsets.all(12)),
-                Text(
-                  "enter a" +
-                      (widget.mode != PasswordEntryMode.set ? " new " : " ") +
-                      "password we can use to encrypt your data",
-                  textAlign: TextAlign.center,
-                  style: TextStyle(
-                    height: 1.3,
+            child: AutofillGroup(
+              child: Column(
+                children: [
+                  Padding(padding: EdgeInsets.all(12)),
+                  Text(
+                    "enter a" +
+                        (widget.mode != PasswordEntryMode.set ? " new " : " ") +
+                        "password we can use to encrypt your data",
+                    textAlign: TextAlign.center,
+                    style: TextStyle(
+                      height: 1.3,
+                    ),
                   ),
-                ),
-                Padding(padding: EdgeInsets.all(8)),
-                Text("we don't store this password, so if you forget, "),
-                Text.rich(
-                  TextSpan(
-                      text: "we cannot decrypt your data",
-                      style: TextStyle(
-                        decoration: TextDecoration.underline,
-                        fontWeight: FontWeight.bold,
-                      )),
-                  style: TextStyle(
-                    height: 1.3,
+                  Padding(padding: EdgeInsets.all(8)),
+                  Text("we don't store this password, so if you forget, "),
+                  Text.rich(
+                    TextSpan(
+                        text: "we cannot decrypt your data",
+                        style: TextStyle(
+                          decoration: TextDecoration.underline,
+                          fontWeight: FontWeight.bold,
+                        )),
+                    style: TextStyle(
+                      height: 1.3,
+                    ),
+                    textAlign: TextAlign.center,
                   ),
-                  textAlign: TextAlign.center,
-                ),
-                Padding(padding: EdgeInsets.all(12)),
-                Padding(
-                  padding: const EdgeInsets.fromLTRB(32, 0, 32, 0),
-                  child: TextFormField(
-                    decoration: InputDecoration(
-                      hintText: "password",
-                      contentPadding: EdgeInsets.all(20),
-                      suffixIcon: _password1InFocus
-                          ? IconButton(
-                              icon: Icon(
-                                _password1Visible
-                                    ? Icons.visibility
-                                    : Icons.visibility_off,
-                                color: Colors.white.withOpacity(0.5),
-                                size: 20,
-                              ),
-                              onPressed: () {
-                                setState(() {
-                                  _password1Visible = !_password1Visible;
-                                });
-                              },
-                            )
-                          : null,
+                  Padding(padding: EdgeInsets.all(12)),
+                  // hidden textForm for suggesting auto-fill service for saving
+                  // password
+                  SizedBox(
+                    width: 0,
+                    height: 0,
+                    child: TextFormField(
+                      autofillHints: [
+                        AutofillHints.email,
+                      ],
+                      autocorrect: false,
+                      keyboardType: TextInputType.emailAddress,
+                      initialValue: email,
+                      textInputAction: TextInputAction.next,
                     ),
-                    obscureText: !_password1Visible,
-                    controller: _passwordController1,
-                    autofocus: false,
-                    autocorrect: false,
-                    keyboardType: TextInputType.visiblePassword,
-                    onChanged: (_) {
-                      setState(() {});
-                    },
-                    textInputAction: TextInputAction.next,
-                    focusNode: _password1FocusNode,
                   ),
-                ),
-                Padding(padding: EdgeInsets.all(8)),
-                Padding(
-                  padding: const EdgeInsets.fromLTRB(32, 0, 32, 0),
-                  child: TextFormField(
-                    decoration: InputDecoration(
-                      hintText: "password again",
-                      contentPadding: EdgeInsets.all(20),
-                      suffixIcon: _password2InFocus
-                          ? IconButton(
-                              icon: Icon(
-                                _password2Visible
-                                    ? Icons.visibility
-                                    : Icons.visibility_off,
-                                color: Colors.white.withOpacity(0.5),
-                                size: 20,
-                              ),
-                              onPressed: () {
-                                setState(() {
-                                  _password2Visible = !_password2Visible;
-                                });
-                              },
-                            )
-                          : null,
+                  Padding(
+                    padding: const EdgeInsets.fromLTRB(32, 0, 32, 0),
+                    child: TextFormField(
+                      autofillHints: [AutofillHints.newPassword],
+                      decoration: InputDecoration(
+                        hintText: "password",
+                        contentPadding: EdgeInsets.all(20),
+                        suffixIcon: _password1InFocus
+                            ? IconButton(
+                                icon: Icon(
+                                  _password1Visible
+                                      ? Icons.visibility
+                                      : Icons.visibility_off,
+                                  color: Colors.white.withOpacity(0.5),
+                                  size: 20,
+                                ),
+                                onPressed: () {
+                                  setState(() {
+                                    _password1Visible = !_password1Visible;
+                                  });
+                                },
+                              )
+                            : null,
+                      ),
+                      obscureText: !_password1Visible,
+                      controller: _passwordController1,
+                      autofocus: false,
+                      autocorrect: false,
+                      keyboardType: TextInputType.visiblePassword,
+                      onChanged: (_) {
+                        setState(() {});
+                      },
+                      textInputAction: TextInputAction.next,
+                      focusNode: _password1FocusNode,
                     ),
-                    obscureText: !_password2Visible,
-                    controller: _passwordController2,
-                    autofocus: false,
-                    autocorrect: false,
-                    keyboardType: TextInputType.visiblePassword,
-                    onChanged: (_) {
-                      setState(() {});
-                    },
-                    focusNode: _password2FocusNode,
                   ),
-                ),
-                Padding(padding: EdgeInsets.all(20)),
-                Container(
-                  width: double.infinity,
-                  height: 64,
-                  padding: EdgeInsets.fromLTRB(40, 0, 40, 0),
-                  child: button(
-                    buttonText,
-                    fontSize: 18,
-                    onPressed: _passwordController1.text.isNotEmpty &&
-                            _passwordController2.text.isNotEmpty
-                        ? _onButtonPress
-                        : null,
+                  Padding(padding: EdgeInsets.all(8)),
+                  Padding(
+                    padding: const EdgeInsets.fromLTRB(32, 0, 32, 0),
+                    child: TextFormField(
+                      autofillHints: [AutofillHints.newPassword],
+                      decoration: InputDecoration(
+                        hintText: "password again",
+                        contentPadding: EdgeInsets.all(20),
+                        suffixIcon: _password2InFocus
+                            ? IconButton(
+                                icon: Icon(
+                                  _password2Visible
+                                      ? Icons.visibility
+                                      : Icons.visibility_off,
+                                  color: Colors.white.withOpacity(0.5),
+                                  size: 20,
+                                ),
+                                onPressed: () {
+                                  setState(() {
+                                    _password2Visible = !_password2Visible;
+                                  });
+                                },
+                              )
+                            : null,
+                      ),
+                      obscureText: !_password2Visible,
+                      controller: _passwordController2,
+                      autofocus: false,
+                      autocorrect: false,
+                      keyboardType: TextInputType.visiblePassword,
+                      onChanged: (_) {
+                        setState(() {});
+                      },
+                      focusNode: _password2FocusNode,
+                    ),
                   ),
-                ),
-              ],
+                  Padding(padding: EdgeInsets.all(20)),
+                  Container(
+                    width: double.infinity,
+                    height: 64,
+                    padding: EdgeInsets.fromLTRB(40, 0, 40, 0),
+                    child: button(
+                      buttonText,
+                      fontSize: 18,
+                      onPressed: _passwordController1.text.isNotEmpty &&
+                              _passwordController2.text.isNotEmpty
+                          ? _onButtonPress
+                          : null,
+                    ),
+                  ),
+                ],
+              ),
             ),
           ),
         ),
@@ -227,7 +247,8 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
             Navigator.of(context).push(
               MaterialPageRoute(
                 builder: (BuildContext context) {
-                  return WebPage("how it works", "https://ente.io/architecture");
+                  return WebPage(
+                      "how it works", "https://ente.io/architecture");
                 },
               ),
             );

+ 1 - 0
lib/ui/password_reentry_page.dart

@@ -53,6 +53,7 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
         Padding(
           padding: const EdgeInsets.fromLTRB(60, 0, 60, 0),
           child: TextFormField(
+            autofillHints: [AutofillHints.password],
             decoration: InputDecoration(
               hintText: "enter your password",
               contentPadding: EdgeInsets.all(20),

+ 7 - 1
lib/ui/recovery_key_dialog.dart

@@ -1,10 +1,12 @@
 import 'dart:io' as io;
 import 'dart:ui';
 
+import 'package:bip39/bip39.dart' as bip39;
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/widgets.dart';
 import 'package:photos/core/configuration.dart';
+import 'package:photos/core/constants.dart';
 import 'package:photos/utils/toast_util.dart';
 import 'package:share_plus/share_plus.dart';
 
@@ -39,7 +41,11 @@ class _RecoveryKeyDialogState extends State<RecoveryKeyDialog> {
 
   @override
   Widget build(BuildContext context) {
-    final recoveryKey = widget.recoveryKey;
+    final String recoveryKey = bip39.entropyToMnemonic(widget.recoveryKey);
+    if (recoveryKey.split(' ').length != kMnemonicKeyWordCount) {
+      throw AssertionError(
+          'recovery code should have $kMnemonicKeyWordCount words');
+    }
     List<Widget> actions = [];
     if (!_hasTriedToSave) {
       actions.add(TextButton(

+ 7 - 2
lib/ui/recovery_page.dart

@@ -86,8 +86,13 @@ class _RecoveryPageState extends State<RecoveryPage> {
                         );
                       } catch (e) {
                         await dialog.hide();
-                        showErrorDialog(context, "incorrect recovery key",
-                            "the recovery key you entered is incorrect");
+                        String errMessage =
+                            'the recovery key you entered is incorrect';
+                        if (e is AssertionError) {
+                          errMessage = '$errMessage : ${e.message}';
+                        }
+                        showErrorDialog(
+                            context, "incorrect recovery key", errMessage);
                       }
                     }
                   : null,

+ 17 - 20
lib/ui/change_collection_name_dialog.dart → lib/ui/rename_dialog.dart

@@ -2,24 +2,25 @@ import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:photos/utils/dialog_util.dart';
 
-class ChangeCollectionNameDialog extends StatefulWidget {
+class RenameDialog extends StatefulWidget {
   final String name;
+  final String type;
+  final int maxLength;
 
-  const ChangeCollectionNameDialog({Key key, this.name}) : super(key: key);
+  const RenameDialog(this.name, this.type, {Key key, this.maxLength = 100})
+      : super(key: key);
 
   @override
-  _ChangeCollectionNameDialogState createState() =>
-      _ChangeCollectionNameDialogState();
+  _RenameDialogState createState() => _RenameDialogState();
 }
 
-class _ChangeCollectionNameDialogState
-    extends State<ChangeCollectionNameDialog> {
-  String _newCollectionName;
+class _RenameDialogState extends State<RenameDialog> {
+  String _newName;
 
   @override
   void initState() {
     super.initState();
-    _newCollectionName = widget.name;
+    _newName = widget.name;
   }
 
   @override
@@ -33,7 +34,7 @@ class _ChangeCollectionNameDialogState
           children: [
             TextFormField(
               decoration: InputDecoration(
-                hintText: 'album name',
+                hintText: '${widget.type} name',
                 hintStyle: TextStyle(
                   color: Colors.white30,
                 ),
@@ -41,12 +42,12 @@ class _ChangeCollectionNameDialogState
               ),
               onChanged: (value) {
                 setState(() {
-                  _newCollectionName = value;
+                  _newName = value;
                 });
               },
               autocorrect: false,
               keyboardType: TextInputType.text,
-              initialValue: _newCollectionName,
+              initialValue: _newName,
               autofocus: true,
             ),
           ],
@@ -72,21 +73,17 @@ class _ChangeCollectionNameDialogState
             ),
           ),
           onPressed: () {
-            if (_newCollectionName.trim().isEmpty) {
+            if (_newName.trim().isEmpty) {
               showErrorDialog(
-                  context, "empty name", "album name cannot be empty");
+                  context, "empty name", "${widget.type} name cannot be empty");
               return;
             }
-            if (_newCollectionName.trim().length > 100) {
+            if (_newName.trim().length > widget.maxLength) {
               showErrorDialog(context, "name too large",
-                  "album name should be less than 100 characters");
+                  "${widget.type} name should be less than ${widget.maxLength} characters");
               return;
             }
-            if (_newCollectionName.trim() == widget.name.trim()) {
-              Navigator.of(context).pop(null);
-              return;
-            }
-            Navigator.of(context).pop(_newCollectionName.trim());
+            Navigator.of(context).pop(_newName.trim());
           },
         ),
       ],

+ 32 - 0
lib/utils/magic_util.dart

@@ -1,10 +1,13 @@
+import 'package:flutter/material.dart';
 import 'package:flutter/widgets.dart';
 import 'package:logging/logging.dart';
+import 'package:path/path.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/events/force_reload_home_gallery_event.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/models/magic_metadata.dart';
 import 'package:photos/services/file_magic_service.dart';
+import 'package:photos/ui/rename_dialog.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/toast_util.dart';
 
@@ -41,6 +44,35 @@ Future<bool> editTime(
   }
 }
 
+Future<bool> editFilename(
+  BuildContext context,
+  File file,
+) async {
+  try {
+    final fileName = file.getDisplayName();
+    final nameWithoutExt = basenameWithoutExtension(fileName);
+    final extName = extension(fileName);
+    var result = await showDialog<String>(
+      context: context,
+      builder: (BuildContext context) {
+        return RenameDialog(nameWithoutExt, 'file', maxLength: 50);
+      },
+      barrierColor: Colors.black.withOpacity(0.85),
+    );
+
+    if (result == null || result.trim() == nameWithoutExt.trim()) {
+      return true;
+    }
+    result = result + extName;
+    await _updatePublicMetadata(
+        context, List.of([file]), kPubMagicKeyEditedName, result);
+    return true;
+  } catch (e, s) {
+    showToast('something went wrong');
+    return false;
+  }
+}
+
 Future<void> _updatePublicMetadata(
     BuildContext context, List<File> files, String key, dynamic value) async {
   if (files.isEmpty) {

+ 21 - 0
pubspec.lock

@@ -50,6 +50,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "0.0.73"
+  bip39:
+    dependency: "direct main"
+    description:
+      name: bip39
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.6"
   boolean_selector:
     dependency: transitive
     description:
@@ -534,6 +541,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "5.0.5"
+  hex:
+    dependency: transitive
+    description:
+      name: hex
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.2.0"
   html:
     dependency: transitive
     description:
@@ -869,6 +883,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "2.0.0"
+  pointycastle:
+    dependency: transitive
+    description:
+      name: pointycastle
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.4.0"
   process:
     dependency: transitive
     description:

+ 2 - 1
pubspec.yaml

@@ -11,7 +11,7 @@ description: ente photos application
 # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
 # Read more about iOS versioning at
 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
-version: 0.3.46+256
+version: 0.3.48+258
 
 environment:
   sdk: ">=2.10.0 <3.0.0"
@@ -21,6 +21,7 @@ dependencies:
   animate_do: ^2.0.0
   archive: ^3.1.2
   background_fetch: ^1.0.1
+  bip39: ^1.0.6
   cached_network_image: ^3.0.0
   chewie:
     path: thirdparty/chewie