Merge branch 'master' into grid

This commit is contained in:
vishnukvmd 2022-12-06 23:29:03 +05:30
commit 7133930af4
21 changed files with 643 additions and 70 deletions

View file

@ -102,6 +102,9 @@ PODS:
- Flutter
- in_app_purchase_storekit (0.0.1):
- Flutter
- keyboard_visibility (0.5.0):
- Flutter
- Reachability
- libwebp (1.2.3):
- libwebp/demux (= 1.2.3)
- libwebp/mux (= 1.2.3)
@ -193,6 +196,7 @@ DEPENDENCIES:
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- image_editor (from `.symlinks/plugins/image_editor/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`)
- media_extension (from `.symlinks/plugins/media_extension/ios`)
- motionphoto (from `.symlinks/plugins/motionphoto/ios`)
@ -271,6 +275,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/image_editor/ios"
in_app_purchase_storekit:
:path: ".symlinks/plugins/in_app_purchase_storekit/ios"
keyboard_visibility:
:path: ".symlinks/plugins/keyboard_visibility/ios"
local_auth:
:path: ".symlinks/plugins/local_auth/ios"
media_extension:
@ -336,6 +342,7 @@ SPEC CHECKSUMS:
GoogleUtilities: 1d20a6ad97ef46f67bbdec158ce00563a671ebb7
image_editor: eab82a302a6623a866da5145b7c4c0ee8a4ffbb4
in_app_purchase_storekit: d7fcf4646136ec258e237872755da8ea6c1b6096
keyboard_visibility: 96a24de806fe6823c3ad956c01ba2ec6d056616f
libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c
local_auth: 1740f55d7af0a2e2a8684ce225fe79d8931e808c
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d

View file

@ -287,6 +287,7 @@
"${BUILT_PRODUCTS_DIR}/fluttertoast/fluttertoast.framework",
"${BUILT_PRODUCTS_DIR}/image_editor/image_editor.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}/local_auth/local_auth.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}/image_editor.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}/local_auth.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_extension.framework",

View file

@ -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 {
final masterKey = getKey()!;
final existingAttributes = getKeyAttributes();

View file

@ -8,7 +8,6 @@ import 'package:flutter/material.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';
@ -35,9 +34,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;
@ -74,23 +71,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, {

View file

@ -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);
}

View file

@ -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") {

View 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;
}
}
}

View 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),
),
],
),
),
);
}
}

View file

@ -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();

View file

@ -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);

View file

@ -162,7 +162,9 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
_reloadEventSubscription.cancel();
_currentIndexSubscription.cancel();
widget.selectedFiles.removeListener(_selectedFilesListener);
_toggleSelectAllFromDay.dispose();
_showSelectAllButton.dispose();
_areAllFromDaySelected.dispose();
super.dispose();
}

View 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)
],
),
),
),
);
},
),
),
);
}
}

View file

@ -42,6 +42,12 @@ class _StorageCardWidgetState extends State<StorageCardWidget> {
precacheImage(_background.image, context);
}
@override
void dispose() {
_isStorageCardPressed.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final inheritedUserDetails = InheritedUserDetails.of(context);

View file

@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/models/file.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';
class FileCaptionWidget extends StatefulWidget {
@ -18,10 +20,12 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
// currentLength/maxLength will show up
static const int counterThreshold = 1000;
int currentLength = 0;
final _textController = TextEditingController();
final _focusNode = FocusNode();
String? editedCaption;
String hintText = fileCaptionDefaultHint;
Widget? keyboardTopButtoms;
@override
void initState() {
@ -49,15 +53,7 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
final textTheme = getEnteTextTheme(context);
return TextField(
onSubmitted: (value) async {
if (editedCaption != null) {
final isSuccesful =
await editFileCaption(context, widget.file, editedCaption);
if (isSuccesful) {
if (mounted) {
Navigator.pop(context);
}
}
}
await _onDoneClick(context);
},
controller: _textController,
focusNode: _focusNode,
@ -94,7 +90,7 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
minLines: 1,
maxLines: 10,
textCapitalization: TextCapitalization.sentences,
keyboardType: TextInputType.text,
keyboardType: TextInputType.multiline,
onChanged: (value) {
setState(() {
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() {
final caption = widget.file.caption;
if (_focusNode.hasFocus && caption != null) {
_textController.text = 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();
}
}
}

View file

@ -20,7 +20,7 @@ class CollectionPage extends StatelessWidget {
final String tagPrefix;
final GalleryType appBarType;
final _selectedFiles = SelectedFiles();
bool hasVerifiedLock;
final bool hasVerifiedLock;
CollectionPage(
this.c, {

View file

@ -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) {

View file

@ -153,7 +153,7 @@ class FileUploader {
}
return CollectionsService.instance
.addToCollection(collectionID, [uploadedFile]).then((aVoid) {
return uploadedFile;
return uploadedFile as File;
});
});
}

View file

@ -26,23 +26,38 @@ Future<void> share(
}) async {
final dialog = createProgressDialog(context, "Preparing...");
await dialog.show();
final List<Future<String>> pathFutures = [];
for (File file in files) {
// 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.
// 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));
if (file.fileType == FileType.livePhoto) {
pathFutures.add(getFile(file, liveVideo: true).then((file) => file.path));
try {
final List<Future<String>> pathFutures = [];
for (File file in files) {
// 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.
// We could have cleared the cache had there been a callback to the share API.
pathFutures.add(
getFile(file, isOrigin: true).then((fetchedFile) => fetchedFile.path),
);
if (file.fileType == FileType.livePhoto) {
pathFutures.add(
getFile(file, liveVideo: true)
.then((fetchedFile) => fetchedFile.path),
);
}
}
final paths = await Future.wait(pathFutures);
await dialog.hide();
return Share.shareFiles(
paths,
// required for ipad https://github.com/flutter/flutter/issues/47220#issuecomment-608453383
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);
}
final paths = await Future.wait(pathFutures);
await dialog.hide();
return Share.shareFiles(
paths,
// required for ipad https://github.com/flutter/flutter/issues/47220#issuecomment-608453383
sharePositionOrigin: shareButtonRect(context, shareButtonKey),
);
}
Rect shareButtonRect(BuildContext context, GlobalKey shareButtonKey) {

View file

@ -700,6 +700,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: "direct main"
description:

View file

@ -72,6 +72,7 @@ dependencies:
implicitly_animated_reorderable_list: ^0.4.0
in_app_purchase: ^3.0.7
intl: ^0.17.0
keyboard_visibility: ^0.5.6
like_button: ^2.0.2
loading_animations: ^2.1.0
local_auth: ^1.1.5

View 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(