Merge branch 'master' into collaboration_view
This commit is contained in:
commit
b71c214c58
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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") {
|
||||
|
|
200
lib/ui/advanced_settings_screen.dart
Normal file
200
lib/ui/advanced_settings_screen.dart
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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: () {
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
32
lib/ui/components/keyboard/keybiard_oveylay.dart
Normal file
32
lib/ui/components/keyboard/keybiard_oveylay.dart
Normal file
|
@ -0,0 +1,32 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class KeyboardOverlay {
|
||||
static OverlayEntry? _overlayEntry;
|
||||
|
||||
static showOverlay(BuildContext context, Widget child) {
|
||||
if (_overlayEntry != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final OverlayState? overlayState = Overlay.of(context);
|
||||
_overlayEntry = OverlayEntry(
|
||||
builder: (context) {
|
||||
return Positioned(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
right: 0.0,
|
||||
left: 0.0,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
overlayState!.insert(_overlayEntry!);
|
||||
}
|
||||
|
||||
static removeOverlay() {
|
||||
if (_overlayEntry != null) {
|
||||
_overlayEntry!.remove();
|
||||
_overlayEntry = null;
|
||||
}
|
||||
}
|
||||
}
|
52
lib/ui/components/keyboard/keyboard_top_button.dart
Normal file
52
lib/ui/components/keyboard/keyboard_top_button.dart
Normal file
|
@ -0,0 +1,52 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
|
||||
class KeyboardTopButton extends StatelessWidget {
|
||||
final VoidCallback? onDoneTap;
|
||||
final VoidCallback? onCancelTap;
|
||||
final String doneText;
|
||||
final String cancelText;
|
||||
|
||||
const KeyboardTopButton({
|
||||
super.key,
|
||||
this.doneText = "Done",
|
||||
this.cancelText = "Cancel",
|
||||
this.onDoneTap,
|
||||
this.onCancelTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final enteTheme = getEnteTextTheme(context);
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(width: 1.0, color: colorScheme.strokeFaint),
|
||||
bottom: BorderSide(width: 1.0, color: colorScheme.strokeFaint),
|
||||
),
|
||||
color: colorScheme.backgroundElevated2,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
CupertinoButton(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),
|
||||
onPressed: onCancelTap,
|
||||
child: Text(cancelText, style: enteTheme.bodyBold),
|
||||
),
|
||||
CupertinoButton(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),
|
||||
onPressed: onDoneTap,
|
||||
child: Text(doneText, style: enteTheme.bodyBold),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/ente_theme_data.dart';
|
||||
import 'package:photos/services/update_service.dart';
|
||||
import 'package:photos/services/user_remote_flag_service.dart';
|
||||
import 'package:photos/ui/account/email_entry_page.dart';
|
||||
import 'package:photos/ui/account/login_page.dart';
|
||||
import 'package:photos/ui/account/password_entry_page.dart';
|
||||
|
@ -154,6 +155,7 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
|
|||
|
||||
void _navigateToSignUpPage() {
|
||||
UpdateService.instance.hideChangeLog().ignore();
|
||||
UserRemoteFlagService.instance.stopPasswordReminder().ignore();
|
||||
Widget page;
|
||||
if (Configuration.instance.getEncryptedToken() == null) {
|
||||
page = const EmailEntryPage();
|
||||
|
@ -181,6 +183,7 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
|
|||
|
||||
void _navigateToSignInPage() {
|
||||
UpdateService.instance.hideChangeLog().ignore();
|
||||
UserRemoteFlagService.instance.stopPasswordReminder().ignore();
|
||||
Widget page;
|
||||
if (Configuration.instance.getEncryptedToken() == null) {
|
||||
page = const LoginPage();
|
||||
|
|
|
@ -25,6 +25,7 @@ import 'package:photos/models/selected_files.dart';
|
|||
import 'package:photos/services/collections_service.dart';
|
||||
import 'package:photos/services/local_sync_service.dart';
|
||||
import 'package:photos/services/update_service.dart';
|
||||
import 'package:photos/services/user_remote_flag_service.dart';
|
||||
import 'package:photos/services/user_service.dart';
|
||||
import 'package:photos/states/user_details_state.dart';
|
||||
import 'package:photos/theme/colors.dart';
|
||||
|
@ -41,6 +42,7 @@ import 'package:photos/ui/home/landing_page_widget.dart';
|
|||
import 'package:photos/ui/home/preserve_footer_widget.dart';
|
||||
import 'package:photos/ui/home/start_backup_hook_widget.dart';
|
||||
import 'package:photos/ui/loading_photos_widget.dart';
|
||||
import 'package:photos/ui/notification/prompts/password_reminder.dart';
|
||||
import 'package:photos/ui/notification/update/change_log_page.dart';
|
||||
import 'package:photos/ui/settings/app_update_dialog.dart';
|
||||
import 'package:photos/ui/settings_page.dart';
|
||||
|
@ -318,6 +320,10 @@ class _HomeWidgetState extends State<HomeWidget> {
|
|||
if (!LocalSyncService.instance.hasCompletedFirstImport()) {
|
||||
return const LoadingPhotosWidget();
|
||||
}
|
||||
|
||||
if (UserRemoteFlagService.instance.showPasswordReminder()) {
|
||||
return const PasswordReminder();
|
||||
}
|
||||
if (_sharedFiles != null && _sharedFiles.isNotEmpty) {
|
||||
ReceiveSharingIntent.reset();
|
||||
return CreateCollectionPage(null, _sharedFiles);
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
371
lib/ui/notification/prompts/password_reminder.dart
Normal file
371
lib/ui/notification/prompts/password_reminder.dart
Normal file
|
@ -0,0 +1,371 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/ente_theme_data.dart';
|
||||
import 'package:photos/services/local_authentication_service.dart';
|
||||
import 'package:photos/services/user_remote_flag_service.dart';
|
||||
import 'package:photos/theme/colors.dart';
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
import 'package:photos/ui/account/password_entry_page.dart';
|
||||
import 'package:photos/ui/common/gradient_button.dart';
|
||||
import 'package:photos/ui/home_widget.dart';
|
||||
import 'package:photos/utils/dialog_util.dart';
|
||||
import 'package:photos/utils/navigation_util.dart';
|
||||
|
||||
class PasswordReminder extends StatefulWidget {
|
||||
const PasswordReminder({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<PasswordReminder> createState() => _PasswordReminderState();
|
||||
}
|
||||
|
||||
class _PasswordReminderState extends State<PasswordReminder> {
|
||||
final _passwordController = TextEditingController();
|
||||
final Logger _logger = Logger((_PasswordReminderState).toString());
|
||||
bool _password2Visible = false;
|
||||
bool _incorrectPassword = false;
|
||||
|
||||
Future<void> _verifyRecoveryKey() async {
|
||||
final dialog = createProgressDialog(context, "Verifying password...");
|
||||
await dialog.show();
|
||||
try {
|
||||
final String inputKey = _passwordController.text;
|
||||
await Configuration.instance.verifyPassword(inputKey);
|
||||
await dialog.hide();
|
||||
UserRemoteFlagService.instance.stopPasswordReminder().ignore();
|
||||
// todo: change this as per figma once the component is ready
|
||||
await showErrorDialog(
|
||||
context,
|
||||
"Password verified",
|
||||
"Great! Thank you for verifying.\n"
|
||||
"\nPlease"
|
||||
" remember to keep your recovery key safely backed up.",
|
||||
);
|
||||
|
||||
unawaited(
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const HomeWidget();
|
||||
},
|
||||
),
|
||||
(route) => false,
|
||||
),
|
||||
);
|
||||
} catch (e, s) {
|
||||
_logger.severe("failed to verify password", e, s);
|
||||
await dialog.hide();
|
||||
_incorrectPassword = true;
|
||||
if (mounted) {
|
||||
setState(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onChangePasswordClick() async {
|
||||
try {
|
||||
final hasAuthenticated =
|
||||
await LocalAuthenticationService.instance.requestLocalAuthentication(
|
||||
context,
|
||||
"Please authenticate to change your password",
|
||||
);
|
||||
if (hasAuthenticated) {
|
||||
UserRemoteFlagService.instance.stopPasswordReminder().ignore();
|
||||
await routeToPage(
|
||||
context,
|
||||
const PasswordEntryPage(
|
||||
mode: PasswordEntryMode.update,
|
||||
),
|
||||
forceCustomPageRoute: true,
|
||||
);
|
||||
unawaited(
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const HomeWidget();
|
||||
},
|
||||
),
|
||||
(route) => false,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
showGenericErrorDialog(context);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSkipClick() async {
|
||||
final enteTextTheme = getEnteTextTheme(context);
|
||||
final enteColor = getEnteColorScheme(context);
|
||||
final content = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"You will not be able to access your photos if you forget "
|
||||
"your password.\n\nIf you do not remember your password, "
|
||||
"now is a good time to change it.",
|
||||
style: enteTextTheme.body.copyWith(
|
||||
color: enteColor.textMuted,
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(8)),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: OutlinedButton(
|
||||
style: Theme.of(context).outlinedButtonTheme.style?.copyWith(
|
||||
textStyle: MaterialStateProperty.resolveWith<TextStyle>(
|
||||
(Set<MaterialState> states) {
|
||||
return enteTextTheme.bodyBold;
|
||||
},
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
|
||||
_onChangePasswordClick();
|
||||
},
|
||||
child: const Text(
|
||||
"Change password",
|
||||
),
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(8)),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: OutlinedButton(
|
||||
style: Theme.of(context).outlinedButtonTheme.style?.copyWith(
|
||||
textStyle: MaterialStateProperty.resolveWith<TextStyle>(
|
||||
(Set<MaterialState> states) {
|
||||
return enteTextTheme.bodyBold;
|
||||
},
|
||||
),
|
||||
backgroundColor: MaterialStateProperty.resolveWith<Color>(
|
||||
(Set<MaterialState> states) {
|
||||
return enteColor.fillFaint;
|
||||
},
|
||||
),
|
||||
foregroundColor: MaterialStateProperty.resolveWith<Color>(
|
||||
(Set<MaterialState> states) {
|
||||
return Theme.of(context).colorScheme.defaultTextColor;
|
||||
},
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
},
|
||||
child: Text(
|
||||
"Cancel",
|
||||
style: enteTextTheme.bodyBold,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
backgroundColor: enteColor.backgroundElevated,
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.report_outlined,
|
||||
size: 36,
|
||||
color: getEnteColorScheme(context).strokeBase,
|
||||
),
|
||||
],
|
||||
),
|
||||
content: content,
|
||||
);
|
||||
},
|
||||
barrierColor: enteColor.backdropBaseMute,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final enteTheme = Theme.of(context).colorScheme.enteTheme;
|
||||
final List<Widget> actions = <Widget>[];
|
||||
actions.add(
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: 1,
|
||||
child: SizedBox(
|
||||
width: 120,
|
||||
height: 32,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.report_outlined,
|
||||
color: warning500,
|
||||
size: 20,
|
||||
),
|
||||
const Padding(padding: EdgeInsets.symmetric(horizontal: 6)),
|
||||
Text(
|
||||
"Skip",
|
||||
style: getEnteTextTheme(context)
|
||||
.bodyBold
|
||||
.copyWith(color: warning500),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
onSelected: (value) async {
|
||||
_onSkipClick();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
leading: null,
|
||||
automaticallyImplyLeading: false,
|
||||
actions: actions,
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: constraints.maxWidth,
|
||||
minHeight: constraints.maxHeight,
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Password reminder',
|
||||
style: enteTheme.textTheme.h3Bold,
|
||||
),
|
||||
Text(
|
||||
Configuration.instance.getEmail()!,
|
||||
style: enteTheme.textTheme.small.copyWith(
|
||||
color: enteTheme.colorScheme.textMuted,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Text(
|
||||
"Enter your password to ensure you remember it."
|
||||
"\n\nThe developer account we use to publish ente on App Store will change in the next version, so you will need to login again when the next version is released.",
|
||||
style: enteTheme.textTheme.small
|
||||
.copyWith(color: enteTheme.colorScheme.textMuted),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
TextFormField(
|
||||
autofillHints: const [AutofillHints.password],
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
hintText: "Password",
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_password2Visible
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_password2Visible = !_password2Visible;
|
||||
});
|
||||
},
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(20),
|
||||
border: UnderlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
),
|
||||
controller: _passwordController,
|
||||
autofocus: false,
|
||||
autocorrect: false,
|
||||
obscureText: !_password2Visible,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
onChanged: (_) {
|
||||
_incorrectPassword = false;
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
_incorrectPassword
|
||||
? const SizedBox(height: 2)
|
||||
: const SizedBox.shrink(),
|
||||
_incorrectPassword
|
||||
? Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
"Incorrect password",
|
||||
style: enteTheme.textTheme.small.copyWith(
|
||||
color: enteTheme.colorScheme.warning700,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
const SizedBox(height: 12),
|
||||
Expanded(
|
||||
child: Container(
|
||||
alignment: Alignment.bottomCenter,
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(0, 12, 0, 40),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
GradientButton(
|
||||
onTap: _verifyRecoveryKey,
|
||||
text: "Verify",
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
97
lib/ui/settings/general_section_widget.dart
Normal file
97
lib/ui/settings/general_section_widget.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)];
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -153,7 +153,7 @@ class FileUploader {
|
|||
}
|
||||
return CollectionsService.instance
|
||||
.addToCollection(collectionID, [uploadedFile]).then((aVoid) {
|
||||
return uploadedFile;
|
||||
return uploadedFile as 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
35
pubspec.lock
35
pubspec.lock
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -9,14 +9,14 @@ void main() {
|
|||
"IMG-20221109-WA0000",
|
||||
'''Screenshot_20220807-195908_Firefox''',
|
||||
'''Screenshot_20220507-195908''',
|
||||
"2019-02-18 16.00.12-DCMX.png",
|
||||
"2022-02-18 16.00.12-DCMX.png",
|
||||
"20221107_231730",
|
||||
"2020-11-01 02.31.02",
|
||||
"IMG_20210921_144423",
|
||||
"2019-10-31 155703",
|
||||
"IMG_20210921_144423_783",
|
||||
"Screenshot_2022-06-21-16-51-29-164_newFormat.heic",
|
||||
"Screenshot 20221106 211633.com.google.android.apps.nbu.paisa.user.jpg"
|
||||
"Screenshot 20221106 211633.com.google.android.apps.nbu.paisa.user.jpg",
|
||||
];
|
||||
for (String val in validParsing) {
|
||||
final parsedValue = parseDateTimeFromFileNameV2(val);
|
||||
|
@ -31,6 +31,21 @@ void main() {
|
|||
}
|
||||
});
|
||||
|
||||
test("test invalid datetime parsing", () {
|
||||
final List<String> badParsing = ["Snapchat-431959199.mp4."];
|
||||
for (String val in badParsing) {
|
||||
final parsedValue = parseDateTimeFromFileNameV2(val);
|
||||
expect(
|
||||
parsedValue == null,
|
||||
true,
|
||||
reason: "parsing should have failed $val",
|
||||
);
|
||||
if (kDebugMode) {
|
||||
debugPrint("Parsed $val as ${parsedValue?.toIso8601String()}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("verify constants", () {
|
||||
final date = DateTime.fromMicrosecondsSinceEpoch(jan011981Time).toUtc();
|
||||
expect(
|
||||
|
|
Loading…
Reference in a new issue