Merge branch 'redesign-with-components' into collaboration_view

This commit is contained in:
Neeraj Gupta 2022-12-14 15:13:54 +05:30
commit 1aadc08fdf
No known key found for this signature in database
GPG key ID: 3C5A1684DC1729E1
26 changed files with 1551 additions and 126 deletions

View file

@ -102,6 +102,9 @@ PODS:
- Flutter - Flutter
- in_app_purchase_storekit (0.0.1): - in_app_purchase_storekit (0.0.1):
- Flutter - Flutter
- keyboard_visibility (0.5.0):
- Flutter
- Reachability
- libwebp (1.2.3): - libwebp (1.2.3):
- libwebp/demux (= 1.2.3) - libwebp/demux (= 1.2.3)
- libwebp/mux (= 1.2.3) - libwebp/mux (= 1.2.3)
@ -193,6 +196,7 @@ DEPENDENCIES:
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- image_editor (from `.symlinks/plugins/image_editor/ios`) - image_editor (from `.symlinks/plugins/image_editor/ios`)
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/ios`) - in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/ios`)
- keyboard_visibility (from `.symlinks/plugins/keyboard_visibility/ios`)
- local_auth (from `.symlinks/plugins/local_auth/ios`) - local_auth (from `.symlinks/plugins/local_auth/ios`)
- media_extension (from `.symlinks/plugins/media_extension/ios`) - media_extension (from `.symlinks/plugins/media_extension/ios`)
- motionphoto (from `.symlinks/plugins/motionphoto/ios`) - motionphoto (from `.symlinks/plugins/motionphoto/ios`)
@ -271,6 +275,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/image_editor/ios" :path: ".symlinks/plugins/image_editor/ios"
in_app_purchase_storekit: in_app_purchase_storekit:
:path: ".symlinks/plugins/in_app_purchase_storekit/ios" :path: ".symlinks/plugins/in_app_purchase_storekit/ios"
keyboard_visibility:
:path: ".symlinks/plugins/keyboard_visibility/ios"
local_auth: local_auth:
:path: ".symlinks/plugins/local_auth/ios" :path: ".symlinks/plugins/local_auth/ios"
media_extension: media_extension:
@ -321,7 +327,7 @@ SPEC CHECKSUMS:
FirebaseInstallations: 0a115432c4e223c5ab20b0dbbe4cbefa793a0e8e FirebaseInstallations: 0a115432c4e223c5ab20b0dbbe4cbefa793a0e8e
FirebaseMessaging: 732623518591384f61c287e3d8f65294beb7ffb3 FirebaseMessaging: 732623518591384f61c287e3d8f65294beb7ffb3
fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545 fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
flutter_image_compress: 5a5e9aee05b6553048b8df1c3bc456d0afaac433 flutter_image_compress: 5a5e9aee05b6553048b8df1c3bc456d0afaac433
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721 flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
@ -336,6 +342,7 @@ SPEC CHECKSUMS:
GoogleUtilities: 1d20a6ad97ef46f67bbdec158ce00563a671ebb7 GoogleUtilities: 1d20a6ad97ef46f67bbdec158ce00563a671ebb7
image_editor: eab82a302a6623a866da5145b7c4c0ee8a4ffbb4 image_editor: eab82a302a6623a866da5145b7c4c0ee8a4ffbb4
in_app_purchase_storekit: d7fcf4646136ec258e237872755da8ea6c1b6096 in_app_purchase_storekit: d7fcf4646136ec258e237872755da8ea6c1b6096
keyboard_visibility: 96a24de806fe6823c3ad956c01ba2ec6d056616f
libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c
local_auth: 1740f55d7af0a2e2a8684ce225fe79d8931e808c local_auth: 1740f55d7af0a2e2a8684ce225fe79d8931e808c
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d

View file

