Merge branch 'master' into cherry_pick_release

This commit is contained in:
Neeraj Gupta 2022-10-11 13:35:12 +05:30 committed by GitHub
commit 0cdc0ae466
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 1486 additions and 1004 deletions

5
.gitmodules vendored
View file

@ -6,11 +6,6 @@
path = thirdparty/plugins
url = https://github.com/ente-io/plugins.git
[submodule "thirdparty/sentry-dart"]
path = thirdparty/sentry-dart
url = https://github.com/ente-io/sentry-dart.git
branch = sentry_flutter_ente
[submodule "thirdparty/extended_image"]
path = thirdparty/extended_image
url = https://github.com/ente-io/extended_image.git

View file

@ -100,7 +100,7 @@ PODS:
- GoogleUtilities/Logger
- image_editor (1.0.0):
- Flutter
- in_app_purchase (0.0.1):
- in_app_purchase_storekit (0.0.1):
- Flutter
- libwebp (1.2.3):
- libwebp/demux (= 1.2.3)
@ -145,13 +145,13 @@ PODS:
- SDWebImageWebPCoder (0.9.1):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.13)
- Sentry (7.25.1):
- Sentry/Core (= 7.25.1)
- Sentry/Core (7.25.1)
- Sentry (7.27.1):
- Sentry/Core (= 7.27.1)
- Sentry/Core (7.27.1)
- sentry_flutter (0.0.1):
- Flutter
- FlutterMacOS
- Sentry (~> 7.25.1)
- Sentry (~> 7.27.1)
- share_plus (0.0.1):
- Flutter
- shared_preferences_ios (0.0.1):
@ -190,7 +190,7 @@ DEPENDENCIES:
- flutter_sodium (from `.symlinks/plugins/flutter_sodium/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- image_editor (from `.symlinks/plugins/image_editor/ios`)
- in_app_purchase (from `.symlinks/plugins/in_app_purchase/ios`)
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/ios`)
- local_auth (from `.symlinks/plugins/local_auth/ios`)
- motionphoto (from `.symlinks/plugins/motionphoto/ios`)
- move_to_background (from `.symlinks/plugins/move_to_background/ios`)
@ -266,8 +266,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/fluttertoast/ios"
image_editor:
:path: ".symlinks/plugins/image_editor/ios"
in_app_purchase:
:path: ".symlinks/plugins/in_app_purchase/ios"
in_app_purchase_storekit:
:path: ".symlinks/plugins/in_app_purchase_storekit/ios"
local_auth:
:path: ".symlinks/plugins/local_auth/ios"
motionphoto:
@ -330,7 +330,7 @@ SPEC CHECKSUMS:
GoogleDataTransport: 1c8145da7117bd68bbbed00cf304edb6a24de00f
GoogleUtilities: 1d20a6ad97ef46f67bbdec158ce00563a671ebb7
image_editor: eab82a302a6623a866da5145b7c4c0ee8a4ffbb4
in_app_purchase: 3e2155afa9d03d4fa32d9e62d567885080ce97d6
in_app_purchase_storekit: d7fcf4646136ec258e237872755da8ea6c1b6096
libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c
local_auth: 1740f55d7af0a2e2a8684ce225fe79d8931e808c
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
@ -347,8 +347,8 @@ SPEC CHECKSUMS:
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
SDWebImage: e5cc87bf736e60f49592f307bdf9e157189298a3
SDWebImageWebPCoder: 18503de6621dd2c420d680e33d46bf8e1d5169b0
Sentry: dd29c18c32b0af9269949f079cf631d581ca76ca
sentry_flutter: 544b23de27343d0cd12d8d16b0fac71dc884f0e6
Sentry: bc644307e2eb6a4c9c55cf117a80b895bb2a25a7
sentry_flutter: 649559f0512e00d3f6fc92cf51f74bc2fe68d1d3
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904

View file

@ -286,7 +286,7 @@
"${BUILT_PRODUCTS_DIR}/flutter_sodium/flutter_sodium.framework",
"${BUILT_PRODUCTS_DIR}/fluttertoast/fluttertoast.framework",
"${BUILT_PRODUCTS_DIR}/image_editor/image_editor.framework",
"${BUILT_PRODUCTS_DIR}/in_app_purchase/in_app_purchase.framework",
"${BUILT_PRODUCTS_DIR}/in_app_purchase_storekit/in_app_purchase_storekit.framework",
"${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework",
"${BUILT_PRODUCTS_DIR}/local_auth/local_auth.framework",
"${BUILT_PRODUCTS_DIR}/motionphoto/motionphoto.framework",
@ -339,7 +339,7 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_sodium.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/fluttertoast.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_editor.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/in_app_purchase.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/in_app_purchase_storekit.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/motionphoto.framework",

View file

@ -341,10 +341,6 @@ extension CustomColorScheme on ColorScheme {
? const Color.fromRGBO(180, 180, 180, 1)
: const Color.fromRGBO(100, 100, 100, 1);
Color get themeSwitchInactiveIconColor => brightness == Brightness.light
? const Color.fromRGBO(0, 0, 0, 1).withOpacity(0.5)
: const Color.fromRGBO(255, 255, 255, 1).withOpacity(0.5);
Color get searchResultsColor => brightness == Brightness.light
? const Color.fromRGBO(245, 245, 245, 1.0)
: const Color.fromRGBO(30, 30, 30, 1.0);

View file

@ -15,4 +15,5 @@ enum TabChangedEventSource {
pageView,
collectionsPage,
backButton,
settingsTitleBar
}

View file

@ -7,7 +7,6 @@ import 'package:background_fetch/background_fetch.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
import 'package:photos/app.dart';
@ -128,7 +127,6 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
} else {
AppLifecycleService.instance.onAppInForeground('init via: $via');
}
InAppPurchaseConnection.enablePendingPurchases();
CryptoUtil.init();
await NotificationService.instance.init();
await Network.instance.init();

View file

