Merge branch 'master' into grid
This commit is contained in:
commit
7133930af4
21 changed files with 643 additions and 70 deletions
|
@ -102,6 +102,9 @@ PODS:
|
||||||
- Flutter
|
- Flutter
|
||||||
- in_app_purchase_storekit (0.0.1):
|
- in_app_purchase_storekit (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- keyboard_visibility (0.5.0):
|
||||||
|
- Flutter
|
||||||
|
- Reachability
|
||||||
- libwebp (1.2.3):
|
- libwebp (1.2.3):
|
||||||
- libwebp/demux (= 1.2.3)
|
- libwebp/demux (= 1.2.3)
|
||||||
- libwebp/mux (= 1.2.3)
|
- libwebp/mux (= 1.2.3)
|
||||||
|
@ -193,6 +196,7 @@ DEPENDENCIES:
|
||||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||||
- image_editor (from `.symlinks/plugins/image_editor/ios`)
|
- image_editor (from `.symlinks/plugins/image_editor/ios`)
|
||||||
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/ios`)
|
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/ios`)
|
||||||
|
- keyboard_visibility (from `.symlinks/plugins/keyboard_visibility/ios`)
|
||||||
- local_auth (from `.symlinks/plugins/local_auth/ios`)
|
- local_auth (from `.symlinks/plugins/local_auth/ios`)
|
||||||
- media_extension (from `.symlinks/plugins/media_extension/ios`)
|
- media_extension (from `.symlinks/plugins/media_extension/ios`)
|
||||||
- motionphoto (from `.symlinks/plugins/motionphoto/ios`)
|
- motionphoto (from `.symlinks/plugins/motionphoto/ios`)
|
||||||
|
@ -271,6 +275,8 @@ EXTERNAL SOURCES:
|
||||||
:path: ".symlinks/plugins/image_editor/ios"
|
:path: ".symlinks/plugins/image_editor/ios"
|
||||||
in_app_purchase_storekit:
|
in_app_purchase_storekit:
|
||||||
:path: ".symlinks/plugins/in_app_purchase_storekit/ios"
|
:path: ".symlinks/plugins/in_app_purchase_storekit/ios"
|
||||||
|
keyboard_visibility:
|
||||||
|
:path: ".symlinks/plugins/keyboard_visibility/ios"
|
||||||
local_auth:
|
local_auth:
|
||||||
:path: ".symlinks/plugins/local_auth/ios"
|
:path: ".symlinks/plugins/local_auth/ios"
|
||||||
media_extension:
|
media_extension:
|
||||||
|
@ -336,6 +342,7 @@ SPEC CHECKSUMS:
|
||||||
GoogleUtilities: 1d20a6ad97ef46f67bbdec158ce00563a671ebb7
|
GoogleUtilities: 1d20a6ad97ef46f67bbdec158ce00563a671ebb7
|
||||||
image_editor: eab82a302a6623a866da5145b7c4c0ee8a4ffbb4
|
image_editor: eab82a302a6623a866da5145b7c4c0ee8a4ffbb4
|
||||||
in_app_purchase_storekit: d7fcf4646136ec258e237872755da8ea6c1b6096
|
in_app_purchase_storekit: d7fcf4646136ec258e237872755da8ea6c1b6096
|
||||||
|
keyboard_visibility: 96a24de806fe6823c3ad956c01ba2ec6d056616f
|
||||||
libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c
|
libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c
|
||||||
local_auth: 1740f55d7af0a2e2a8684ce225fe79d8931e808c
|
local_auth: 1740f55d7af0a2e2a8684ce225fe79d8931e808c
|
||||||
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
|
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
|
||||||
|
|
|
@ -287,6 +287,7 @@
|
||||||
"${BUILT_PRODUCTS_DIR}/fluttertoast/fluttertoast.framework",
|
"${BUILT_PRODUCTS_DIR}/fluttertoast/fluttertoast.framework",
|
||||||
"${BUILT_PRODUCTS_DIR}/image_editor/image_editor.framework",
|
"${BUILT_PRODUCTS_DIR}/image_editor/image_editor.framework",
|
||||||
"${BUILT_PRODUCTS_DIR}/in_app_purchase_storekit/in_app_purchase_storekit.framework",
|
"${BUILT_PRODUCTS_DIR}/in_app_purchase_storekit/in_app_purchase_storekit.framework",
|
||||||
|
"${BUILT_PRODUCTS_DIR}/keyboard_visibility/keyboard_visibility.framework",
|
||||||
"${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework",
|
"${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework",
|
||||||
"${BUILT_PRODUCTS_DIR}/local_auth/local_auth.framework",
|
"${BUILT_PRODUCTS_DIR}/local_auth/local_auth.framework",
|
||||||
"${BUILT_PRODUCTS_DIR}/media_extension/media_extension.framework",
|
"${BUILT_PRODUCTS_DIR}/media_extension/media_extension.framework",
|
||||||
|
@ -341,6 +342,7 @@
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/fluttertoast.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/fluttertoast.framework",
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_editor.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_editor.framework",
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/in_app_purchase_storekit.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/in_app_purchase_storekit.framework",
|
||||||
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/keyboard_visibility.framework",
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework",
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth.framework",
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_extension.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_extension.framework",
|
||||||
|
|
|
@ -324,6 +324,32 @@ class Configuration {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> verifyPassword(String password) async {
|
||||||
|
final KeyAttributes attributes = getKeyAttributes()!;
|
||||||
|
_logger.info('state validation done');
|
||||||
|
final kek = await CryptoUtil.deriveKey(
|
||||||
|
utf8.encode(password) as Uint8List,
|
||||||
|
Sodium.base642bin(attributes.kekSalt),
|
||||||
|
attributes.memLimit,
|
||||||
|
attributes.opsLimit,
|
||||||
|
).onError((e, s) {
|
||||||
|
_logger.severe('deriveKey failed', e, s);
|
||||||
|
throw KeyDerivationError();
|
||||||
|
});
|
||||||
|
|
||||||
|
_logger.info('user-key done');
|
||||||
|
try {
|
||||||
|
final Uint8List key = CryptoUtil.decryptSync(
|
||||||
|
Sodium.base642bin(attributes.encryptedKey),
|
||||||
|
kek,
|
||||||
|
Sodium.base642bin(attributes.keyDecryptionNonce),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
_logger.severe('master-key failed, incorrect password?', e);
|
||||||
|
throw Exception("Incorrect password");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<KeyAttributes> createNewRecoveryKey() async {
|
Future<KeyAttributes> createNewRecoveryKey() async {
|
||||||
final masterKey = getKey()!;
|
final masterKey = getKey()!;
|
||||||
final existingAttributes = getKeyAttributes();
|
final existingAttributes = getKeyAttributes();
|
||||||
|
|
|
@ -8,7 +8,6 @@ import 'package:flutter/material.dart';
|
||||||
// import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
|
// import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
|
||||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:photos/core/configuration.dart';
|
|
||||||
import 'package:photos/core/errors.dart';
|
import 'package:photos/core/errors.dart';
|
||||||
import 'package:photos/core/network.dart';
|
import 'package:photos/core/network.dart';
|
||||||
import 'package:photos/models/billing_plan.dart';
|
import 'package:photos/models/billing_plan.dart';
|
||||||
|
@ -35,9 +34,7 @@ class BillingService {
|
||||||
static final BillingService instance = BillingService._privateConstructor();
|
static final BillingService instance = BillingService._privateConstructor();
|
||||||
|
|
||||||
final _logger = Logger("BillingService");
|
final _logger = Logger("BillingService");
|
||||||
final _dio = Network.instance.getDio();
|
|
||||||
final _enteDio = Network.instance.enteDio;
|
final _enteDio = Network.instance.enteDio;
|
||||||
final _config = Configuration.instance;
|
|
||||||
|
|
||||||
bool _isOnSubscriptionPage = false;
|
bool _isOnSubscriptionPage = false;
|
||||||
|
|
||||||
|
@ -74,23 +71,16 @@ class BillingService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<BillingPlans> getBillingPlans() {
|
Future<BillingPlans> getBillingPlans() {
|
||||||
_future ??= (_config.isLoggedIn()
|
_future ??= _fetchBillingPlans().then((response) {
|
||||||
? _fetchPublicBillingPlans()
|
|
||||||
: _fetchPrivateBillingPlans())
|
|
||||||
.then((response) {
|
|
||||||
return BillingPlans.fromMap(response.data);
|
return BillingPlans.fromMap(response.data);
|
||||||
});
|
});
|
||||||
return _future;
|
return _future;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response<dynamic>> _fetchPrivateBillingPlans() {
|
Future<Response<dynamic>> _fetchBillingPlans() {
|
||||||
return _enteDio.get("/billing/user-plans/");
|
return _enteDio.get("/billing/user-plans/");
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response<dynamic>> _fetchPublicBillingPlans() {
|
|
||||||
return _dio.get(_config.getHttpEndpoint() + "/billing/plans/v2");
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Subscription> verifySubscription(
|
Future<Subscription> verifySubscription(
|
||||||
final productID,
|
final productID,
|
||||||
final verificationData, {
|
final verificationData, {
|
||||||
|
|
|
@ -38,7 +38,8 @@ class UpdateService {
|
||||||
return _prefs.setInt(changeLogVersionKey, currentChangeLogVersion);
|
return _prefs.setInt(changeLogVersionKey, currentChangeLogVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> resetChangeLog() {
|
Future<bool> resetChangeLog() async {
|
||||||
|
await _prefs.remove("userNotify.passwordReminderFlag");
|
||||||
return _prefs.remove(changeLogVersionKey);
|
return _prefs.remove(changeLogVersionKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,8 @@ class UserRemoteFlagService {
|
||||||
UserRemoteFlagService._privateConstructor();
|
UserRemoteFlagService._privateConstructor();
|
||||||
|
|
||||||
static const String recoveryVerificationFlag = "recoveryKeyVerified";
|
static const String recoveryVerificationFlag = "recoveryKeyVerified";
|
||||||
|
static const String _passwordReminderFlag = "userNotify"
|
||||||
|
".passwordReminderFlag";
|
||||||
static const String needRecoveryKeyVerification =
|
static const String needRecoveryKeyVerification =
|
||||||
"needRecoveryKeyVerification";
|
"needRecoveryKeyVerification";
|
||||||
|
|
||||||
|
@ -27,6 +29,20 @@ class UserRemoteFlagService {
|
||||||
_prefs = await SharedPreferences.getInstance();
|
_prefs = await SharedPreferences.getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool showPasswordReminder() {
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !_prefs.containsKey(_passwordReminderFlag);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> stopPasswordReminder() async {
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
return Future.value(true);
|
||||||
|
}
|
||||||
|
return _prefs.setBool(_passwordReminderFlag, true);
|
||||||
|
}
|
||||||
|
|
||||||
bool shouldShowRecoveryVerification() {
|
bool shouldShowRecoveryVerification() {
|
||||||
if (!_prefs.containsKey(needRecoveryKeyVerification)) {
|
if (!_prefs.containsKey(needRecoveryKeyVerification)) {
|
||||||
// fetch the status from remote
|
// fetch the status from remote
|
||||||
|
@ -46,14 +62,13 @@ class UserRemoteFlagService {
|
||||||
// recovery key in the past or not. This helps in avoid showing the same
|
// recovery key in the past or not. This helps in avoid showing the same
|
||||||
// prompt to the user on re-install or signing into a different device
|
// prompt to the user on re-install or signing into a different device
|
||||||
Future<void> markRecoveryVerificationAsDone() async {
|
Future<void> markRecoveryVerificationAsDone() async {
|
||||||
await _updateKeyValue(recoveryVerificationFlag, true.toString());
|
await _updateKeyValue(_passwordReminderFlag, true.toString());
|
||||||
await _prefs.setBool(needRecoveryKeyVerification, false);
|
await _prefs.setBool(needRecoveryKeyVerification, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _refreshRecoveryVerificationFlag() async {
|
Future<void> _refreshRecoveryVerificationFlag() async {
|
||||||
_logger.finest('refresh recovery key verification flag');
|
_logger.finest('refresh recovery key verification flag');
|
||||||
final remoteStatusValue =
|
final remoteStatusValue = await _getValue(_passwordReminderFlag, "false");
|
||||||
await _getValue(recoveryVerificationFlag, "false");
|
|
||||||
final bool isNeedVerificationFlagSet =
|
final bool isNeedVerificationFlagSet =
|
||||||
_prefs.containsKey(needRecoveryKeyVerification);
|
_prefs.containsKey(needRecoveryKeyVerification);
|
||||||
if (remoteStatusValue.toLowerCase() == "true") {
|
if (remoteStatusValue.toLowerCase() == "true") {
|
||||||
|
|
32
lib/ui/components/keyboard/keybiard_oveylay.dart
Normal file
32
lib/ui/components/keyboard/keybiard_oveylay.dart
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class KeyboardOverlay {
|
||||||
|
static OverlayEntry? _overlayEntry;
|
||||||
|
|
||||||
|
static showOverlay(BuildContext context, Widget child) {
|
||||||
|
if (_overlayEntry != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final OverlayState? overlayState = Overlay.of(context);
|
||||||
|
_overlayEntry = OverlayEntry(
|
||||||
|
builder: (context) {
|
||||||
|
return Positioned(
|
||||||
|
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||||
|
right: 0.0,
|
||||||
|
left: 0.0,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
overlayState!.insert(_overlayEntry!);
|
||||||
|
}
|
||||||
|
|
||||||
|
static removeOverlay() {
|
||||||
|
if (_overlayEntry != null) {
|
||||||
|
_overlayEntry!.remove();
|
||||||
|
_overlayEntry = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
52
lib/ui/components/keyboard/keyboard_top_button.dart
Normal file
52
lib/ui/components/keyboard/keyboard_top_button.dart
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:photos/theme/ente_theme.dart';
|
||||||
|
|
||||||
|
class KeyboardTopButton extends StatelessWidget {
|
||||||
|
final VoidCallback? onDoneTap;
|
||||||
|
final VoidCallback? onCancelTap;
|
||||||
|
final String doneText;
|
||||||
|
final String cancelText;
|
||||||
|
|
||||||
|
const KeyboardTopButton({
|
||||||
|
super.key,
|
||||||
|
this.doneText = "Done",
|
||||||
|
this.cancelText = "Cancel",
|
||||||
|
this.onDoneTap,
|
||||||
|
this.onCancelTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final enteTheme = getEnteTextTheme(context);
|
||||||
|
final colorScheme = getEnteColorScheme(context);
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(width: 1.0, color: colorScheme.strokeFaint),
|
||||||
|
bottom: BorderSide(width: 1.0, color: colorScheme.strokeFaint),
|
||||||
|
),
|
||||||
|
color: colorScheme.backgroundElevated2,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
CupertinoButton(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),
|
||||||
|
onPressed: onCancelTap,
|
||||||
|
child: Text(cancelText, style: enteTheme.bodyBold),
|
||||||
|
),
|
||||||
|
CupertinoButton(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),
|
||||||
|
onPressed: onDoneTap,
|
||||||
|
child: Text(doneText, style: enteTheme.bodyBold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:photos/core/configuration.dart';
|
import 'package:photos/core/configuration.dart';
|
||||||
import 'package:photos/ente_theme_data.dart';
|
import 'package:photos/ente_theme_data.dart';
|
||||||
import 'package:photos/services/update_service.dart';
|
import 'package:photos/services/update_service.dart';
|
||||||
|
import 'package:photos/services/user_remote_flag_service.dart';
|
||||||
import 'package:photos/ui/account/email_entry_page.dart';
|
import 'package:photos/ui/account/email_entry_page.dart';
|
||||||
import 'package:photos/ui/account/login_page.dart';
|
import 'package:photos/ui/account/login_page.dart';
|
||||||
import 'package:photos/ui/account/password_entry_page.dart';
|
import 'package:photos/ui/account/password_entry_page.dart';
|
||||||
|
@ -154,6 +155,7 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
|
||||||
|
|
||||||
void _navigateToSignUpPage() {
|
void _navigateToSignUpPage() {
|
||||||
UpdateService.instance.hideChangeLog().ignore();
|
UpdateService.instance.hideChangeLog().ignore();
|
||||||
|
UserRemoteFlagService.instance.stopPasswordReminder().ignore();
|
||||||
Widget page;
|
Widget page;
|
||||||
if (Configuration.instance.getEncryptedToken() == null) {
|
if (Configuration.instance.getEncryptedToken() == null) {
|
||||||
page = const EmailEntryPage();
|
page = const EmailEntryPage();
|
||||||
|
@ -181,6 +183,7 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
|
||||||
|
|
||||||
void _navigateToSignInPage() {
|
void _navigateToSignInPage() {
|
||||||
UpdateService.instance.hideChangeLog().ignore();
|
UpdateService.instance.hideChangeLog().ignore();
|
||||||
|
UserRemoteFlagService.instance.stopPasswordReminder().ignore();
|
||||||
Widget page;
|
Widget page;
|
||||||
if (Configuration.instance.getEncryptedToken() == null) {
|
if (Configuration.instance.getEncryptedToken() == null) {
|
||||||
page = const LoginPage();
|
page = const LoginPage();
|
||||||
|
|
|
@ -25,6 +25,7 @@ import 'package:photos/models/selected_files.dart';
|
||||||
import 'package:photos/services/collections_service.dart';
|
import 'package:photos/services/collections_service.dart';
|
||||||
import 'package:photos/services/local_sync_service.dart';
|
import 'package:photos/services/local_sync_service.dart';
|
||||||
import 'package:photos/services/update_service.dart';
|
import 'package:photos/services/update_service.dart';
|
||||||
|
import 'package:photos/services/user_remote_flag_service.dart';
|
||||||
import 'package:photos/services/user_service.dart';
|
import 'package:photos/services/user_service.dart';
|
||||||
import 'package:photos/states/user_details_state.dart';
|
import 'package:photos/states/user_details_state.dart';
|
||||||
import 'package:photos/theme/colors.dart';
|
import 'package:photos/theme/colors.dart';
|
||||||
|
@ -41,6 +42,7 @@ import 'package:photos/ui/home/landing_page_widget.dart';
|
||||||
import 'package:photos/ui/home/preserve_footer_widget.dart';
|
import 'package:photos/ui/home/preserve_footer_widget.dart';
|
||||||
import 'package:photos/ui/home/start_backup_hook_widget.dart';
|
import 'package:photos/ui/home/start_backup_hook_widget.dart';
|
||||||
import 'package:photos/ui/loading_photos_widget.dart';
|
import 'package:photos/ui/loading_photos_widget.dart';
|
||||||
|
import 'package:photos/ui/notification/prompts/password_reminder.dart';
|
||||||
import 'package:photos/ui/notification/update/change_log_page.dart';
|
import 'package:photos/ui/notification/update/change_log_page.dart';
|
||||||
import 'package:photos/ui/settings/app_update_dialog.dart';
|
import 'package:photos/ui/settings/app_update_dialog.dart';
|
||||||
import 'package:photos/ui/settings_page.dart';
|
import 'package:photos/ui/settings_page.dart';
|
||||||
|
@ -318,6 +320,10 @@ class _HomeWidgetState extends State<HomeWidget> {
|
||||||
if (!LocalSyncService.instance.hasCompletedFirstImport()) {
|
if (!LocalSyncService.instance.hasCompletedFirstImport()) {
|
||||||
return const LoadingPhotosWidget();
|
return const LoadingPhotosWidget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (UserRemoteFlagService.instance.showPasswordReminder()) {
|
||||||
|
return const PasswordReminder();
|
||||||
|
}
|
||||||
if (_sharedFiles != null && _sharedFiles.isNotEmpty) {
|
if (_sharedFiles != null && _sharedFiles.isNotEmpty) {
|
||||||
ReceiveSharingIntent.reset();
|
ReceiveSharingIntent.reset();
|
||||||
return CreateCollectionPage(null, _sharedFiles);
|
return CreateCollectionPage(null, _sharedFiles);
|
||||||
|
|
|
@ -162,7 +162,9 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
|
||||||
_reloadEventSubscription.cancel();
|
_reloadEventSubscription.cancel();
|
||||||
_currentIndexSubscription.cancel();
|
_currentIndexSubscription.cancel();
|
||||||
widget.selectedFiles.removeListener(_selectedFilesListener);
|
widget.selectedFiles.removeListener(_selectedFilesListener);
|
||||||
|
_toggleSelectAllFromDay.dispose();
|
||||||
|
_showSelectAllButton.dispose();
|
||||||
|
_areAllFromDaySelected.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
371
lib/ui/notification/prompts/password_reminder.dart
Normal file
371
lib/ui/notification/prompts/password_reminder.dart
Normal file
|
@ -0,0 +1,371 @@
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:pedantic/pedantic.dart';
|
||||||
|
import 'package:photos/core/configuration.dart';
|
||||||
|
import 'package:photos/ente_theme_data.dart';
|
||||||
|
import 'package:photos/services/local_authentication_service.dart';
|
||||||
|
import 'package:photos/services/user_remote_flag_service.dart';
|
||||||
|
import 'package:photos/theme/colors.dart';
|
||||||
|
import 'package:photos/theme/ente_theme.dart';
|
||||||
|
import 'package:photos/ui/account/password_entry_page.dart';
|
||||||
|
import 'package:photos/ui/common/gradient_button.dart';
|
||||||
|
import 'package:photos/ui/home_widget.dart';
|
||||||
|
import 'package:photos/utils/dialog_util.dart';
|
||||||
|
import 'package:photos/utils/navigation_util.dart';
|
||||||
|
|
||||||
|
class PasswordReminder extends StatefulWidget {
|
||||||
|
const PasswordReminder({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PasswordReminder> createState() => _PasswordReminderState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PasswordReminderState extends State<PasswordReminder> {
|
||||||
|
final _passwordController = TextEditingController();
|
||||||
|
final Logger _logger = Logger((_PasswordReminderState).toString());
|
||||||
|
bool _password2Visible = false;
|
||||||
|
bool _incorrectPassword = false;
|
||||||
|
|
||||||
|
Future<void> _verifyRecoveryKey() async {
|
||||||
|
final dialog = createProgressDialog(context, "Verifying password...");
|
||||||
|
await dialog.show();
|
||||||
|
try {
|
||||||
|
final String inputKey = _passwordController.text;
|
||||||
|
await Configuration.instance.verifyPassword(inputKey);
|
||||||
|
await dialog.hide();
|
||||||
|
UserRemoteFlagService.instance.stopPasswordReminder().ignore();
|
||||||
|
// todo: change this as per figma once the component is ready
|
||||||
|
await showErrorDialog(
|
||||||
|
context,
|
||||||
|
"Password verified",
|
||||||
|
"Great! Thank you for verifying.\n"
|
||||||
|
"\nPlease"
|
||||||
|
" remember to keep your recovery key safely backed up.",
|
||||||
|
);
|
||||||
|
|
||||||
|
unawaited(
|
||||||
|
Navigator.of(context).pushAndRemoveUntil(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return const HomeWidget();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(route) => false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
_logger.severe("failed to verify password", e, s);
|
||||||
|
await dialog.hide();
|
||||||
|
_incorrectPassword = true;
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onChangePasswordClick() async {
|
||||||
|
try {
|
||||||
|
final hasAuthenticated =
|
||||||
|
await LocalAuthenticationService.instance.requestLocalAuthentication(
|
||||||
|
context,
|
||||||
|
"Please authenticate to change your password",
|
||||||
|
);
|
||||||
|
if (hasAuthenticated) {
|
||||||
|
UserRemoteFlagService.instance.stopPasswordReminder().ignore();
|
||||||
|
await routeToPage(
|
||||||
|
context,
|
||||||
|
const PasswordEntryPage(
|
||||||
|
mode: PasswordEntryMode.update,
|
||||||
|
),
|
||||||
|
forceCustomPageRoute: true,
|
||||||
|
);
|
||||||
|
unawaited(
|
||||||
|
Navigator.of(context).pushAndRemoveUntil(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return const HomeWidget();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(route) => false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showGenericErrorDialog(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onSkipClick() async {
|
||||||
|
final enteTextTheme = getEnteTextTheme(context);
|
||||||
|
final enteColor = getEnteColorScheme(context);
|
||||||
|
final content = Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"You will not be able to access your photos if you forget "
|
||||||
|
"your password.\n\nIf you do not remember your password, "
|
||||||
|
"now is a good time to change it.",
|
||||||
|
style: enteTextTheme.body.copyWith(
|
||||||
|
color: enteColor.textMuted,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Padding(padding: EdgeInsets.all(8)),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 52,
|
||||||
|
child: OutlinedButton(
|
||||||
|
style: Theme.of(context).outlinedButtonTheme.style?.copyWith(
|
||||||
|
textStyle: MaterialStateProperty.resolveWith<TextStyle>(
|
||||||
|
(Set<MaterialState> states) {
|
||||||
|
return enteTextTheme.bodyBold;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||||
|
|
||||||
|
_onChangePasswordClick();
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
"Change password",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Padding(padding: EdgeInsets.all(8)),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 52,
|
||||||
|
child: OutlinedButton(
|
||||||
|
style: Theme.of(context).outlinedButtonTheme.style?.copyWith(
|
||||||
|
textStyle: MaterialStateProperty.resolveWith<TextStyle>(
|
||||||
|
(Set<MaterialState> states) {
|
||||||
|
return enteTextTheme.bodyBold;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
backgroundColor: MaterialStateProperty.resolveWith<Color>(
|
||||||
|
(Set<MaterialState> states) {
|
||||||
|
return enteColor.fillFaint;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
foregroundColor: MaterialStateProperty.resolveWith<Color>(
|
||||||
|
(Set<MaterialState> states) {
|
||||||
|
return Theme.of(context).colorScheme.defaultTextColor;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
"Cancel",
|
||||||
|
style: enteTextTheme.bodyBold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
backgroundColor: enteColor.backgroundElevated,
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.report_outlined,
|
||||||
|
size: 36,
|
||||||
|
color: getEnteColorScheme(context).strokeBase,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: content,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
barrierColor: enteColor.backdropBaseMute,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_passwordController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final enteTheme = Theme.of(context).colorScheme.enteTheme;
|
||||||
|
final List<Widget> actions = <Widget>[];
|
||||||
|
actions.add(
|
||||||
|
PopupMenuButton(
|
||||||
|
itemBuilder: (context) {
|
||||||
|
return [
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 1,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 120,
|
||||||
|
height: 32,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.report_outlined,
|
||||||
|
color: warning500,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const Padding(padding: EdgeInsets.symmetric(horizontal: 6)),
|
||||||
|
Text(
|
||||||
|
"Skip",
|
||||||
|
style: getEnteTextTheme(context)
|
||||||
|
.bodyBold
|
||||||
|
.copyWith(color: warning500),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
onSelected: (value) async {
|
||||||
|
_onSkipClick();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
elevation: 0,
|
||||||
|
leading: null,
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
actions: actions,
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minWidth: constraints.maxWidth,
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Password reminder',
|
||||||
|
style: enteTheme.textTheme.h3Bold,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
Configuration.instance.getEmail()!,
|
||||||
|
style: enteTheme.textTheme.small.copyWith(
|
||||||
|
color: enteTheme.colorScheme.textMuted,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
Text(
|
||||||
|
"Enter your password to ensure you remember it."
|
||||||
|
"\n\nThe developer account we use to publish ente on App Store will change in the next version, so you will need to login again when the next version is released.",
|
||||||
|
style: enteTheme.textTheme.small
|
||||||
|
.copyWith(color: enteTheme.colorScheme.textMuted),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
TextFormField(
|
||||||
|
autofillHints: const [AutofillHints.password],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
filled: true,
|
||||||
|
hintText: "Password",
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_password2Visible
|
||||||
|
? Icons.visibility
|
||||||
|
: Icons.visibility_off,
|
||||||
|
color: Theme.of(context).iconTheme.color,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_password2Visible = !_password2Visible;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.all(20),
|
||||||
|
border: UnderlineInputBorder(
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontFeatures: [FontFeature.tabularFigures()],
|
||||||
|
),
|
||||||
|
controller: _passwordController,
|
||||||
|
autofocus: false,
|
||||||
|
autocorrect: false,
|
||||||
|
obscureText: !_password2Visible,
|
||||||
|
keyboardType: TextInputType.visiblePassword,
|
||||||
|
onChanged: (_) {
|
||||||
|
_incorrectPassword = false;
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_incorrectPassword
|
||||||
|
? const SizedBox(height: 2)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
_incorrectPassword
|
||||||
|
? Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
"Incorrect password",
|
||||||
|
style: enteTheme.textTheme.small.copyWith(
|
||||||
|
color: enteTheme.colorScheme.warning700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.fromLTRB(0, 12, 0, 40),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
GradientButton(
|
||||||
|
onTap: _verifyRecoveryKey,
|
||||||
|
text: "Verify",
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -42,6 +42,12 @@ class _StorageCardWidgetState extends State<StorageCardWidget> {
|
||||||
precacheImage(_background.image, context);
|
precacheImage(_background.image, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_isStorageCardPressed.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final inheritedUserDetails = InheritedUserDetails.of(context);
|
final inheritedUserDetails = InheritedUserDetails.of(context);
|
||||||
|
|
|
@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:photos/core/constants.dart';
|
import 'package:photos/core/constants.dart';
|
||||||
import 'package:photos/models/file.dart';
|
import 'package:photos/models/file.dart';
|
||||||
import 'package:photos/theme/ente_theme.dart';
|
import 'package:photos/theme/ente_theme.dart';
|
||||||
|
import 'package:photos/ui/components/keyboard/keybiard_oveylay.dart';
|
||||||
|
import 'package:photos/ui/components/keyboard/keyboard_top_button.dart';
|
||||||
import 'package:photos/utils/magic_util.dart';
|
import 'package:photos/utils/magic_util.dart';
|
||||||
|
|
||||||
class FileCaptionWidget extends StatefulWidget {
|
class FileCaptionWidget extends StatefulWidget {
|
||||||
|
@ -18,10 +20,12 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
|
||||||
// currentLength/maxLength will show up
|
// currentLength/maxLength will show up
|
||||||
static const int counterThreshold = 1000;
|
static const int counterThreshold = 1000;
|
||||||
int currentLength = 0;
|
int currentLength = 0;
|
||||||
|
|
||||||
final _textController = TextEditingController();
|
final _textController = TextEditingController();
|
||||||
final _focusNode = FocusNode();
|
final _focusNode = FocusNode();
|
||||||
String? editedCaption;
|
String? editedCaption;
|
||||||
String hintText = fileCaptionDefaultHint;
|
String hintText = fileCaptionDefaultHint;
|
||||||
|
Widget? keyboardTopButtoms;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -49,15 +53,7 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
|
||||||
final textTheme = getEnteTextTheme(context);
|
final textTheme = getEnteTextTheme(context);
|
||||||
return TextField(
|
return TextField(
|
||||||
onSubmitted: (value) async {
|
onSubmitted: (value) async {
|
||||||
if (editedCaption != null) {
|
await _onDoneClick(context);
|
||||||
final isSuccesful =
|
|
||||||
await editFileCaption(context, widget.file, editedCaption);
|
|
||||||
if (isSuccesful) {
|
|
||||||
if (mounted) {
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
controller: _textController,
|
controller: _textController,
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
|
@ -94,7 +90,7 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
|
||||||
minLines: 1,
|
minLines: 1,
|
||||||
maxLines: 10,
|
maxLines: 10,
|
||||||
textCapitalization: TextCapitalization.sentences,
|
textCapitalization: TextCapitalization.sentences,
|
||||||
keyboardType: TextInputType.text,
|
keyboardType: TextInputType.multiline,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
hintText = fileCaptionDefaultHint;
|
hintText = fileCaptionDefaultHint;
|
||||||
|
@ -105,11 +101,46 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _onDoneClick(BuildContext context) async {
|
||||||
|
if (editedCaption != null) {
|
||||||
|
final isSuccesful =
|
||||||
|
await editFileCaption(context, widget.file, editedCaption);
|
||||||
|
if (isSuccesful) {
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onCancelTap() {
|
||||||
|
_textController.text = widget.file.caption ?? '';
|
||||||
|
_focusNode.unfocus();
|
||||||
|
editedCaption = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDoneTap() {
|
||||||
|
_focusNode.unfocus();
|
||||||
|
_onDoneClick(context);
|
||||||
|
}
|
||||||
|
|
||||||
void _focusNodeListener() {
|
void _focusNodeListener() {
|
||||||
final caption = widget.file.caption;
|
final caption = widget.file.caption;
|
||||||
if (_focusNode.hasFocus && caption != null) {
|
if (_focusNode.hasFocus && caption != null) {
|
||||||
_textController.text = caption;
|
_textController.text = caption;
|
||||||
editedCaption = caption;
|
editedCaption = caption;
|
||||||
}
|
}
|
||||||
|
final bool hasFocus = _focusNode.hasFocus;
|
||||||
|
keyboardTopButtoms ??= KeyboardTopButton(
|
||||||
|
onDoneTap: onDoneTap,
|
||||||
|
onCancelTap: onCancelTap,
|
||||||
|
);
|
||||||
|
if (hasFocus) {
|
||||||
|
KeyboardOverlay.showOverlay(context, keyboardTopButtoms!);
|
||||||
|
} else {
|
||||||
|
debugPrint("Removing listener");
|
||||||
|
|
||||||
|
KeyboardOverlay.removeOverlay();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ class CollectionPage extends StatelessWidget {
|
||||||
final String tagPrefix;
|
final String tagPrefix;
|
||||||
final GalleryType appBarType;
|
final GalleryType appBarType;
|
||||||
final _selectedFiles = SelectedFiles();
|
final _selectedFiles = SelectedFiles();
|
||||||
bool hasVerifiedLock;
|
final bool hasVerifiedLock;
|
||||||
|
|
||||||
CollectionPage(
|
CollectionPage(
|
||||||
this.c, {
|
this.c, {
|
||||||
|
|
|
@ -45,7 +45,7 @@ Map<int, String> _days = {
|
||||||
7: "Sun",
|
7: "Sun",
|
||||||
};
|
};
|
||||||
|
|
||||||
final currentYear = int.parse(DateTime.now().year.toString());
|
final currentYear = DateTime.now().year;
|
||||||
const searchStartYear = 1970;
|
const searchStartYear = 1970;
|
||||||
|
|
||||||
//Jun 2022
|
//Jun 2022
|
||||||
|
@ -267,30 +267,18 @@ bool isValidDate({
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("Use parseDateTimeV2 ")
|
|
||||||
DateTime? parseDateFromFileName(String fileName) {
|
|
||||||
if (fileName.startsWith('IMG-') || fileName.startsWith('VID-')) {
|
|
||||||
// Whatsapp media files
|
|
||||||
return DateTime.tryParse(fileName.split('-')[1]);
|
|
||||||
} else if (fileName.startsWith("Screenshot_")) {
|
|
||||||
// Screenshots on droid
|
|
||||||
return DateTime.tryParse(
|
|
||||||
(fileName).replaceAll('Screenshot_', '').replaceAll('-', 'T'),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return DateTime.tryParse(
|
|
||||||
(fileName)
|
|
||||||
.replaceAll("IMG_", "")
|
|
||||||
.replaceAll("VID_", "")
|
|
||||||
.replaceAll("DCIM_", "")
|
|
||||||
.replaceAll("_", " "),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final RegExp exp = RegExp('[\\.A-Za-z]*');
|
final RegExp exp = RegExp('[\\.A-Za-z]*');
|
||||||
|
|
||||||
DateTime? parseDateTimeFromFileNameV2(String fileName) {
|
DateTime? parseDateTimeFromFileNameV2(
|
||||||
|
String fileName, {
|
||||||
|
/* to avoid parsing incorrect date time from the filename, the max and min
|
||||||
|
year limits the chances of parsing incorrect date times
|
||||||
|
*/
|
||||||
|
int minYear = 1990,
|
||||||
|
int? maxYear,
|
||||||
|
}) {
|
||||||
|
// add next year to avoid corner cases for 31st Dec
|
||||||
|
maxYear ??= currentYear + 1;
|
||||||
String val = fileName.replaceAll(exp, '');
|
String val = fileName.replaceAll(exp, '');
|
||||||
if (val.isNotEmpty && !isNumeric(val[0])) {
|
if (val.isNotEmpty && !isNumeric(val[0])) {
|
||||||
val = val.substring(1, val.length);
|
val = val.substring(1, val.length);
|
||||||
|
@ -319,8 +307,11 @@ DateTime? parseDateTimeFromFileNameV2(String fileName) {
|
||||||
if (kDebugMode && result == null) {
|
if (kDebugMode && result == null) {
|
||||||
debugPrint("Failed to parse $fileName dateTime from $valForParser");
|
debugPrint("Failed to parse $fileName dateTime from $valForParser");
|
||||||
}
|
}
|
||||||
|
if (result != null && result.year >= minYear && result.year <= maxYear) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
bool isNumeric(String? s) {
|
bool isNumeric(String? s) {
|
||||||
if (s == null) {
|
if (s == null) {
|
||||||
|
|
|
@ -153,7 +153,7 @@ class FileUploader {
|
||||||
}
|
}
|
||||||
return CollectionsService.instance
|
return CollectionsService.instance
|
||||||
.addToCollection(collectionID, [uploadedFile]).then((aVoid) {
|
.addToCollection(collectionID, [uploadedFile]).then((aVoid) {
|
||||||
return uploadedFile;
|
return uploadedFile as File;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,14 +26,20 @@ Future<void> share(
|
||||||
}) async {
|
}) async {
|
||||||
final dialog = createProgressDialog(context, "Preparing...");
|
final dialog = createProgressDialog(context, "Preparing...");
|
||||||
await dialog.show();
|
await dialog.show();
|
||||||
|
try {
|
||||||
final List<Future<String>> pathFutures = [];
|
final List<Future<String>> pathFutures = [];
|
||||||
for (File file in files) {
|
for (File file in files) {
|
||||||
// Note: We are requesting the origin file for performance reasons on iOS.
|
// Note: We are requesting the origin file for performance reasons on iOS.
|
||||||
// This will eat up storage, which will be reset only when the app restarts.
|
// This will eat up storage, which will be reset only when the app restarts.
|
||||||
// We could have cleared the cache had there been a callback to the share API.
|
// We could have cleared the cache had there been a callback to the share API.
|
||||||
pathFutures.add(getFile(file, isOrigin: true).then((file) => file.path));
|
pathFutures.add(
|
||||||
|
getFile(file, isOrigin: true).then((fetchedFile) => fetchedFile.path),
|
||||||
|
);
|
||||||
if (file.fileType == FileType.livePhoto) {
|
if (file.fileType == FileType.livePhoto) {
|
||||||
pathFutures.add(getFile(file, liveVideo: true).then((file) => file.path));
|
pathFutures.add(
|
||||||
|
getFile(file, liveVideo: true)
|
||||||
|
.then((fetchedFile) => fetchedFile.path),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final paths = await Future.wait(pathFutures);
|
final paths = await Future.wait(pathFutures);
|
||||||
|
@ -43,6 +49,15 @@ Future<void> share(
|
||||||
// required for ipad https://github.com/flutter/flutter/issues/47220#issuecomment-608453383
|
// required for ipad https://github.com/flutter/flutter/issues/47220#issuecomment-608453383
|
||||||
sharePositionOrigin: shareButtonRect(context, shareButtonKey),
|
sharePositionOrigin: shareButtonRect(context, shareButtonKey),
|
||||||
);
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
_logger.severe(
|
||||||
|
"failed to fetch files for system share ${files.length}",
|
||||||
|
e,
|
||||||
|
s,
|
||||||
|
);
|
||||||
|
await dialog.hide();
|
||||||
|
await showGenericErrorDialog(context);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rect shareButtonRect(BuildContext context, GlobalKey shareButtonKey) {
|
Rect shareButtonRect(BuildContext context, GlobalKey shareButtonKey) {
|
||||||
|
|
|
@ -700,6 +700,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.7.0"
|
version: "4.7.0"
|
||||||
|
keyboard_visibility:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: keyboard_visibility
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.6"
|
||||||
like_button:
|
like_button:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -72,6 +72,7 @@ dependencies:
|
||||||
implicitly_animated_reorderable_list: ^0.4.0
|
implicitly_animated_reorderable_list: ^0.4.0
|
||||||
in_app_purchase: ^3.0.7
|
in_app_purchase: ^3.0.7
|
||||||
intl: ^0.17.0
|
intl: ^0.17.0
|
||||||
|
keyboard_visibility: ^0.5.6
|
||||||
like_button: ^2.0.2
|
like_button: ^2.0.2
|
||||||
loading_animations: ^2.1.0
|
loading_animations: ^2.1.0
|
||||||
local_auth: ^1.1.5
|
local_auth: ^1.1.5
|
||||||
|
|
|
@ -9,14 +9,14 @@ void main() {
|
||||||
"IMG-20221109-WA0000",
|
"IMG-20221109-WA0000",
|
||||||
'''Screenshot_20220807-195908_Firefox''',
|
'''Screenshot_20220807-195908_Firefox''',
|
||||||
'''Screenshot_20220507-195908''',
|
'''Screenshot_20220507-195908''',
|
||||||
"2019-02-18 16.00.12-DCMX.png",
|
"2022-02-18 16.00.12-DCMX.png",
|
||||||
"20221107_231730",
|
"20221107_231730",
|
||||||
"2020-11-01 02.31.02",
|
"2020-11-01 02.31.02",
|
||||||
"IMG_20210921_144423",
|
"IMG_20210921_144423",
|
||||||
"2019-10-31 155703",
|
"2019-10-31 155703",
|
||||||
"IMG_20210921_144423_783",
|
"IMG_20210921_144423_783",
|
||||||
"Screenshot_2022-06-21-16-51-29-164_newFormat.heic",
|
"Screenshot_2022-06-21-16-51-29-164_newFormat.heic",
|
||||||
"Screenshot 20221106 211633.com.google.android.apps.nbu.paisa.user.jpg"
|
"Screenshot 20221106 211633.com.google.android.apps.nbu.paisa.user.jpg",
|
||||||
];
|
];
|
||||||
for (String val in validParsing) {
|
for (String val in validParsing) {
|
||||||
final parsedValue = parseDateTimeFromFileNameV2(val);
|
final parsedValue = parseDateTimeFromFileNameV2(val);
|
||||||
|
@ -31,6 +31,21 @@ void main() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("test invalid datetime parsing", () {
|
||||||
|
final List<String> badParsing = ["Snapchat-431959199.mp4."];
|
||||||
|
for (String val in badParsing) {
|
||||||
|
final parsedValue = parseDateTimeFromFileNameV2(val);
|
||||||
|
expect(
|
||||||
|
parsedValue == null,
|
||||||
|
true,
|
||||||
|
reason: "parsing should have failed $val",
|
||||||
|
);
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint("Parsed $val as ${parsedValue?.toIso8601String()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("verify constants", () {
|
test("verify constants", () {
|
||||||
final date = DateTime.fromMicrosecondsSinceEpoch(jan011981Time).toUtc();
|
final date = DateTime.fromMicrosecondsSinceEpoch(jan011981Time).toUtc();
|
||||||
expect(
|
expect(
|
||||||
|
|
Loading…
Add table
Reference in a new issue