Merge branch 'master' into redesign-with-components
This commit is contained in:
commit
6b5ef752b5
10 changed files with 461 additions and 43 deletions
|
@ -323,6 +323,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 {
|
||||
final masterKey = getKey()!;
|
||||
final existingAttributes = getKeyAttributes();
|
||||
|
|
|
@ -7,7 +7,6 @@ import 'package:dio/dio.dart';
|
|||
// import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/errors.dart';
|
||||
import 'package:photos/core/network.dart';
|
||||
import 'package:photos/models/billing_plan.dart';
|
||||
|
@ -30,9 +29,7 @@ class BillingService {
|
|||
static final BillingService instance = BillingService._privateConstructor();
|
||||
|
||||
final _logger = Logger("BillingService");
|
||||
final _dio = Network.instance.getDio();
|
||||
final _enteDio = Network.instance.enteDio;
|
||||
final _config = Configuration.instance;
|
||||
|
||||
bool _isOnSubscriptionPage = false;
|
||||
|
||||
|
@ -69,23 +66,16 @@ class BillingService {
|
|||
}
|
||||
|
||||
Future<BillingPlans> getBillingPlans() {
|
||||
_future ??= (_config.isLoggedIn()
|
||||
? _fetchPublicBillingPlans()
|
||||
: _fetchPrivateBillingPlans())
|
||||
.then((response) {
|
||||
_future ??= _fetchBillingPlans().then((response) {
|
||||
return BillingPlans.fromMap(response.data);
|
||||
});
|
||||
return _future;
|
||||
}
|
||||
|
||||
Future<Response<dynamic>> _fetchPrivateBillingPlans() {
|
||||
Future<Response<dynamic>> _fetchBillingPlans() {
|
||||
return _enteDio.get("/billing/user-plans/");
|
||||
}
|
||||
|
||||
Future<Response<dynamic>> _fetchPublicBillingPlans() {
|
||||
return _dio.get(_config.getHttpEndpoint() + "/billing/plans/v2");
|
||||
}
|
||||
|
||||
Future<Subscription> verifySubscription(
|
||||
final productID,
|
||||
final verificationData, {
|
||||
|
|
|
@ -38,7 +38,8 @@ class UpdateService {
|
|||
return _prefs.setInt(changeLogVersionKey, currentChangeLogVersion);
|
||||
}
|
||||
|
||||
Future<bool> resetChangeLog() {
|
||||
Future<bool> resetChangeLog() async {
|
||||
await _prefs.remove("userNotify.passwordReminderFlag");
|
||||
return _prefs.remove(changeLogVersionKey);
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,8 @@ class UserRemoteFlagService {
|
|||
UserRemoteFlagService._privateConstructor();
|
||||
|
||||
static const String recoveryVerificationFlag = "recoveryKeyVerified";
|
||||
static const String _passwordReminderFlag = "userNotify"
|
||||
".passwordReminderFlag";
|
||||
static const String needRecoveryKeyVerification =
|
||||
"needRecoveryKeyVerification";
|
||||
|
||||
|
@ -27,6 +29,20 @@ class UserRemoteFlagService {
|
|||
_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() {
|
||||
if (!_prefs.containsKey(needRecoveryKeyVerification)) {
|
||||
// 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
|
||||
// prompt to the user on re-install or signing into a different device
|
||||
Future<void> markRecoveryVerificationAsDone() async {
|
||||
await _updateKeyValue(recoveryVerificationFlag, true.toString());
|
||||
await _updateKeyValue(_passwordReminderFlag, true.toString());
|
||||
await _prefs.setBool(needRecoveryKeyVerification, false);
|
||||
}
|
||||
|
||||
Future<void> _refreshRecoveryVerificationFlag() async {
|
||||
_logger.finest('refresh recovery key verification flag');
|
||||
final remoteStatusValue =
|
||||
await _getValue(recoveryVerificationFlag, "false");
|
||||
final remoteStatusValue = await _getValue(_passwordReminderFlag, "false");
|
||||
final bool isNeedVerificationFlagSet =
|
||||
_prefs.containsKey(needRecoveryKeyVerification);
|
||||
if (remoteStatusValue.toLowerCase() == "true") {
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/ente_theme_data.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/login_page.dart';
|
||||
import 'package:photos/ui/account/password_entry_page.dart';
|
||||
|
@ -154,6 +155,7 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
|
|||
|
||||
void _navigateToSignUpPage() {
|
||||
UpdateService.instance.hideChangeLog().ignore();
|
||||
UserRemoteFlagService.instance.stopPasswordReminder().ignore();
|
||||
Widget page;
|
||||
if (Configuration.instance.getEncryptedToken() == null) {
|
||||
page = const EmailEntryPage();
|
||||
|
@ -181,6 +183,7 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
|
|||
|
||||
void _navigateToSignInPage() {
|
||||
UpdateService.instance.hideChangeLog().ignore();
|
||||
UserRemoteFlagService.instance.stopPasswordReminder().ignore();
|
||||
Widget page;
|
||||
if (Configuration.instance.getEncryptedToken() == null) {
|
||||
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/local_sync_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/states/user_details_state.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/start_backup_hook_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/settings/app_update_dialog.dart';
|
||||
import 'package:photos/ui/settings_page.dart';
|
||||
|
@ -318,6 +320,10 @@ class _HomeWidgetState extends State<HomeWidget> {
|
|||
if (!LocalSyncService.instance.hasCompletedFirstImport()) {
|
||||
return const LoadingPhotosWidget();
|
||||
}
|
||||
|
||||
if (UserRemoteFlagService.instance.showPasswordReminder()) {
|
||||
return const PasswordReminder();
|
||||
}
|
||||
if (_sharedFiles != null && _sharedFiles.isNotEmpty) {
|
||||
ReceiveSharingIntent.reset();
|
||||
return CreateCollectionPage(null, _sharedFiles);
|
||||
|
|
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)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -45,7 +45,7 @@ Map<int, String> _days = {
|
|||
7: "Sun",
|
||||
};
|
||||
|
||||
final currentYear = int.parse(DateTime.now().year.toString());
|
||||
final currentYear = DateTime.now().year;
|
||||
const searchStartYear = 1970;
|
||||
|
||||
//Jun 2022
|
||||
|
@ -267,30 +267,18 @@ bool isValidDate({
|
|||
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]*');
|
||||
|
||||
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, '');
|
||||
if (val.isNotEmpty && !isNumeric(val[0])) {
|
||||
val = val.substring(1, val.length);
|
||||
|
@ -319,7 +307,10 @@ DateTime? parseDateTimeFromFileNameV2(String fileName) {
|
|||
if (kDebugMode && result == null) {
|
||||
debugPrint("Failed to parse $fileName dateTime from $valForParser");
|
||||
}
|
||||
return result;
|
||||
if (result != null && result.year >= minYear && result.year <= maxYear) {
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool isNumeric(String? s) {
|
||||
|
|
|
@ -153,7 +153,7 @@ class FileUploader {
|
|||
}
|
||||
return CollectionsService.instance
|
||||
.addToCollection(collectionID, [uploadedFile]).then((aVoid) {
|
||||
return uploadedFile;
|
||||
return uploadedFile as File;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -9,14 +9,14 @@ void main() {
|
|||
"IMG-20221109-WA0000",
|
||||
'''Screenshot_20220807-195908_Firefox''',
|
||||
'''Screenshot_20220507-195908''',
|
||||
"2019-02-18 16.00.12-DCMX.png",
|
||||
"2022-02-18 16.00.12-DCMX.png",
|
||||
"20221107_231730",
|
||||
"2020-11-01 02.31.02",
|
||||
"IMG_20210921_144423",
|
||||
"2019-10-31 155703",
|
||||
"IMG_20210921_144423_783",
|
||||
"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) {
|
||||
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", () {
|
||||
final date = DateTime.fromMicrosecondsSinceEpoch(jan011981Time).toUtc();
|
||||
expect(
|
||||
|
|
Loading…
Reference in a new issue