@ -1,9 +1,10 @@
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:photos/models/subscription.dart';
class UserDetails {
class UserDetails extends Equatable {
final String email;
final int usage;
final int fileCount;
@ -11,7 +12,7 @@ class UserDetails {
final Subscription subscription;
final FamilyData? familyData;
UserDetails(
const UserDetails(
this.email,
this.usage,
this.fileCount,
@ -20,6 +21,16 @@ class UserDetails {
this.familyData,
);
@override
List<Object?> get props => [
email,
usage,
fileCount,
sharedCollectionsCount,
subscription,
familyData
];
bool isPartOfFamily() {
return familyData?.members?.isNotEmpty ?? false;
}

View file

@ -38,12 +38,11 @@ class BillingService {
Future<BillingPlans> _future;
Future<void> init() async {
InAppPurchaseConnection.enablePendingPurchases();
// if (Platform.isIOS && kDebugMode) {
// await FlutterInappPurchase.instance.initConnection;
// FlutterInappPurchase.instance.clearTransactionIOS();
// }
InAppPurchaseConnection.instance.purchaseUpdatedStream.listen((purchases) {
InAppPurchase.instance.purchaseStream.listen((purchases) {
if (_isOnSubscriptionPage) {
return;
}
@ -54,11 +53,11 @@ class BillingService {
purchase.verificationData.serverVerificationData,
).then((response) {
if (response != null) {
InAppPurchaseConnection.instance.completePurchase(purchase);
InAppPurchase.instance.completePurchase(purchase);
}
});
} else if (Platform.isIOS && purchase.pendingCompletePurchase) {
InAppPurchaseConnection.instance.completePurchase(purchase);
InAppPurchase.instance.completePurchase(purchase);
}
}
});

View file

@ -292,7 +292,7 @@ class CollectionsService {
Uint8List _getDecryptedKey(Collection collection) {
final encryptedKey = Sodium.base642bin(collection.encryptedKey);
if (collection.owner.id == _config.getUserID()) {
if(_config.getKey() == null) {
if (_config.getKey() == null) {
throw Exception("key can not be null");
}
return CryptoUtil.decryptSync(
@ -334,6 +334,23 @@ class CollectionsService {
}
}
Future<void> leaveAlbum(Collection collection) async {
try {
await _dio.post(
Configuration.instance.getHttpEndpoint() +
"/collections/leave/${collection.id}",
options: Options(
headers: {"X-Auth-Token": Configuration.instance.getToken()},
),
);
// trigger sync to fetch the latest name from server
sync();
} catch (e, s) {
_logger.severe("failed to leave collection", e, s);
rethrow;
}
}
Future<void> updateMagicMetadata(
Collection collection,
Map<String, dynamic> newMetadataUpdate,

View file

@ -0,0 +1,82 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/events/tab_changed_event.dart';
import 'package:photos/events/user_details_changed_event.dart';
import 'package:photos/models/user_details.dart';
// ignore: import_of_legacy_library_into_null_safe
import 'package:photos/services/user_service.dart';
class UserDetailsStateWidget extends StatefulWidget {
final Widget child;
const UserDetailsStateWidget({
required this.child,
Key? key,
}) : super(key: key);
@override
State<UserDetailsStateWidget> createState() => UserDetailsStateWidgetState();
}
class UserDetailsStateWidgetState extends State<UserDetailsStateWidget> {
late Future<UserDetails> userDetails;
late StreamSubscription<UserDetailsChangedEvent> _userDetailsChangedEvent;
late StreamSubscription<TabChangedEvent> _tabChangedEventSubscription;
@override
void initState() {
_fetchUserDetails();
_userDetailsChangedEvent =
Bus.instance.on<UserDetailsChangedEvent>().listen((event) {
_fetchUserDetails();
});
_tabChangedEventSubscription =
Bus.instance.on<TabChangedEvent>().listen((event) {
if (event.selectedIndex == 3) {
_fetchUserDetails();
}
});
super.initState();
}
@override
void dispose() {
_userDetailsChangedEvent.cancel();
_tabChangedEventSubscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) => InheritedUserDetails(
userDetailsState: this,
userDetails: userDetails,
child: widget.child,
);
void _fetchUserDetails() {
userDetails = UserService.instance.getUserDetailsV2(memoryCount: true);
if (mounted) {
setState(() {});
}
}
}
class InheritedUserDetails extends InheritedWidget {
final UserDetailsStateWidgetState userDetailsState;
final Future<UserDetails> userDetails;
const InheritedUserDetails({
Key? key,
required Widget child,
required this.userDetails,
required this.userDetailsState,
}) : super(key: key, child: child);
static InheritedUserDetails? of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<InheritedUserDetails>();
@override
bool updateShouldNotify(covariant InheritedUserDetails oldWidget) =>
userDetails != oldWidget.userDetails;
}

View file

@ -1,4 +1,5 @@
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/theme/colors.dart';
import 'package:photos/theme/effects.dart';
import 'package:photos/theme/text_style.dart';
@ -34,3 +35,11 @@ EnteTheme darkTheme = EnteTheme(
shadowMenu: shadowMenuDark,
shadowButton: shadowButtonDark,
);
EnteColorScheme getEnteColorScheme(BuildContext context) {
return Theme.of(context).colorScheme.enteTheme.colorScheme;
}
EnteTextTheme getEnteTextTheme(BuildContext context) {
return Theme.of(context).colorScheme.enteTheme.textTheme;
}

View file

@ -3,6 +3,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/db/device_files_db.dart';
import 'package:photos/db/files_db.dart';
@ -40,6 +41,7 @@ class _DeviceFoldersGridViewWidgetState
@override
Widget build(BuildContext context) {
final logger = Logger((_DeviceFoldersGridViewWidgetState).toString());
final bool isMigrationDone =
LocalSyncService.instance.isDeviceFileMigrationDone();
return Padding(
@ -75,7 +77,8 @@ class _DeviceFoldersGridViewWidgetState
itemCount: snapshot.data.length,
);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
logger.severe("failed to load device galler", snapshot.error);
return const Text("Failed to load albums");
} else {
return const EnteLoadingWidget();
}

View file

@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:photos/ente_theme_data.dart';
class CaptionedTextWidget extends StatelessWidget {
final String title;
final String? subTitle;
final TextStyle? textStyle;
final bool makeTextBold;
final Color? textColor;
const CaptionedTextWidget({
required this.title,
this.subTitle,
this.textStyle,
this.makeTextBold = false,
this.textColor,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final enteColorScheme = Theme.of(context).colorScheme.enteTheme.colorScheme;
final enteTextTheme = Theme.of(context).colorScheme.enteTheme.textTheme;
return Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12, 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: enteColorScheme.textMuted,
),
)
: const TextSpan(text: ''),
],
),
),
)
],
),
),
);
}
}

View file

@ -0,0 +1,65 @@
import 'package:expandable/expandable.dart';
import 'package:flutter/material.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/ui/components/captioned_text_widget.dart';
import 'package:photos/ui/components/menu_item_widget.dart';
import 'package:photos/ui/settings/common_settings.dart';
class ExpandableMenuItemWidget extends StatefulWidget {
final String title;
final Widget selectionOptionsWidget;
final IconData leadingIcon;
const ExpandableMenuItemWidget({
required this.title,
required this.selectionOptionsWidget,
required this.leadingIcon,
Key? key,
}) : super(key: key);
@override
State<ExpandableMenuItemWidget> createState() =>
_ExpandableMenuItemWidgetState();
}
class _ExpandableMenuItemWidgetState extends State<ExpandableMenuItemWidget> {
final expandableController = ExpandableController(initialExpanded: false);
@override
void dispose() {
expandableController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final enteColorScheme = Theme.of(context).colorScheme.enteTheme.colorScheme;
return Container(
decoration: BoxDecoration(
color: enteColorScheme.backgroundElevated2,
borderRadius: BorderRadius.circular(4),
),
child: ExpandableNotifier(
controller: expandableController,
child: ScrollOnExpand(
child: ExpandablePanel(
header: MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: widget.title,
makeTextBold: true,
),
isHeaderOfExpansion: true,
leadingIcon: widget.leadingIcon,
trailingIcon: Icons.expand_more,
menuItemColor: enteColorScheme.fillFaint,
expandableController: expandableController,
),
collapsed: const SizedBox.shrink(),
expanded: widget.selectionOptionsWidget,
theme: getExpandableTheme(context),
controller: expandableController,
),
),
),
);
}
}

View file

@ -0,0 +1,140 @@
import 'package:expandable/expandable.dart';
import 'package:flutter/material.dart';
import 'package:photos/ente_theme_data.dart';
class MenuItemWidget extends StatefulWidget {
final Widget captionedTextWidget;
final bool isHeaderOfExpansion;
// leading icon can be passed without specifing size of icon, this component sets size to 20x20 irrespective of passed icon's size
final IconData? leadingIcon;
final Color? leadingIconColor;
// 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 bool trailingIconIsMuted;
final VoidCallback? onTap;
final VoidCallback? onDoubleTap;
final Color? menuItemColor;
final bool alignCaptionedTextToLeft;
final double borderRadius;
final ExpandableController? expandableController;
const MenuItemWidget({
required this.captionedTextWidget,
this.isHeaderOfExpansion = false,
this.leadingIcon,
this.leadingIconColor,
this.trailingIcon,
this.trailingSwitch,
this.trailingIconIsMuted = false,
this.onTap,
this.onDoubleTap,
this.menuItemColor,
this.alignCaptionedTextToLeft = false,
this.borderRadius = 4.0,
this.expandableController,
Key? key,
}) : super(key: key);
@override
State<MenuItemWidget> createState() => _MenuItemWidgetState();
}
class _MenuItemWidgetState extends State<MenuItemWidget> {
@override
void initState() {
if (widget.expandableController != null) {
widget.expandableController!.addListener(() {
setState(() {});
});
}
super.initState();
}
@override
void dispose() {
if (widget.expandableController != null) {
widget.expandableController!.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return widget.isHeaderOfExpansion
? menuItemWidget(context)
: GestureDetector(
onTap: widget.onTap,
onDoubleTap: widget.onDoubleTap,
child: menuItemWidget(context),
);
}
Widget menuItemWidget(BuildContext context) {
final enteColorScheme = Theme.of(context).colorScheme.enteTheme.colorScheme;
final borderRadius = Radius.circular(widget.borderRadius);
final isExpanded = widget.expandableController?.value;
final bottomBorderRadius = isExpanded != null && isExpanded
? const Radius.circular(0)
: borderRadius;
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: borderRadius,
topRight: borderRadius,
bottomLeft: bottomBorderRadius,
bottomRight: bottomBorderRadius,
),
color: widget.menuItemColor,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
widget.alignCaptionedTextToLeft && widget.leadingIcon == null
? const SizedBox.shrink()
: Padding(
padding: const EdgeInsets.only(right: 10),
child: SizedBox(
height: 20,
width: 20,
child: widget.leadingIcon == null
? const SizedBox.shrink()
: FittedBox(
fit: BoxFit.contain,
child: Icon(
widget.leadingIcon,
color: widget.leadingIconColor,
),
),
),
),
widget.captionedTextWidget,
widget.expandableController != null
? AnimatedOpacity(
duration: const Duration(milliseconds: 100),
opacity: isExpanded! ? 0 : 1,
child: AnimatedSwitcher(
transitionBuilder: (child, animation) {
return ScaleTransition(scale: animation, child: child);
},
duration: const Duration(milliseconds: 200),
child: isExpanded!
? const SizedBox.shrink()
: Icon(widget.trailingIcon),
),
)
: widget.trailingIcon != null
? Icon(
widget.trailingIcon,
color: widget.trailingIconIsMuted
? enteColorScheme.strokeMuted
: null,
)
: widget.trailingSwitch ?? const SizedBox.shrink(),
],
),
);
}
}

View file

@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:photos/ente_theme_data.dart';
typedef OnChangedCallBack = void Function(bool);
class ToggleSwitchWidget extends StatefulWidget {
final bool value;
final OnChangedCallBack onChanged;
const ToggleSwitchWidget({
required this.value,
required this.onChanged,
Key? key,
}) : super(key: key);
@override
State<ToggleSwitchWidget> createState() => _ToggleSwitchWidgetState();
}
class _ToggleSwitchWidgetState extends State<ToggleSwitchWidget> {
@override
Widget build(BuildContext context) {
final enteColorScheme = Theme.of(context).colorScheme.enteTheme.colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: SizedBox(
height: 30,
child: FittedBox(
fit: BoxFit.contain,
child: Switch.adaptive(
activeColor: enteColorScheme.primary400,
inactiveTrackColor: enteColorScheme.fillMuted,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
value: widget.value,
onChanged: widget.onChanged,
),
),
),
);
}
}

