Merge remote-tracking branch 'origin' into bg_sync
This commit is contained in:
commit
6185579656
21 changed files with 431 additions and 270 deletions
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -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,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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
},
|
||||
)
|
||||
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,
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
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,
|
||||
),
|
||||
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");
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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());
|
||||
},
|
||||
),
|
||||
],
|
|
@ -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
pubspec.lock
21
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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue