Merge branch 'master' into collaboration_view

This commit is contained in:
Neeraj Gupta 2022-12-08 14:06:00 +05:30
commit b71c214c58
No known key found for this signature in database
GPG key ID: 3C5A1684DC1729E1
44 changed files with 1274 additions and 403 deletions

View file

@ -321,7 +321,7 @@ SPEC CHECKSUMS:
FirebaseInstallations: 0a115432c4e223c5ab20b0dbbe4cbefa793a0e8e
FirebaseMessaging: 732623518591384f61c287e3d8f65294beb7ffb3
fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
flutter_image_compress: 5a5e9aee05b6553048b8df1c3bc456d0afaac433
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721

View file

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

View file

@ -15,6 +15,9 @@ const int jan011981Time = 347155200000000;
const int galleryLoadStartTime = -8000000000000000; // Wednesday, March 6, 1748
const int galleryLoadEndTime = 9223372036854775807; // 2^63 -1
const int batchSize = 1000;
const photoGridSizeDefault = 4;
const photoGridSizeMin = 2;
const photoGridSizeMax = 6;
// used to identify which ente file are available in app cache
// todo: 6Jun22: delete old media identifier after 3 months

View file

@ -3,15 +3,19 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
// import 'package:flutter/foundation.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';
import 'package:photos/models/subscription.dart';
import 'package:photos/models/user_details.dart';
import 'package:photos/services/user_service.dart';
import 'package:photos/ui/common/web_page.dart';
import 'package:photos/utils/dialog_util.dart';
const kWebPaymentRedirectUrl = "https://payments.ente.io/frameRedirect";
const kWebPaymentBaseEndpoint = String.fromEnvironment(
@ -30,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;
@ -69,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, {
@ -169,4 +164,38 @@ class BillingService {
void setIsOnSubscriptionPage(bool isOnSubscriptionPage) {
_isOnSubscriptionPage = isOnSubscriptionPage;
}
Future<void> launchFamilyPortal(
BuildContext context,
UserDetails userDetails,
) async {
if (userDetails.subscription.productID == freeProductID) {
await showErrorDialog(
context,
"Share your storage plan with your family members!",
"Customers on paid plans can add up to 5 family members without paying extra. Each member gets their own private space.",
);
return;
}
final dialog = createProgressDialog(context, "Please wait...");
await dialog.show();
try {
final String jwtToken = await UserService.instance.getFamiliesToken();
final bool familyExist = userDetails.isPartOfFamily();
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return WebPage(
"Family",
'$kFamilyPlanManagementUrl?token=$jwtToken&isFamilyCreated=$familyExist',
);
},
),
);
} catch (e) {
await dialog.hide();
showGenericErrorDialog(context);
}
await dialog.hide();
}
}

View file

@ -9,6 +9,7 @@ import 'package:photos/core/constants.dart';
import 'package:photos/core/network.dart';
import 'package:photos/services/notification_service.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tuple/tuple.dart';
class UpdateService {
UpdateService._privateConstructor();
@ -38,7 +39,8 @@ class UpdateService {
return _prefs.setInt(changeLogVersionKey, currentChangeLogVersion);
}
Future<bool> resetChangeLog() {
Future<bool> resetChangeLog() async {
await _prefs.remove("userNotify.passwordReminderFlag");
return _prefs.remove(changeLogVersionKey);
}
@ -120,6 +122,32 @@ class UpdateService {
}
return _packageInfo.packageName.startsWith("io.ente.photos.independent");
}
bool isFdroidFlavor() {
if (Platform.isIOS) {
return false;
}
return _packageInfo.packageName.startsWith("io.ente.photos.fdroid");
}
// getRateDetails returns details about the place
Tuple2<String, String> getRateDetails() {
if (isFdroidFlavor() || isIndependentFlavor()) {
return const Tuple2(
"AlternativeTo",
"https://alternativeto.net/software/ente/about/",
);
}
return Platform.isAndroid
? const Tuple2(
"play store",
"https://play.google.com/store/apps/details?id=io.ente.photos",
)
: const Tuple2(
"app store",
"https://apps.apple.com/in/app/ente-photos/id1542026904",
);
}
}
class LatestVersionInfo {

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,200 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/events/force_reload_home_gallery_event.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/components/captioned_text_widget.dart';
import 'package:photos/ui/components/icon_button_widget.dart';
import 'package:photos/ui/components/menu_item_widget.dart';
import 'package:photos/ui/components/title_bar_title_widget.dart';
import 'package:photos/ui/components/title_bar_widget.dart';
import 'package:photos/utils/local_settings.dart';
class AdvancedSettingsScreen extends StatefulWidget {
const AdvancedSettingsScreen({super.key});
@override
State<AdvancedSettingsScreen> createState() => _AdvancedSettingsScreenState();
}
class _AdvancedSettingsScreenState extends State<AdvancedSettingsScreen> {
late int _photoGridSize, _chosenGridSize;
@override
void initState() {
_photoGridSize = LocalSettings.instance.getPhotoGridSize();
_chosenGridSize = _photoGridSize;
super.initState();
}
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
return Scaffold(
body: CustomScrollView(
primary: false,
slivers: <Widget>[
TitleBarWidget(
flexibleSpaceTitle: const TitleBarTitleWidget(
title: "Advanced",
),
actionIcons: [
IconButtonWidget(
icon: Icons.close_outlined,
iconButtonType: IconButtonType.secondary,
onTap: () {
Navigator.pop(context);
Navigator.pop(context);
},
),
],
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Column(
children: [
GestureDetector(
onTap: () {
_showPhotoGridSizePicker();
},
child: MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: "Photo grid size",
subTitle: _photoGridSize.toString(),
),
menuItemColor: colorScheme.fillFaint,
trailingWidget: Icon(
Icons.chevron_right,
color: colorScheme.strokeMuted,
),
borderRadius: 8,
alignCaptionedTextToLeft: true,
// isBottomBorderRadiusRemoved: true,
isGestureDetectorDisabled: true,
),
),
],
),
],
),
),
);
},
childCount: 1,
),
),
],
),
);
}
Future<void> _showPhotoGridSizePicker() async {
final textTheme = getEnteTextTheme(context);
final List<Text> options = [];
for (int gridSize = photoGridSizeMin;
gridSize <= photoGridSizeMax;
gridSize++) {
options.add(
Text(
gridSize.toString(),
style: textTheme.body,
),
);
}
return showCupertinoModalPopup(
context: context,
builder: (context) {
return Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Container(
decoration: BoxDecoration(
color: getEnteColorScheme(context).backgroundElevated2,
border: const Border(
bottom: BorderSide(
color: Color(0xff999999),
width: 0.0,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
CupertinoButton(
onPressed: () {
Navigator.of(context).pop('cancel');
},
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 5.0,
),
child: Text(
'Cancel',
style: textTheme.body,
),
),
CupertinoButton(
onPressed: () async {
await LocalSettings.instance
.setPhotoGridSize(_chosenGridSize);
Bus.instance.fire(
ForceReloadHomeGalleryEvent("grid size changed"),
);
_photoGridSize = _chosenGridSize;
setState(() {});
Navigator.of(context).pop('');
},
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 2.0,
),
child: Text(
'Confirm',
style: textTheme.body,
),
)
],
),
),
Container(
height: 220.0,
color: const Color(0xfff7f7f7),
child: CupertinoPicker(
backgroundColor: getEnteColorScheme(context).backgroundElevated,
onSelectedItemChanged: (index) {
_chosenGridSize = _getPhotoGridSizeFromIndex(index);
setState(() {});
},
scrollController: FixedExtentScrollController(
initialItem: _getIndexFromPhotoGridSize(_chosenGridSize),
),
magnification: 1.3,
useMagnifier: true,
itemExtent: 25,
diameterRatio: 1,
children: options,
),
)
],
);
},
);
}
int _getPhotoGridSizeFromIndex(int index) {
return index + 2;
}
int _getIndexFromPhotoGridSize(int gridSize) {
return gridSize - 2;
}
}

View file

@ -54,7 +54,7 @@ class BackupSettingsScreen extends StatelessWidget {
title: "Backup over mobile data",
),
menuItemColor: colorScheme.fillFaint,
trailingSwitch: ToggleSwitchWidget(
trailingWidget: ToggleSwitchWidget(
value: () => Configuration.instance
.shouldBackupOverMobileData(),
onChanged: () async {
@ -79,7 +79,7 @@ class BackupSettingsScreen extends StatelessWidget {
title: "Backup videos",
),
menuItemColor: colorScheme.fillFaint,
trailingSwitch: ToggleSwitchWidget(
trailingWidget: ToggleSwitchWidget(
value: () =>
Configuration.instance.shouldBackupVideos(),
onChanged: () => Configuration.instance
@ -104,7 +104,7 @@ class BackupSettingsScreen extends StatelessWidget {
title: "Disable auto lock",
),
menuItemColor: colorScheme.fillFaint,
trailingSwitch: ToggleSwitchWidget(
trailingWidget: ToggleSwitchWidget(
value: () => Configuration.instance
.shouldKeepDeviceAwake(),
onChanged: () {

View file

@ -23,35 +23,42 @@ class CaptionedTextWidget extends StatelessWidget {
final enteColorScheme = Theme.of(context).colorScheme.enteTheme.colorScheme;
final enteTextTheme = Theme.of(context).colorScheme.enteTheme.textTheme;
final List<Widget> children = [
Flexible(
child: Text(
title,
style: textStyle ??
(makeTextBold
? enteTextTheme.bodyBold.copyWith(color: textColor)
: enteTextTheme.body.copyWith(color: textColor)),
),
),
];
if (subTitle != null) {
children.add(const SizedBox(width: 4));
children.add(
Text(
'\u2022',
style: enteTextTheme.small.copyWith(
color: enteColorScheme.textMuted,
),
),
);
children.add(const SizedBox(width: 4));
children.add(
Text(
subTitle!,
style: enteTextTheme.small.copyWith(
color: enteColorScheme.textMuted,
),
),
);
}
return Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 2),
child: Row(
children: [
Flexible(
child: RichText(
text: TextSpan(
style: textStyle ??
(makeTextBold
? enteTextTheme.bodyBold.copyWith(color: textColor)
: enteTextTheme.body.copyWith(color: textColor)),
children: [
TextSpan(
text: title,
),
subTitle != null
? TextSpan(
text: ' \u2022 $subTitle',
style: enteTextTheme.small.copyWith(
color: subTitleColor ?? enteColorScheme.textMuted,
),
)
: const TextSpan(text: ''),
],
),
),
)
],
children: children,
),
),
);

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

@ -18,7 +18,7 @@ class MenuItemWidget extends StatefulWidget {
/// trailing icon can be passed without size as default size set by
/// flutter is what this component expects
final IconData? trailingIcon;
final Widget? trailingSwitch;
final Widget? trailingWidget;
final bool trailingIconIsMuted;
final VoidCallback? onTap;
final VoidCallback? onDoubleTap;
@ -41,7 +41,7 @@ class MenuItemWidget extends StatefulWidget {
this.leadingIconSize = 20.0,
this.leadingIconWidget,
this.trailingIcon,
this.trailingSwitch,
this.trailingWidget,
this.trailingIconIsMuted = false,
this.onTap,
this.onDoubleTap,
@ -177,7 +177,7 @@ class _MenuItemWidgetState extends State<MenuItemWidget> {
? enteColorScheme.strokeMuted
: null,
)
: widget.trailingSwitch ?? const SizedBox.shrink(),
: widget.trailingWidget ?? const SizedBox.shrink(),
],
),
);

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

@ -5,7 +5,6 @@ import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/constants.dart';
@ -33,6 +32,7 @@ class LazyLoadingGallery extends StatefulWidget {
final String tag;
final String logTag;
final Stream<int> currentIndexStream;
final int photoGirdSize;
LazyLoadingGallery(
this.files,
@ -44,6 +44,7 @@ class LazyLoadingGallery extends StatefulWidget {
this.tag,
this.currentIndexStream, {
this.logTag = "",
this.photoGirdSize = photoGridSizeDefault,
Key key,
}) : super(key: key ?? UniqueKey());
@ -162,7 +163,9 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
_reloadEventSubscription.cancel();
_currentIndexSubscription.cancel();
widget.selectedFiles.removeListener(_selectedFilesListener);
_toggleSelectAllFromDay.dispose();
_showSelectAllButton.dispose();
_areAllFromDaySelected.dispose();
super.dispose();
}
@ -185,12 +188,10 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.all(12),
child: getDayWidget(
context,
_files[0].creationTime,
),
getDayWidget(
context,
_files[0].creationTime,
widget.photoGirdSize,
),
ValueListenableBuilder(
valueListenable: _showSelectAllButton,
@ -230,7 +231,12 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
)
],
),
_shouldRender ? _getGallery() : PlaceHolderWidget(_files.length),
_shouldRender
? _getGallery()
: PlaceHolderWidget(
_files.length,
widget.photoGirdSize,
),
],
);
}
@ -251,6 +257,7 @@ class _LazyLoadingGalleryState extends State<LazyLoadingGallery> {
_files.length > kRecycleLimit,
_toggleSelectAllFromDay,
_areAllFromDaySelected,
widget.photoGirdSize,
),
);
}
@ -278,6 +285,7 @@ class LazyLoadingGridView extends StatefulWidget {
final bool shouldRecycle;
final ValueNotifier toggleSelectAllFromDay;
final ValueNotifier areAllFilesSelected;
final int photoGridSize;
LazyLoadingGridView(
this.tag,
@ -287,7 +295,8 @@ class LazyLoadingGridView extends StatefulWidget {
this.shouldRender,
this.shouldRecycle,
this.toggleSelectAllFromDay,
this.areAllFilesSelected, {
this.areAllFilesSelected,
this.photoGridSize, {
Key key,
}) : super(key: key ?? UniqueKey());
@ -352,7 +361,7 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
},
child: _shouldRender
? _getGridView()
: PlaceHolderWidget(widget.filesInDay.length),
: PlaceHolderWidget(widget.filesInDay.length, widget.photoGridSize),
);
}
@ -367,7 +376,8 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
});
}
},
child: PlaceHolderWidget(widget.filesInDay.length),
child:
PlaceHolderWidget(widget.filesInDay.length, widget.photoGridSize),
);
} else {
return _getGridView();
@ -377,16 +387,16 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
Widget _getGridView() {
return GridView.builder(
shrinkWrap: true,
physics:
const NeverScrollableScrollPhysics(), // to disable GridView's scrolling
physics: const NeverScrollableScrollPhysics(),
// to disable GridView's scrolling
itemBuilder: (context, index) {
return _buildFile(context, widget.filesInDay[index]);
},
itemCount: widget.filesInDay.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisSpacing: 2,
mainAxisSpacing: 2,
crossAxisCount: 4,
crossAxisCount: widget.photoGridSize,
),
padding: const EdgeInsets.all(0),
);
@ -424,6 +434,9 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
serverLoadDeferDuration: thumbnailServerLoadDeferDuration,
shouldShowLivePhotoOverlay: true,
key: Key(widget.tag + file.tag),
thumbnailSize: widget.photoGridSize < photoGridSizeDefault
? thumbnailLargeSize
: thumbnailSmallSize,
),
),
),

View file

@ -4,18 +4,20 @@ import 'package:flutter/material.dart';
class PlaceHolderWidget extends StatelessWidget {
const PlaceHolderWidget(
this.count, {
this.count,
this.columns, {
Key key,
}) : super(key: key);
final int count;
final int count, columns;
static final _gridViewCache = <int, GridView>{};
static final _gridViewCache = <String, GridView>{};
@override
Widget build(BuildContext context) {
if (!_gridViewCache.containsKey(count)) {
_gridViewCache[count] = GridView.builder(
final key = _getCacheKey(count, columns);
if (!_gridViewCache.containsKey(key)) {
_gridViewCache[key] = GridView.builder(
padding: const EdgeInsets.all(0),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
@ -26,11 +28,15 @@ class PlaceHolderWidget extends StatelessWidget {
);
},
itemCount: count,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
),
);
}
return _gridViewCache[count];
return _gridViewCache[key];
}
String _getCacheKey(int totalCount, int columns) {
return totalCount.toString() + ":" + columns.toString();
}
}

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

@ -278,7 +278,7 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
alignment: Alignment.topCenter,
child: GestureDetector(
onTap: () async {
await _launchFamilyPortal();
_billingService.launchFamilyPortal(context, _userDetails);
},
child: Container(
padding: const EdgeInsets.fromLTRB(40, 0, 40, 80),
@ -327,36 +327,6 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
await _dialog.hide();
}
Future<void> _launchFamilyPortal() async {
if (_userDetails.subscription.productID == freeProductID) {
await showErrorDialog(
context,
"Now you can share your storage plan with your family members!",
"Customers on paid plans can add up to 5 family members without paying extra. Each member gets their own private space.",
);
return;
}
await _dialog.show();
try {
final String jwtToken = await _userService.getFamiliesToken();
final bool familyExist = _userDetails.isPartOfFamily();
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return WebPage(
"Family",
'$kFamilyPlanManagementUrl?token=$jwtToken&isFamilyCreated=$familyExist',
);
},
),
).then((value) => onWebPaymentGoBack);
} catch (e) {
await _dialog.hide();
showGenericErrorDialog(context);
}
await _dialog.hide();
}
Widget _stripeRenewOrCancelButton() {
final bool isRenewCancelled =
_currentSubscription.attributes?.isCancelled ?? false;

View file

@ -16,7 +16,6 @@ import 'package:photos/services/billing_service.dart';
import 'package:photos/services/user_service.dart';
import 'package:photos/ui/common/loading_widget.dart';
import 'package:photos/ui/common/progress_dialog.dart';
import 'package:photos/ui/common/web_page.dart';
import 'package:photos/ui/payment/child_subscription_widget.dart';
import 'package:photos/ui/payment/skip_subscription_widget.dart';
import 'package:photos/ui/payment/subscription_common_widgets.dart';
@ -286,7 +285,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
alignment: Alignment.topCenter,
child: GestureDetector(
onTap: () async {
_launchFamilyPortal();
_billingService.launchFamilyPortal(context, _userDetails);
},
child: Container(
padding: const EdgeInsets.fromLTRB(40, 0, 40, 80),
@ -465,35 +464,4 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
),
);
}
// todo: refactor manage family in common widget
Future<void> _launchFamilyPortal() async {
if (_userDetails.subscription.productID == freeProductID) {
await showErrorDialog(
context,
"Share your storage plan with your family members!",
"Customers on paid plans can add up to 5 family members without paying extra. Each member gets their own private space.",
);
return;
}
await _dialog.show();
try {
final String jwtToken = await _userService.getFamiliesToken();
final bool familyExist = _userDetails.isPartOfFamily();
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return WebPage(
"Family",
'$kFamilyPlanManagementUrl?token=$jwtToken&isFamilyCreated=$familyExist',
);
},
),
);
} catch (e) {
await _dialog.hide();
showGenericErrorDialog(context);
}
await _dialog.hide();
}
}

View file

@ -28,25 +28,10 @@ class AboutSectionWidget extends StatelessWidget {
Widget _getSectionOptions(BuildContext context) {
return Column(
children: [
sectionOptionSpacing,
const AboutMenuItemWidget(
title: "FAQ",
url: "https://ente.io/faq",
),
sectionOptionSpacing,
const AboutMenuItemWidget(
title: "Terms",
url: "https://ente.io/terms",
),
sectionOptionSpacing,
const AboutMenuItemWidget(
title: "Privacy",
url: "https://ente.io/privacy",
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Source code",
title: "We are open source!",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
@ -56,6 +41,16 @@ class AboutSectionWidget extends StatelessWidget {
},
),
sectionOptionSpacing,
const AboutMenuItemWidget(
title: "Privacy",
url: "https://ente.io/privacy",
),
sectionOptionSpacing,
const AboutMenuItemWidget(
title: "Terms",
url: "https://ente.io/terms",
),
sectionOptionSpacing,
UpdateService.instance.isIndependent()
? Column(
children: [

View file

@ -4,10 +4,12 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/services/local_authentication_service.dart';
import 'package:photos/services/user_service.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/account/change_email_dialog.dart';
import 'package:photos/ui/account/delete_account_page.dart';
import 'package:photos/ui/account/password_entry_page.dart';
import 'package:photos/ui/account/recovery_key_page.dart';
import 'package:photos/ui/components/captioned_text_widget.dart';
@ -122,6 +124,30 @@ class AccountSectionWidget extends StatelessWidget {
},
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Logout",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () {
_onLogoutTapped(context);
},
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Delete account",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () {
routeToPage(context, const DeleteAccountPage());
},
),
sectionOptionSpacing,
],
);
}
@ -131,4 +157,48 @@ class AccountSectionWidget extends StatelessWidget {
await UserService.instance.getOrCreateRecoveryKey(context),
);
}
Future<void> _onLogoutTapped(BuildContext context) async {
final AlertDialog alert = AlertDialog(
title: const Text(
"Logout",
style: TextStyle(
color: Colors.red,
),
),
content: const Text("Are you sure you want to logout?"),
actions: [
TextButton(
child: const Text(
"Yes, logout",
style: TextStyle(
color: Colors.red,
),
),
onPressed: () async {
Navigator.of(context, rootNavigator: true).pop('dialog');
await UserService.instance.logout(context);
},
),
TextButton(
child: Text(
"No",
style: TextStyle(
color: Theme.of(context).colorScheme.greenAlternative,
),
),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop('dialog');
},
),
],
);
await showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}
}

View file

@ -8,6 +8,7 @@ import 'package:photos/models/backup_status.dart';
import 'package:photos/models/duplicate_files.dart';
import 'package:photos/services/deduplication_service.dart';
import 'package:photos/services/sync_service.dart';
import 'package:photos/services/update_service.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/backup_folder_selection_page.dart';
import 'package:photos/ui/backup_settings_screen.dart';
@ -81,7 +82,7 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
[
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Free up space",
title: "Free up device space",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
@ -117,7 +118,7 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Deduplicate files",
title: "Remove duplicates",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
@ -175,16 +176,8 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop('dialog');
// TODO: Replace with https://pub.dev/packages/in_app_review
if (Platform.isAndroid) {
launchUrlString(
"https://play.google.com/store/apps/details?id=io.ente.photos",
);
} else {
launchUrlString(
"https://apps.apple.com/in/app/ente-photos/id1542026904",
);
}
final url = UpdateService.instance.getRateDetails().item2;
launchUrlString(url);
},
),
TextButton(
@ -238,15 +231,8 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
onPressed: () {
Navigator.of(context, rootNavigator: true).pop('dialog');
// TODO: Replace with https://pub.dev/packages/in_app_review
if (Platform.isAndroid) {
launchUrlString(
"https://play.google.com/store/apps/details?id=io.ente.photos",
);
} else {
launchUrlString(
"https://apps.apple.com/in/app/ente-photos/id1542026904",
);
}
final url = UpdateService.instance.getRateDetails().item2;
launchUrlString(url);
},
),
TextButton(

View file

@ -1,101 +0,0 @@
// @dart=2.9
import 'package:flutter/material.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/services/user_service.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/account/delete_account_page.dart';
import 'package:photos/ui/components/captioned_text_widget.dart';
import 'package:photos/ui/components/expandable_menu_item_widget.dart';
import 'package:photos/ui/components/menu_item_widget.dart';
import 'package:photos/ui/settings/common_settings.dart';
import 'package:photos/utils/navigation_util.dart';
class DangerSectionWidget extends StatelessWidget {
const DangerSectionWidget({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ExpandableMenuItemWidget(
title: "Exit",
selectionOptionsWidget: _getSectionOptions(context),
leadingIcon: Icons.logout_outlined,
);
}
Widget _getSectionOptions(BuildContext context) {
return Column(
children: [
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Logout",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () {
_onLogoutTapped(context);
},
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Delete account",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () {
routeToPage(context, const DeleteAccountPage());
},
),
sectionOptionSpacing,
],
);
}
Future<void> _onLogoutTapped(BuildContext context) async {
final AlertDialog alert = AlertDialog(
title: const Text(
"Logout",
style: TextStyle(
color: Colors.red,
),
),
content: const Text("Are you sure you want to logout?"),
actions: [
TextButton(
child: const Text(
"Yes, logout",
style: TextStyle(
color: Colors.red,
),
),
onPressed: () async {
Navigator.of(context, rootNavigator: true).pop('dialog');
await UserService.instance.logout(context);
},
),
TextButton(
child: Text(
"No",
style: TextStyle(
color: Theme.of(context).colorScheme.greenAlternative,
),
),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop('dialog');
},
),
],
);
await showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}
}

View file

@ -0,0 +1,97 @@
// @dart=2.9
import 'package:flutter/material.dart';
import 'package:photos/services/billing_service.dart';
import 'package:photos/services/user_service.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/advanced_settings_screen.dart';
import 'package:photos/ui/components/captioned_text_widget.dart';
import 'package:photos/ui/components/expandable_menu_item_widget.dart';
import 'package:photos/ui/components/menu_item_widget.dart';
import 'package:photos/ui/payment/subscription.dart';
import 'package:photos/ui/settings/common_settings.dart';
import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/navigation_util.dart';
class GeneralSectionWidget extends StatelessWidget {
const GeneralSectionWidget({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ExpandableMenuItemWidget(
title: "General",
selectionOptionsWidget: _getSectionOptions(context),
leadingIcon: Icons.graphic_eq,
);
}
Widget _getSectionOptions(BuildContext context) {
return Column(
children: [
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Manage subscription",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () {
_onManageSubscriptionTapped(context);
},
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Family plans",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () {
_onFamilyPlansTapped(context);
},
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Advanced",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () {
_onAdvancedTapped(context);
},
),
sectionOptionSpacing,
],
);
}
void _onManageSubscriptionTapped(BuildContext context) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return getSubscriptionPage();
},
),
);
}
Future<void> _onFamilyPlansTapped(BuildContext context) async {
final dialog = createProgressDialog(context, "Please wait...");
await dialog.show();
final userDetails =
await UserService.instance.getUserDetailsV2(memoryCount: false);
await dialog.hide();
BillingService.instance.launchFamilyPortal(context, userDetails);
}
void _onAdvancedTapped(BuildContext context) {
routeToPage(
context,
const AdvancedSettingsScreen(),
);
}
}

View file

@ -68,7 +68,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
captionedTextWidget: const CaptionedTextWidget(
title: "Two-factor",
),
trailingSwitch: ToggleSwitchWidget(
trailingWidget: ToggleSwitchWidget(
value: () => UserService.instance.hasEnabledTwoFactor(),
onChanged: () async {
final hasAuthenticated = await LocalAuthenticationService
@ -101,7 +101,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
captionedTextWidget: const CaptionedTextWidget(
title: "Lockscreen",
),
trailingSwitch: ToggleSwitchWidget(
trailingWidget: ToggleSwitchWidget(
value: () => _config.shouldShowLockScreen(),
onChanged: () async {
await LocalAuthenticationService.instance
@ -123,7 +123,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
captionedTextWidget: const CaptionedTextWidget(
title: "Hide from recents",
),
trailingSwitch: ToggleSwitchWidget(
trailingWidget: ToggleSwitchWidget(
value: () => _config.shouldHideFromRecents(),
onChanged: _hideFromRecentsOnChanged,
),
@ -135,7 +135,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
children.addAll([
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Active sessions",
title: "View active sessions",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,

View file

@ -1,7 +1,5 @@
// @dart=2.9
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:photos/services/update_service.dart';
import 'package:photos/theme/ente_theme.dart';
@ -24,28 +22,39 @@ class SocialSectionWidget extends StatelessWidget {
}
Widget _getSectionOptions(BuildContext context) {
final List<Widget> options = [
sectionOptionSpacing,
const SocialsMenuItemWidget("Twitter", "https://twitter.com/enteio"),
sectionOptionSpacing,
const SocialsMenuItemWidget("Discord", "https://ente.io/discord"),
sectionOptionSpacing,
const SocialsMenuItemWidget("Reddit", "https://reddit.com/r/enteio"),
sectionOptionSpacing,
];
final List<Widget> options = [];
final result = UpdateService.instance.getRateDetails();
final String ratePlace = result.item1;
final String rateUrl = result.item2;
if (!UpdateService.instance.isIndependent()) {
options.addAll(
[
SocialsMenuItemWidget(
"Rate us! ✨",
Platform.isAndroid
? "https://play.google.com/store/apps/details?id=io.ente.photos"
: "https://apps.apple.com/in/app/ente-photos/id1542026904",
),
SocialsMenuItemWidget("Rate us on $ratePlace", rateUrl),
sectionOptionSpacing,
],
);
}
options.addAll(
[
sectionOptionSpacing,
const SocialsMenuItemWidget("Blog", "https://ente.io/blog"),
sectionOptionSpacing,
const SocialsMenuItemWidget("Twitter", "https://twitter.com/enteio"),
sectionOptionSpacing,
const SocialsMenuItemWidget("Mastodon", "https://mstdn.social/@ente"),
sectionOptionSpacing,
const SocialsMenuItemWidget(
"Matrix",
"https://ente.io/matrix/",
),
sectionOptionSpacing,
const SocialsMenuItemWidget("Discord", "https://ente.io/discord"),
sectionOptionSpacing,
const SocialsMenuItemWidget("Reddit", "https://reddit.com/r/enteio"),
sectionOptionSpacing,
],
);
return Column(children: options);
}
}

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

@ -10,6 +10,7 @@ import 'package:photos/ui/common/web_page.dart';
import 'package:photos/ui/components/captioned_text_widget.dart';
import 'package:photos/ui/components/expandable_menu_item_widget.dart';
import 'package:photos/ui/components/menu_item_widget.dart';
import 'package:photos/ui/settings/about_section_widget.dart';
import 'package:photos/ui/settings/common_settings.dart';
import 'package:photos/utils/email_util.dart';
@ -33,7 +34,7 @@ class SupportSectionWidget extends StatelessWidget {
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Email",
title: "Contact support",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
@ -43,9 +44,14 @@ class SupportSectionWidget extends StatelessWidget {
},
),
sectionOptionSpacing,
const AboutMenuItemWidget(
title: "Frequently asked questions",
url: "https://ente.io/faq",
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Roadmap",
title: "Suggest features",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
@ -59,7 +65,7 @@ class SupportSectionWidget extends StatelessWidget {
final url = Configuration.instance.isLoggedIn()
? endpoint + "?token=" + Configuration.instance.getToken()
: roadmapURL;
return WebPage("Roadmap", url);
return WebPage("Suggest features", url);
},
),
);

View file

@ -12,8 +12,8 @@ import 'package:photos/ui/settings/about_section_widget.dart';
import 'package:photos/ui/settings/account_section_widget.dart';
import 'package:photos/ui/settings/app_version_widget.dart';
import 'package:photos/ui/settings/backup_section_widget.dart';
import 'package:photos/ui/settings/danger_section_widget.dart';
import 'package:photos/ui/settings/debug_section_widget.dart';
import 'package:photos/ui/settings/general_section_widget.dart';
import 'package:photos/ui/settings/security_section_widget.dart';
import 'package:photos/ui/settings/settings_title_bar_widget.dart';
import 'package:photos/ui/settings/social_section_widget.dart';
@ -77,6 +77,8 @@ class SettingsPage extends StatelessWidget {
contents.addAll([
const SecuritySectionWidget(),
sectionSpacing,
const GeneralSectionWidget(),
sectionSpacing,
]);
if (Platform.isAndroid || kDebugMode) {
@ -93,12 +95,6 @@ class SettingsPage extends StatelessWidget {
sectionSpacing,
const AboutSectionWidget(),
]);
if (hasLoggedIn) {
contents.addAll([
sectionSpacing,
const DangerSectionWidget(),
]);
}
if (FeatureFlagService.instance.isInternalUserOrDebugBuild() &&
hasLoggedIn) {

View file

@ -128,7 +128,7 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
isTopBorderRadiusRemoved: true,
menuItemColor: getEnteColorScheme(context).fillFaint,
pressedColor: getEnteColorScheme(context).fillFaint,
trailingSwitch: Switch.adaptive(
trailingWidget: Switch.adaptive(
value: widget.collection.publicURLs?.firstOrNull
?.enableDownload ??
true,
@ -177,7 +177,7 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
isTopBorderRadiusRemoved: true,
menuItemColor: getEnteColorScheme(context).fillFaint,
pressedColor: getEnteColorScheme(context).fillFaint,
trailingSwitch: Switch.adaptive(
trailingWidget: Switch.adaptive(
value: widget.collection.publicURLs?.firstOrNull
?.passwordEnabled ??
false,

View file

@ -23,7 +23,7 @@ class UserAvatarWidget extends StatelessWidget {
final enteTextTheme = getEnteTextTheme(context);
final colorScheme = getEnteColorScheme(context);
final displayChar = (user.name == null || user.name!.isEmpty)
? ((user.email?.isEmpty ?? true) ? " " : user.email.substring(0, 1))
? ((user.email.isEmpty ?? true) ? " " : user.email.substring(0, 1))
: user.name!.substring(0, 1);
final randomColor = colorScheme.avatarColors[
(user.id ?? 0).remainder(colorScheme.avatarColors.length)];

View file

@ -204,6 +204,8 @@ class FadingBottomBarState extends State<FadingBottomBar> {
),
child: Text(
widget.file.caption,
overflow: TextOverflow.ellipsis,
maxLines: 4,
style: getEnteTextTheme(context)
.small
.copyWith(color: textBaseDark),

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 {
@ -13,12 +15,17 @@ class FileCaptionWidget extends StatefulWidget {
}
class _FileCaptionWidgetState extends State<FileCaptionWidget> {
int maxLength = 280;
static const int maxLength = 5000;
// counterThreshold represents the nun of char after which
// 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() {
@ -46,21 +53,13 @@ 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,
decoration: InputDecoration(
counterStyle: textTheme.mini.copyWith(color: colorScheme.textMuted),
counterText: currentLength > 99
counterText: currentLength >= counterThreshold
? currentLength.toString() + " / " + maxLength.toString()
: "",
contentPadding: const EdgeInsets.all(16),
@ -89,9 +88,9 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
cursorWidth: 1.5,
maxLength: maxLength,
minLines: 1,
maxLines: 6,
maxLines: 10,
textCapitalization: TextCapitalization.sentences,
keyboardType: TextInputType.text,
keyboardType: TextInputType.multiline,
onChanged: (value) {
setState(() {
hintText = fileCaptionDefaultHint;
@ -102,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

@ -29,6 +29,7 @@ class ThumbnailWidget extends StatefulWidget {
final bool shouldShowLivePhotoOverlay;
final Duration diskLoadDeferDuration;
final Duration serverLoadDeferDuration;
final int thumbnailSize;
ThumbnailWidget(
this.file, {
@ -40,6 +41,7 @@ class ThumbnailWidget extends StatefulWidget {
this.showFavForAlbumOnly = false,
this.diskLoadDeferDuration,
this.serverLoadDeferDuration,
this.thumbnailSize = thumbnailSmallSize,
}) : super(key: key ?? Key(file.tag));
@override
@ -179,7 +181,10 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
}
Future _getThumbnailFromDisk() async {
getThumbnailFromLocal(widget.file).then((thumbData) async {
getThumbnailFromLocal(
widget.file,
size: widget.thumbnailSize,
).then((thumbData) async {
if (thumbData == null) {
if (widget.file.uploadedFileID != null) {
_logger.fine("Removing localID reference for " + widget.file.tag);

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

@ -19,6 +19,7 @@ import 'package:photos/ui/huge_listview/huge_listview.dart';
import 'package:photos/ui/huge_listview/lazy_loading_gallery.dart';
import 'package:photos/ui/viewer/gallery/empty_state.dart';
import 'package:photos/utils/date_time_util.dart';
import 'package:photos/utils/local_settings.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
typedef GalleryLoader = Future<FileLoadResult> Function(
@ -77,6 +78,7 @@ class _GalleryState extends State<Gallery> {
StreamSubscription<TabDoubleTapEvent> _tabDoubleTapEvent;
final _forceReloadEventSubscriptions = <StreamSubscription<Event>>[];
String _logTag;
int _photoGridSize;
@override
void initState() {
@ -200,6 +202,7 @@ class _GalleryState extends State<Gallery> {
if (!_hasLoadedFiles) {
return const EnteLoadingWidget();
}
_photoGridSize = LocalSettings.instance.getPhotoGridSize();
return _getListView();
}
@ -246,6 +249,7 @@ class _GalleryState extends State<Gallery> {
.where((event) => event.tag == widget.tagPrefix)
.map((event) => event.index),
logTag: _logTag,
photoGirdSize: _photoGridSize,
);
if (widget.header != null && index == 0) {
gallery = Column(children: [widget.header, gallery]);

View file

@ -18,6 +18,7 @@ import 'package:photos/models/magic_metadata.dart';
import 'package:photos/models/selected_files.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/services/sync_service.dart';
import 'package:photos/services/update_service.dart';
import 'package:photos/ui/common/dialogs.dart';
import 'package:photos/ui/common/rename_dialog.dart';
import 'package:photos/ui/sharing/share_collection_page.dart';
@ -213,15 +214,8 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
onPressed: () {
Navigator.of(context, rootNavigator: true).pop('dialog');
// TODO: Replace with https://pub.dev/packages/in_app_review
if (Platform.isAndroid) {
launchUrlString(
"https://play.google.com/store/apps/details?id=io.ente.photos",
);
} else {
launchUrlString(
"https://apps.apple.com/in/app/ente-photos/id1542026904",
);
}
final url = UpdateService.instance.getRateDetails().item2;
launchUrlString(url);
},
),
TextButton(
@ -255,7 +249,8 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
final List<Widget> actions = <Widget>[];
if (Configuration.instance.hasConfiguredAccount() &&
widget.selectedFiles.files.isEmpty &&
widget.type == GalleryType.ownedCollection) {
widget.type == GalleryType.ownedCollection &&
widget.collection?.type != CollectionType.favorites) {
actions.add(
Tooltip(
message: "Share",

View file

@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/theme/ente_theme.dart';
const Set<int> monthWith31Days = {1, 3, 5, 7, 8, 10, 12};
@ -45,7 +46,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
@ -195,16 +196,23 @@ bool isLeapYear(DateTime dateTime) {
Widget getDayWidget(
BuildContext context,
int timestamp,
int photoGridSize,
) {
final colorScheme = getEnteColorScheme(context);
final textTheme = getEnteTextTheme(context);
return Container(
alignment: Alignment.centerLeft,
child: Text(
getDayTitle(timestamp),
style: (getDayTitle(timestamp) == "Today")
? textTheme.body
: textTheme.body.copyWith(color: colorScheme.textMuted),
final textStyle =
photoGridSize < photoGridSizeMax ? textTheme.body : textTheme.small;
final double paddingValue = photoGridSize < photoGridSizeMax ? 12.0 : 8.0;
return Padding(
padding: EdgeInsets.all(paddingValue),
child: Container(
alignment: Alignment.centerLeft,
child: Text(
getDayTitle(timestamp),
style: (getDayTitle(timestamp) == "Today")
? textStyle
: textStyle.copyWith(color: colorScheme.textMuted),
),
),
);
}
@ -267,30 +275,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 +315,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

@ -1,3 +1,4 @@
import 'package:photos/core/constants.dart';
import 'package:shared_preferences/shared_preferences.dart';
enum AlbumSortKey {
@ -11,6 +12,8 @@ class LocalSettings {
static final LocalSettings instance = LocalSettings._privateConstructor();
static const kCollectionSortPref = "collection_sort_pref";
static const kPhotoGridSize = "photo_grid_size";
late SharedPreferences _prefs;
void init(SharedPreferences preferences) {
@ -24,4 +27,16 @@ class LocalSettings {
Future<bool> setAlbumSortKey(AlbumSortKey key) {
return _prefs.setInt(kCollectionSortPref, key.index);
}
int getPhotoGridSize() {
if (_prefs.containsKey(kPhotoGridSize)) {
return _prefs.getInt(kPhotoGridSize)!;
} else {
return photoGridSizeDefault;
}
}
Future<void> setPhotoGridSize(int value) async {
await _prefs.setInt(kPhotoGridSize, value);
}
}

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

@ -49,7 +49,7 @@ packages:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.8.2"
version: "2.9.0"
background_fetch:
dependency: "direct main"
description:
@ -98,14 +98,7 @@ packages:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1"
version: "1.2.1"
chewie:
dependency: "direct main"
description:
@ -119,7 +112,7 @@ packages:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
version: "1.1.1"
collection:
dependency: "direct main"
description:
@ -308,7 +301,7 @@ packages:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
version: "1.3.1"
fast_base58:
dependency: "direct main"
description:
@ -762,14 +755,14 @@ packages:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.11"
version: "0.12.12"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.4"
version: "0.1.5"
media_extension:
dependency: "direct main"
description:
@ -785,7 +778,7 @@ packages:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
version: "1.8.0"
mime:
dependency: transitive
description:
@ -913,7 +906,7 @@ packages:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.1"
version: "1.8.2"
path_drawing:
dependency: transitive
description:
@ -1254,7 +1247,7 @@ packages:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.2"
version: "1.9.0"
sprintf:
dependency: transitive
description:
@ -1310,7 +1303,7 @@ packages:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
version: "1.1.1"
syncfusion_flutter_core:
dependency: "direct main"
description:
@ -1338,28 +1331,28 @@ packages:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
version: "1.2.1"
test:
dependency: "direct dev"
description:
name: test
url: "https://pub.dartlang.org"
source: hosted
version: "1.21.1"
version: "1.21.4"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.9"
version: "0.4.12"
test_core:
dependency: transitive
description:
name: test_core
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.13"
version: "0.4.16"
timezone:
dependency: transitive
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(