View file

@ -289,7 +289,9 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
RichText(
text: TextSpan(
text: "Manage family",
style: Theme.of(context).textTheme.overline,
style: Theme.of(context).textTheme.bodyMedium.copyWith(
decoration: TextDecoration.underline,
),
),
textAlign: TextAlign.center,
),

View file

@ -132,7 +132,9 @@ class SubFaqWidget extends StatelessWidget {
child: RichText(
text: TextSpan(
text: "Questions?",
style: Theme.of(context).textTheme.overline,
style: Theme.of(context).textTheme.bodyMedium.copyWith(
decoration: TextDecoration.underline,
),
),
),
),

View file

@ -60,9 +60,8 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
}
void _setupPurchaseUpdateStreamListener() {
_purchaseUpdateSubscription = InAppPurchaseConnection
.instance.purchaseUpdatedStream
.listen((purchases) async {
_purchaseUpdateSubscription =
InAppPurchase.instance.purchaseStream.listen((purchases) async {
if (!_dialog.isShowing()) {
await _dialog.show();
}
@ -74,7 +73,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
purchase.productID,
purchase.verificationData.serverVerificationData,
);
await InAppPurchaseConnection.instance.completePurchase(purchase);
await InAppPurchase.instance.completePurchase(purchase);
String text = "Thank you for subscribing!";
if (!widget.isOnboarding) {
final isUpgrade = _hasActiveSubscription &&
@ -121,7 +120,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
return;
}
} else if (Platform.isIOS && purchase.pendingCompletePurchase) {
await InAppPurchaseConnection.instance.completePurchase(purchase);
await InAppPurchase.instance.completePurchase(purchase);
await _dialog.hide();
} else if (purchase.status == PurchaseStatus.error) {
await _dialog.hide();
@ -172,7 +171,9 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
}).toList();
_freePlan = billingPlans.freePlan;
_hasLoadedData = true;
setState(() {});
if (mounted) {
setState(() {});
}
});
}
@ -294,7 +295,9 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
RichText(
text: TextSpan(
text: "Manage family",
style: Theme.of(context).textTheme.overline,
style: Theme.of(context).textTheme.bodyMedium.copyWith(
decoration: TextDecoration.underline,
),
),
textAlign: TextAlign.center,
),
@ -389,14 +392,13 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
showErrorDialog(
context,
"Sorry",
"you cannot downgrade to this plan",
"You cannot downgrade to this plan",
);
return;
}
await _dialog.show();
final ProductDetailsResponse response =
await InAppPurchaseConnection.instance
.queryProductDetails({productID});
await InAppPurchase.instance.queryProductDetails({productID});
if (response.notFoundIDs.isNotEmpty) {
_logger.severe(
"Could not find products: " + response.notFoundIDs.toString(),
@ -410,34 +412,15 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
_currentSubscription.productID != freeProductID &&
_currentSubscription.productID != plan.androidID;
if (isCrossGradingOnAndroid) {
final existingProductDetailsResponse =
await InAppPurchaseConnection.instance
.queryProductDetails({_currentSubscription.productID});
if (existingProductDetailsResponse.notFoundIDs.isNotEmpty) {
_logger.severe(
"Could not find existing products: " +
response.notFoundIDs.toString(),
);
await _dialog.hide();
showGenericErrorDialog(context);
return;
}
final subscriptionChangeParam = ChangeSubscriptionParam(
oldPurchaseDetails: PurchaseDetails(
purchaseID: null,
productID: _currentSubscription.productID,
verificationData: null,
transactionDate: null,
),
);
await InAppPurchaseConnection.instance.buyNonConsumable(
purchaseParam: PurchaseParam(
productDetails: response.productDetails[0],
changeSubscriptionParam: subscriptionChangeParam,
),
await _dialog.hide();
showErrorDialog(
context,
"Could not update subscription",
"Please contact support@ente.io and we will be happy to help!",
);
return;
} else {
await InAppPurchaseConnection.instance.buyNonConsumable(
await InAppPurchase.instance.buyNonConsumable(
purchaseParam: PurchaseParam(
productDetails: response.productDetails[0],
),

View file

@ -0,0 +1,127 @@
// @dart=2.9
import 'package:flutter/material.dart';
import 'package:photos/services/update_service.dart';
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/app_update_dialog.dart';
import 'package:photos/ui/settings/common_settings.dart';
import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/toast_util.dart';
import 'package:url_launcher/url_launcher.dart';
class AboutSectionWidget extends StatelessWidget {
const AboutSectionWidget({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ExpandableMenuItemWidget(
title: "About",
selectionOptionsWidget: _getSectionOptions(context),
leadingIcon: Icons.info_outline,
);
}
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",
),
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
launchUrl(Uri.parse("https://github.com/ente-io/frame"));
},
),
sectionOptionSpacing,
UpdateService.instance.isIndependent()
? Column(
children: [
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Check for updates",
),
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
final dialog =
createProgressDialog(context, "Checking...");
await dialog.show();
final shouldUpdate =
await UpdateService.instance.shouldUpdate();
await dialog.hide();
if (shouldUpdate) {
showDialog(
context: context,
builder: (BuildContext context) {
return AppUpdateDialog(
UpdateService.instance.getLatestVersionInfo(),
);
},
barrierColor: Colors.black.withOpacity(0.85),
);
} else {
showToast(context, "You are on the latest version");
}
},
),
sectionOptionSpacing,
],
)
: const SizedBox.shrink(),
],
);
}
}
class AboutMenuItemWidget extends StatelessWidget {
final String title;
final String url;
final String webPageTitle;
const AboutMenuItemWidget({
@required this.title,
@required this.url,
this.webPageTitle,
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: title,
),
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return WebPage(webPageTitle ?? title, url);
},
),
);
},
);
}
}

View file

@ -1,6 +1,5 @@
// @dart=2.9
import 'package:expandable/expandable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
import 'package:photos/services/local_authentication_service.dart';
@ -8,35 +7,35 @@ import 'package:photos/services/user_service.dart';
import 'package:photos/ui/account/change_email_dialog.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';
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/ui/settings/settings_section_title.dart';
import 'package:photos/ui/settings/settings_text_item.dart';
import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/navigation_util.dart';
class AccountSectionWidget extends StatefulWidget {
class AccountSectionWidget extends StatelessWidget {
const AccountSectionWidget({Key key}) : super(key: key);
@override
AccountSectionWidgetState createState() => AccountSectionWidgetState();
}
class AccountSectionWidgetState extends State<AccountSectionWidget> {
@override
Widget build(BuildContext context) {
return ExpandablePanel(
header: const SettingsSectionTitle("Account"),
collapsed: const SizedBox.shrink(),
expanded: _getSectionOptions(context),
theme: getExpandableTheme(context),
return ExpandableMenuItemWidget(
title: "Account",
selectionOptionsWidget: _getSectionOptions(context),
leadingIcon: Icons.account_circle_outlined,
);
}
Column _getSectionOptions(BuildContext context) {
return Column(
children: [
GestureDetector(
behavior: HitTestBehavior.translucent,
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Recovery key",
),
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
final hasAuthenticated = await LocalAuthenticationService.instance
.requestLocalAuthentication(
@ -46,7 +45,7 @@ class AccountSectionWidgetState extends State<AccountSectionWidget> {
if (hasAuthenticated) {
String recoveryKey;
try {
recoveryKey = await _getOrCreateRecoveryKey();
recoveryKey = await _getOrCreateRecoveryKey(context);
} catch (e) {
showGenericErrorDialog(context);
return;
@ -62,14 +61,14 @@ class AccountSectionWidgetState extends State<AccountSectionWidget> {
);
}
},
child: const SettingsTextItem(
text: "Recovery key",
icon: Icons.navigate_next,
),
),
sectionOptionDivider,
GestureDetector(
behavior: HitTestBehavior.translucent,
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Change email",
),
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
final hasAuthenticated = await LocalAuthenticationService.instance
.requestLocalAuthentication(
@ -87,14 +86,14 @@ class AccountSectionWidgetState extends State<AccountSectionWidget> {
);
}
},
child: const SettingsTextItem(
text: "Change email",
icon: Icons.navigate_next,
),
),
sectionOptionDivider,
GestureDetector(
behavior: HitTestBehavior.translucent,
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Change password",
),
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
final hasAuthenticated = await LocalAuthenticationService.instance
.requestLocalAuthentication(
@ -113,16 +112,13 @@ class AccountSectionWidgetState extends State<AccountSectionWidget> {
);
}
},
child: const SettingsTextItem(
text: "Change password",
icon: Icons.navigate_next,
),
),
sectionOptionSpacing,
],
);
}
Future<String> _getOrCreateRecoveryKey() async {
Future<String> _getOrCreateRecoveryKey(BuildContext context) async {
return Sodium.bin2hex(
await UserService.instance.getOrCreateRecoveryKey(context),
);

View file

@ -2,7 +2,6 @@
import 'dart:io';
import 'package:expandable/expandable.dart';
import 'package:flutter/material.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/ente_theme_data.dart';
@ -12,9 +11,11 @@ import 'package:photos/services/deduplication_service.dart';
import 'package:photos/services/sync_service.dart';
import 'package:photos/ui/backup_folder_selection_page.dart';
import 'package:photos/ui/common/dialogs.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/components/toggle_switch_widget.dart';
import 'package:photos/ui/settings/common_settings.dart';
import 'package:photos/ui/settings/settings_section_title.dart';
import 'package:photos/ui/settings/settings_text_item.dart';
import 'package:photos/ui/tools/deduplicate_page.dart';
import 'package:photos/ui/tools/free_space_page.dart';
import 'package:photos/utils/data_util.dart';
@ -33,19 +34,23 @@ class BackupSectionWidget extends StatefulWidget {
class BackupSectionWidgetState extends State<BackupSectionWidget> {
@override
Widget build(BuildContext context) {
return ExpandablePanel(
header: const SettingsSectionTitle("Backup"),
collapsed: Container(),
expanded: _getSectionOptions(context),
theme: getExpandableTheme(context),
return ExpandableMenuItemWidget(
title: "Backup",
selectionOptionsWidget: _getSectionOptions(context),
leadingIcon: Icons.backup_outlined,
);
}
Widget _getSectionOptions(BuildContext context) {
final List<Widget> sectionOptions = [
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Backed up folders",
),
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () {
routeToPage(
context,
const BackupFolderSelectionPage(
@ -53,93 +58,72 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
),
);
},
child: const SettingsTextItem(
text: "Backed up folders",
icon: Icons.navigate_next,
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Backup over mobile data",
),
trailingSwitch: ToggleSwitchWidget(
value: Configuration.instance.shouldBackupOverMobileData(),
onChanged: (value) async {
Configuration.instance.setBackupOverMobileData(value);
setState(() {});
},
),
),
sectionOptionDivider,
SizedBox(
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Backup over mobile data",
style: Theme.of(context).textTheme.subtitle1,
),
Switch.adaptive(
value: Configuration.instance.shouldBackupOverMobileData(),
onChanged: (value) async {
Configuration.instance.setBackupOverMobileData(value);
setState(() {});
},
),
],
),
),
sectionOptionDivider,
SizedBox(
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Backup videos",
style: Theme.of(context).textTheme.subtitle1,
),
Switch.adaptive(
value: Configuration.instance.shouldBackupVideos(),
onChanged: (value) async {
Configuration.instance.setShouldBackupVideos(value);
setState(() {});
},
),
],
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Backup videos",
),
trailingSwitch: ToggleSwitchWidget(
value: Configuration.instance.shouldBackupVideos(),
onChanged: (value) async {
Configuration.instance.setShouldBackupVideos(value);
setState(() {});
},
),
),
sectionOptionSpacing,
];
if (Platform.isIOS) {
sectionOptions.addAll([
sectionOptionDivider,
SizedBox(
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Disable auto lock",
style: Theme.of(context).textTheme.subtitle1,
),
Switch.adaptive(
value: Configuration.instance.shouldKeepDeviceAwake(),
onChanged: (value) async {
if (value) {
final choice = await showChoiceDialog(
context,
"Disable automatic screen lock when ente is running?",
"This will ensure faster uploads by ensuring your device does not sleep when uploads are in progress.",
firstAction: "No",
secondAction: "Yes",
);
if (choice != DialogUserChoice.secondChoice) {
return;
}
}
await Configuration.instance.setShouldKeepDeviceAwake(value);
setState(() {});
},
),
],
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Disable auto lock",
),
trailingSwitch: ToggleSwitchWidget(
value: Configuration.instance.shouldKeepDeviceAwake(),
onChanged: (value) async {
if (value) {
final choice = await showChoiceDialog(
context,
"Disable automatic screen lock when ente is running?",
"This will ensure faster uploads by ensuring your device does not sleep when uploads are in progress.",
firstAction: "No",
secondAction: "Yes",
);
if (choice != DialogUserChoice.secondChoice) {
return;
}
}
await Configuration.instance.setShouldKeepDeviceAwake(value);
setState(() {});
},
),
),
sectionOptionSpacing,
]);
}
sectionOptions.addAll(
[
sectionOptionDivider,
GestureDetector(
behavior: HitTestBehavior.translucent,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Free up space",
),
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
final dialog = createProgressDialog(context, "Calculating...");
await dialog.show();
@ -160,20 +144,21 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
"You've no files on this device that can be deleted",
);
} else {
final bool result = await routeToPage(context, FreeSpacePage(status));
final bool result =
await routeToPage(context, FreeSpacePage(status));
if (result == true) {
_showSpaceFreedDialog(status);
}
}
},
child: const SettingsTextItem(
text: "Free up space",
icon: Icons.navigate_next,
),
),
sectionOptionDivider,
GestureDetector(
behavior: HitTestBehavior.translucent,
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Deduplicate files",
),
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
final dialog = createProgressDialog(context, "Calculating...");
await dialog.show();
@ -202,11 +187,8 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
}
}
},
child: const SettingsTextItem(
text: "Deduplicate files",
icon: Icons.navigate_next,
),
),
sectionOptionSpacing,
],
);
return Column(

View file

@ -1,23 +1,15 @@
// @dart=2.9
import 'dart:io';
import 'package:expandable/expandable.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
Widget sectionOptionDivider = Padding(
padding: EdgeInsets.all(Platform.isIOS ? 4 : 2),
);
Widget sectionOptionSpacing = const SizedBox(height: 6);
ExpandableThemeData getExpandableTheme(BuildContext context) {
return ExpandableThemeData(
expandIcon: CupertinoIcons.chevron_down,
collapseIcon: CupertinoIcons.chevron_up,
iconPadding: const EdgeInsets.all(4),
iconColor: Theme.of(context).colorScheme.onSurface,
iconSize: 20.0,
iconRotationAngle: -3.14 / 2,
hasIcon: true,
return const ExpandableThemeData(
hasIcon: false,
useInkWell: false,
tapBodyToCollapse: true,
tapBodyToExpand: true,
animationDuration: Duration(milliseconds: 400),
);
}

View file

@ -1,60 +1,58 @@
// @dart=2.9
import 'package:expandable/expandable.dart';
import 'package:flutter/material.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/services/user_service.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/ui/settings/settings_section_title.dart';
import 'package:photos/ui/settings/settings_text_item.dart';
import 'package:photos/utils/navigation_util.dart';
class DangerSectionWidget extends StatefulWidget {
class DangerSectionWidget extends StatelessWidget {
const DangerSectionWidget({Key key}) : super(key: key);
@override
State<DangerSectionWidget> createState() => _DangerSectionWidgetState();
}
class _DangerSectionWidgetState extends State<DangerSectionWidget> {
@override
Widget build(BuildContext context) {
return ExpandablePanel(
header: const SettingsSectionTitle("Exit", color: Colors.red),
collapsed: Container(),
expanded: _getSectionOptions(context),
theme: getExpandableTheme(context),
return ExpandableMenuItemWidget(
title: "Exit",
selectionOptionsWidget: _getSectionOptions(context),
leadingIcon: Icons.logout_outlined,
);
}
Widget _getSectionOptions(BuildContext context) {
return Column(
children: [
GestureDetector(
behavior: HitTestBehavior.translucent,
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Logout",
),
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () {
_onLogoutTapped();
_onLogoutTapped(context);
},
child:
const SettingsTextItem(text: "Logout", icon: Icons.navigate_next),
),
sectionOptionDivider,
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Delete account",
),
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () {
routeToPage(context, const DeleteAccountPage());
},
child: const SettingsTextItem(
text: "Delete account",
icon: Icons.navigate_next,
),
),
sectionOptionSpacing,
],
);
}
Future<void> _onLogoutTapped() async {
Future<void> _onLogoutTapped(BuildContext context) async {
final AlertDialog alert = AlertDialog(
title: const Text(
"Logout",

View file

@ -1,65 +1,69 @@
// @dart=2.9
import 'package:expandable/expandable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/services/ignored_files_service.dart';
import 'package:photos/services/local_sync_service.dart';
import 'package:photos/services/sync_service.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/ui/settings/settings_section_title.dart';
import 'package:photos/ui/settings/settings_text_item.dart';
import 'package:photos/utils/toast_util.dart';
class DebugSectionWidget extends StatelessWidget {
const DebugSectionWidget({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ExpandablePanel(
header: const SettingsSectionTitle("Debug"),
collapsed: Container(),
expanded: _getSectionOptions(context),
theme: getExpandableTheme(context),
return ExpandableMenuItemWidget(
title: "Debug",
selectionOptionsWidget: _getSectionOptions(context),
leadingIcon: Icons.bug_report_outlined,
);
}
Widget _getSectionOptions(BuildContext context) {
return Column(
children: [
GestureDetector(
behavior: HitTestBehavior.translucent,
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Key attributes",
),
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
_showKeyAttributesDialog(context);
},
child: const SettingsTextItem(
text: "Key attributes",
icon: Icons.navigate_next,
),
),
GestureDetector(
behavior: HitTestBehavior.translucent,
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Delete Local Import DB",
),
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
await LocalSyncService.instance.resetLocalSync();
showToast(context, "Done");
},
child: const SettingsTextItem(
text: "Delete Local Import DB",
icon: Icons.navigate_next,
),
),
GestureDetector(
behavior: HitTestBehavior.translucent,
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Allow auto-upload for ignored files",
),
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
await IgnoredFilesService.instance.reset();
SyncService.instance.sync();
showToast(context, "Done");
},
child: const SettingsTextItem(
text: "Allow auto-upload for ignored files",
icon: Icons.navigate_next,
),
),
sectionOptionSpacing,
],
);
}

View file

@ -1,45 +1,27 @@
// @dart=2.9
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/events/tab_changed_event.dart';
import 'package:photos/events/user_details_changed_event.dart';
import 'package:logging/logging.dart';
import 'package:photos/models/user_details.dart';
import 'package:photos/services/user_service.dart';
import 'package:photos/states/user_details_state.dart';
import 'package:photos/ui/common/loading_widget.dart';
// ignore: import_of_legacy_library_into_null_safe
import 'package:photos/ui/payment/subscription.dart';
import 'package:photos/utils/data_util.dart';
class DetailsSectionWidget extends StatefulWidget {
const DetailsSectionWidget({Key key}) : super(key: key);
const DetailsSectionWidget({Key? key}) : super(key: key);
@override
State<DetailsSectionWidget> createState() => _DetailsSectionWidgetState();
}
class _DetailsSectionWidgetState extends State<DetailsSectionWidget> {
UserDetails _userDetails;
StreamSubscription<UserDetailsChangedEvent> _userDetailsChangedEvent;
StreamSubscription<TabChangedEvent> _tabChangedEventSubscription;
Image _background;
late Image _background;
final _logger = Logger((_DetailsSectionWidgetState).toString());
@override
void initState() {
super.initState();
_fetchUserDetails();
_userDetailsChangedEvent =
Bus.instance.on<UserDetailsChangedEvent>().listen((event) {
_fetchUserDetails();
});
_tabChangedEventSubscription =
Bus.instance.on<TabChangedEvent>().listen((event) {
if (event.selectedIndex == 3) {
_fetchUserDetails();
}
});
_background = const Image(
image: AssetImage("assets/storage_card_background.png"),
fit: BoxFit.fill,
@ -54,45 +36,40 @@ class _DetailsSectionWidgetState extends State<DetailsSectionWidget> {
precacheImage(_background.image, context);
}
void _fetchUserDetails() {
UserService.instance.getUserDetailsV2(memoryCount: true).then((details) {
if (mounted) {
setState(() {
_userDetails = details;
});
}
});
}
@override
void dispose() {
_userDetailsChangedEvent.cancel();
_tabChangedEventSubscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return getSubscriptionPage();
},
),
);
},
child: getContainer(),
);
final inheritedUserDetails = InheritedUserDetails.of(context);
if (inheritedUserDetails == null) {
_logger.severe(
(InheritedUserDetails).toString() +
' not found before ' +
(_DetailsSectionWidgetState).toString() +
' on tree',
);
throw Error();
} else {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return getSubscriptionPage();
},
),
);
},
child: containerForUserDetails(inheritedUserDetails),
);
}
}
Widget getContainer() {
return SizedBox(
width: 350,
height: 175,
// constraints: BoxConstraints(maxWidth: 390, maxHeight: 195),
Widget containerForUserDetails(
InheritedUserDetails inheritedUserDetails,
) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 428, maxHeight: 175),
child: Stack(
children: [
Container(
@ -103,162 +80,19 @@ class _DetailsSectionWidgetState extends State<DetailsSectionWidget> {
child: _background,
),
),
_userDetails == null
? const EnteLoadingWidget()
: Padding(
padding: const EdgeInsets.only(
top: 20,
bottom: 20,
left: 16,
right: 16,
),
child: Container(
color: Colors.transparent,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Align(
alignment: Alignment.topLeft,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Storage",
style: Theme.of(context)
.textTheme
.subtitle2
.copyWith(
color: Colors.white.withOpacity(0.7),
),
),
Text(
"${convertBytesToReadableFormat(_userDetails.getFreeStorage())} of ${convertBytesToReadableFormat(_userDetails.getTotalStorage())} free",
style: Theme.of(context)
.textTheme
.headline5
.copyWith(color: Colors.white),
),
],
),
),
Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Stack(
children: <Widget>[
Container(
color: Colors.white.withOpacity(0.2),
width: MediaQuery.of(context).size.width,
height: 4,
),
Container(
color: Colors.white.withOpacity(0.75),
width: MediaQuery.of(context).size.width *
((_userDetails
.getFamilyOrPersonalUsage()) /
_userDetails.getTotalStorage()),
height: 4,
),
Container(
color: Colors.white,
width: MediaQuery.of(context).size.width *
(_userDetails.usage /
_userDetails.getTotalStorage()),
height: 4,
),
],
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_userDetails.isPartOfFamily()
? Row(
children: [
Container(
width: 8.71,
height: 8.99,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
),
),
const Padding(
padding: EdgeInsets.only(right: 4),
),
Text(
"You",
style: Theme.of(context)
.textTheme
.bodyText1
.copyWith(
color: Colors.white,
fontSize: 12,
),
),
const Padding(
padding: EdgeInsets.only(right: 12),
),
Container(
width: 8.71,
height: 8.99,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white
.withOpacity(0.75),
),
),
const Padding(
padding: EdgeInsets.only(right: 4),
),
Text(
"Family",
style: Theme.of(context)
.textTheme
.bodyText1
.copyWith(
color: Colors.white,
fontSize: 12,
),
),
],
)
: Text(
"${convertBytesToReadableFormat(_userDetails.getFamilyOrPersonalUsage())} used",
style: Theme.of(context)
.textTheme
.bodyText1
.copyWith(
color: Colors.white,
fontSize: 12,
),
),
Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Text(
"${NumberFormat().format(_userDetails.fileCount)} Memories",
style: Theme.of(context)
.textTheme
.bodyText1
.copyWith(
color: Colors.white,
fontSize: 12,
),
),
)
],
),
],
)
],
),
),
),
FutureBuilder(
future: inheritedUserDetails.userDetails,
builder: (context, snapshot) {
if (snapshot.hasData) {
return userDetails(snapshot.data as UserDetails);
}
if (snapshot.hasError) {
_logger.severe('failed to load user details', snapshot.error);
return const EnteLoadingWidget();
}
return const EnteLoadingWidget();
},
),
const Align(
alignment: Alignment.centerRight,
child: Icon(
@ -271,4 +105,150 @@ class _DetailsSectionWidgetState extends State<DetailsSectionWidget> {
),
);
}
Widget userDetails(UserDetails userDetails) {
return Padding(
padding: const EdgeInsets.only(
top: 20,
bottom: 20,
left: 16,
right: 16,
),
child: Container(
color: Colors.transparent,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Align(
alignment: Alignment.topLeft,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Storage",
style: Theme.of(context).textTheme.subtitle2!.copyWith(
color: Colors.white.withOpacity(0.7),
),
),
Text(
"${convertBytesToReadableFormat(userDetails.getFreeStorage())} of ${convertBytesToReadableFormat(userDetails.getTotalStorage())} free",
style: Theme.of(context)
.textTheme
.headline5!
.copyWith(color: Colors.white),
),
],
),
),
Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Stack(
children: <Widget>[
Container(
color: Colors.white.withOpacity(0.2),
width: MediaQuery.of(context).size.width,
height: 4,
),
Container(
color: Colors.white.withOpacity(0.75),
width: MediaQuery.of(context).size.width *
((userDetails.getFamilyOrPersonalUsage()) /
userDetails.getTotalStorage()),
height: 4,
),
Container(
color: Colors.white,
width: MediaQuery.of(context).size.width *
(userDetails.usage / userDetails.getTotalStorage()),
height: 4,
),
],
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
userDetails.isPartOfFamily()
? Row(
children: [
Container(
width: 8.71,
height: 8.99,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
),
),
const Padding(
padding: EdgeInsets.only(right: 4),
),
Text(
"You",
style: Theme.of(context)
.textTheme
.bodyText1!
.copyWith(
color: Colors.white,
fontSize: 12,
),
),
const Padding(
padding: EdgeInsets.only(right: 12),
),
Container(
width: 8.71,
height: 8.99,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withOpacity(0.75),
),
),
const Padding(
padding: EdgeInsets.only(right: 4),
),
Text(
"Family",
style: Theme.of(context)
.textTheme
.bodyText1!
.copyWith(
color: Colors.white,
fontSize: 12,
),
),
],
)
: Text(
"${convertBytesToReadableFormat(userDetails.getFamilyOrPersonalUsage())} used",
style:
Theme.of(context).textTheme.bodyText1!.copyWith(
color: Colors.white,
fontSize: 12,
),
),
Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Text(
"${NumberFormat().format(userDetails.fileCount)} Memories",
style: Theme.of(context).textTheme.bodyText1!.copyWith(
color: Colors.white,
fontSize: 12,
),
),
)
],
),
],
)
],
),
),
);
}
}

View file

@ -1,125 +0,0 @@
// @dart=2.9
import 'package:expandable/expandable.dart';
import 'package:flutter/material.dart';
import 'package:photos/services/update_service.dart';
import 'package:photos/ui/common/web_page.dart';
import 'package:photos/ui/settings/app_update_dialog.dart';
import 'package:photos/ui/settings/common_settings.dart';
import 'package:photos/ui/settings/settings_section_title.dart';
import 'package:photos/ui/settings/settings_text_item.dart';
import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/toast_util.dart';
import 'package:url_launcher/url_launcher.dart';
class InfoSectionWidget extends StatelessWidget {
const InfoSectionWidget({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ExpandablePanel(
header: const SettingsSectionTitle("About"),
collapsed: Container(),
expanded: _getSectionOptions(context),
theme: getExpandableTheme(context),
);
}
Widget _getSectionOptions(BuildContext context) {
return Column(
children: [
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return const WebPage("FAQ", "https://ente.io/faq");
},
),
);
},
child: const SettingsTextItem(text: "FAQ", icon: Icons.navigate_next),
),
sectionOptionDivider,
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return const WebPage("terms", "https://ente.io/terms");
},
),
);
},
child:
const SettingsTextItem(text: "Terms", icon: Icons.navigate_next),
),
sectionOptionDivider,
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return const WebPage("privacy", "https://ente.io/privacy");
},
),
);
},
child: const SettingsTextItem(
text: "Privacy",
icon: Icons.navigate_next,
),
),
sectionOptionDivider,
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
launchUrl(Uri.parse("https://github.com/ente-io/frame"));
},
child: const SettingsTextItem(
text: "Source code",
icon: Icons.navigate_next,
),
),
sectionOptionDivider,
UpdateService.instance.isIndependent()
? Column(
children: [
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
final dialog =
createProgressDialog(context, "Checking...");
await dialog.show();
final shouldUpdate =
await UpdateService.instance.shouldUpdate();
await dialog.hide();
if (shouldUpdate) {
showDialog(
context: context,
builder: (BuildContext context) {
return AppUpdateDialog(
UpdateService.instance.getLatestVersionInfo(),
);
},
barrierColor: Colors.black.withOpacity(0.85),
);
} else {
showToast(context, "You are on the latest version");
}
},
child: const SettingsTextItem(
text: "Check for updates",
icon: Icons.navigate_next,
),
),
],
)
: const SizedBox.shrink(),
],
);
}
}

View file

@ -3,7 +3,6 @@
import 'dart:async';
import 'dart:io';
import 'package:expandable/expandable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_windowmanager/flutter_windowmanager.dart';
import 'package:photos/core/configuration.dart';
@ -14,9 +13,11 @@ import 'package:photos/services/local_authentication_service.dart';
import 'package:photos/services/user_service.dart';
import 'package:photos/ui/account/sessions_page.dart';
import 'package:photos/ui/common/loading_widget.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/components/toggle_switch_widget.dart';
import 'package:photos/ui/settings/common_settings.dart';
import 'package:photos/ui/settings/settings_section_title.dart';
import 'package:photos/ui/settings/settings_text_item.dart';
class SecuritySectionWidget extends StatefulWidget {
const SecuritySectionWidget({Key key}) : super(key: key);
@ -49,11 +50,10 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
@override
Widget build(BuildContext context) {
return ExpandablePanel(
header: const SettingsSectionTitle("Security"),
collapsed: Container(),
expanded: _getSectionOptions(context),
theme: getExpandableTheme(context),
return ExpandableMenuItemWidget(
title: "Security",
selectionOptionsWidget: _getSectionOptions(context),
leadingIcon: Icons.local_police_outlined,
);
}
@ -62,21 +62,16 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
if (_config.hasConfiguredAccount()) {
children.addAll(
[
const Padding(padding: EdgeInsets.all(2)),
SizedBox(
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Two-factor",
style: Theme.of(context).textTheme.subtitle1,
sectionOptionSpacing,
FutureBuilder(
future: UserService.instance.fetchTwoFactorStatus(),
builder: (_, snapshot) {
return MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Two-factor",
),
FutureBuilder(
future: UserService.instance.fetchTwoFactorStatus(),
builder: (_, snapshot) {
if (snapshot.hasData) {
return Switch.adaptive(
trailingSwitch: snapshot.hasData
? ToggleSwitchWidget(
value: snapshot.data,
onChanged: (value) async {
final hasAuthenticated =
@ -93,155 +88,136 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
}
}
},
);
} else if (snapshot.hasError) {
return Icon(
Icons.error_outline,
color: Colors.white.withOpacity(0.8),
);
}
return const EnteLoadingWidget();
},
),
],
),
)
: snapshot.hasError
? const Icon(Icons.error_outline_outlined)
: const EnteLoadingWidget(),
);
},
),
sectionOptionSpacing,
],
);
}
children.addAll([
sectionOptionDivider,
SizedBox(
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Lockscreen",
style: Theme.of(context).textTheme.subtitle1,
),
Switch.adaptive(
value: _config.shouldShowLockScreen(),
onChanged: (value) async {
final hasAuthenticated = await LocalAuthenticationService
.instance
.requestLocalAuthForLockScreen(
context,
value,
"Please authenticate to change lockscreen setting",
"To enable lockscreen, please setup device passcode or screen lock in your system settings.",
);
if (hasAuthenticated) {
setState(() {});
}
},
),
],
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Lockscreen",
),
trailingSwitch: ToggleSwitchWidget(
value: _config.shouldShowLockScreen(),
onChanged: (value) async {
final hasAuthenticated = await LocalAuthenticationService.instance
.requestLocalAuthForLockScreen(
context,
value,
"Please authenticate to change lockscreen setting",
"To enable lockscreen, please setup device passcode or screen lock in your system settings.",
);
if (hasAuthenticated) {
setState(() {});
}
},
),
),
sectionOptionSpacing,
]);
if (Platform.isAndroid) {
children.addAll(
[
sectionOptionDivider,
SizedBox(
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Hide from recents",
style: Theme.of(context).textTheme.subtitle1,
),
Switch.adaptive(
value: _config.shouldHideFromRecents(),
onChanged: (value) async {
if (value) {
final AlertDialog alert = AlertDialog(
title: const Text("Hide from recents?"),
content: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
"Hiding from the task switcher will prevent you from taking screenshots in this app.",
style: TextStyle(
height: 1.5,
),
),
Padding(padding: EdgeInsets.all(8)),
Text(
"Are you sure?",
style: TextStyle(
height: 1.5,
),
),
],
),
),
actions: [
TextButton(
child: Text(
"No",
style: TextStyle(
color: Theme.of(context)
.colorScheme
.defaultTextColor,
),
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Hide from recents",
),
trailingSwitch: Switch.adaptive(
value: _config.shouldHideFromRecents(),
onChanged: (value) async {
if (value) {
final AlertDialog alert = AlertDialog(
title: const Text("Hide from recents?"),
content: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
"Hiding from the task switcher will prevent you from taking screenshots in this app.",
style: TextStyle(
height: 1.5,
),
onPressed: () {
Navigator.of(context, rootNavigator: true)
.pop('dialog');
},
),
TextButton(
child: Text(
"Yes",
style: TextStyle(
color: Theme.of(context)
.colorScheme
.defaultTextColor,
),
Padding(padding: EdgeInsets.all(8)),
Text(
"Are you sure?",
style: TextStyle(
height: 1.5,
),
onPressed: () async {
Navigator.of(context, rootNavigator: true)
.pop('dialog');
await _config.setShouldHideFromRecents(true);
await FlutterWindowManager.addFlags(
FlutterWindowManager.FLAG_SECURE,
);
setState(() {});
},
),
],
);
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
),
),
actions: [
TextButton(
child: Text(
"No",
style: TextStyle(
color:
Theme.of(context).colorScheme.defaultTextColor,
),
),
onPressed: () {
Navigator.of(context, rootNavigator: true)
.pop('dialog');
},
);
} else {
await _config.setShouldHideFromRecents(false);
await FlutterWindowManager.clearFlags(
FlutterWindowManager.FLAG_SECURE,
);
setState(() {});
}
},
),
],
),
TextButton(
child: Text(
"Yes",
style: TextStyle(
color:
Theme.of(context).colorScheme.defaultTextColor,
),
),
onPressed: () async {
Navigator.of(context, rootNavigator: true)
.pop('dialog');
await _config.setShouldHideFromRecents(true);
await FlutterWindowManager.addFlags(
FlutterWindowManager.FLAG_SECURE,
);
setState(() {});
},
),
],
);
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
} else {
await _config.setShouldHideFromRecents(false);
await FlutterWindowManager.clearFlags(
FlutterWindowManager.FLAG_SECURE,
);
setState(() {});
}
},
),
),
sectionOptionSpacing,
],
);
}
children.addAll([
sectionOptionDivider,
GestureDetector(
behavior: HitTestBehavior.translucent,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Active sessions",
),
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
final hasAuthenticated = await LocalAuthenticationService.instance
.requestLocalAuthentication(
@ -258,11 +234,8 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
);
}
},
child: const SettingsTextItem(
text: "Active sessions",
icon: Icons.navigate_next,
),
),
sectionOptionSpacing,
]);
return Column(
children: children,

View file

@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/events/tab_changed_event.dart';
import 'package:photos/models/user_details.dart';
import 'package:photos/states/user_details_state.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/common/loading_widget.dart';
class SettingsTitleBarWidget extends StatelessWidget {
const SettingsTitleBarWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final logger = Logger((SettingsTitleBarWidget).toString());
return Container(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 20, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
visualDensity: const VisualDensity(horizontal: -2, vertical: -2),
onPressed: () {
Bus.instance.fire(
TabChangedEvent(
0,
TabChangedEventSource.settingsTitleBar,
),
);
},
icon: const Icon(Icons.keyboard_double_arrow_left_outlined),
),
FutureBuilder(
future: InheritedUserDetails.of(context)?.userDetails,
builder: (context, snapshot) {
if (InheritedUserDetails.of(context) == null) {
logger.severe(
(InheritedUserDetails).toString() +
' not found before ' +
(SettingsTitleBarWidget).toString() +
' on tree',
);
throw Error();
}
if (snapshot.hasData) {
final userDetails = snapshot.data as UserDetails;
return Text(
"${NumberFormat().format(userDetails.fileCount)} Memories",
style: getEnteTextTheme(context).largeBold,
);
}
if (snapshot.hasError) {
logger.severe('failed to load user details');
return const EnteLoadingWidget();
} else {
return const EnteLoadingWidget();
}
},
)
],
),
),
);
}
}

View file

@ -2,12 +2,12 @@
import 'dart:io';
import 'package:expandable/expandable.dart';
import 'package:flutter/material.dart';
import 'package:photos/services/update_service.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/ui/settings/settings_section_title.dart';
import 'package:photos/ui/settings/settings_text_item.dart';
import 'package:url_launcher/url_launcher_string.dart';
class SocialSectionWidget extends StatelessWidget {
@ -15,68 +15,57 @@ class SocialSectionWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ExpandablePanel(
header: const SettingsSectionTitle("Social"),
collapsed: Container(),
expanded: _getSectionOptions(context),
theme: getExpandableTheme(context),
return ExpandableMenuItemWidget(
title: "Social",
selectionOptionsWidget: _getSectionOptions(context),
leadingIcon: Icons.interests_outlined,
);
}
Widget _getSectionOptions(BuildContext context) {
final List<Widget> options = [
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
launchUrlString("https://twitter.com/enteio");
},
child:
const SettingsTextItem(text: "Twitter", icon: Icons.navigate_next),
),
sectionOptionDivider,
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
launchUrlString("https://ente.io/discord");
},
child:
const SettingsTextItem(text: "Discord", icon: Icons.navigate_next),
),
sectionOptionDivider,
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
launchUrlString("https://reddit.com/r/enteio");
},
child:
const SettingsTextItem(text: "Reddit", icon: Icons.navigate_next),
),
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,
];
if (!UpdateService.instance.isIndependent()) {
options.addAll(
[
sectionOptionDivider,
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
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",
);
}
},
child: const SettingsTextItem(
text: "Rate us! ✨",
icon: Icons.navigate_next,
),
)
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",
),
sectionOptionSpacing,
],
);
}
return Column(children: options);
}
}
class SocialsMenuItemWidget extends StatelessWidget {
final String text;
final String urlSring;
const SocialsMenuItemWidget(this.text, this.urlSring, {Key key})
: super(key: key);
@override
Widget build(BuildContext context) {
return MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: text,
),
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () {
launchUrlString(urlSring);
},
);
}
}

View file

@ -2,14 +2,14 @@
import 'dart:io';
import 'package:expandable/expandable.dart';
import 'package:flutter/material.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/core/constants.dart';
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/common_settings.dart';
import 'package:photos/ui/settings/settings_section_title.dart';
import 'package:photos/ui/settings/settings_text_item.dart';
import 'package:photos/utils/email_util.dart';
class SupportSectionWidget extends StatelessWidget {
@ -17,11 +17,10 @@ class SupportSectionWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ExpandablePanel(
header: const SettingsSectionTitle("Support"),
collapsed: Container(),
expanded: _getSectionOptions(context),
theme: getExpandableTheme(context),
return ExpandableMenuItemWidget(
title: "Support",
selectionOptionsWidget: _getSectionOptions(context),
leadingIcon: Icons.help_outline_outlined,
);
}
@ -30,17 +29,24 @@ class SupportSectionWidget extends StatelessWidget {
Platform.isAndroid ? "android-bugs@ente.io" : "ios-bugs@ente.io";
return Column(
children: [
GestureDetector(
behavior: HitTestBehavior.translucent,
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Email",
),
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
await sendEmail(context, to: supportEmail);
},
child:
const SettingsTextItem(text: "Email", icon: Icons.navigate_next),
),
sectionOptionDivider,
GestureDetector(
behavior: HitTestBehavior.translucent,
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Roadmap",
),
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
@ -56,14 +62,14 @@ class SupportSectionWidget extends StatelessWidget {
),
);
},
child: const SettingsTextItem(
text: "Roadmap",
icon: Icons.navigate_next,
),
),
sectionOptionDivider,
GestureDetector(
behavior: HitTestBehavior.translucent,
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Report a bug",
),
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
await sendLogs(context, "Report bug", bugsEmail);
},
@ -71,11 +77,8 @@ class SupportSectionWidget extends StatelessWidget {
final zipFilePath = await getZippedLogsFile(context);
await shareLogs(context, bugsEmail, zipFilePath);
},
child: const SettingsTextItem(
text: "Report bug 🐞",
icon: Icons.navigate_next,
),
),
sectionOptionSpacing,
],
);
}

View file

@ -1,8 +1,13 @@
// @dart=2.9
import 'package:adaptive_theme/adaptive_theme.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:photos/ente_theme_data.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';
class ThemeSwitchWidget extends StatefulWidget {
const ThemeSwitchWidget({Key key}) : super(key: key);
@ -12,13 +17,14 @@ class ThemeSwitchWidget extends StatefulWidget {
}
class _ThemeSwitchWidgetState extends State<ThemeSwitchWidget> {
AdaptiveThemeMode themeMode;
AdaptiveThemeMode currentThemeMode;
@override
void initState() {
super.initState();
AdaptiveTheme.getThemeMode().then(
(value) {
themeMode = value ?? AdaptiveThemeMode.system;
currentThemeMode = value ?? AdaptiveThemeMode.system;
debugPrint('theme value $value');
if (mounted) {
setState(() => {});
@ -27,44 +33,51 @@ class _ThemeSwitchWidgetState extends State<ThemeSwitchWidget> {
);
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
return ExpandableMenuItemWidget(
title: "Theme",
selectionOptionsWidget: _getSectionOptions(context),
leadingIcon: Theme.of(context).brightness == Brightness.light
? Icons.light_mode_outlined
: Icons.dark_mode_outlined,
);
}
Widget _getSectionOptions(BuildContext context) {
return Column(
children: [
sectionOptionSpacing,
_menuItem(context, AdaptiveThemeMode.light),
sectionOptionSpacing,
_menuItem(context, AdaptiveThemeMode.dark),
sectionOptionSpacing,
_menuItem(context, AdaptiveThemeMode.system),
sectionOptionSpacing,
],
);
}
Widget _menuItem(BuildContext context, AdaptiveThemeMode themeMode) {
return MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: toBeginningOfSentenceCase(themeMode.name),
textStyle: Theme.of(context).colorScheme.enteTheme.textTheme.body,
),
isHeaderOfExpansion: false,
trailingIcon: currentThemeMode == themeMode ? Icons.check : null,
onTap: () async {
await showCupertinoModalPopup(
context: context,
builder: (_) => CupertinoActionSheet(
title: Text(
"Theme",
style: Theme.of(context)
.textTheme
.headline4
.copyWith(color: Colors.white),
),
actions: [
for (var mode in AdaptiveThemeMode.values)
CupertinoActionSheetAction(
child: Text(mode.modeName),
onPressed: () async {
AdaptiveTheme.of(context).setThemeMode(mode);
themeMode = mode;
Navigator.of(context, rootNavigator: true).pop();
if (mounted) {
setState(() => {});
}
},
),
],
cancelButton: CupertinoActionSheetAction(
child: const Text("Cancel"),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop();
},
),
),
);
AdaptiveTheme.of(context).setThemeMode(themeMode);
currentThemeMode = themeMode;
if (mounted) {
setState(() {});
}
},
child: Text(themeMode?.modeName ?? ">"),
);
}
}

View file

@ -6,15 +6,18 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/services/feature_flag_service.dart';
import 'package:photos/states/user_details_state.dart';
import 'package:photos/theme/colors.dart';
import 'package:photos/theme/ente_theme.dart';
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/details_section_widget.dart';
import 'package:photos/ui/settings/info_section_widget.dart';
import 'package:photos/ui/settings/security_section_widget.dart';
import 'package:photos/ui/settings/settings_section_title.dart';
import 'package:photos/ui/settings/settings_title_bar_widget.dart';
import 'package:photos/ui/settings/social_section_widget.dart';
import 'package:photos/ui/settings/support_section_widget.dart';
import 'package:photos/ui/settings/theme_switch_widget.dart';
@ -25,17 +28,22 @@ class SettingsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final enteColorScheme = getEnteColorScheme(context);
return Scaffold(
body: _getBody(context),
body: Container(
color: enteColorScheme.backgroundElevated,
child: _getBody(context, enteColorScheme),
),
);
}
Widget _getBody(BuildContext context) {
Widget _getBody(BuildContext context, EnteColorScheme colorScheme) {
final hasLoggedIn = Configuration.instance.getToken() != null;
final enteTextTheme = getEnteTextTheme(context);
final List<Widget> contents = [];
contents.add(
Container(
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Align(
alignment: Alignment.centerLeft,
child: AnimatedBuilder(
@ -44,70 +52,57 @@ class SettingsPage extends StatelessWidget {
builder: (BuildContext context, Widget child) {
return Text(
emailNotifier.value,
style: Theme.of(context)
.textTheme
.subtitle1
.copyWith(overflow: TextOverflow.ellipsis),
style: enteTextTheme.body.copyWith(
color: colorScheme.textMuted,
overflow: TextOverflow.ellipsis,
),
);
},
),
),
),
);
final sectionDivider = Divider(
height: 20,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.12),
);
contents.add(const Padding(padding: EdgeInsets.all(4)));
const sectionSpacing = SizedBox(height: 8);
contents.add(const SizedBox(height: 8));
if (hasLoggedIn) {
contents.addAll([
const DetailsSectionWidget(),
const Padding(padding: EdgeInsets.only(bottom: 24)),
const SizedBox(height: 12),
const BackupSectionWidget(),
sectionDivider,
sectionSpacing,
const AccountSectionWidget(),
sectionDivider,
sectionSpacing,
]);
}
contents.addAll([
const SecuritySectionWidget(),
sectionDivider,
sectionSpacing,
]);
if (Platform.isAndroid || kDebugMode) {
contents.addAll([
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: const [
SettingsSectionTitle("Theme"),
Padding(
padding: EdgeInsets.only(right: 4),
child: ThemeSwitchWidget(),
),
],
),
sectionDivider,
const ThemeSwitchWidget(),
sectionSpacing,
]);
}
contents.addAll([
const SupportSectionWidget(),
sectionDivider,
sectionSpacing,
const SocialSectionWidget(),
sectionDivider,
const InfoSectionWidget(),
sectionSpacing,
const AboutSectionWidget(),
]);
if (hasLoggedIn) {
contents.addAll([
sectionDivider,
sectionSpacing,
const DangerSectionWidget(),
]);
}
if (FeatureFlagService.instance.isInternalUserOrDebugBuild() &&
hasLoggedIn) {
contents.addAll([sectionDivider, const DebugSectionWidget()]);
contents.addAll([sectionSpacing, const DebugSectionWidget()]);
}
contents.add(const AppVersionWidget());
contents.add(
@ -116,16 +111,24 @@ class SettingsPage extends StatelessWidget {
),
);
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 20),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 350),
child: Column(
children: contents,
return UserDetailsStateWidget(
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SettingsTitleBarWidget(),
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 428),
child: Column(
children: contents,
),
),
),
),
),
],
),
),
);