@ -16,7 +16,7 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
DA6BE5E826B3BC8600656280 /* (null) in Resources */ = {isa = PBXBuildFile; }; DA6BE5E826B3BC8600656280 /* BuildFile in Resources */ = {isa = PBXBuildFile; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */ /* Begin PBXCopyFilesBuildPhase section */
@ -213,7 +213,7 @@
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
DA6BE5E826B3BC8600656280 /* (null) in Resources */, DA6BE5E826B3BC8600656280 /* BuildFile in Resources */,
277218A0270F596900FFE3CC /* GoogleService-Info.plist in Resources */, 277218A0270F596900FFE3CC /* GoogleService-Info.plist in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -287,6 +287,7 @@
"${BUILT_PRODUCTS_DIR}/fluttertoast/fluttertoast.framework", "${BUILT_PRODUCTS_DIR}/fluttertoast/fluttertoast.framework",
"${BUILT_PRODUCTS_DIR}/image_editor/image_editor.framework", "${BUILT_PRODUCTS_DIR}/image_editor/image_editor.framework",
"${BUILT_PRODUCTS_DIR}/in_app_purchase_storekit/in_app_purchase_storekit.framework", "${BUILT_PRODUCTS_DIR}/in_app_purchase_storekit/in_app_purchase_storekit.framework",
"${BUILT_PRODUCTS_DIR}/keyboard_visibility/keyboard_visibility.framework",
"${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework", "${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework",
"${BUILT_PRODUCTS_DIR}/local_auth/local_auth.framework", "${BUILT_PRODUCTS_DIR}/local_auth/local_auth.framework",
"${BUILT_PRODUCTS_DIR}/media_extension/media_extension.framework", "${BUILT_PRODUCTS_DIR}/media_extension/media_extension.framework",
@ -341,6 +342,7 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/fluttertoast.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/fluttertoast.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_editor.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_editor.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/in_app_purchase_storekit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/in_app_purchase_storekit.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/keyboard_visibility.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_extension.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_extension.framework",
@ -499,7 +501,10 @@
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
); );
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
@ -656,7 +661,10 @@
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
); );
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
@ -690,7 +698,10 @@
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
); );
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",

View file

@ -44,9 +44,11 @@ const supportEmail = 'support@ente.io';
class FFDefault { class FFDefault {
static const bool enableStripe = true; static const bool enableStripe = true;
static const bool disableCFWorker = false; static const bool disableCFWorker = false;
static const bool enableCollect = false;
} }
const kDefaultProductionEndpoint = 'https://api.ente.io'; const kDefaultProductionEndpoint = 'https://api.ente.io';
const int intMaxValue = 9223372036854775807; const int intMaxValue = 9223372036854775807;
//Screen width of iPhone 14 pro max in points is taken as maximum
const double restrictedMaxWidth = 430;

View file

@ -343,6 +343,9 @@ extension CustomColorScheme on ColorScheme {
EnteTheme get enteTheme => EnteTheme get enteTheme =>
brightness == Brightness.light ? lightTheme : darkTheme; brightness == Brightness.light ? lightTheme : darkTheme;
EnteTheme get inverseEnteTheme =>
brightness == Brightness.light ? darkTheme : lightTheme;
} }
OutlinedButtonThemeData buildOutlinedButtonThemeData({ OutlinedButtonThemeData buildOutlinedButtonThemeData({

View file

@ -62,18 +62,6 @@ class FeatureFlagService {
} }
} }
bool enableCollect() {
if (isInternalUserOrDebugBuild()) {
return true;
}
try {
return _getFeatureFlags().enableCollect;
} catch (e) {
_logger.severe(e);
return FFDefault.enableCollect;
}
}
bool isInternalUserOrDebugBuild() { bool isInternalUserOrDebugBuild() {
final String? email = Configuration.instance.getEmail(); final String? email = Configuration.instance.getEmail();
return (email != null && email.endsWith("@ente.io")) || kDebugMode; return (email != null && email.endsWith("@ente.io")) || kDebugMode;
@ -97,24 +85,20 @@ class FeatureFlags {
static FeatureFlags defaultFlags = FeatureFlags( static FeatureFlags defaultFlags = FeatureFlags(
disableCFWorker: FFDefault.disableCFWorker, disableCFWorker: FFDefault.disableCFWorker,
enableStripe: FFDefault.enableStripe, enableStripe: FFDefault.enableStripe,
enableCollect: FFDefault.enableCollect,
); );
final bool disableCFWorker; final bool disableCFWorker;
final bool enableStripe; final bool enableStripe;
final bool enableCollect;
FeatureFlags({ FeatureFlags({
required this.disableCFWorker, required this.disableCFWorker,
required this.enableStripe, required this.enableStripe,
required this.enableCollect,
}); });
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return { return {
"disableCFWorker": disableCFWorker, "disableCFWorker": disableCFWorker,
"enableStripe": enableStripe, "enableStripe": enableStripe,
"enableCollect": enableCollect,
}; };
} }
@ -127,7 +111,6 @@ class FeatureFlags {
return FeatureFlags( return FeatureFlags(
disableCFWorker: json["disableCFWorker"] ?? FFDefault.disableCFWorker, disableCFWorker: json["disableCFWorker"] ?? FFDefault.disableCFWorker,
enableStripe: json["enableStripe"] ?? FFDefault.enableStripe, enableStripe: json["enableStripe"] ?? FFDefault.enableStripe,
enableCollect: json["enableCollect"] ?? FFDefault.enableCollect,
); );
} }
} }

View file

@ -23,6 +23,7 @@ class EnteColorScheme {
final Color fillBase; final Color fillBase;
final Color fillMuted; final Color fillMuted;
final Color fillFaint; final Color fillFaint;
final Color fillFaintPressed;
// Stroke Colors // Stroke Colors
final Color strokeBase; final Color strokeBase;
@ -63,6 +64,7 @@ class EnteColorScheme {
this.fillBase, this.fillBase,
this.fillMuted, this.fillMuted,
this.fillFaint, this.fillFaint,
this.fillFaintPressed,
this.strokeBase, this.strokeBase,
this.strokeMuted, this.strokeMuted,
this.strokeFaint, this.strokeFaint,
@ -97,6 +99,7 @@ const EnteColorScheme lightScheme = EnteColorScheme(
fillBaseLight, fillBaseLight,
fillMutedLight, fillMutedLight,
fillFaintLight, fillFaintLight,
fillFaintPressedLight,
strokeBaseLight, strokeBaseLight,
strokeMutedLight, strokeMutedLight,
strokeFaintLight, strokeFaintLight,
@ -105,7 +108,8 @@ const EnteColorScheme lightScheme = EnteColorScheme(
blurStrokeFaintLight, blurStrokeFaintLight,
blurStrokePressedLight, blurStrokePressedLight,
tabIconLight, tabIconLight,
avatarLight); avatarLight,
);
const EnteColorScheme darkScheme = EnteColorScheme( const EnteColorScheme darkScheme = EnteColorScheme(
backgroundBaseDark, backgroundBaseDark,
@ -121,6 +125,7 @@ const EnteColorScheme darkScheme = EnteColorScheme(
fillBaseDark, fillBaseDark,
fillMutedDark, fillMutedDark,
fillFaintDark, fillFaintDark,
fillFaintPressedDark,
strokeBaseDark, strokeBaseDark,
strokeMutedDark, strokeMutedDark,
strokeFaintDark, strokeFaintDark,
@ -165,10 +170,12 @@ const Color blurTextBaseDark = Color.fromRGBO(255, 255, 255, 0.95);
const Color fillBaseLight = Color.fromRGBO(0, 0, 0, 1); const Color fillBaseLight = Color.fromRGBO(0, 0, 0, 1);
const Color fillMutedLight = Color.fromRGBO(0, 0, 0, 0.12); const Color fillMutedLight = Color.fromRGBO(0, 0, 0, 0.12);
const Color fillFaintLight = Color.fromRGBO(0, 0, 0, 0.04); const Color fillFaintLight = Color.fromRGBO(0, 0, 0, 0.04);
const Color fillFaintPressedLight = Color.fromRGBO(0, 0, 0, 0.08);
const Color fillBaseDark = Color.fromRGBO(255, 255, 255, 1); const Color fillBaseDark = Color.fromRGBO(255, 255, 255, 1);
const Color fillMutedDark = Color.fromRGBO(255, 255, 255, 0.16); const Color fillMutedDark = Color.fromRGBO(255, 255, 255, 0.16);
const Color fillFaintDark = Color.fromRGBO(255, 255, 255, 0.12); const Color fillFaintDark = Color.fromRGBO(255, 255, 255, 0.12);
const Color fillFaintPressedDark = Color.fromRGBO(255, 255, 255, 0.06);
// Stroke Colors // Stroke Colors
const Color strokeBaseLight = Color.fromRGBO(0, 0, 0, 1); const Color strokeBaseLight = Color.fromRGBO(0, 0, 0, 1);

View file

@ -36,10 +36,20 @@ EnteTheme darkTheme = EnteTheme(
shadowButton: shadowButtonDark, shadowButton: shadowButtonDark,
); );
EnteColorScheme getEnteColorScheme(BuildContext context) { EnteColorScheme getEnteColorScheme(
return Theme.of(context).colorScheme.enteTheme.colorScheme; BuildContext context, {
bool inverse = false,
}) {
return inverse
? Theme.of(context).colorScheme.inverseEnteTheme.colorScheme
: Theme.of(context).colorScheme.enteTheme.colorScheme;
} }
EnteTextTheme getEnteTextTheme(BuildContext context) { EnteTextTheme getEnteTextTheme(
return Theme.of(context).colorScheme.enteTheme.textTheme; BuildContext context, {
bool inverse = false,
}) {
return inverse
? Theme.of(context).colorScheme.inverseEnteTheme.textTheme
: Theme.of(context).colorScheme.enteTheme.textTheme;
} }

View file

@ -1,16 +1,17 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:photos/theme/ente_theme.dart'; import 'package:photos/theme/ente_theme.dart';
class EnteLoadingWidget extends StatelessWidget { class EnteLoadingWidget extends StatelessWidget {
final Color? color; final Color? color;
const EnteLoadingWidget({this.color, Key? key}) : super(key: key); final bool is20pts;
const EnteLoadingWidget({this.is20pts = false, this.color, Key? key})
: super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Center( return Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(5), padding: EdgeInsets.all(is20pts ? 3 : 5),
child: SizedBox.fromSize( child: SizedBox.fromSize(
size: const Size.square(14), size: const Size.square(14),
child: CircularProgressIndicator( child: CircularProgressIndicator(

View file

@ -0,0 +1,181 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/theme/colors.dart';
import 'package:photos/theme/effects.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/utils/separators_util.dart';
enum ActionSheetType {
defaultActionSheet,
iconOnly,
}
void showActionSheet({
required BuildContext context,
required List<Widget> buttons,
required ActionSheetType actionSheetType,
bool isCheckIconGreen = false,
String? title,
String? body,
}) {
showMaterialModalBottomSheet(
backgroundColor: Colors.transparent,
barrierColor: backdropMutedDark,
useRootNavigator: true,
context: context,
builder: (_) {
return ActionSheetWidget(
title: title,
body: body,
actionButtons: buttons,
actionSheetType: actionSheetType,
isCheckIconGreen: isCheckIconGreen,
);
},
);
}
class ActionSheetWidget extends StatelessWidget {
final String? title;
final String? body;
final List<Widget> actionButtons;
final ActionSheetType actionSheetType;
final bool isCheckIconGreen;
const ActionSheetWidget({
required this.actionButtons,
required this.actionSheetType,
required this.isCheckIconGreen,
this.title,
this.body,
super.key,
});
@override
Widget build(BuildContext context) {
final isTitleAndBodyNull = title == null && body == null;
final blur = MediaQuery.of(context).platformBrightness == Brightness.light
? blurMuted
: blurBase;
final extraWidth = MediaQuery.of(context).size.width - restrictedMaxWidth;
final double? horizontalPadding = extraWidth > 0 ? extraWidth / 2 : null;
return Padding(
padding: EdgeInsets.fromLTRB(
horizontalPadding ?? 12,
12,
horizontalPadding ?? 12,
32,
),
child: Container(
decoration: BoxDecoration(boxShadow: shadowMenuLight),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur),
child: Container(
color: backdropBaseDark,
child: Padding(
padding: EdgeInsets.fromLTRB(
24,
24,
24,
isTitleAndBodyNull ? 24 : 28,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
isTitleAndBodyNull
? const SizedBox.shrink()
: Padding(
padding: const EdgeInsets.only(bottom: 36),
child: ContentContainerWidget(
title: title,
body: body,
actionSheetType: actionSheetType,
isCheckIconGreen: isCheckIconGreen,
),
),
ActionButtons(
actionButtons,
),
],
),
),
),
),
),
),
);
}
}
class ContentContainerWidget extends StatelessWidget {
final String? title;
final String? body;
final ActionSheetType actionSheetType;
final bool isCheckIconGreen;
const ContentContainerWidget({
required this.actionSheetType,
required this.isCheckIconGreen,
this.title,
this.body,
super.key,
});
@override
Widget build(BuildContext context) {
final textTheme = getEnteTextTheme(context);
return Column(
mainAxisSize: MainAxisSize.min,
//todo: set cross axis to center when icon should be shown in place of body
crossAxisAlignment: actionSheetType == ActionSheetType.defaultActionSheet
? CrossAxisAlignment.stretch
: CrossAxisAlignment.center,
children: [
title == null
? const SizedBox.shrink()
: Text(
title!,
style: textTheme.h3Bold
.copyWith(color: textBaseDark), //constant color
),
title == null || body == null
? const SizedBox.shrink()
: const SizedBox(height: 19),
actionSheetType == ActionSheetType.defaultActionSheet
? body == null
? const SizedBox.shrink()
: Text(
body!,
style: textTheme.body
.copyWith(color: textMutedDark), //constant color
)
: Icon(Icons.check_outlined,
size: 48,
color: isCheckIconGreen
? getEnteColorScheme(context).primary700
: strokeBaseDark)
],
);
}
}
class ActionButtons extends StatelessWidget {
final List<Widget> actionButtons;
const ActionButtons(this.actionButtons, {super.key});
@override
Widget build(BuildContext context) {
final actionButtonsWithSeparators = actionButtons;
return Column(
children:
//Separator height is 8pts in figma. -2pts here as the action
//buttons are 2pts extra in height in code compared to figma because
//of the border(1pt top + 1pt bottom) of action buttons.
addSeparators(actionButtonsWithSeparators, const SizedBox(height: 6)),
);
}
}

View file

@ -0,0 +1,111 @@
import 'package:flutter/material.dart';
import 'package:photos/theme/ente_theme.dart';
class BlurMenuItemWidget extends StatefulWidget {
final IconData? leadingIcon;
final String? labelText;
final Color? menuItemColor;
final Color? pressedColor;
final VoidCallback? onTap;
const BlurMenuItemWidget({
this.leadingIcon,
this.labelText,
this.menuItemColor,
this.pressedColor,
this.onTap,
super.key,
});
@override
State<BlurMenuItemWidget> createState() => _BlurMenuItemWidgetState();
}
class _BlurMenuItemWidgetState extends State<BlurMenuItemWidget> {
Color? menuItemColor;
@override
void initState() {
menuItemColor = widget.menuItemColor;
super.initState();
}
@override
void didChangeDependencies() {
menuItemColor = widget.menuItemColor;
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
return GestureDetector(
onTap: widget.onTap,
onTapDown: _onTapDown,
onTapUp: _onTapUp,
onTapCancel: _onCancel,
child: AnimatedContainer(
duration: const Duration(milliseconds: 20),
color: menuItemColor,
padding: const EdgeInsets.only(left: 16, right: 12),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 14),
child: Row(
children: [
widget.leadingIcon != null
? Padding(
padding: const EdgeInsets.only(right: 10),
child: Icon(
widget.leadingIcon,
size: 20,
color: colorScheme.blurStrokeBase,
),
)
: const SizedBox.shrink(),
widget.labelText != null
? Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: Row(
children: [
Flexible(
child: Text(
widget.labelText!,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: getEnteTextTheme(context)
.bodyBold
.copyWith(color: colorScheme.blurTextBase),
),
),
],
),
),
)
: const SizedBox.shrink(),
],
),
),
),
);
}
void _onTapDown(details) {
setState(() {
menuItemColor = widget.pressedColor ?? widget.menuItemColor;
});
}
void _onTapUp(details) {
Future.delayed(
const Duration(milliseconds: 100),
() => setState(() {
menuItemColor = widget.menuItemColor;
}),
);
}
void _onCancel() {
setState(() {
menuItemColor = widget.menuItemColor;
});
}
}

View file

@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
import 'package:photos/models/selected_files.dart';
import 'package:photos/theme/ente_theme.dart';
class ActionBarWidget extends StatefulWidget {
final String? text;
final List<Widget> iconButtons;
final SelectedFiles? selectedFiles;
const ActionBarWidget({
required this.iconButtons,
this.text,
this.selectedFiles,
super.key,
});
@override
State<ActionBarWidget> createState() => _ActionBarWidgetState();
}
class _ActionBarWidgetState extends State<ActionBarWidget> {
final ValueNotifier<int> _selectedFilesNotifier = ValueNotifier(0);
@override
void initState() {
widget.selectedFiles?.addListener(_selectedFilesListener);
super.initState();
}
@override
void dispose() {
widget.selectedFiles?.removeListener(_selectedFilesListener);
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _actionBarWidgets(context),
),
);
}
List<Widget> _actionBarWidgets(BuildContext context) {
final actionBarWidgets = <Widget>[];
final initialLength = widget.iconButtons.length;
final textTheme = getEnteTextTheme(context);
final colorScheme = getEnteColorScheme(context);
actionBarWidgets.addAll(widget.iconButtons);
if (widget.text != null) {
//adds 12 px spacing at the start and between iconButton elements
for (var i = 0; i < initialLength; i++) {
actionBarWidgets.insert(
2 * i,
const SizedBox(
width: 12,
),
);
}
actionBarWidgets.insertAll(0, [
const SizedBox(width: 20),
Flexible(
child: Row(
children: [
widget.selectedFiles != null
? ValueListenableBuilder(
valueListenable: _selectedFilesNotifier,
builder: (context, value, child) {
return Text(
"${_selectedFilesNotifier.value} selectedsss",
style: textTheme.small.copyWith(
color: colorScheme.blurTextBase,
),
);
},
)
: Text(
widget.text!,
style: textTheme.small
.copyWith(color: colorScheme.textMuted),
),
],
),
),
]);
//to add whitespace of 8pts or 12 pts at the end
if (widget.iconButtons.length > 1) {
actionBarWidgets.add(
const SizedBox(width: 8),
);
} else {
actionBarWidgets.add(
const SizedBox(width: 12),
);
}
}
return actionBarWidgets;
}
void _selectedFilesListener() {
if (widget.selectedFiles!.files.isNotEmpty) {
_selectedFilesNotifier.value = widget.selectedFiles!.files.length;
}
}
}

View file

@ -0,0 +1,176 @@
import 'dart:ui';
import 'package:expandable/expandable.dart';
import 'package:flutter/material.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/models/selected_files.dart';
import 'package:photos/theme/effects.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/components/bottom_action_bar/action_bar_widget.dart';
import 'package:photos/ui/components/icon_button_widget.dart';
class BottomActionBarWidget extends StatelessWidget {
final String? text;
final List<Widget>? iconButtons;
final Widget expandedMenu;
final SelectedFiles? selectedFiles;
final VoidCallback? onCancel;
final bool hasSmallerBottomPadding;
BottomActionBarWidget({
required this.expandedMenu,
required this.hasSmallerBottomPadding,
this.selectedFiles,
this.text,
this.iconButtons,
this.onCancel,
super.key,
});
final ExpandableController _expandableController =
ExpandableController(initialExpanded: false);
@override
Widget build(BuildContext context) {
final widthOfScreen = MediaQuery.of(context).size.width;
final colorScheme = getEnteColorScheme(context);
final textTheme = getEnteTextTheme(context);
final double leftRightPadding = widthOfScreen > restrictedMaxWidth
? (widthOfScreen - restrictedMaxWidth) / 2
: 0;
return ClipRRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: blurBase, sigmaY: blurBase),
child: Container(
color: colorScheme.backdropBase,
padding: EdgeInsets.only(
top: 4,
bottom: hasSmallerBottomPadding ? 24 : 36,
right: leftRightPadding,
left: leftRightPadding,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ExpandableNotifier(
controller: _expandableController,
child: ExpandablePanel(
theme: _getExpandableTheme(),
header: Padding(
padding: EdgeInsets.symmetric(
horizontal: text == null ? 12 : 0,
),
child: ActionBarWidget(
selectedFiles: selectedFiles,
text: text,
iconButtons: _iconButtons(context),
),
),
expanded: expandedMenu,
collapsed: const SizedBox.shrink(),
controller: _expandableController,
),
),
GestureDetector(
onTap: () {
onCancel?.call();
_expandableController.value = false;
},
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
child: Center(
child: Text(
"Cancel",
style: textTheme.bodyBold
.copyWith(color: colorScheme.blurTextBase),
),
),
),
)
],
),
),
),
);
}
List<Widget> _iconButtons(BuildContext context) {
final iconButtonsWithExpansionIcon = <Widget?>[
...?iconButtons,
ExpansionIconWidget(expandableController: _expandableController)
];
iconButtonsWithExpansionIcon.removeWhere((element) => element == null);
return iconButtonsWithExpansionIcon as List<Widget>;
}
ExpandableThemeData _getExpandableTheme() {
return const ExpandableThemeData(
hasIcon: false,
useInkWell: false,
tapBodyToCollapse: false,
tapBodyToExpand: false,
tapHeaderToExpand: false,
animationDuration: Duration(milliseconds: 400),
crossFadePoint: 0.5,
);
}
}
class ExpansionIconWidget extends StatefulWidget {
final ExpandableController expandableController;
const ExpansionIconWidget({required this.expandableController, super.key});
@override
State<ExpansionIconWidget> createState() => _ExpansionIconWidgetState();
}
class _ExpansionIconWidgetState extends State<ExpansionIconWidget> {
@override
void initState() {
widget.expandableController.addListener(_expandableListener);
super.initState();
}
@override
void dispose() {
widget.expandableController.removeListener(_expandableListener);
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
switchInCurve: Curves.easeInOutExpo,
child: widget.expandableController.value
? IconButtonWidget(
key: const ValueKey<bool>(false),
onTap: () {
widget.expandableController.value = false;
setState(() {});
},
icon: Icons.expand_more_outlined,
iconButtonType: IconButtonType.primary,
)
: IconButtonWidget(
key: const ValueKey<bool>(true),
onTap: () {
widget.expandableController.value = true;
setState(() {});
},
icon: Icons.more_horiz_outlined,
iconButtonType: IconButtonType.primary,
),
);
}
_expandableListener() {
if (mounted) {
setState(() {});
}
}
}

View file

@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:photos/ui/components/blur_menu_item_widget.dart';
import 'package:photos/ui/components/divider_widget.dart';
class ExpandedMenuWidget extends StatelessWidget {
final List<List<BlurMenuItemWidget>> items;
const ExpandedMenuWidget({
required this.items,
super.key,
});
@override
Widget build(BuildContext context) {
const double itemHeight = 48;
const double whiteSpaceBetweenSections = 16;
const double dividerHeightBetweenItems = 1;
int numberOfDividers = 0;
double combinedHeightOfItems = 0;
for (List<BlurMenuItemWidget> group in items) {
//no divider if there is only one item in the section/group
if (group.length != 1) {
numberOfDividers += (group.length - 1);
}
combinedHeightOfItems += group.length * itemHeight;
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
child: SizedBox(
height: combinedHeightOfItems +
(dividerHeightBetweenItems * numberOfDividers) +
(whiteSpaceBetweenSections * (items.length - 1)),
child: ListView.separated(
padding: const EdgeInsets.all(0),
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, sectionIndex) {
return ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: SizedBox(
height: itemHeight * items[sectionIndex].length +
(dividerHeightBetweenItems *
(items[sectionIndex].length - 1)),
child: ListView.separated(
padding: const EdgeInsets.all(0),
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, itemIndex) {
return items[sectionIndex][itemIndex];
},
separatorBuilder: (context, index) {
return const DividerWidget(
dividerType: DividerType.bottomBar,
);
},
itemCount: items[sectionIndex].length,
),
),
);
},
separatorBuilder: (context, index) {
return const SizedBox(height: whiteSpaceBetweenSections);
},
itemCount: items.length,
),
),
);
}
}

View file

@ -71,7 +71,7 @@ class _ExpandableMenuItemWidgetState extends State<ExpandableMenuItemWidget> {
padding: const EdgeInsets.only(bottom: 4), padding: const EdgeInsets.only(bottom: 4),
child: widget.selectionOptionsWidget, child: widget.selectionOptionsWidget,
), ),
theme: getExpandableTheme(context), theme: getExpandableTheme(),
controller: expandableController, controller: expandableController,
), ),
), ),

View file

@ -0,0 +1,320 @@
import 'package:flutter/material.dart';
import 'package:photos/theme/colors.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/theme/text_style.dart';
import 'package:photos/ui/common/loading_widget.dart';
import 'package:photos/ui/components/models/button_type.dart';
import 'package:photos/ui/components/models/large_button_style.dart';
import 'package:photos/utils/debouncer.dart';
enum ExecutionState {
idle,
inProgress,
successful,
}
typedef FutureVoidCallback = Future<void> Function();
class LargeButtonWidget extends StatelessWidget {
final IconData? icon;
final String? labelText;
final ButtonType buttonType;
final FutureVoidCallback? onTap;
final bool isDisabled;
///setting this flag to true will make the button appear like how it would
///on dark theme irrespective of the app's theme.
final bool isInActionSheet;
const LargeButtonWidget({
required this.buttonType,
this.icon,
this.labelText,
this.onTap,
this.isInActionSheet = false,
this.isDisabled = false,
super.key,
});
@override
Widget build(BuildContext context) {
final colorScheme =
isInActionSheet ? darkScheme : getEnteColorScheme(context);
final inverseColorScheme = isInActionSheet
? lightScheme
: getEnteColorScheme(context, inverse: true);
final textTheme =
isInActionSheet ? darkTextTheme : getEnteTextTheme(context);
final inverseTextTheme = isInActionSheet
? lightTextTheme
: getEnteTextTheme(context, inverse: true);
final buttonStyle = LargeButtonStyle(
//Dummy default values since we need to keep these properties non-nullable
defaultButtonColor: Colors.transparent,
defaultBorderColor: Colors.transparent,
defaultIconColor: Colors.transparent,
defaultLabelStyle: textTheme.body,
);
buttonStyle.defaultButtonColor = buttonType.defaultButtonColor(colorScheme);
buttonStyle.pressedButtonColor = buttonType.pressedButtonColor(colorScheme);
buttonStyle.disabledButtonColor =
buttonType.disabledButtonColor(colorScheme);
buttonStyle.defaultBorderColor = buttonType.defaultBorderColor(colorScheme);
buttonStyle.pressedBorderColor = buttonType.pressedBorderColor(colorScheme);
buttonStyle.disabledBorderColor =
buttonType.disabledBorderColor(colorScheme);
buttonStyle.defaultIconColor = buttonType.defaultIconColor(
colorScheme: colorScheme,
inverseColorScheme: inverseColorScheme,
);
buttonStyle.pressedIconColor = buttonType.pressedIconColor(colorScheme);
buttonStyle.disabledIconColor = buttonType.disabledIconColor(colorScheme);
buttonStyle.defaultLabelStyle = buttonType.defaultLabelStyle(
textTheme: textTheme,
inverseTextTheme: inverseTextTheme,
);
buttonStyle.pressedLabelStyle =
buttonType.pressedLabelStyle(textTheme, colorScheme);
buttonStyle.disabledLabelStyle =
buttonType.disabledLabelStyle(textTheme, colorScheme);
buttonStyle.checkIconColor = buttonType.checkIconColor(colorScheme);
return LargeButtonChildWidget(
buttonStyle: buttonStyle,
buttonType: buttonType,
isDisabled: isDisabled,
onTap: onTap,
labelText: labelText,
icon: icon,
);
}
}
class LargeButtonChildWidget extends StatefulWidget {
final LargeButtonStyle buttonStyle;
final FutureVoidCallback? onTap;
final ButtonType buttonType;
final String? labelText;
final IconData? icon;
final bool isDisabled;
const LargeButtonChildWidget({
required this.buttonStyle,
required this.buttonType,
required this.isDisabled,
this.onTap,
this.labelText,
this.icon,
super.key,
});
@override
State<LargeButtonChildWidget> createState() => _LargeButtonChildWidgetState();
}
class _LargeButtonChildWidgetState extends State<LargeButtonChildWidget> {
late Color buttonColor;
late Color borderColor;
late Color iconColor;
late TextStyle labelStyle;
late Color checkIconColor;
late Color loadingIconColor;
late bool hasExecutionStates;
final _debouncer = Debouncer(const Duration(milliseconds: 300));
ExecutionState executionState = ExecutionState.idle;
@override
void initState() {
checkIconColor = widget.buttonStyle.checkIconColor ??
widget.buttonStyle.defaultIconColor;
loadingIconColor = widget.buttonStyle.defaultIconColor;
hasExecutionStates = widget.buttonType.hasExecutionStates;
if (widget.isDisabled) {
buttonColor = widget.buttonStyle.disabledButtonColor ??
widget.buttonStyle.defaultButtonColor;
borderColor = widget.buttonStyle.disabledBorderColor ??
widget.buttonStyle.defaultBorderColor;
iconColor = widget.buttonStyle.disabledIconColor ??
widget.buttonStyle.defaultIconColor;
labelStyle = widget.buttonStyle.disabledLabelStyle ??
widget.buttonStyle.defaultLabelStyle;
} else {
buttonColor = widget.buttonStyle.defaultButtonColor;
borderColor = widget.buttonStyle.defaultBorderColor;
iconColor = widget.buttonStyle.defaultIconColor;
labelStyle = widget.buttonStyle.defaultLabelStyle;
}
super.initState();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _shouldRegisterGestures ? _onTap : null,
onTapDown: _shouldRegisterGestures ? _onTapDown : null,
onTapUp: _shouldRegisterGestures ? _onTapUp : null,
onTapCancel: _shouldRegisterGestures ? _onTapCancel : null,
child: AnimatedContainer(
duration: const Duration(milliseconds: 16),
width: double.infinity,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(4)),
color: buttonColor,
border: Border.all(color: borderColor),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 175),
switchInCurve: Curves.easeInOutExpo,
switchOutCurve: Curves.easeInOutExpo,
child: executionState == ExecutionState.idle
? widget.buttonType.hasTrailingIcon
? Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
widget.labelText == null
? const SizedBox.shrink()
: Flexible(
child: Padding(
padding: widget.icon == null
? const EdgeInsets.symmetric(
horizontal: 8,
)
: const EdgeInsets.only(right: 16),
child: Text(
widget.labelText!,
overflow: TextOverflow.ellipsis,
maxLines: 2,
style: labelStyle,
),
),
),
widget.icon == null
? const SizedBox.shrink()
: Icon(
widget.icon,
size: 20,
color: iconColor,
),
],
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
widget.icon == null
? const SizedBox.shrink()
: Icon(
widget.icon,
size: 20,
color: iconColor,
),
widget.icon == null || widget.labelText == null
? const SizedBox.shrink()
: const SizedBox(width: 8),
widget.labelText == null
? const SizedBox.shrink()
: Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
),
child: Text(
widget.labelText!,
style: labelStyle,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
)
],
)
: executionState == ExecutionState.inProgress
? EnteLoadingWidget(
is20pts: true,
color: loadingIconColor,
)
: executionState == ExecutionState.successful
? Icon(
Icons.check_outlined,
size: 20,
color: checkIconColor,
)
: const SizedBox.shrink(), //fallback
),
),
),
);
}
bool get _shouldRegisterGestures =>
!widget.isDisabled &&
(widget.onTap != null) &&
executionState == ExecutionState.idle;
void _onTap() async {
if (hasExecutionStates) {
_debouncer.run(
() => Future(() {
setState(() {
executionState = ExecutionState.inProgress;
});
}),
);
await widget.onTap!
.call()
.onError((error, stackTrace) => _debouncer.cancelDebounce());
_debouncer.cancelDebounce();
// when the time taken by widget.onTap is approximately equal to the debounce
// time, the callback is getting executed when/after the if condition
// below is executing/executed which results in execution state stuck at
// idle state. This Future is for delaying the execution of the if
// condition so that the calback in the debouncer finishes execution before.
await Future.delayed(const Duration(milliseconds: 5));
if (executionState == ExecutionState.inProgress) {
setState(() {
executionState = ExecutionState.successful;
Future.delayed(const Duration(seconds: 2), () {
setState(() {
executionState = ExecutionState.idle;
});
});
});
}
} else {
widget.onTap!.call();
}
}
void _onTapDown(details) {
setState(() {
buttonColor = widget.buttonStyle.pressedButtonColor ??
widget.buttonStyle.defaultButtonColor;
borderColor = widget.buttonStyle.pressedBorderColor ??
widget.buttonStyle.defaultBorderColor;
iconColor = widget.buttonStyle.pressedIconColor ??
widget.buttonStyle.defaultIconColor;
labelStyle = widget.buttonStyle.pressedLabelStyle ??
widget.buttonStyle.defaultLabelStyle;
});
}
void _onTapUp(details) {
Future.delayed(
const Duration(milliseconds: 84),
() => setState(() {
setAllStylesToDefault();
}),
);
}
void _onTapCancel() {
setState(() {
setAllStylesToDefault();
});
}
void setAllStylesToDefault() {
buttonColor = widget.buttonStyle.defaultButtonColor;
borderColor = widget.buttonStyle.defaultBorderColor;
iconColor = widget.buttonStyle.defaultIconColor;
labelStyle = widget.buttonStyle.defaultLabelStyle;
}
}

View file

@ -0,0 +1,197 @@
import 'package:flutter/material.dart';
import 'package:photos/theme/colors.dart';
import 'package:photos/theme/text_style.dart';
enum ButtonType {
primary,
secondary,
neutral,
trailingIcon,
critical,
tertiaryCritical,
trailingIconPrimary,
trailingIconSecondary;
bool get isPrimary =>
this == ButtonType.primary || this == ButtonType.trailingIconPrimary;
bool get hasTrailingIcon =>
this == ButtonType.trailingIcon ||
this == ButtonType.trailingIconPrimary ||
this == ButtonType.trailingIconSecondary;
bool get isSecondary =>
this == ButtonType.secondary || this == ButtonType.trailingIconSecondary;
bool get isCritical =>
this == ButtonType.critical || this == ButtonType.tertiaryCritical;
Color defaultButtonColor(EnteColorScheme colorScheme) {
if (isPrimary) {
return colorScheme.primary500;
}
if (isSecondary) {
return colorScheme.fillFaint;
}
if (this == ButtonType.neutral || this == ButtonType.trailingIcon) {
return colorScheme.fillBase;
}
if (this == ButtonType.critical) {
return colorScheme.warning700;
}
if (this == ButtonType.tertiaryCritical) {
return Colors.transparent;
}
return Colors.transparent;
}
//Returning null to fallback to default color
Color? pressedButtonColor(EnteColorScheme colorScheme) {
if (this == ButtonType.primary) {
return colorScheme.primary700;
}
return null;
}
//Returning null to fallback to default color
Color? disabledButtonColor(EnteColorScheme colorScheme) {
if (this == ButtonType.primary || this == ButtonType.critical) {
return colorScheme.fillFaint;
}
return null;
}
Color defaultBorderColor(EnteColorScheme colorScheme) {
if (this == ButtonType.tertiaryCritical) {
return colorScheme.warning700;
}
return Colors.transparent;
}
//Returning null to fallback to default color
Color? pressedBorderColor(EnteColorScheme colorScheme) {
if (this == ButtonType.primary) {
return colorScheme.strokeMuted;
}
if (this == ButtonType.secondary ||
this == ButtonType.critical ||
this == ButtonType.tertiaryCritical) {
return colorScheme.strokeBase;
}
return null;
}
//Returning null to fallback to default color
Color? disabledBorderColor(EnteColorScheme colorScheme) {
if (this == ButtonType.primary ||
this == ButtonType.secondary ||
this == ButtonType.critical) {
return Colors.transparent;
}
if (this == ButtonType.tertiaryCritical) {
return colorScheme.strokeMuted;
}
return null;
}
Color defaultIconColor({
required EnteColorScheme colorScheme,
required EnteColorScheme inverseColorScheme,
}) {
if (isPrimary || this == ButtonType.critical) {
return strokeBaseDark;
}
if (isSecondary) {
return colorScheme.strokeBase;
}
if (this == ButtonType.neutral || this == ButtonType.trailingIcon) {
return inverseColorScheme.strokeBase;
}
if (this == ButtonType.tertiaryCritical) {
return colorScheme.warning500;
}
//fallback
return colorScheme.strokeBase;
}
//Returning null to fallback to default color
Color? pressedIconColor(EnteColorScheme colorScheme) {
if (this == ButtonType.tertiaryCritical) {
return colorScheme.strokeBase;
}
return null;
}
//Returning null to fallback to default color
Color? disabledIconColor(EnteColorScheme colorScheme) {
if (this == ButtonType.primary || this == ButtonType.secondary) {
return colorScheme.strokeMuted;
}
if (this == ButtonType.critical || this == ButtonType.tertiaryCritical) {
return colorScheme.strokeFaint;
}
return null;
}
TextStyle defaultLabelStyle({
required EnteTextTheme textTheme,
required EnteTextTheme inverseTextTheme,
}) {
if (isPrimary || this == ButtonType.critical) {
return textTheme.bodyBold.copyWith(color: textBaseDark);
}
if (isSecondary) {
return textTheme.bodyBold;
}
if (this == ButtonType.neutral || this == ButtonType.trailingIcon) {
return inverseTextTheme.bodyBold;
}
if (this == ButtonType.tertiaryCritical) {
return textTheme.bodyBold.copyWith(color: warning500);
}
//fallback
return textTheme.bodyBold;
}
//Returning null to fallback to default color
TextStyle? pressedLabelStyle(
EnteTextTheme textTheme,
EnteColorScheme colorScheme,
) {
if (this == ButtonType.tertiaryCritical) {
return textTheme.bodyBold.copyWith(color: colorScheme.strokeBase);
}
return null;
}
//Returning null to fallback to default color
TextStyle? disabledLabelStyle(
EnteTextTheme textTheme,
EnteColorScheme colorScheme,
) {
if (this == ButtonType.primary ||
this == ButtonType.secondary ||
this == ButtonType.critical ||
this == ButtonType.tertiaryCritical) {
return textTheme.bodyBold.copyWith(color: colorScheme.textFaint);
}
return null;
}
Color? checkIconColor(EnteColorScheme colorScheme) {
if (this == ButtonType.secondary) {
return colorScheme.primary500;
}
return null;
}
bool get hasExecutionStates {
if (this == ButtonType.primary ||
this == ButtonType.secondary ||
this == ButtonType.neutral) {
return true;
} else {
return false;
}
}
}

View file

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
class LargeButtonStyle {
Color defaultButtonColor;
Color? pressedButtonColor;
Color? disabledButtonColor;
Color defaultBorderColor;
Color? pressedBorderColor;
Color? disabledBorderColor;
Color defaultIconColor;
Color? pressedIconColor;
Color? disabledIconColor;
TextStyle defaultLabelStyle;
TextStyle? pressedLabelStyle;
TextStyle? disabledLabelStyle;
Color? checkIconColor;
LargeButtonStyle({
required this.defaultButtonColor,
this.pressedButtonColor,
this.disabledButtonColor,
required this.defaultBorderColor,
this.pressedBorderColor,
this.disabledBorderColor,
required this.defaultIconColor,
this.pressedIconColor,
this.disabledIconColor,
required this.defaultLabelStyle,
this.pressedLabelStyle,
this.disabledLabelStyle,
this.checkIconColor,
});
}

View file

@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
Widget sectionOptionSpacing = const SizedBox(height: 6); Widget sectionOptionSpacing = const SizedBox(height: 6);
ExpandableThemeData getExpandableTheme(BuildContext context) { ExpandableThemeData getExpandableTheme() {
return const ExpandableThemeData( return const ExpandableThemeData(
hasIcon: false, hasIcon: false,
useInkWell: false, useInkWell: false,

View file

@ -10,7 +10,6 @@ import 'package:flutter_sodium/flutter_sodium.dart';
import 'package:photos/ente_theme_data.dart'; import 'package:photos/ente_theme_data.dart';
import 'package:photos/models/collection.dart'; import 'package:photos/models/collection.dart';
import 'package:photos/services/collections_service.dart'; import 'package:photos/services/collections_service.dart';
import 'package:photos/services/feature_flag_service.dart';
import 'package:photos/theme/colors.dart'; import 'package:photos/theme/colors.dart';
import 'package:photos/theme/ente_theme.dart'; import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
@ -62,42 +61,6 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final enteColorScheme = getEnteColorScheme(context); final enteColorScheme = getEnteColorScheme(context);
final PublicURL url = widget.collection?.publicURLs?.firstOrNull; final PublicURL url = widget.collection?.publicURLs?.firstOrNull;
final enableCollectFeature = FeatureFlagService.instance.enableCollect();
final Widget collect = enableCollectFeature
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Allow adding photos",
),
alignCaptionedTextToLeft: true,
menuItemColor: getEnteColorScheme(context).fillFaint,
pressedColor: getEnteColorScheme(context).fillFaint,
trailingWidget: Switch.adaptive(
value: widget
.collection.publicURLs?.firstOrNull?.enableCollect ??
false,
onChanged: (value) async {
await _updateUrlSettings(
context,
{'enableCollect': value},
);
setState(() {});
},
),
),
const MenuSectionDescriptionWidget(
content:
"Allow people with the link to also add photos to the shared "
"album.",
),
const SizedBox(height: 24)
],
)
: const SizedBox.shrink();
return Scaffold( return Scaffold(
backgroundColor: Theme.of(context).backgroundColor, backgroundColor: Theme.of(context).backgroundColor,
appBar: AppBar( appBar: AppBar(
@ -114,7 +77,33 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
collect, MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Allow adding photos",
),
alignCaptionedTextToLeft: true,
menuItemColor: getEnteColorScheme(context).fillFaint,
pressedColor: getEnteColorScheme(context).fillFaint,
trailingWidget: Switch.adaptive(
value: widget.collection.publicURLs?.firstOrNull
?.enableCollect ??
false,
onChanged: (value) async {
await _updateUrlSettings(
context,
{'enableCollect': value},
);
setState(() {});
},
),
),
const MenuSectionDescriptionWidget(
content:
"Allow people with the link to also add photos to the shared "
"album.",
),
const SizedBox(height: 24),
MenuItemWidget( MenuItemWidget(
alignCaptionedTextToLeft: true, alignCaptionedTextToLeft: true,
captionedTextWidget: CaptionedTextWidget( captionedTextWidget: CaptionedTextWidget(

View file

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

View file

@ -1,6 +1,7 @@
// @dart=2.9 // @dart=2.9
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/core/event_bus.dart'; import 'package:photos/core/event_bus.dart';
import 'package:photos/db/files_db.dart'; import 'package:photos/db/files_db.dart';
import 'package:photos/events/collection_updated_event.dart'; import 'package:photos/events/collection_updated_event.dart';
@ -10,19 +11,24 @@ import 'package:photos/models/file_load_result.dart';
import 'package:photos/models/gallery_type.dart'; import 'package:photos/models/gallery_type.dart';
import 'package:photos/models/selected_files.dart'; import 'package:photos/models/selected_files.dart';
import 'package:photos/services/ignored_files_service.dart'; import 'package:photos/services/ignored_files_service.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/components/blur_menu_item_widget.dart';
import 'package:photos/ui/components/bottom_action_bar/bottom_action_bar_widget.dart';
import 'package:photos/ui/components/bottom_action_bar/expanded_menu_widget.dart';
import 'package:photos/ui/components/icon_button_widget.dart';
import 'package:photos/ui/viewer/gallery/empty_state.dart'; import 'package:photos/ui/viewer/gallery/empty_state.dart';
import 'package:photos/ui/viewer/gallery/gallery.dart'; import 'package:photos/ui/viewer/gallery/gallery.dart';
import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart'; import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart';
import 'package:photos/ui/viewer/gallery/gallery_overlay_widget.dart'; import 'package:photos/utils/delete_file_util.dart';
import 'package:photos/utils/share_util.dart';
class CollectionPage extends StatelessWidget { class CollectionPage extends StatefulWidget {
final CollectionWithThumbnail c; final CollectionWithThumbnail c;
final String tagPrefix; final String tagPrefix;
final GalleryType appBarType; final GalleryType appBarType;
final _selectedFiles = SelectedFiles();
final bool hasVerifiedLock; final bool hasVerifiedLock;
CollectionPage( const CollectionPage(
this.c, { this.c, {
this.tagPrefix = "collection", this.tagPrefix = "collection",
this.appBarType = GalleryType.ownedCollection, this.appBarType = GalleryType.ownedCollection,
@ -30,17 +36,41 @@ class CollectionPage extends StatelessWidget {
Key key, Key key,
}) : super(key: key); }) : super(key: key);
@override
State<CollectionPage> createState() => _CollectionPageState();
}
class _CollectionPageState extends State<CollectionPage> {
final _selectedFiles = SelectedFiles();
final GlobalKey shareButtonKey = GlobalKey();
final ValueNotifier<double> _bottomPosition = ValueNotifier(-150.0);
@override
void initState() {
_selectedFiles.addListener(_selectedFilesListener);
super.initState();
}
@override
void dispose() {
_selectedFiles.removeListener(_selectedFilesListener);
super.dispose();
}
@override @override
Widget build(Object context) { Widget build(Object context) {
if (hasVerifiedLock == false && c.collection.isHidden()) { if (widget.hasVerifiedLock == false && widget.c.collection.isHidden()) {
return const EmptyState(); return const EmptyState();
} }
final initialFiles = c.thumbnail != null ? [c.thumbnail] : null; final int ownerID = Configuration.instance.getUserID();
final int collectionOwner = widget.c.collection.owner.id;
final initialFiles =
widget.c.thumbnail != null ? [widget.c.thumbnail] : null;
final gallery = Gallery( final gallery = Gallery(
asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) async { asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) async {
final FileLoadResult result = final FileLoadResult result =
await FilesDB.instance.getFilesInCollection( await FilesDB.instance.getFilesInCollection(
c.collection.id, widget.c.collection.id,
creationStartTime, creationStartTime,
creationEndTime, creationEndTime,
limit: limit, limit: limit,
@ -57,38 +87,89 @@ class CollectionPage extends StatelessWidget {
}, },
reloadEvent: Bus.instance reloadEvent: Bus.instance
.on<CollectionUpdatedEvent>() .on<CollectionUpdatedEvent>()
.where((event) => event.collectionID == c.collection.id), .where((event) => event.collectionID == widget.c.collection.id),
removalEventTypes: const { removalEventTypes: const {
EventType.deletedFromRemote, EventType.deletedFromRemote,
EventType.deletedFromEverywhere, EventType.deletedFromEverywhere,
EventType.hide, EventType.hide,
}, },
tagPrefix: tagPrefix, tagPrefix: widget.tagPrefix,
selectedFiles: _selectedFiles, selectedFiles: _selectedFiles,
initialFiles: initialFiles, initialFiles: initialFiles,
albumName: c.collection.name, albumName: widget.c.collection.name,
); );
return Scaffold( return Scaffold(
appBar: PreferredSize( appBar: PreferredSize(
preferredSize: const Size.fromHeight(50.0), preferredSize: const Size.fromHeight(50.0),
child: GalleryAppBarWidget( child: GalleryAppBarWidget(
appBarType, widget.appBarType,
c.collection.name, widget.c.collection.name,
_selectedFiles, _selectedFiles,
collection: c.collection, collection: widget.c.collection,
), ),
), ),
body: Stack( body: Stack(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
children: [ children: [
gallery, gallery,
GalleryOverlayWidget( ValueListenableBuilder(
appBarType, valueListenable: _bottomPosition,
_selectedFiles, builder: (context, value, child) {
collection: c.collection, final colorScheme = getEnteColorScheme(context);
return AnimatedPositioned(
curve: Curves.easeInOutExpo,
bottom: _bottomPosition.value,
right: 0,
left: 0,
duration: const Duration(milliseconds: 400),
child: BottomActionBarWidget(
selectedFiles: _selectedFiles,
hasSmallerBottomPadding: true,
expandedMenu: ExpandedMenuWidget(
items: [
[
BlurMenuItemWidget(
leadingIcon: Icons.add_outlined,
labelText: "One",
menuItemColor: colorScheme.fillFaint,
),
],
],
),
text: _selectedFiles.files.length.toString() + ' selected',
onCancel: () {
if (_selectedFiles.files.isNotEmpty) {
_selectedFiles.clearAll();
}
},
iconButtons: [
IconButtonWidget(
icon: Icons.delete_outlined,
iconButtonType: IconButtonType.primary,
onTap: () => showDeleteSheet(context, _selectedFiles),
),
IconButtonWidget(
icon: Icons.ios_share_outlined,
iconButtonType: IconButtonType.primary,
onTap: () => shareSelected(
context,
shareButtonKey,
_selectedFiles.files,
),
),
],
),
);
},
), ),
], ],
), ),
); );
} }
_selectedFilesListener() {
_selectedFiles.files.isNotEmpty
? _bottomPosition.value = 0.0
: _bottomPosition.value = -150.0;
}
} }

View file

@ -6,6 +6,7 @@ import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:device_info/device_info.dart'; import 'package:device_info/device_info.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
@ -16,6 +17,7 @@ import 'package:photos/events/collection_updated_event.dart';
import 'package:photos/events/files_updated_event.dart'; import 'package:photos/events/files_updated_event.dart';
import 'package:photos/events/local_photos_updated_event.dart'; import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/models/file.dart'; import 'package:photos/models/file.dart';
import 'package:photos/models/selected_files.dart';
import 'package:photos/models/trash_item_request.dart'; import 'package:photos/models/trash_item_request.dart';
import 'package:photos/services/remote_sync_service.dart'; import 'package:photos/services/remote_sync_service.dart';
import 'package:photos/services/sync_service.dart'; import 'package:photos/services/sync_service.dart';
@ -485,3 +487,100 @@ Future<bool> shouldProceedWithDeletion(BuildContext context) async {
); );
return choice == DialogUserChoice.secondChoice; return choice == DialogUserChoice.secondChoice;
} }
void showDeleteSheet(BuildContext context, SelectedFiles selectedFiles) {
final count = selectedFiles.files.length;
bool containsUploadedFile = false, containsLocalFile = false;
for (final file in selectedFiles.files) {
if (file.uploadedFileID != null) {
containsUploadedFile = true;
}
if (file.localID != null) {
containsLocalFile = true;
}
}
final actions = <Widget>[];
if (containsUploadedFile && containsLocalFile) {
actions.add(
CupertinoActionSheetAction(
isDestructiveAction: true,
onPressed: () async {
Navigator.of(context, rootNavigator: true).pop();
await deleteFilesOnDeviceOnly(
context,
selectedFiles.files.toList(),
);
selectedFiles.clearAll();
showToast(context, "Files deleted from device");
},
child: const Text("Device"),
),
);
actions.add(
CupertinoActionSheetAction(
isDestructiveAction: true,
onPressed: () async {
Navigator.of(context, rootNavigator: true).pop();
await deleteFilesFromRemoteOnly(
context,
selectedFiles.files.toList(),
);
selectedFiles.clearAll();
showShortToast(context, "Moved to trash");
},
child: const Text("ente"),
),
);
actions.add(
CupertinoActionSheetAction(
isDestructiveAction: true,
onPressed: () async {
Navigator.of(context, rootNavigator: true).pop();
await deleteFilesFromEverywhere(
context,
selectedFiles.files.toList(),
);
selectedFiles.clearAll();
},
child: const Text("Everywhere"),
),
);
} else {
actions.add(
CupertinoActionSheetAction(
isDestructiveAction: true,
onPressed: () async {
Navigator.of(context, rootNavigator: true).pop();
await deleteFilesFromEverywhere(
context,
selectedFiles.files.toList(),
);
selectedFiles.clearAll();
},
child: const Text("Delete"),
),
);
}
final action = CupertinoActionSheet(
title: Text(
"Delete " +
count.toString() +
" file" +
(count == 1 ? "" : "s") +
(containsUploadedFile && containsLocalFile ? " from" : "?"),
),
actions: actions,
cancelButton: CupertinoActionSheetAction(
child: const Text("Cancel"),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop();
},
),
);
showCupertinoModalPopup(
context: context,
builder: (_) => action,
barrierColor: Colors.black.withOpacity(0.75),
);
}

View file

@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
//This method returns a newly declared list with separators. It will not
//modify the original list
List<Widget> addSeparators(List<Widget> listOfWidgets, Widget separator) {
final int initialLength = listOfWidgets.length;
final listOfWidgetsWithSeparators = <Widget>[];
listOfWidgetsWithSeparators.addAll(listOfWidgets);
for (var i = 1; i < initialLength; i++) {
listOfWidgetsWithSeparators.insert((2 * i) - 1, separator);
}
return listOfWidgetsWithSeparators;
}

View file

@ -146,3 +146,15 @@ DateTime parseDateFromFileNam1e(String fileName) {
); );
} }
} }
void shareSelected(
BuildContext context,
GlobalKey shareButtonKey,
Set<File> selectedFiles,
) {
share(
context,
selectedFiles.toList(),
shareButtonKey: shareButtonKey,
);
}

View file

@ -49,7 +49,7 @@ packages:
name: async name: async
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.9.0" version: "2.8.2"
background_fetch: background_fetch:
dependency: "direct main" dependency: "direct main"
description: description:
@ -98,7 +98,14 @@ packages:
name: characters name: characters
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.1" version: "1.2.0"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1"
chewie: chewie:
dependency: "direct main" dependency: "direct main"
description: description:
@ -112,7 +119,7 @@ packages:
name: clock name: clock
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.1" version: "1.1.0"
collection: collection:
dependency: "direct main" dependency: "direct main"
description: description:
@ -301,7 +308,7 @@ packages:
name: fake_async name: fake_async
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.1" version: "1.3.0"
fast_base58: fast_base58:
dependency: "direct main" dependency: "direct main"
description: description:
@ -700,6 +707,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.7.0" version: "4.7.0"
keyboard_visibility:
dependency: "direct main"
description:
name: keyboard_visibility
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.6"
like_button: like_button:
dependency: "direct main" dependency: "direct main"
description: description:
@ -755,14 +769,14 @@ packages:
name: matcher name: matcher
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.12.12" version: "0.12.11"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.5" version: "0.1.4"
media_extension: media_extension:
dependency: "direct main" dependency: "direct main"
description: description:
@ -778,7 +792,7 @@ packages:
name: meta name: meta
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.0" version: "1.7.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@ -906,7 +920,7 @@ packages:
name: path name: path
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.2" version: "1.8.1"
path_drawing: path_drawing:
dependency: transitive dependency: transitive
description: description:
@ -1247,7 +1261,7 @@ packages:
name: source_span name: source_span
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.9.0" version: "1.8.2"
sprintf: sprintf:
dependency: transitive dependency: transitive
description: description:
@ -1303,7 +1317,7 @@ packages:
name: string_scanner name: string_scanner
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.1" version: "1.1.0"
syncfusion_flutter_core: syncfusion_flutter_core:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1331,28 +1345,28 @@ packages:
name: term_glyph name: term_glyph
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.1" version: "1.2.0"
test: test:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: test name: test
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.21.4" version: "1.21.1"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.12" version: "0.4.9"
test_core: test_core:
dependency: transitive dependency: transitive
description: description:
name: test_core name: test_core
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.16" version: "0.4.13"
timezone: timezone:
dependency: transitive dependency: transitive
description: description:

View file

@ -12,7 +12,7 @@ description: ente photos application
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.6.72+392 version: 0.6.74+394
environment: environment:
sdk: '>=2.17.0 <3.0.0' sdk: '>=2.17.0 <3.0.0'