View file

@ -49,6 +49,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
Function() _selectedFilesListener;
String _appBarTitle;
final GlobalKey shareButtonKey = GlobalKey();
@override
void initState() {
_selectedFilesListener = () {
@ -125,6 +126,33 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
}
}
Future<dynamic> _leaveAlbum(BuildContext context) async {
final DialogUserChoice result = await showChoiceDialog(
context,
"Leave shared album?",
"You will leave the album, and it will stop being visible to you.",
firstAction: "Cancel",
secondAction: "Yes, Leave",
secondActionColor:
Theme.of(context).colorScheme.enteTheme.colorScheme.warning700,
);
if (result != DialogUserChoice.secondChoice) {
return;
}
final dialog = createProgressDialog(context, "Leaving album...");
await dialog.show();
try {
await CollectionsService.instance.leaveAlbum(widget.collection);
await dialog.hide();
if (mounted) {
Navigator.of(context).pop();
}
} catch (e) {
await dialog.hide();
showGenericErrorDialog(context);
}
}
List<Widget> _getDefaultActions(BuildContext context) {
final List<Widget> actions = <Widget>[];
if (Configuration.instance.hasConfiguredAccount() &&
@ -158,78 +186,101 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
),
);
}
final List<PopupMenuItem> items = [];
if (widget.type == GalleryType.ownedCollection) {
actions.add(
PopupMenuButton(
itemBuilder: (context) {
final List<PopupMenuItem> items = [];
if (widget.collection.type != CollectionType.favorites) {
items.add(
PopupMenuItem(
value: 1,
child: Row(
children: const [
Icon(Icons.edit),
Padding(
padding: EdgeInsets.all(8),
),
Text("Rename album"),
],
),
),
);
}
final bool isArchived = widget.collection.isArchived();
items.add(
PopupMenuItem(
value: 2,
child: Row(
children: [
Icon(isArchived ? Icons.visibility : Icons.visibility_off),
const Padding(
padding: EdgeInsets.all(8),
),
Text(isArchived ? "Unhide album" : "Hide album"),
],
if (widget.collection.type != CollectionType.favorites) {
items.add(
PopupMenuItem(
value: 1,
child: Row(
children: const [
Icon(Icons.edit),
Padding(
padding: EdgeInsets.all(8),
),
Text("Rename album"),
],
),
),
);
}
final bool isArchived = widget.collection.isArchived();
items.add(
PopupMenuItem(
value: 2,
child: Row(
children: [
Icon(isArchived ? Icons.visibility : Icons.visibility_off),
const Padding(
padding: EdgeInsets.all(8),
),
);
if (widget.collection.type != CollectionType.favorites) {
items.add(
PopupMenuItem(
value: 3,
child: Row(
children: const [
Icon(Icons.delete_outline),
Padding(
padding: EdgeInsets.all(8),
),
Text("Delete album"),
],
),
Text(isArchived ? "Unhide album" : "Hide album"),
],
),
),
);
if (widget.collection.type != CollectionType.favorites) {
items.add(
PopupMenuItem(
value: 3,
child: Row(
children: const [
Icon(Icons.delete_outline),
Padding(
padding: EdgeInsets.all(8),
),
);
}
return items;
},
onSelected: (value) async {
if (value == 1) {
await _renameAlbum(context);
} else if (value == 2) {
await changeCollectionVisibility(
context,
widget.collection,
widget.collection.isArchived()
? visibilityVisible
: visibilityArchive,
);
} else if (value == 3) {
await _trashCollection();
}
},
Text("Delete album"),
],
),
),
);
}
} // ownedCollection open ends
if (widget.type == GalleryType.sharedCollection) {
items.add(
PopupMenuItem(
value: 4,
child: Row(
children: const [
Icon(Icons.logout),
Padding(
padding: EdgeInsets.all(8),
),
Text("Leave album"),
],
),
),
);
}
actions.add(
PopupMenuButton(
itemBuilder: (context) {
return items;
},
onSelected: (value) async {
if (value == 1) {
await _renameAlbum(context);
} else if (value == 2) {
await changeCollectionVisibility(
context,
widget.collection,
widget.collection.isArchived()
? visibilityVisible
: visibilityArchive,
);
} else if (value == 3) {
await _trashCollection();
} else if (value == 4) {
await _leaveAlbum(context);
} else {
showToast(context, "Something went wrong");
}
},
),
);
return actions;
}

View file

@ -225,6 +225,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.17"
equatable:
dependency: "direct main"
description:
name: equatable
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
event_bus:
dependency: "direct main"
description:
@ -601,7 +608,28 @@ packages:
name: in_app_purchase
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.2"
version: "3.0.7"
in_app_purchase_android:
dependency: transitive
description:
name: in_app_purchase_android
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.3+5"
in_app_purchase_platform_interface:
dependency: transitive
description:
name: in_app_purchase_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.2"
in_app_purchase_storekit:
dependency: transitive
description:
name: in_app_purchase_storekit
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.2+2"
intl:
dependency: "direct main"
description:
@ -957,17 +985,17 @@ packages:
sentry:
dependency: "direct main"
description:
path: "thirdparty/sentry-dart/dart"
relative: true
source: path
version: "6.11.0"
name: sentry
url: "https://pub.dartlang.org"
source: hosted
version: "6.12.1"
sentry_flutter:
dependency: "direct main"
description:
path: "thirdparty/sentry-dart/flutter"
relative: true
source: path
version: "6.11.0"
name: sentry_flutter
url: "https://pub.dartlang.org"
source: hosted
version: "6.12.1"
share_plus:
dependency: "direct main"
description:

View file

@ -11,6 +11,7 @@ description: ente photos application
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.6.42+372
environment:
@ -35,6 +36,7 @@ dependencies:
dots_indicator: ^2.0.0
dotted_border: ^2.0.0+2
email_validator: ^2.0.1
equatable: ^2.0.5
event_bus: ^2.0.0
exif: ^3.0.0
expandable: ^5.0.1
@ -68,7 +70,7 @@ dependencies:
image: ^3.0.2
image_editor: ^1.0.0
implicitly_animated_reorderable_list: ^0.4.0
in_app_purchase: ^0.5.2
in_app_purchase: ^3.0.7
intl: ^0.17.0
like_button: ^2.0.2
loading_animations: ^2.1.0
@ -94,10 +96,8 @@ dependencies:
quiver: ^3.0.1
receive_sharing_intent: ^1.4.5
scrollable_positioned_list: ^0.2.2
sentry:
path: thirdparty/sentry-dart/dart
sentry_flutter:
path: thirdparty/sentry-dart/flutter
sentry: ^6.12.1
sentry_flutter: ^6.12.1
share_plus: ^4.0.10
shared_preferences: ^2.0.5
sqflite: ^2.0.0+3