Merge branch 'main' into minor_copy_change

This commit is contained in:
Neeraj Gupta 2023-02-27 09:28:17 +05:30 committed by GitHub
commit c441a65814
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
105 changed files with 2807 additions and 876 deletions

View file

@ -1,107 +1,108 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="io.ente.photos">
xmlns:tools="http://schemas.android.com/tools"
package="io.ente.photos">
<application android:name="${applicationName}"
android:label="@string/app_name"
android:icon="@mipmap/launcher_icon"
android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true"
android:allowBackup="false"
android:fullBackupContent="false"
android:largeHeap="true">
android:label="@string/app_name"
android:icon="@mipmap/launcher_icon"
android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true"
android:allowBackup="false"
android:fullBackupContent="false"
android:largeHeap="true">
<activity android:name=".MainActivity" android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:exported="true"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- <intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
</intent-filter> -->
<intent-filter>
<action android:name="android.intent.action.PICK" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
</intent-filter>
<!-- file provider to share files having a file:// URI -->
android:theme="@style/LaunchTheme"
android:exported="true"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.PICK" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
</intent-filter>
<!--Filter to support sharing images into our app-->
<!-- file provider to share files having a file:// URI -->
<!--Filter
to support sharing images into our app-->
<intent-filter android:label="@string/backup">
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/*"/>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter android:label="@string/backup">
<action android:name="android.intent.action.SEND_MULTIPLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/*"/>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter android:label="@string/backup">
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="video/*"/>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter android:label="@string/backup">
<action android:name="android.intent.action.SEND_MULTIPLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="video/*"/>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data android:name="flutterEmbedding" android:value="2"/>
<meta-data android:name="flutterEmbedding" android:value="2" />
<meta-data android:name="asset_statements"
android:resource="@string/asset_statements"/>
android:resource="@string/asset_statements" />
<meta-data android:name="io.sentry.dsn"
android:value="https://2235e5c99219488ea93da34b9ac1cb68@sentry.ente.io/4"/>
android:value="https://2235e5c99219488ea93da34b9ac1cb68@sentry.ente.io/4" />
<meta-data android:name="firebase_analytics_collection_deactivated"
android:value="true"/>
android:value="true" />
</application>
<!-- Android 11: https://developer.android.com/preview/privacy/package-visibility -->
<!-- https://developer.android.com/training/package-visibility/use-cases -->
<queries>
<intent>
<action android:name="android.intent.action.SENDTO"/>
<data android:scheme="mailto"/>
<action android:name="android.intent.action.SENDTO" />
<data android:scheme="mailto" />
</intent>
</queries>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.MANAGE_MEDIA"/>
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.MANAGE_MEDIA" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<uses-permission
android:name="android.permission.READ_MEDIA_IMAGES"/> <!-- If you want to read images-->
android:name="android.permission.READ_MEDIA_IMAGES" /> <!-- If you want to read images-->
<uses-permission
android:name="android.permission.READ_MEDIA_VIDEO"/> <!-- If you want to read videos-->
android:name="android.permission.READ_MEDIA_VIDEO" /> <!-- If you want to read videos-->
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32"/>
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29"
tools:ignore="ScopedStorage"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="com.android.vending.BILLING"/>
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="com.android.vending.BILLING" />
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -6,4 +6,4 @@
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -6,4 +6,4 @@
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>
</layer-list>

View file

@ -6,4 +6,4 @@
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -6,4 +6,4 @@
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>
</layer-list>

View file

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View file

@ -4,7 +4,9 @@
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground">#000000</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
@ -15,4 +17,4 @@
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
</resources>

View file

@ -7,6 +7,7 @@
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
@ -17,4 +18,4 @@
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
</resources>

View file

@ -4,7 +4,9 @@
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground">#ffffff</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
@ -15,4 +17,4 @@
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
</resources>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -3,9 +3,9 @@ PODS:
- Flutter
- camera_avfoundation (0.0.1):
- Flutter
- connectivity (0.0.1):
- connectivity_plus (0.0.1):
- Flutter
- Reachability
- ReachabilitySwift
- device_info (0.0.1):
- Flutter
- Firebase/CoreOnly (10.3.0):
@ -136,7 +136,7 @@ PODS:
- Flutter
- FlutterMacOS
- PromisesObjC (2.1.1)
- Reachability (3.2)
- ReachabilitySwift (5.0.0)
- receive_sharing_intent (0.0.1):
- Flutter
- SDWebImage (5.15.2):
@ -178,7 +178,7 @@ PODS:
DEPENDENCIES:
- background_fetch (from `.symlinks/plugins/background_fetch/ios`)
- camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`)
- connectivity (from `.symlinks/plugins/connectivity/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info (from `.symlinks/plugins/device_info/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
@ -231,7 +231,7 @@ SPEC REPOS:
- nanopb
- OrderedSet
- PromisesObjC
- Reachability
- ReachabilitySwift
- SDWebImage
- SDWebImageWebPCoder
- Sentry
@ -242,8 +242,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/background_fetch/ios"
camera_avfoundation:
:path: ".symlinks/plugins/camera_avfoundation/ios"
connectivity:
:path: ".symlinks/plugins/connectivity/ios"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
device_info:
:path: ".symlinks/plugins/device_info/ios"
firebase_core:
@ -320,7 +320,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
background_fetch: bd64e544b303ee4cd4cf2fe8cb2187b72aecf9ca
camera_avfoundation: 07c77549ea54ad95d8581be86617c094a46280d9
connectivity: c4130b2985d4ef6fd26f9702e886bd5260681467
connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e
device_info: d7d233b645a32c40dfdc212de5cf646ca482f175
Firebase: f92fc551ead69c94168d36c2b26188263860acd9
firebase_core: f95c8bbe65213d406d592573ad42a12d64849cb8
@ -358,7 +358,7 @@ SPEC CHECKSUMS:
path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
PromisesObjC: ab77feca74fa2823e7af4249b8326368e61014cb
Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
SDWebImage: 8ab87d4b3e5cc4927bd47f78db6ceb0b94442577
SDWebImageWebPCoder: 4851414d9f8894e328e8b97c93ea4f4f4e4418ae

View file

@ -268,14 +268,14 @@
"${BUILT_PRODUCTS_DIR}/Mantle/Mantle.framework",
"${BUILT_PRODUCTS_DIR}/OrderedSet/OrderedSet.framework",
"${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework",
"${BUILT_PRODUCTS_DIR}/Reachability/Reachability.framework",
"${BUILT_PRODUCTS_DIR}/ReachabilitySwift/Reachability.framework",
"${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework",
"${BUILT_PRODUCTS_DIR}/SDWebImageWebPCoder/SDWebImageWebPCoder.framework",
"${BUILT_PRODUCTS_DIR}/Sentry/Sentry.framework",
"${BUILT_PRODUCTS_DIR}/Toast/Toast.framework",
"${BUILT_PRODUCTS_DIR}/background_fetch/background_fetch.framework",
"${BUILT_PRODUCTS_DIR}/camera_avfoundation/camera_avfoundation.framework",
"${BUILT_PRODUCTS_DIR}/connectivity/connectivity.framework",
"${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework",
"${BUILT_PRODUCTS_DIR}/device_info/device_info.framework",
"${BUILT_PRODUCTS_DIR}/fk_user_agent/fk_user_agent.framework",
"${BUILT_PRODUCTS_DIR}/flutter_email_sender/flutter_email_sender.framework",
@ -331,7 +331,7 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Toast.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/background_fetch.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/camera_avfoundation.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/fk_user_agent.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_email_sender.framework",

View file

@ -2,8 +2,7 @@
"images" : [
{
"filename" : "background.png",
"idiom" : "universal",
"scale" : "1x"
"idiom" : "universal"
},
{
"appearances" : [
@ -13,36 +12,7 @@
}
],
"filename" : "darkbackground.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -38,7 +38,7 @@
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="1024" height="1024"/>
<image name="LaunchImage" width="1152" height="1152"/>
<image name="LaunchBackground" width="1" height="1"/>
</resources>
</document>
</document>

View file

@ -23,19 +23,19 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
<string>12.0</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>googlegmail</string>
<string>x-dispatch</string>
<string>readdle-spark</string>
<string>airmail</string>
<string>ms-outlook</string>
<string>ymail</string>
<string>fastmail</string>
<string>superhuman</string>
<string>protonmail</string>
</array>
<array>
<string>googlegmail</string>
<string>x-dispatch</string>
<string>readdle-spark</string>
<string>airmail</string>
<string>ms-outlook</string>
<string>ymail</string>
<string>fastmail</string>
<string>superhuman</string>
<string>protonmail</string>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
@ -66,6 +66,8 @@
<false/>
<key>NSFaceIDUsageDescription</key>
<string>Please allow ente to lock itself with FaceID or TouchID</string>
<key>NSCameraUsageDescription</key>
<string>Please allow access to your camera so that you can take photos within ente</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Please allow access to your photos so that ente can encrypt and back them up.</string>
<key>UIBackgroundModes</key>
@ -99,5 +101,5 @@
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</dict>
</plist>

View file

@ -4,16 +4,16 @@ import 'package:adaptive_theme/adaptive_theme.dart';
import 'package:background_fetch/background_fetch.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:logging/logging.dart';
import 'package:media_extension/media_extension.dart';
import 'package:media_extension/media_extension_action_types.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/services/app_lifecycle_service.dart';
import 'package:photos/services/sync_service.dart';
import 'package:photos/ui/home_widget.dart';
import "package:photos/ui/viewer/actions/file_viewer.dart";
import "package:photos/utils/intent_util.dart";
class EnteApp extends StatefulWidget {
final Future<void> Function(String) runBackgroundTask;
@ -31,63 +31,46 @@ class EnteApp extends StatefulWidget {
class _EnteAppState extends State<EnteApp> with WidgetsBindingObserver {
final _logger = Logger("EnteAppState");
final _mediaExtensionPlugin = MediaExtension();
@override
void initState() {
_logger.info('init App');
super.initState();
setupIntentAction();
WidgetsBinding.instance.addObserver(this);
}
Future<bool> initIntentAction() async {
if (!Platform.isAndroid) {
AppLifecycleService.instance.setIntentAction(IntentAction.main);
return true;
}
IntentAction intentAction = IntentAction.main;
try {
final actionResult = await _mediaExtensionPlugin.getIntentAction();
intentAction = actionResult.action!;
} on PlatformException {
intentAction = IntentAction.main;
} catch (error) {
_logger.info(error);
intentAction = IntentAction.main;
}
if (intentAction == IntentAction.main) {
void setupIntentAction() async {
final mediaExtentionAction = Platform.isAndroid
? await initIntentAction()
: MediaExtentionAction(action: IntentAction.main);
AppLifecycleService.instance.setMediaExtensionAction(mediaExtentionAction);
if (mediaExtentionAction.action == IntentAction.main) {
_configureBackgroundFetch();
}
AppLifecycleService.instance.setIntentAction(intentAction);
return true;
}
@override
Widget build(BuildContext context) {
if (Platform.isAndroid || kDebugMode) {
return FutureBuilder(
future: initIntentAction(),
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
return snapshot.data != null && snapshot.data == true
? AdaptiveTheme(
light: lightThemeData,
dark: darkThemeData,
initial: AdaptiveThemeMode.system,
builder: (lightTheme, dartTheme) => MaterialApp(
title: "ente",
themeMode: ThemeMode.system,
theme: lightTheme,
darkTheme: dartTheme,
home: const HomeWidget(),
debugShowCheckedModeBanner: false,
builder: EasyLoading.init(),
supportedLocales: AppLocalizations.supportedLocales,
localizationsDelegates:
AppLocalizations.localizationsDelegates,
),
)
: Container();
},
return AdaptiveTheme(
light: lightThemeData,
dark: darkThemeData,
initial: AdaptiveThemeMode.system,
builder: (lightTheme, dartTheme) => MaterialApp(
title: "ente",
themeMode: ThemeMode.system,
theme: lightTheme,
darkTheme: dartTheme,
home: AppLifecycleService.instance.mediaExtensionAction.action ==
IntentAction.view
? const FileViewer()
: const HomeWidget(),
debugShowCheckedModeBanner: false,
builder: EasyLoading.init(),
supportedLocales: AppLocalizations.supportedLocales,
localizationsDelegates: AppLocalizations.localizationsDelegates,
),
);
} else {
return MaterialApp(

View file

@ -1091,13 +1091,17 @@ class FilesDB {
return count ?? 0;
}
Future<int> fileCountWithVisibility(int visibility, int ownerID) async {
Future<int> archivedFilesCount(
int visibility,
int ownerID,
Set<int> hiddenCollections,
) async {
final db = await instance.database;
final count = Sqflite.firstIntValue(
await db.rawQuery(
'SELECT COUNT(distinct($columnUploadedFileID)) FROM $filesTable where '
'$columnMMdVisibility'
' = $visibility AND $columnOwnerID = $ownerID',
' = $visibility AND $columnOwnerID = $ownerID AND $columnCollectionID NOT IN (${hiddenCollections.join(', ')})',
),
);
return count ?? 0;

View file

@ -0,0 +1,14 @@
import "package:flutter/services.dart";
class UpperCaseTextFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
return TextEditingValue(
text: newValue.text.toUpperCase(),
selection: newValue.selection,
);
}
}

View file

@ -0,0 +1,22 @@
import "package:dio/dio.dart";
import "package:photos/models/api/storage_bonus/storage_bonus.dart";
class StorageBonusGateway {
final Dio _enteDio;
StorageBonusGateway(this._enteDio);
Future<ReferralView> getReferralView() async {
final response = await _enteDio.get("/storage-bonus/referral-view");
return ReferralView.fromJson(response.data);
}
Future<void> claimReferralCode(String code) {
return _enteDio.post("/storage-bonus/referral-claim?code=$code");
}
Future<BonusDetails> getBonusDetails() async {
final response = await _enteDio.get("/storage-bonus/details");
return BonusDetails.fromJson(response.data);
}
}

View file

@ -5,6 +5,7 @@ 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:flutter/rendering.dart";
import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
import 'package:photos/app.dart';
@ -28,6 +29,7 @@ import "package:photos/services/object_detection/object_detection_service.dart";
import 'package:photos/services/push_service.dart';
import 'package:photos/services/remote_sync_service.dart';
import 'package:photos/services/search_service.dart';
import "package:photos/services/storage_bonus_service.dart";
import 'package:photos/services/sync_service.dart';
import 'package:photos/services/trash_sync_service.dart';
import 'package:photos/services/update_service.dart';
@ -53,6 +55,7 @@ const kFGTaskDeathTimeoutInMicroseconds = 5000000;
const kBackgroundLockLatency = Duration(seconds: 3);
void main() async {
debugRepaintRainbowEnabled = false;
WidgetsFlutterBinding.ensureInitialized();
await _runInForeground();
BackgroundFetch.registerHeadlessTask(_headlessTaskHandler);
@ -153,6 +156,7 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
LocalSettings.instance.init(preferences);
LocalFileUpdateService.instance.init(preferences);
SearchService.instance.init();
StorageBonusService.instance.init(preferences);
if (Platform.isIOS) {
PushService.instance.init().then((_) {
FirebaseMessaging.onBackgroundMessage(
@ -161,7 +165,9 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
});
}
FeatureFlagService.instance.init();
await ObjectDetectionService.instance.init();
if (FeatureFlagService.instance.isInternalUserOrDebugBuild()) {
await ObjectDetectionService.instance.init();
}
_logger.info("Initialization done");
}

View file

@ -0,0 +1,150 @@
class ReferralView {
PlanInfo planInfo;
String code;
bool enableApplyCode;
bool isFamilyMember;
bool hasAppliedCode;
int claimedStorage;
ReferralView({
required this.planInfo,
required this.code,
required this.enableApplyCode,
required this.isFamilyMember,
required this.hasAppliedCode,
required this.claimedStorage,
});
factory ReferralView.fromJson(Map<String, dynamic> json) => ReferralView(
planInfo: PlanInfo.fromJson(json["planInfo"]),
code: json["code"],
enableApplyCode: json["enableApplyCode"],
isFamilyMember: json["isFamilyMember"],
hasAppliedCode: json["hasAppliedCode"],
claimedStorage: json["claimedStorage"],
);
Map<String, dynamic> toJson() => {
"planInfo": planInfo.toJson(),
"code": code,
"enableApplyCode": enableApplyCode,
"isFamilyMember": isFamilyMember,
"hasAppliedCode": hasAppliedCode,
"claimedStorage": claimedStorage,
};
}
class PlanInfo {
bool isEnabled;
String planType;
int storageInGB;
int maxClaimableStorageInGB;
PlanInfo({
required this.isEnabled,
required this.planType,
required this.storageInGB,
required this.maxClaimableStorageInGB,
});
factory PlanInfo.fromJson(Map<String, dynamic> json) => PlanInfo(
isEnabled: json["isEnabled"],
planType: json["planType"],
storageInGB: json["storageInGB"],
maxClaimableStorageInGB: json["maxClaimableStorageInGB"],
);
Map<String, dynamic> toJson() => {
"isEnabled": isEnabled,
"planType": planType,
"storageInGB": storageInGB,
"maxClaimableStorageInGB": maxClaimableStorageInGB,
};
}
class ReferralStat {
String planType;
int totalCount;
int upgradedCount;
ReferralStat(this.planType, this.totalCount, this.upgradedCount);
factory ReferralStat.fromJson(Map<String, dynamic> json) {
return ReferralStat(
json['planType'],
json['totalCount'],
json['upgradedCount'],
);
}
Map<String, dynamic> toJson() {
return {
'planType': planType,
'totalCount': totalCount,
'upgradedCount': upgradedCount,
};
}
}
class Bonus {
int storage;
String type;
int validTill;
bool isRevoked;
Bonus(this.storage, this.type, this.validTill, this.isRevoked);
// fromJson
factory Bonus.fromJson(Map<String, dynamic> json) {
return Bonus(
json['storage'],
json['type'],
json['validTill'],
json['isRevoked'],
);
}
Map<String, dynamic> toJson() {
return {
'storage': storage,
'type': type,
'validTill': validTill,
'isRevoked': isRevoked,
};
}
}
class BonusDetails {
List<ReferralStat> referralStats;
List<Bonus> bonuses;
int refCount;
int refUpgradeCount;
bool hasAppliedCode;
BonusDetails({
required this.referralStats,
required this.bonuses,
required this.refCount,
required this.refUpgradeCount,
required this.hasAppliedCode,
});
factory BonusDetails.fromJson(Map<String, dynamic> json) => BonusDetails(
referralStats: List<ReferralStat>.from(
json["referralStats"].map((x) => ReferralStat.fromJson(x))),
bonuses:
List<Bonus>.from(json["bonuses"].map((x) => Bonus.fromJson(x))),
refCount: json["refCount"],
refUpgradeCount: json["refUpgradeCount"],
hasAppliedCode: json["hasAppliedCode"],
);
Map<String, dynamic> toJson() => {
"referralStats":
List<dynamic>.from(referralStats.map((x) => x.toJson())),
"bonuses": List<dynamic>.from(bonuses.map((x) => x.toJson())),
"refCount": refCount,
"refUpgradeCount": refUpgradeCount,
"hasAppliedCode": hasAppliedCode,
};
}

View file

@ -5,15 +5,17 @@ class AppLifecycleService {
final _logger = Logger("AppLifecycleService");
bool isForeground = false;
IntentAction intentAction = IntentAction.main;
MediaExtentionAction mediaExtensionAction =
MediaExtentionAction(action: IntentAction.main);
static final AppLifecycleService instance =
AppLifecycleService._privateConstructor();
AppLifecycleService._privateConstructor();
void setIntentAction(IntentAction intentAction) {
this.intentAction = intentAction;
void setMediaExtensionAction(MediaExtentionAction mediaExtensionAction) {
_logger.info("App invoked via ${mediaExtensionAction.action}");
this.mediaExtensionAction = mediaExtensionAction;
}
void onAppInForeground(String reason) {

View file

@ -168,12 +168,17 @@ class BillingService {
if (userDetails.subscription.productID == freeProductID) {
await showErrorDialog(
context,
"Share your storage plan with your family members!",
"Customers on paid plans can add up to 5 family members without paying extra. Each member gets their own private space.",
"Family plans",
"Add 5 family members to your existing plan without paying extra.\n"
"\nEach member gets their own private space, and cannot see each "
"other's files unless they're shared.\n\nFamily plans are "
"available to customers who have a paid ente subscription.\n\n"
"Subscribe now to get started!",
);
return;
}
final dialog = createProgressDialog(context, "Please wait...");
final dialog =
createProgressDialog(context, "Please wait...", isDismissible: true);
await dialog.show();
try {
final String jwtToken = await UserService.instance.getFamiliesToken();

View file

@ -1,7 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:collection/collection.dart';
import 'package:dio/dio.dart';
@ -183,6 +182,15 @@ class CollectionsService {
.toSet();
}
Future<List<CollectionWithThumbnail>> getArchivedCollectionWithThumb() async {
final allCollections = await getCollectionsWithThumbnails();
return allCollections
.where(
(c) => c.collection.isArchived() && !c.collection.isHidden(),
)
.toList();
}
Set<int> getHiddenCollections() {
return _collectionIDToCollections.values
.toList()
@ -646,7 +654,7 @@ class CollectionsService {
}
rethrow;
} catch (e, s) {
_logger.severe("failed to rename collection", e, s);
_logger.severe("failed to update ShareUrl", e, s);
rethrow;
}
}

View file

@ -1,8 +1,10 @@
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/constants.dart';
import "package:photos/core/event_bus.dart";
import 'package:photos/db/files_db.dart';
import 'package:photos/db/memories_db.dart';
import "package:photos/events/files_updated_event.dart";
import 'package:photos/models/filters/important_items_filter.dart';
import 'package:photos/models/memory.dart';
import 'package:photos/services/collections_service.dart';
@ -34,6 +36,17 @@ class MemoriesService extends ChangeNotifier {
DateTime.now().microsecondsSinceEpoch - (7 * microSecondsInDay),
);
});
Bus.instance.on<FilesUpdatedEvent>().where((event) {
return event.type == EventType.deletedFromEverywhere;
}).listen((event) {
final generatedIDs = event.updatedFiles
.where((element) => element.generatedID != null)
.map((e) => e.generatedID!)
.toSet();
_cachedMemories?.removeWhere((element) {
return generatedIDs.contains(element.file.generatedID);
});
});
}
void clearCache() {

View file

@ -0,0 +1,37 @@
import "package:photos/core/network/network.dart";
import "package:photos/gateways/storage_bonus_gw.dart";
import "package:shared_preferences/shared_preferences.dart";
class StorageBonusService {
late StorageBonusGateway gateway;
late SharedPreferences prefs;
final int minTapCountBeforeHidingBanner = 5;
final String _showStorageBonusTapCount = "showStorageBonus.tap_count";
void init(SharedPreferences preferences) {
prefs = preferences;
gateway = StorageBonusGateway(NetworkClient.instance.enteDio);
}
StorageBonusService._privateConstructor();
static StorageBonusService instance =
StorageBonusService._privateConstructor();
// returns true if _showStorageBonusTapCount value is less than minTapCountBeforeHidingBanner
bool shouldShowStorageBonus() {
final tapCount = prefs.getInt(_showStorageBonusTapCount) ?? 0;
return tapCount <= minTapCountBeforeHidingBanner;
}
void markStorageBonusAsDone() {
final tapCount = prefs.getInt(_showStorageBonusTapCount) ?? 0;
prefs.setInt(_showStorageBonusTapCount, tapCount + 1).ignore();
}
// getter for gateway
StorageBonusGateway getGateway() {
return gateway;
}
}

View file

@ -1,7 +1,7 @@
import 'dart:async';
import 'dart:io';
import 'package:connectivity/connectivity.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/dio.dart';
import 'package:logging/logging.dart';
import 'package:photo_manager/photo_manager.dart';

View file

@ -16,7 +16,7 @@ class UpdateService {
static final UpdateService instance = UpdateService._privateConstructor();
static const kUpdateAvailableShownTimeKey = "update_available_shown_time_key";
static const changeLogVersionKey = "update_change_log_key";
static const currentChangeLogVersion = 4;
static const currentChangeLogVersion = 6;
LatestVersionInfo? _latestVersion;
final _logger = Logger("UpdateService");

View file

@ -48,6 +48,10 @@ class EnteColorScheme {
final Color warning800;
final Color caution500;
//golden colors
final Color golden700;
final Color golden500;
//other colors
final Color tabIcon;
final List<Color> avatarColors;
@ -86,6 +90,8 @@ class EnteColorScheme {
this.warning500 = _warning500,
this.warning400 = _warning400,
this.caution500 = _caution500,
this.golden700 = _golden700,
this.golden500 = _golden500,
});
}
@ -224,6 +230,9 @@ const Color _warning800 = Color(0xFFF53434);
const Color _caution500 = Color.fromRGBO(255, 194, 71, 1);
const Color _golden700 = Color(0xFFFDB816);
const Color _golden500 = Color(0xFFFFC336);
const List<Color> avatarLight = [
Color.fromRGBO(118, 84, 154, 1),
Color.fromRGBO(223, 120, 97, 1),

View file

@ -86,6 +86,26 @@ class EnteTextTheme {
final TextStyle brandSmall;
final TextStyle brandMedium;
// textMuted variants
final TextStyle h1Muted;
final TextStyle h2Muted;
final TextStyle h3Muted;
final TextStyle largeMuted;
final TextStyle bodyMuted;
final TextStyle smallMuted;
final TextStyle miniMuted;
final TextStyle tinyMuted;
// textFaint variants
final TextStyle h1Faint;
final TextStyle h2Faint;
final TextStyle h3Faint;
final TextStyle largeFaint;
final TextStyle bodyFaint;
final TextStyle smallFaint;
final TextStyle miniFaint;
final TextStyle tinyFaint;
const EnteTextTheme({
required this.h1,
required this.h1Bold,
@ -105,13 +125,42 @@ class EnteTextTheme {
required this.tinyBold,
required this.brandSmall,
required this.brandMedium,
required this.h1Muted,
required this.h2Muted,
required this.h3Muted,
required this.largeMuted,
required this.bodyMuted,
required this.smallMuted,
required this.miniMuted,
required this.tinyMuted,
required this.h1Faint,
required this.h2Faint,
required this.h3Faint,
required this.largeFaint,
required this.bodyFaint,
required this.smallFaint,
required this.miniFaint,
required this.tinyFaint,
});
}
EnteTextTheme lightTextTheme = _buildEnteTextStyle(textBaseLight);
EnteTextTheme darkTextTheme = _buildEnteTextStyle(textBaseDark);
EnteTextTheme lightTextTheme = _buildEnteTextStyle(
textBaseLight,
textMutedLight,
textFaintLight,
);
EnteTextTheme _buildEnteTextStyle(Color color) {
EnteTextTheme darkTextTheme = _buildEnteTextStyle(
textBaseDark,
textMutedDark,
textFaintDark,
);
EnteTextTheme _buildEnteTextStyle(
Color color,
Color textMuted,
Color textFaint,
) {
return EnteTextTheme(
h1: h1.copyWith(color: color),
h1Bold: h1.copyWith(color: color, fontWeight: _boldWeight),
@ -131,5 +180,21 @@ EnteTextTheme _buildEnteTextStyle(Color color) {
tinyBold: tiny.copyWith(color: color, fontWeight: _boldWeight),
brandSmall: brandStyleSmall.copyWith(color: color),
brandMedium: brandStyleMedium.copyWith(color: color),
h1Muted: h1.copyWith(color: textMuted),
h2Muted: h2.copyWith(color: textMuted),
h3Muted: h3.copyWith(color: textMuted),
largeMuted: large.copyWith(color: textMuted),
bodyMuted: body.copyWith(color: textMuted),
smallMuted: small.copyWith(color: textMuted),
miniMuted: mini.copyWith(color: textMuted),
tinyMuted: tiny.copyWith(color: textMuted),
h1Faint: h1.copyWith(color: textFaint),
h2Faint: h2.copyWith(color: textFaint),
h3Faint: h3.copyWith(color: textFaint),
largeFaint: large.copyWith(color: textFaint),
bodyFaint: body.copyWith(color: textFaint),
smallFaint: small.copyWith(color: textFaint),
miniFaint: mini.copyWith(color: textFaint),
tinyFaint: tiny.copyWith(color: textFaint),
);
}

View file

@ -0,0 +1,143 @@
import "package:flutter/cupertino.dart";
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
import "package:photos/models/file.dart";
import "package:photos/models/file_type.dart";
import "package:photos/theme/colors.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/components/action_sheet_widget.dart";
import "package:photos/ui/components/button_widget.dart";
import "package:photos/ui/components/models/button_type.dart";
import "package:photos/ui/viewer/file/file_info_widget.dart";
import "package:photos/utils/delete_file_util.dart";
import "package:photos/utils/dialog_util.dart";
import "package:photos/utils/toast_util.dart";
Future<void> showSingleFileDeleteSheet(
BuildContext context,
File file, {
Function(File)? onFileRemoved,
}) async {
final List<ButtonWidget> buttons = [];
final String fileType = file.fileType == FileType.video ? "video" : "photo";
final bool isBothLocalAndRemote =
file.uploadedFileID != null && file.localID != null;
final bool isLocalOnly = file.uploadedFileID == null && file.localID != null;
final bool isRemoteOnly = file.uploadedFileID != null && file.localID == null;
const String bodyHighlight = "It will be deleted from all albums.";
String body = "";
if (isBothLocalAndRemote) {
body = "This $fileType is in both ente and your device.";
} else if (isRemoteOnly) {
body = "This $fileType will be deleted from ente.";
} else if (isLocalOnly) {
body = "This $fileType will be deleted from your device.";
} else {
throw AssertionError("Unexpected state");
}
// Add option to delete from ente
if (isBothLocalAndRemote || isRemoteOnly) {
buttons.add(
ButtonWidget(
labelText: isBothLocalAndRemote ? "Delete from ente" : "Yes, delete",
buttonType: ButtonType.neutral,
buttonSize: ButtonSize.large,
shouldStickToDarkTheme: true,
buttonAction: ButtonAction.first,
shouldSurfaceExecutionStates: true,
isInAlert: true,
onTap: () async {
await deleteFilesFromRemoteOnly(context, [file]);
showShortToast(context, "Moved to trash");
if (isRemoteOnly) {
Navigator.of(context, rootNavigator: true).pop();
if (onFileRemoved != null) {
onFileRemoved(file);
}
}
},
),
);
}
// Add option to delete from local
if (isBothLocalAndRemote || isLocalOnly) {
buttons.add(
ButtonWidget(
labelText: isBothLocalAndRemote ? "Delete from device" : "Yes, delete",
buttonType: ButtonType.neutral,
buttonSize: ButtonSize.large,
shouldStickToDarkTheme: true,
buttonAction: ButtonAction.second,
shouldSurfaceExecutionStates: false,
isInAlert: true,
onTap: () async {
await deleteFilesOnDeviceOnly(context, [file]);
if (isLocalOnly) {
Navigator.of(context, rootNavigator: true).pop();
if (onFileRemoved != null) {
onFileRemoved(file);
}
}
},
),
);
}
if (isBothLocalAndRemote) {
buttons.add(
ButtonWidget(
labelText: "Delete from both",
buttonType: ButtonType.neutral,
buttonSize: ButtonSize.large,
shouldStickToDarkTheme: true,
buttonAction: ButtonAction.third,
shouldSurfaceExecutionStates: true,
isInAlert: true,
onTap: () async {
await deleteFilesFromEverywhere(context, [file]);
Navigator.of(context, rootNavigator: true).pop();
if (onFileRemoved != null) {
onFileRemoved(file);
}
},
),
);
}
buttons.add(
const ButtonWidget(
labelText: "Cancel",
buttonType: ButtonType.secondary,
buttonSize: ButtonSize.large,
shouldStickToDarkTheme: true,
buttonAction: ButtonAction.fourth,
isInAlert: true,
),
);
final actionResult = await showActionSheet(
context: context,
buttons: buttons,
actionSheetType: ActionSheetType.defaultActionSheet,
body: body,
bodyHighlight: bodyHighlight,
);
if (actionResult?.action != null &&
actionResult!.action == ButtonAction.error) {
showGenericErrorDialog(context: context);
}
}
Future<void> showInfoSheet(BuildContext context, File file) async {
final colorScheme = getEnteColorScheme(context);
return showBarModalBottomSheet(
topControl: const SizedBox.shrink(),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0)),
backgroundColor: colorScheme.backgroundElevated,
barrierColor: backdropFaintDark,
context: context,
builder: (BuildContext context) {
return Padding(
padding:
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: FileInfoWidget(file),
);
},
);
}

View file

@ -1,11 +1,15 @@
import "dart:async";
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import "package:fluttertoast/fluttertoast.dart";
import 'package:logging/logging.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:photos/core/configuration.dart';
import "package:photos/core/event_bus.dart";
import 'package:photos/db/files_db.dart';
import "package:photos/events/tab_changed_event.dart";
import 'package:photos/models/collection.dart';
import 'package:photos/models/collection_items.dart';
import 'package:photos/models/file.dart';
@ -15,6 +19,7 @@ import 'package:photos/services/ignored_files_service.dart';
import 'package:photos/services/remote_sync_service.dart';
import 'package:photos/theme/colors.dart';
import 'package:photos/theme/ente_theme.dart';
import "package:photos/ui/actions/collection/collection_sharing_actions.dart";
import 'package:photos/ui/common/loading_widget.dart';
import 'package:photos/ui/components/album_list_item_widget.dart';
import 'package:photos/ui/components/bottom_of_title_bar_widget.dart';
@ -22,6 +27,7 @@ import 'package:photos/ui/components/button_widget.dart';
import 'package:photos/ui/components/models/button_type.dart';
import 'package:photos/ui/components/new_album_list_widget.dart';
import 'package:photos/ui/components/title_bar_title_widget.dart';
import "package:photos/ui/sharing/share_collection_page.dart";
import 'package:photos/ui/viewer/gallery/collection_page.dart';
import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/navigation_util.dart';
@ -29,7 +35,14 @@ import 'package:photos/utils/share_util.dart';
import 'package:photos/utils/toast_util.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
enum CollectionActionType { addFiles, moveFiles, restoreFiles, unHide }
enum CollectionActionType {
addFiles,
moveFiles,
restoreFiles,
unHide,
shareCollection,
collectPhotos,
}
String _actionName(CollectionActionType type, bool plural) {
bool addTitleSuffix = false;
@ -50,21 +63,27 @@ String _actionName(CollectionActionType type, bool plural) {
case CollectionActionType.unHide:
text = "Unhide to album";
break;
case CollectionActionType.shareCollection:
text = "Share";
break;
case CollectionActionType.collectPhotos:
text = "Share";
break;
}
return addTitleSuffix ? text + titleSuffix : text;
}
void createCollectionSheet(
void showCollectionActionSheet(
BuildContext context, {
SelectedFiles? selectedFiles,
List<SharedMediaFile>? sharedFiles,
BuildContext context, {
CollectionActionType actionType = CollectionActionType.addFiles,
bool showOptionToCreateNewAlbum = true,
}) {
showBarModalBottomSheet(
context: context,
builder: (context) {
return CreateCollectionSheet(
return CollectionActionSheet(
selectedFiles: selectedFiles,
sharedFiles: sharedFiles,
actionType: actionType,
@ -84,12 +103,12 @@ void createCollectionSheet(
);
}
class CreateCollectionSheet extends StatefulWidget {
class CollectionActionSheet extends StatefulWidget {
final SelectedFiles? selectedFiles;
final List<SharedMediaFile>? sharedFiles;
final CollectionActionType actionType;
final bool showOptionToCreateNewAlbum;
const CreateCollectionSheet({
const CollectionActionSheet({
required this.selectedFiles,
required this.sharedFiles,
required this.actionType,
@ -98,17 +117,17 @@ class CreateCollectionSheet extends StatefulWidget {
});
@override
State<CreateCollectionSheet> createState() => _CreateCollectionSheetState();
State<CollectionActionSheet> createState() => _CollectionActionSheetState();
}
class _CreateCollectionSheetState extends State<CreateCollectionSheet> {
final _logger = Logger((_CreateCollectionSheetState).toString());
class _CollectionActionSheetState extends State<CollectionActionSheet> {
final _logger = Logger((_CollectionActionSheetState).toString());
@override
Widget build(BuildContext context) {
final filesCount = widget.sharedFiles != null
? widget.sharedFiles!.length
: widget.selectedFiles!.files.length;
: widget.selectedFiles?.files.length ?? 0;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@ -125,7 +144,9 @@ class _CreateCollectionSheetState extends State<CreateCollectionSheet> {
title: TitleBarTitleWidget(
title: _actionName(widget.actionType, filesCount > 1),
),
caption: "Create or select album",
caption: widget.showOptionToCreateNewAlbum
? "Create or select album"
: "Select album",
),
Flexible(
child: Column(
@ -147,32 +168,18 @@ class _CreateCollectionSheetState extends State<CreateCollectionSheet> {
} else if (snapshot.hasData) {
final collectionsWithThumbnail = snapshot
.data as List<CollectionWithThumbnail>;
_removeIncomingCollections(
collectionsWithThumbnail,
);
return ListView.separated(
itemBuilder: (context, index) {
if (index == 0 &&
widget.showOptionToCreateNewAlbum) {
return GestureDetector(
onTap: () async {
final result =
await showTextInputDialog(
context,
title: "Album title",
submitButtonLabel: "OK",
hintText: "Enter album name",
onSubmit: _nameAlbum,
showOnlyLoadingState: true,
textCapitalization:
TextCapitalization.words,
await _createNewAlbumOnTap(
filesCount,
);
if (result is Exception) {
showGenericErrorDialog(
context: context,
);
_logger.severe(
"Failed to name album",
result,
);
}
},
behavior: HitTestBehavior.opaque,
child:
@ -244,12 +251,48 @@ class _CreateCollectionSheetState extends State<CreateCollectionSheet> {
);
}
Future<void> _createNewAlbumOnTap(int filesCount) async {
if (filesCount > 0) {
final result = await showTextInputDialog(
context,
title: "Album title",
submitButtonLabel: "OK",
hintText: "Enter album name",
onSubmit: _nameAlbum,
showOnlyLoadingState: true,
textCapitalization: TextCapitalization.words,
);
if (result is Exception) {
showGenericErrorDialog(
context: context,
);
_logger.severe(
"Failed to name album",
result,
);
}
} else {
Navigator.pop(context);
await showToast(
context,
"Long press to select photos and click + to create an album",
toastLength: Toast.LENGTH_LONG,
);
Bus.instance.fire(
TabChangedEvent(
0,
TabChangedEventSource.collectionsPage,
),
);
}
}
Future<void> _nameAlbum(String albumName) async {
if (albumName.isNotEmpty) {
final collection = await _createAlbum(albumName);
if (collection != null) {
if (await _runCollectionAction(
collectionID: collection.id,
collection: collection,
showProgressDialog: false,
)) {
if (widget.actionType == CollectionActionType.restoreFiles) {
@ -281,16 +324,29 @@ class _CreateCollectionSheetState extends State<CreateCollectionSheet> {
}
Future<void> _albumListItemOnTap(CollectionWithThumbnail item) async {
if (await _runCollectionAction(collectionID: item.collection.id)) {
showShortToast(
context,
widget.actionType == CollectionActionType.addFiles
? "Added successfully to " + item.collection.name!
: "Moved successfully to " + item.collection.name!,
);
_navigateToCollection(
item.collection,
);
if (await _runCollectionAction(collection: item.collection)) {
late final String toastMessage;
bool shouldNavigateToCollection = false;
if (widget.actionType == CollectionActionType.addFiles) {
toastMessage = "Added successfully to " + item.collection.name!;
shouldNavigateToCollection = true;
} else if (widget.actionType == CollectionActionType.moveFiles) {
toastMessage = "Moved successfully to " + item.collection.name!;
shouldNavigateToCollection = true;
} else {
toastMessage = "";
}
if (toastMessage.isNotEmpty) {
showShortToast(
context,
toastMessage,
);
}
if (shouldNavigateToCollection) {
_navigateToCollection(
item.collection,
);
}
}
}
@ -326,25 +382,117 @@ class _CreateCollectionSheetState extends State<CreateCollectionSheet> {
);
}
_removeIncomingCollections(List<CollectionWithThumbnail> items) {
if (widget.actionType == CollectionActionType.shareCollection ||
widget.actionType == CollectionActionType.collectPhotos) {
final ownerID = Configuration.instance.getUserID();
items.removeWhere(
(e) => !e.collection.isOwner(ownerID!),
);
}
}
Future<bool> _runCollectionAction({
required int collectionID,
required Collection collection,
bool showProgressDialog = true,
}) async {
switch (widget.actionType) {
case CollectionActionType.addFiles:
return _addToCollection(
collectionID: collectionID,
collectionID: collection.id,
showProgressDialog: showProgressDialog,
);
case CollectionActionType.moveFiles:
return _moveFilesToCollection(collectionID);
return _moveFilesToCollection(collection.id);
case CollectionActionType.unHide:
return _moveFilesToCollection(collectionID);
return _moveFilesToCollection(collection.id);
case CollectionActionType.restoreFiles:
return _restoreFilesToCollection(collectionID);
return _restoreFilesToCollection(collection.id);
case CollectionActionType.shareCollection:
return _showShareCollectionPage(collection);
case CollectionActionType.collectPhotos:
return _createCollaborativeLink(collection);
}
}
Future<bool> _createCollaborativeLink(Collection collection) async {
final CollectionActions collectionActions =
CollectionActions(CollectionsService.instance);
if (collection.hasLink) {
if (collection.publicURLs!.first!.enableCollect) {
if (Configuration.instance.getUserID() == collection.owner!.id) {
unawaited(
routeToPage(
context,
ShareCollectionPage(collection),
),
);
}
showToast(context, "This album already has a collaborative link");
return Future.value(false);
} else {
try {
unawaited(
routeToPage(
context,
ShareCollectionPage(collection),
),
);
CollectionsService.instance
.updateShareUrl(collection, {'enableCollect': true}).then(
(value) => showToast(
context,
"Collaborative link created for " + collection.name!,
),
);
return true;
} catch (e) {
showGenericErrorDialog(context: context);
return false;
}
}
}
final bool result = await collectionActions.enableUrl(
context,
collection,
enableCollect: true,
);
if (result) {
showToast(
context,
"Collaborative link created for " + collection.name!,
);
if (Configuration.instance.getUserID() == collection.owner!.id) {
unawaited(
routeToPage(
context,
ShareCollectionPage(collection),
),
);
} else {
showGenericErrorDialog(context: context);
_logger.severe("Cannot share collections owned by others");
}
}
return result;
}
Future<bool> _showShareCollectionPage(Collection collection) {
if (Configuration.instance.getUserID() == collection.owner!.id) {
unawaited(
routeToPage(
context,
ShareCollectionPage(collection),
),
);
} else {
showGenericErrorDialog(context: context);
_logger.severe("Cannot share collections owned by others");
}
return Future.value(true);
}
Future<bool> _addToCollection({
required int collectionID,
required bool showProgressDialog,
@ -457,8 +605,11 @@ class _CreateCollectionSheetState extends State<CreateCollectionSheet> {
}
Future<bool> _restoreFilesToCollection(int toCollectionID) async {
final dialog = createProgressDialog(context, "Restoring files...",
isDismissible: true);
final dialog = createProgressDialog(
context,
"Restoring files...",
isDismissible: true,
);
await dialog.show();
try {
await CollectionsService.instance

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/models/magic_metadata.dart';
import "package:photos/services/collections_service.dart";
import 'package:photos/ui/viewer/gallery/archive_page.dart';
import 'package:photos/utils/navigation_util.dart';
@ -15,6 +16,8 @@ class ArchivedCollectionsButtonWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final Set<int> hiddenCollectionId =
CollectionsService.instance.getHiddenCollections();
return OutlinedButton(
style: OutlinedButton.styleFrom(
backgroundColor: Theme.of(context).backgroundColor,
@ -43,9 +46,10 @@ class ArchivedCollectionsButtonWidget extends StatelessWidget {
),
const Padding(padding: EdgeInsets.all(6)),
FutureBuilder<int>(
future: FilesDB.instance.fileCountWithVisibility(
future: FilesDB.instance.archivedFilesCount(
visibilityArchive,
Configuration.instance.getUserID()!,
hiddenCollectionId,
),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data! > 0) {

View file

@ -85,9 +85,12 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
final List<CollectionWithThumbnail> collectionsWithThumbnail =
await CollectionsService.instance.getCollectionsWithThumbnails();
// Remove uncategorized collection
collectionsWithThumbnail
.removeWhere((t) => t.collection.type == CollectionType.uncategorized);
// Remove uncategorized collection and archived collections
collectionsWithThumbnail.removeWhere(
(t) =>
t.collection.type == CollectionType.uncategorized ||
t.collection.isArchived(),
);
final ListMatch<CollectionWithThumbnail> favMathResult =
collectionsWithThumbnail.splitMatch(
(element) => element.collection.type == CollectionType.favorites,

View file

@ -0,0 +1,100 @@
import "dart:async";
import "package:flutter/cupertino.dart";
import "package:logging/logging.dart";
import "package:photos/core/event_bus.dart";
import "package:photos/events/collection_updated_event.dart";
import "package:photos/models/collection_items.dart";
import "package:photos/ui/collections/collection_item_widget.dart";
import "package:photos/ui/common/loading_widget.dart";
import "package:photos/ui/components/divider_widget.dart";
class AlbumHorizontalListWidget extends StatefulWidget {
final Future<List<CollectionWithThumbnail>> Function() collectionsFuture;
const AlbumHorizontalListWidget(
this.collectionsFuture, {
Key? key,
}) : super(key: key);
@override
State<AlbumHorizontalListWidget> createState() =>
_AlbumHorizontalListWidgetState();
}
class _AlbumHorizontalListWidgetState extends State<AlbumHorizontalListWidget> {
late StreamSubscription<CollectionUpdatedEvent>
_collectionUpdatesSubscription;
late Logger _logger;
@override
void initState() {
_collectionUpdatesSubscription =
Bus.instance.on<CollectionUpdatedEvent>().listen((event) {
setState(() {});
});
_logger = Logger((_AlbumHorizontalListWidgetState).toString());
super.initState();
}
@override
void dispose() {
_collectionUpdatesSubscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
debugPrint('$runtimeType widget build');
return FutureBuilder<List<CollectionWithThumbnail>>(
future: widget.collectionsFuture(),
builder: (context, snapshot) {
if (snapshot.hasError) {
_logger.severe("failed to fetch albums", snapshot.error);
return const Text("Something went wrong");
} else if (snapshot.hasData) {
if (snapshot.data!.isEmpty) {
return const SizedBox.shrink();
}
final collectionsWithThumbnail =
snapshot.data as List<CollectionWithThumbnail>;
return Align(
alignment: Alignment.centerLeft,
child: SizedBox(
height: 190,
child: Column(
children: [
Expanded(
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: collectionsWithThumbnail.length,
padding: const EdgeInsets.fromLTRB(6, 6, 6, 6),
itemBuilder: (context, index) {
final item = collectionsWithThumbnail[index];
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () async {},
child: Padding(
padding: const EdgeInsets.all(2.0),
child: CollectionItem(
item,
120,
shouldRender: true,
),
),
);
},
),
),
const DividerWidget(dividerType: DividerType.solid),
],
),
),
);
} else {
return const EnteLoadingWidget();
}
},
);
}
}

View file

@ -0,0 +1,34 @@
import "package:flutter/material.dart";
import "package:photos/theme/ente_theme.dart";
///https://www.figma.com/file/SYtMyLBs5SAOkTbfMMzhqt/ente-Visual-Design?node-id=11379%3A67490&t=VI5KulbW3HMM5MVz-4
class EmptyStateItemWidget extends StatelessWidget {
final String textContent;
const EmptyStateItemWidget(this.textContent, {super.key});
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
final textTheme = getEnteTextTheme(context);
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.check_outlined,
size: 17,
color: colorScheme.strokeFaint,
),
const SizedBox(width: 6),
Flexible(
child: Text(
textContent,
style: textTheme.small.copyWith(
color: colorScheme.textFaint,
),
),
),
],
);
}
}

View file

@ -1,71 +0,0 @@
import 'package:flutter/material.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/theme/colors.dart';
import 'package:photos/theme/text_style.dart';
import 'package:photos/ui/components/icon_button_widget.dart';
class NotificationWarningWidget extends StatelessWidget {
final IconData warningIcon;
final IconData actionIcon;
final String text;
final GestureTapCallback onTap;
const NotificationWarningWidget({
Key? key,
required this.warningIcon,
required this.actionIcon,
required this.text,
required this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12),
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
boxShadow: Theme.of(context).colorScheme.enteTheme.shadowMenu,
color: warning500,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(
warningIcon,
size: 36,
color: Colors.white,
),
const SizedBox(width: 12),
Flexible(
child: Text(
text,
style: darkTextTheme.bodyBold,
textAlign: TextAlign.left,
),
),
const SizedBox(width: 12),
IconButtonWidget(
icon: actionIcon,
iconButtonType: IconButtonType.rounded,
iconColor: strokeBaseDark,
defaultColor: fillFaintDark,
pressedColor: fillMutedDark,
onTap: onTap,
)
],
),
),
),
),
),
);
}
}

View file

@ -0,0 +1,109 @@
import 'package:flutter/material.dart';
import 'package:photos/ente_theme_data.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/components/icon_button_widget.dart';
// CreateNotificationType enum
enum NotificationType {
warning,
banner,
goldenBanner,
}
class NotificationWidget extends StatelessWidget {
final IconData startIcon;
final IconData actionIcon;
final String text;
final String? subText;
final GestureTapCallback onTap;
final NotificationType type;
const NotificationWidget({
Key? key,
required this.startIcon,
required this.actionIcon,
required this.text,
required this.onTap,
this.subText,
this.type = NotificationType.warning,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
LinearGradient? backgroundGradient;
Color? backgroundColor;
switch (type) {
case NotificationType.warning:
backgroundColor = warning500;
break;
case NotificationType.banner:
backgroundColor = backgroundElevated2Dark;
break;
case NotificationType.goldenBanner:
backgroundGradient = LinearGradient(
colors: [colorScheme.golden700, colorScheme.golden500],
stops: const [0.25, 1],
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
);
}
return Center(
child: GestureDetector(
onTap: onTap,
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
boxShadow: Theme.of(context).colorScheme.enteTheme.shadowMenu,
color: backgroundColor,
gradient: backgroundGradient,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(
startIcon,
size: 36,
color: Colors.white,
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
text,
style: darkTextTheme.bodyBold,
textAlign: TextAlign.left,
),
subText != null
? Text(
subText!,
style: darkTextTheme.mini
.copyWith(color: textMutedDark),
)
: const SizedBox.shrink(),
],
),
const SizedBox(width: 12),
IconButtonWidget(
icon: actionIcon,
iconButtonType: IconButtonType.rounded,
iconColor: strokeBaseDark,
defaultColor: fillFaintDark,
pressedColor: fillMutedDark,
onTap: onTap,
)
],
),
),
),
),
);
}
}

View file

@ -142,6 +142,7 @@ class _TextInputWidgetState extends State<TextInputWidget> {
shouldSurfaceExecutionStates:
widget.shouldSurfaceExecutionStates,
obscureTextNotifier: _obscureTextNotifier,
isPasswordInput: widget.isPasswordInput,
),
),
),
@ -294,10 +295,12 @@ class SuffixIconWidget extends StatelessWidget {
final ExecutionState executionState;
final bool shouldSurfaceExecutionStates;
final ValueNotifier? obscureTextNotifier;
final bool isPasswordInput;
const SuffixIconWidget({
required this.executionState,
required this.shouldSurfaceExecutionStates,
this.obscureTextNotifier,
this.isPasswordInput = false,
super.key,
});
@ -307,7 +310,8 @@ class SuffixIconWidget extends StatelessWidget {
final colorScheme = getEnteColorScheme(context);
if (executionState == ExecutionState.idle ||
!shouldSurfaceExecutionStates) {
if (obscureTextNotifier != null) {
if (isPasswordInput) {
assert(obscureTextNotifier != null);
trailingWidget = GestureDetector(
onTap: () {
obscureTextNotifier!.value = !obscureTextNotifier!.value;

View file

@ -0,0 +1,188 @@
import "package:flutter/material.dart";
import "package:logging/logging.dart";
import "package:photos/extensions/input_formatter.dart";
import "package:photos/models/api/storage_bonus/storage_bonus.dart";
import "package:photos/models/user_details.dart";
import "package:photos/services/storage_bonus_service.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/components/button_widget.dart";
import "package:photos/ui/components/icon_button_widget.dart";
import "package:photos/ui/components/models/button_type.dart";
import "package:photos/ui/components/title_bar_title_widget.dart";
import "package:photos/ui/components/title_bar_widget.dart";
import "package:photos/ui/growth/code_success_screen.dart";
import "package:photos/utils/dialog_util.dart";
class ApplyCodeScreen extends StatefulWidget {
// referrerView and userDetails used to render code_success_screen
final ReferralView referralView;
final UserDetails userDetails;
const ApplyCodeScreen(
this.referralView,
this.userDetails, {
super.key,
});
@override
State<ApplyCodeScreen> createState() => _ApplyCodeScreenState();
}
class _ApplyCodeScreenState extends State<ApplyCodeScreen> {
late TextEditingController _textController;
late FocusNode textFieldFocusNode;
String code = "";
@override
void initState() {
_textController = TextEditingController();
textFieldFocusNode = FocusNode();
super.initState();
}
@override
void dispose() {
_textController.dispose();
textFieldFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
final textStyle = getEnteTextTheme(context);
textFieldFocusNode.requestFocus();
return Scaffold(
body: CustomScrollView(
primary: false,
slivers: <Widget>[
TitleBarWidget(
flexibleSpaceTitle: const TitleBarTitleWidget(
title: "Apply code",
),
actionIcons: [
IconButtonWidget(
icon: Icons.close_outlined,
iconButtonType: IconButtonType.secondary,
onTap: () {
// Go three screen back, similar to pop thrice
Navigator.of(context)
..pop()
..pop()
..pop();
},
),
],
),
SliverList(
delegate: SliverChildBuilderDelegate(
(delegateBuildContext, index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Column(
children: [
Text(
"Enter the code provided by your friend to "
"claim free storage for both of you",
style: textStyle.small
.copyWith(color: colorScheme.textMuted),
),
const SizedBox(height: 24),
_getInputField(),
// Container with 8 border radius and red color
],
),
],
),
),
);
},
childCount: 1,
),
),
SliverFillRemaining(
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
ButtonWidget(
buttonType: ButtonType.neutral,
buttonSize: ButtonSize.large,
labelText: "Apply",
isDisabled: code.trim().length < 4,
onTap: () async {
try {
await StorageBonusService.instance
.getGateway()
.claimReferralCode(code.trim().toUpperCase());
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => CodeSuccessScreen(
widget.referralView,
widget.userDetails,
),
),
);
} catch (e) {
Logger('$runtimeType')
.severe("failed to apply referral", e);
showErrorDialogForException(
context: context,
exception: e as Exception,
apiErrorPrefix: "Failed to apply code");
}
},
)
],
),
),
),
),
],
),
);
}
Widget _getInputField() {
return TextFormField(
controller: _textController,
focusNode: textFieldFocusNode,
style: getEnteTextTheme(context).body,
inputFormatters: [UpperCaseTextFormatter()],
textCapitalization: TextCapitalization.sentences,
decoration: InputDecoration(
focusedBorder: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(4.0)),
borderSide:
BorderSide(color: getEnteColorScheme(context).strokeMuted),
),
fillColor: getEnteColorScheme(context).fillFaint,
filled: true,
hintText: 'Enter referral code',
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
border: UnderlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(8),
),
),
onChanged: (value) {
code = value.trim();
setState(() {});
},
autocorrect: false,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
);
}
}

View file

@ -0,0 +1,163 @@
import "package:flutter/material.dart";
import "package:flutter_animate/flutter_animate.dart";
import "package:photos/models/api/storage_bonus/storage_bonus.dart";
import "package:photos/models/user_details.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/components/captioned_text_widget.dart";
import "package:photos/ui/components/icon_button_widget.dart";
import "package:photos/ui/components/menu_item_widget/menu_item_widget.dart";
import "package:photos/ui/components/title_bar_title_widget.dart";
import "package:photos/ui/components/title_bar_widget.dart";
import "package:photos/ui/growth/referral_code_widget.dart";
import "package:photos/ui/growth/storage_details_screen.dart";
import "package:photos/utils/navigation_util.dart";
import "package:photos/utils/share_util.dart";
class CodeSuccessScreen extends StatelessWidget {
final ReferralView referralView;
final UserDetails userDetails;
const CodeSuccessScreen(this.referralView, this.userDetails, {super.key});
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
final textStyle = getEnteTextTheme(context);
return Scaffold(
body: CustomScrollView(
primary: false,
slivers: <Widget>[
TitleBarWidget(
flexibleSpaceTitle: const TitleBarTitleWidget(
title: "Code applied",
),
actionIcons: [
IconButtonWidget(
icon: Icons.close_outlined,
iconButtonType: IconButtonType.secondary,
onTap: () {
Navigator.pop(context);
},
),
],
),
SliverList(
delegate: SliverChildBuilderDelegate(
(delegateBuildContext, index) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Icons.check,
color: colorScheme.primary500,
size: 96,
)
.animate()
.scaleXY(
begin: 0.5,
end: 1,
duration: 750.ms,
curve: Curves.easeInOutCubic,
delay: 250.ms,
)
.fadeIn(
duration: 500.ms,
curve: Curves.easeInOutCubic,
),
Text(
"${referralView.planInfo.storageInGB} GB",
style: textStyle.h2Bold,
),
Text(
"Claimed",
style: textStyle.body
.copyWith(color: colorScheme.textMuted),
),
const SizedBox(height: 32),
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Details",
),
menuItemColor: colorScheme.fillFaint,
trailingWidget: Icon(
Icons.chevron_right_outlined,
color: colorScheme.strokeBase,
),
singleBorderRadius: 8,
alignCaptionedTextToLeft: true,
onTap: () async {
routeToPage(
context,
StorageDetailsScreen(referralView, userDetails),
);
},
),
const SizedBox(height: 32),
InkWell(
onTap: () {
shareText(
"ente referral code: ${referralView.code} \n\nApply it in Settings → General → Referrals to get ${referralView.planInfo.storageInGB} GB free after you signup for a paid plan\n\nhttps://ente.io",
);
},
child: Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border.all(
color: colorScheme.strokeFaint,
width: 1,
),
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 12,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"Claim more!",
style: textStyle.body,
),
const SizedBox(height: 8),
Text(
"${referralView.planInfo.storageInGB} GB each time someone signs up for a paid plan and applies your code",
style: textStyle.small
.copyWith(color: colorScheme.textMuted),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ReferralCodeWidget(referralView.code),
const SizedBox(height: 16),
Text(
"They also get ${referralView.planInfo.storageInGB} GB",
style: textStyle.small
.copyWith(color: colorScheme.textMuted),
textAlign: TextAlign.center,
),
],
),
),
),
)
],
),
),
);
},
childCount: 1,
),
),
],
),
);
}
}

View file

@ -0,0 +1,52 @@
import "package:dotted_border/dotted_border.dart";
import "package:flutter/material.dart";
import "package:photos/theme/ente_theme.dart";
// Figma: https://www.figma.com/file/SYtMyLBs5SAOkTbfMMzhqt/ente-Visual-Design?node-id=11219%3A62974&t=BRCLJhxXP11Q3Wyw-0
class ReferralCodeWidget extends StatelessWidget {
final String codeValue;
const ReferralCodeWidget(this.codeValue, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
final textStyle = getEnteTextTheme(context);
return Center(
child: Container(
color: colorScheme.backgroundElevated2,
child: DottedBorder(
color: colorScheme.strokeMuted,
strokeWidth: 1,
dashPattern: const [6, 6],
radius: const Radius.circular(8),
child: Padding(
padding: const EdgeInsets.only(
left: 26.0,
top: 14,
right: 12,
bottom: 14,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
codeValue,
style: textStyle.bodyBold.copyWith(
color: colorScheme.primary700,
),
),
const SizedBox(width: 12),
Icon(
Icons.adaptive.share,
size: 22,
color: colorScheme.strokeMuted,
)
],
),
),
),
),
);
}
}

View file

@ -0,0 +1,288 @@
import "package:flutter/material.dart";
import "package:photos/models/api/storage_bonus/storage_bonus.dart";
import "package:photos/models/user_details.dart";
import "package:photos/services/storage_bonus_service.dart";
import "package:photos/services/user_service.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/common/loading_widget.dart";
import "package:photos/ui/common/web_page.dart";
import "package:photos/ui/components/captioned_text_widget.dart";
import "package:photos/ui/components/divider_widget.dart";
import "package:photos/ui/components/icon_button_widget.dart";
import "package:photos/ui/components/menu_item_widget/menu_item_widget.dart";
import "package:photos/ui/components/title_bar_title_widget.dart";
import "package:photos/ui/components/title_bar_widget.dart";
import "package:photos/ui/growth/apply_code_screen.dart";
import "package:photos/ui/growth/referral_code_widget.dart";
import "package:photos/ui/growth/storage_details_screen.dart";
import "package:photos/utils/data_util.dart";
import "package:photos/utils/navigation_util.dart";
import "package:photos/utils/share_util.dart";
class ReferralScreen extends StatefulWidget {
const ReferralScreen({super.key});
@override
State<ReferralScreen> createState() => _ReferralScreenState();
}
class _ReferralScreenState extends State<ReferralScreen> {
bool canApplyCode = true;
void _safeUIUpdate() {
if (mounted) {
setState(() => {});
}
}
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
primary: false,
slivers: <Widget>[
TitleBarWidget(
flexibleSpaceTitle: const TitleBarTitleWidget(
title: "Claim free storage",
),
flexibleSpaceCaption: "Invite your friends",
actionIcons: [
IconButtonWidget(
icon: Icons.close_outlined,
iconButtonType: IconButtonType.secondary,
onTap: () {
Navigator.pop(context);
Navigator.pop(context);
},
),
],
),
SliverList(
delegate: SliverChildBuilderDelegate(
(delegateBuildContext, index) {
return Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
child: FutureBuilder<ReferralView>(
future: StorageBonusService.instance
.getGateway()
.getReferralView(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ReferralWidget(
snapshot.data!,
UserService.instance.getCachedUserDetails()!,
_safeUIUpdate,
);
} else if (snapshot.hasError) {
return const Center(
child: Padding(
padding: EdgeInsets.all(24.0),
child: Text(
"Unable to fetch referral details. Please try again later.",
),
),
);
}
{
return const EnteLoadingWidget();
}
},
),
);
},
childCount: 1,
),
),
],
),
);
}
}
class ReferralWidget extends StatelessWidget {
final ReferralView referralView;
final UserDetails userDetails;
final Function notifyParent;
const ReferralWidget(
this.referralView,
this.userDetails,
this.notifyParent, {
super.key,
});
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
final textStyle = getEnteTextTheme(context);
final bool isReferralEnabled = referralView.planInfo.isEnabled;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Container with 8 border radius and red color
isReferralEnabled
? InkWell(
onTap: () {
shareText(
"ente referral code: ${referralView.code} \n\nApply it in Settings → General → Referrals to get 10 GB free after you signup for a paid plan\n\nhttps://ente.io",
);
},
child: Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border.all(
color: colorScheme.strokeFaint,
width: 1,
),
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 12,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"1. Give this code to your "
"friends",
),
const SizedBox(height: 12),
ReferralCodeWidget(referralView.code),
const SizedBox(height: 12),
const Text(
"2. They sign up for a paid plan",
),
const SizedBox(height: 12),
Text(
"3. Both of you get ${referralView.planInfo.storageInGB} "
"GB* free",
),
],
),
),
),
)
: Padding(
padding: const EdgeInsets.symmetric(vertical: 48),
child: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
color: colorScheme.strokeMuted,
),
const SizedBox(height: 12),
Text(
"Referrals are currently paused",
style: textStyle.small
.copyWith(color: colorScheme.textFaint),
),
],
),
),
),
const SizedBox(height: 4),
isReferralEnabled
? Text(
"* You can at max double your storage",
style: textStyle.mini.copyWith(
color: colorScheme.textMuted,
),
textAlign: TextAlign.left,
)
: const SizedBox.shrink(),
const SizedBox(height: 24),
referralView.enableApplyCode
? MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Apply code",
),
menuItemColor: colorScheme.fillFaint,
trailingWidget: Icon(
Icons.chevron_right_outlined,
color: colorScheme.strokeBase,
),
singleBorderRadius: 8,
alignCaptionedTextToLeft: true,
isBottomBorderRadiusRemoved: true,
onTap: () async {
await routeToPage(
context,
ApplyCodeScreen(referralView, userDetails),
);
notifyParent();
},
)
: const SizedBox.shrink(),
referralView.enableApplyCode
? DividerWidget(
dividerType: DividerType.menu,
bgColor: colorScheme.fillFaint,
)
: const SizedBox.shrink(),
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "FAQ",
),
menuItemColor: colorScheme.fillFaint,
trailingWidget: Icon(
Icons.chevron_right_outlined,
color: colorScheme.strokeBase,
),
singleBorderRadius: 8,
isTopBorderRadiusRemoved: referralView.enableApplyCode,
alignCaptionedTextToLeft: true,
onTap: () async {
routeToPage(
context,
const WebPage(
"FAQ",
"https://ente.io/faq/general/referral-program",
),
);
},
),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 6.0,
),
child: Text(
"${referralView.isFamilyMember ? 'Your family has' : 'You have'} claimed "
"${convertBytesToAbsoluteGBs(referralView.claimedStorage)} GB so far",
style: textStyle.small.copyWith(
color: colorScheme.textMuted,
),
),
),
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Details",
),
menuItemColor: colorScheme.fillFaint,
trailingWidget: Icon(
Icons.chevron_right_outlined,
color: colorScheme.strokeBase,
),
singleBorderRadius: 8,
alignCaptionedTextToLeft: true,
onTap: () async {
routeToPage(
context,
StorageDetailsScreen(referralView, userDetails),
);
},
),
],
);
}
}

View file

@ -0,0 +1,221 @@
import "dart:math";
import "package:flutter/material.dart";
import "package:photos/models/api/storage_bonus/storage_bonus.dart";
import "package:photos/models/user_details.dart";
import "package:photos/services/storage_bonus_service.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/common/loading_widget.dart";
import "package:photos/ui/components/icon_button_widget.dart";
import "package:photos/ui/components/title_bar_title_widget.dart";
import "package:photos/ui/components/title_bar_widget.dart";
import "package:photos/utils/data_util.dart";
class StorageDetailsScreen extends StatefulWidget {
final ReferralView referralView;
final UserDetails userDetails;
const StorageDetailsScreen(this.referralView, this.userDetails, {super.key});
@override
State<StorageDetailsScreen> createState() => _StorageDetailsScreenState();
}
class _StorageDetailsScreenState extends State<StorageDetailsScreen> {
bool canApplyCode = true;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
final textStyle = getEnteTextTheme(context);
return Scaffold(
body: CustomScrollView(
primary: false,
slivers: <Widget>[
TitleBarWidget(
flexibleSpaceTitle: const TitleBarTitleWidget(
title: "Claim free storage",
),
flexibleSpaceCaption: "Details",
actionIcons: [
IconButtonWidget(
icon: Icons.close_outlined,
iconButtonType: IconButtonType.secondary,
onTap: () {
Navigator.of(context)
..pop()
..pop()
..pop();
},
),
],
),
SliverList(
delegate: SliverChildBuilderDelegate(
(delegateBuildContext, index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
// wrap the child inside a FutureBuilder to get the
// current state of the TextField
child: FutureBuilder<BonusDetails>(
future: StorageBonusService.instance
.getGateway()
.getBonusDetails(),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const Center(
child: Padding(
padding: EdgeInsets.only(top: 48.0),
child: EnteLoadingWidget(),
),
);
}
if (snapshot.hasError) {
debugPrint(snapshot.error.toString());
return const Text("Oops, something went wrong");
} else {
final BonusDetails data = snapshot.data!;
return Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BonusInfoSection(
sectionName: "People using your code",
leftValue: data.refUpgradeCount,
leftUnitName: "eligible",
rightValue: data.refUpgradeCount >= 0
? data.refCount
: null,
rightUnitName: "total",
showUnit: data.refCount > 0,
),
data.hasAppliedCode
? const BonusInfoSection(
sectionName: "Code used by you",
leftValue: 1,
showUnit: false,
)
: const SizedBox.shrink(),
BonusInfoSection(
sectionName: "Free storage claimed",
leftValue: convertBytesToAbsoluteGBs(
widget.referralView.claimedStorage,
),
leftUnitName: "GB",
rightValue: null,
),
BonusInfoSection(
sectionName: "Free storage usable",
leftValue: convertBytesToAbsoluteGBs(
min(
widget.referralView.claimedStorage,
widget.userDetails.getTotalStorage(),
),
),
leftUnitName: "GB",
rightValue: convertBytesToAbsoluteGBs(
widget.userDetails.getTotalStorage(),
),
rightUnitName: "GB",
),
const SizedBox(
height: 24,
),
Text(
"Usable storage is limited by your current"
" plan. Excess"
" claimed storage will automatically become"
" usable when you upgrade your plan.",
style: textStyle.small
.copyWith(color: colorScheme.textMuted),
)
],
),
);
}
},
),
),
);
},
childCount: 1,
),
),
],
),
);
}
}
class BonusInfoSection extends StatelessWidget {
final String sectionName;
final bool showUnit;
final String leftUnitName;
final String rightUnitName;
final int leftValue;
final int? rightValue;
const BonusInfoSection({
super.key,
required this.sectionName,
required this.leftValue,
this.leftUnitName = "GB",
this.rightValue,
this.rightUnitName = "GB",
this.showUnit = true,
});
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
final textStyle = getEnteTextTheme(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
sectionName,
style: textStyle.body.copyWith(
color: colorScheme.textMuted,
),
),
const SizedBox(height: 2),
RichText(
text: TextSpan(
children: [
TextSpan(
text: leftValue.toString(),
style: textStyle.h3,
),
TextSpan(
text: showUnit ? " $leftUnitName" : "",
style: textStyle.large,
),
TextSpan(
text: (rightValue != null && rightValue! > 0)
? " / ${rightValue.toString()}"
: "",
style: textStyle.h3,
),
TextSpan(
text: showUnit && (rightValue != null && rightValue! > 0)
? " $rightUnitName"
: "",
style: textStyle.large,
),
],
),
),
const SizedBox(height: 24),
],
);
}
}

View file

@ -219,7 +219,7 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
ButtonWidget(
buttonType: ButtonType.neutral,
buttonAction: ButtonAction.first,
labelText: "Ok",
labelText: "OK",
isInAlert: true,
),
],

View file

@ -1,8 +1,11 @@
import "dart:io";
import "package:flutter/cupertino.dart";
import 'package:flutter/material.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/models/memory.dart';
import 'package:photos/services/memories_service.dart';
import 'package:photos/ui/extents_page_view.dart';
import "package:photos/ui/actions/file/file_actions.dart";
import 'package:photos/ui/viewer/file/file_widget.dart';
import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
import 'package:photos/utils/date_time_util.dart';
@ -315,6 +318,22 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
);
}
void onFileDeleted() {
if (widget.memories.length == 1) {
Navigator.pop(context);
} else {
setState(() {
if (_index != 0) {
_pageController?.jumpToPage(_index - 1);
}
widget.memories.removeAt(_index);
if (_index != 0) {
_index--;
}
});
}
}
Hero _buildInfoText() {
return Hero(
tag: widget.title,
@ -346,17 +365,48 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
Widget _buildBottomIcons() {
final file = widget.memories[_index].file;
return Container(
alignment: Alignment.bottomRight,
padding: const EdgeInsets.fromLTRB(0, 0, 26, 20),
child: IconButton(
icon: Icon(
Icons.adaptive.share,
color: Colors.white, //same for both themes
return SafeArea(
child: Container(
alignment: Alignment.bottomRight,
padding: const EdgeInsets.fromLTRB(26, 0, 26, 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: Icon(
Platform.isAndroid ? Icons.info_outline : CupertinoIcons.info,
color: Colors.white, //same for both themes
),
onPressed: () {
showInfoSheet(context, file);
},
),
IconButton(
icon: Icon(
Platform.isAndroid
? Icons.delete_outline
: CupertinoIcons.delete,
color: Colors.white, //same for both themes
),
onPressed: () async {
await showSingleFileDeleteSheet(
context,
file,
onFileRemoved: (file) => {onFileDeleted()},
);
},
),
IconButton(
icon: Icon(
Icons.adaptive.share,
color: Colors.white, //same for both themes
),
onPressed: () {
share(context, [file]);
},
),
],
),
onPressed: () {
share(context, [file]);
},
),
);
}
@ -381,7 +431,7 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
Widget _buildSwiper() {
_pageController = PageController(initialPage: _index);
return ExtentsPageView.extents(
return PageView.builder(
itemBuilder: (BuildContext context, int index) {
if (index < widget.memories.length - 1) {
final nextFile = widget.memories[index + 1].file;
@ -405,7 +455,6 @@ class _FullScreenMemoryState extends State<FullScreenMemory> {
},
itemCount: widget.memories.length,
controller: _pageController,
extents: 1,
onPageChanged: (index) async {
await MemoriesService.instance.markMemoryAsSeen(widget.memories[index]);
if (mounted) {

View file

@ -10,7 +10,7 @@ import 'package:photos/services/user_remote_flag_service.dart';
import 'package:photos/theme/text_style.dart';
import 'package:photos/ui/account/verify_recovery_page.dart';
import 'package:photos/ui/components/home_header_widget.dart';
import 'package:photos/ui/components/notification_warning_widget.dart';
import 'package:photos/ui/components/notification_widget.dart';
import 'package:photos/ui/home/header_error_widget.dart';
import 'package:photos/utils/navigation_util.dart';
@ -97,17 +97,21 @@ class _StatusBarWidgetState extends State<StatusBarWidget> {
? HeaderErrorWidget(error: _syncError)
: const SizedBox.shrink(),
UserRemoteFlagService.instance.shouldShowRecoveryVerification()
? NotificationWarningWidget(
warningIcon: Icons.error_outline,
actionIcon: Icons.arrow_forward,
text: "Confirm your recovery key",
onTap: () async => {
await routeToPage(
context,
const VerifyRecoveryPage(),
forceCustomPageRoute: true,
)
},
? Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12),
child: NotificationWidget(
startIcon: Icons.error_outline,
actionIcon: Icons.arrow_forward,
text: "Confirm your recovery key",
onTap: () async => {
await routeToPage(
context,
const VerifyRecoveryPage(),
forceCustomPageRoute: true,
)
},
),
)
: const SizedBox.shrink()
],

View file

@ -28,9 +28,9 @@ import 'package:photos/services/user_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/collection_action_sheet.dart';
import 'package:photos/ui/collections_gallery_widget.dart';
import 'package:photos/ui/common/bottom_shadow.dart';
import 'package:photos/ui/create_collection_sheet.dart';
import 'package:photos/ui/extents_page_view.dart';
import 'package:photos/ui/home/grant_permissions_widget.dart';
import 'package:photos/ui/home/header_widget.dart';
@ -43,7 +43,7 @@ import 'package:photos/ui/loading_photos_widget.dart';
import 'package:photos/ui/notification/update/change_log_page.dart';
import 'package:photos/ui/settings/app_update_dialog.dart';
import 'package:photos/ui/settings_page.dart';
import 'package:photos/ui/shared_collections_gallery.dart';
import "package:photos/ui/shared_collections_gallery.dart";
import 'package:photos/utils/dialog_util.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:uni_links/uni_links.dart';
@ -264,7 +264,7 @@ class _HomeWidgetState extends State<HomeWidget> {
_logger.info("Building home_Widget with tab $_selectedTabIndex");
bool isSettingsOpen = false;
final enableDrawer = LocalSyncService.instance.hasCompletedFirstImport();
final action = AppLifecycleService.instance.mediaExtensionAction.action;
return UserDetailsStateWidget(
child: WillPopScope(
child: Scaffold(
@ -297,9 +297,7 @@ class _HomeWidgetState extends State<HomeWidget> {
Navigator.pop(context);
return false;
}
if (Platform.isAndroid &&
AppLifecycleService.instance.intentAction ==
IntentAction.main) {
if (Platform.isAndroid && action == IntentAction.main) {
MoveToBackground.moveTaskToBack();
return false;
} else {
@ -336,10 +334,9 @@ class _HomeWidgetState extends State<HomeWidget> {
_shouldRenderCreateCollectionSheet = false;
ReceiveSharingIntent.reset();
Future.delayed(const Duration(milliseconds: 10), () {
createCollectionSheet(
null,
_sharedFiles,
showCollectionActionSheet(
context,
sharedFiles: _sharedFiles,
actionType: CollectionActionType.addFiles,
);
});

View file

@ -311,7 +311,6 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
bool? _shouldRender;
int? _currentUserID;
late StreamSubscription<ClearSelectionsEvent> _clearSelectionsEvent;
final _mediaExtensionPlugin = MediaExtension();
@override
void initState() {
@ -426,16 +425,18 @@ class _LazyLoadingGridViewState extends State<LazyLoadingGridView> {
if (widget.selectedFiles.files.isNotEmpty) {
_selectFile(file);
} else {
if (AppLifecycleService.instance.intentAction == IntentAction.pick) {
if (AppLifecycleService.instance.mediaExtensionAction.action ==
IntentAction.pick) {
final ioFile = await getFile(file);
_mediaExtensionPlugin.setResult("file://${ioFile!.path}");
MediaExtension().setResult("file://${ioFile!.path}");
} else {
_routeToDetailPage(file, context);
}
}
},
onLongPress: () {
if (AppLifecycleService.instance.intentAction == IntentAction.main) {
if (AppLifecycleService.instance.mediaExtensionAction.action ==
IntentAction.main) {
HapticFeedback.lightImpact();
_selectFile(file);
}

View file

@ -0,0 +1,118 @@
import "package:flutter/material.dart";
import "package:photos/core/constants.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/collection_action_sheet.dart";
import "package:photos/ui/components/button_widget.dart";
import "package:photos/ui/components/empty_state_item_widget.dart";
import "package:photos/ui/components/models/button_type.dart";
import "package:photos/utils/share_util.dart";
class NewSharedCollectionsGallery extends StatelessWidget {
const NewSharedCollectionsGallery({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: restrictedMaxWidth),
child: const EmptyStateWidget(),
),
);
}
}
class EmptyStateWidget extends StatelessWidget {
const EmptyStateWidget({super.key});
@override
Widget build(BuildContext context) {
final textTheme = getEnteTextTheme(context);
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 114),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 4, vertical: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Private sharing",
style: textTheme.h3Bold,
textAlign: TextAlign.start,
),
const SizedBox(height: 24),
Column(
mainAxisSize: MainAxisSize.min,
children: const [
EmptyStateItemWidget(
"Share only with the people you want",
),
SizedBox(height: 12),
EmptyStateItemWidget(
"Use public links for people not on ente",
),
SizedBox(height: 12),
EmptyStateItemWidget(
"Allow people to add photos",
),
],
),
],
),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ButtonWidget(
buttonType: ButtonType.trailingIconPrimary,
labelText: "Share an album now",
icon: Icons.arrow_forward_outlined,
onTap: () async {
showCollectionActionSheet(
context,
actionType: CollectionActionType.shareCollection,
);
},
),
const SizedBox(height: 6),
ButtonWidget(
buttonType: ButtonType.trailingIconSecondary,
labelText: "Collect event photos",
icon: Icons.add_photo_alternate_outlined,
onTap: () async {
showCollectionActionSheet(
context,
actionType: CollectionActionType.collectPhotos,
);
},
),
const SizedBox(height: 6),
ButtonWidget(
buttonType: ButtonType.trailingIconSecondary,
labelText: "Invite your friends",
icon: Icons.ios_share_outlined,
onTap: () async {
shareText("Check out https://ente.io");
},
),
],
),
),
],
),
),
),
);
}
}

View file

@ -1,3 +1,5 @@
import "dart:io";
import 'package:flutter/material.dart';
import 'package:photos/services/update_service.dart';
import 'package:photos/theme/ente_theme.dart';
@ -103,37 +105,24 @@ class _ChangeLogPageState extends State<ChangeLogPage> {
final List<ChangeLogEntry> items = [];
items.add(
ChangeLogEntry(
"Collaborative albums ✨",
"Much awaited, they're here now - create albums where multiple ente "
"users can add photos!\n\nWhen sharing an album, you can specify if"
" you want to add someone as a viewer or a collaborator. Collaborators can add photos "
"to the shared album.\n\nAlbums can have both collaborators and viewers, and as many as "
"you like. Storage is only counted once, for the person who uploaded the photo."
"\n\nHead over to the sharing options for an album to start adding collaborators.",
),
);
items.add(
ChangeLogEntry(
"Uncategorized",
"You can now keep photos that do not belong to a specific album."
"\n\nThis will simplify deletion and make it safer since now ente "
"will have a place to put photos that don't belong to any album "
"instead of always deleting them.\n\nThis will also allow you to "
"choose between keeping vs deleting photos present in the album, "
"when deleting an album.\n\nUncategorized photos can be seen from "
"the bottom of the albums tab.",
),
);
items.add(
ChangeLogEntry(
'''Cleaner album picker''',
"Among other improvements, the list of albums that is shown when adding "
"or moving photos gets a facelift, and an issue causing the photo "
"zoom to be reset after loading the full resolution photo has been fixed.",
isFeature: false,
"Referrals ✨",
"You can now double your storage by referring your friends and family"
". Both you and your loved ones will get 10 GB of storage when "
"they upgrade to a paid plan.\n\nGo to Settings -> General -> "
"Referral to get started!",
),
);
if (Platform.isAndroid) {
items.add(
ChangeLogEntry(
"Pick Files",
"While sharing photos and videos through other apps, ente will now "
"be an option to pick files from. This means you can now easily"
" attach files backed up to ente.\n\nConsider this the first "
"step towards making ente your default gallery app!",
),
);
}
return Container(
padding: const EdgeInsets.only(left: 16),

View file

@ -1,5 +1,6 @@
import 'dart:async';
import "package:flutter/foundation.dart";
import 'package:flutter/material.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/models/billing_plan.dart';
@ -7,12 +8,15 @@ import 'package:photos/models/subscription.dart';
import 'package:photos/models/user_details.dart';
import 'package:photos/services/billing_service.dart';
import 'package:photos/services/user_service.dart';
import "package:photos/theme/colors.dart";
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/common/bottom_shadow.dart';
import 'package:photos/ui/common/loading_widget.dart';
import 'package:photos/ui/common/progress_dialog.dart';
import 'package:photos/ui/common/web_page.dart';
import 'package:photos/ui/components/button_widget.dart';
import "package:photos/ui/components/captioned_text_widget.dart";
import "package:photos/ui/components/menu_item_widget/menu_item_widget.dart";
import 'package:photos/ui/payment/child_subscription_widget.dart';
import 'package:photos/ui/payment/payment_web_page.dart';
import 'package:photos/ui/payment/skip_subscription_widget.dart';
@ -50,6 +54,7 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
bool _isLoading = false;
bool _isStripeSubscriber = false;
bool _showYearlyPlan = false;
EnteColorScheme colorScheme = darkScheme;
@override
void initState() {
@ -112,6 +117,7 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
@override
Widget build(BuildContext context) {
colorScheme = getEnteColorScheme(context);
final appBar = PreferredSize(
preferredSize: const Size(double.infinity, 60),
child: Container(
@ -143,7 +149,7 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
)
: AppBar(
elevation: 0,
title: const Text("Subscription"),
title: const Text("Subscription${kDebugMode ? ' Stripe' : ''}"),
),
),
);
@ -205,7 +211,9 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
if (widget.isOnboarding) {
widgets.add(SkipSubscriptionWidget(freePlan: _freePlan));
}
widgets.add(const SubFaqWidget());
widgets.add(
SubFaqWidget(isOnboarding: widget.isOnboarding),
);
}
// only active subscription can be renewed/canceled
@ -214,90 +222,49 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
}
if (_currentSubscription!.productID != freeProductID) {
widgets.addAll([
Align(
alignment: Alignment.center,
child: GestureDetector(
onTap: () async {
final String paymentProvider =
_currentSubscription!.paymentProvider;
switch (_currentSubscription!.paymentProvider) {
case stripe:
await _launchStripePortal();
break;
case playStore:
launchUrlString(
"https://play.google.com/store/account/subscriptions?sku=" +
_currentSubscription!.productID +
"&package=io.ente.photos",
);
break;
case appStore:
launchUrlString("https://apps.apple.com/account/billing");
break;
default:
final String capitalizedWord = paymentProvider.isNotEmpty
? '${paymentProvider[0].toUpperCase()}${paymentProvider.substring(1).toLowerCase()}'
: '';
showErrorDialog(
context,
"Sorry",
"Please contact us at support@ente.io to manage your "
"$capitalizedWord subscription.",
);
}
},
child: Container(
padding: const EdgeInsets.fromLTRB(40, 80, 40, 20),
child: Column(
children: [
RichText(
text: TextSpan(
text: "Payment details",
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontFamily: 'Inter-Medium',
fontSize: 14,
decoration: TextDecoration.underline,
),
),
textAlign: TextAlign.center,
),
],
),
widgets.add(
Padding(
padding: const EdgeInsets.fromLTRB(16, 40, 16, 4),
child: MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Payment details",
),
menuItemColor: colorScheme.fillFaint,
trailingWidget: Icon(
Icons.chevron_right_outlined,
color: colorScheme.strokeBase,
),
singleBorderRadius: 4,
alignCaptionedTextToLeft: true,
onTap: () async {
_onStripSupportedPaymentDetailsTap();
},
),
),
]);
);
}
if (!widget.isOnboarding) {
widgets.addAll([
Align(
alignment: Alignment.topCenter,
child: GestureDetector(
widgets.add(
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 80),
child: MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Manage Family",
),
menuItemColor: colorScheme.fillFaint,
trailingWidget: Icon(
Icons.chevron_right_outlined,
color: colorScheme.strokeBase,
),
singleBorderRadius: 4,
alignCaptionedTextToLeft: true,
onTap: () async {
_billingService.launchFamilyPortal(context, _userDetails);
},
child: Container(
padding: const EdgeInsets.fromLTRB(40, 0, 40, 80),
child: Column(
children: [
RichText(
text: TextSpan(
text: "Manage family",
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
decoration: TextDecoration.underline,
),
),
textAlign: TextAlign.center,
),
],
),
),
),
),
]);
);
}
return SingleChildScrollView(
@ -308,6 +275,37 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
);
}
// _onStripSupportedPaymentDetailsTap action allows the user to update
// their stripe payment details
void _onStripSupportedPaymentDetailsTap() async {
final String paymentProvider = _currentSubscription!.paymentProvider;
switch (_currentSubscription!.paymentProvider) {
case stripe:
await _launchStripePortal();
break;
case playStore:
launchUrlString(
"https://play.google.com/store/account/subscriptions?sku=" +
_currentSubscription!.productID +
"&package=io.ente.photos",
);
break;
case appStore:
launchUrlString("https://apps.apple.com/account/billing");
break;
default:
final String capitalizedWord = paymentProvider.isNotEmpty
? '${paymentProvider[0].toUpperCase()}${paymentProvider.substring(1).toLowerCase()}'
: '';
showErrorDialog(
context,
"Sorry",
"Please contact us at support@ente.io to manage your "
"$capitalizedWord subscription.",
);
}
}
Future<void> _launchStripePortal() async {
await _dialog.show();
try {
@ -336,9 +334,8 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
title,
style: TextStyle(
color: (isRenewCancelled
? Colors.greenAccent
: Theme.of(context).colorScheme.onSurface)
.withOpacity(isRenewCancelled ? 1.0 : 0.2),
? colorScheme.primary700
: colorScheme.textMuted),
),
),
onPressed: () async {

View file

@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/models/subscription.dart';
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/components/captioned_text_widget.dart";
import "package:photos/ui/components/menu_item_widget/menu_item_widget.dart";
import 'package:photos/ui/payment/billing_questions_widget.dart';
import 'package:photos/utils/data_util.dart';
import 'package:photos/utils/date_time_util.dart';
@ -38,11 +41,14 @@ class _SubscriptionHeaderWidgetState extends State<SubscriptionHeaderWidget> {
),
],
),
const SizedBox(
height: 10,
),
const SizedBox(height: 10),
Text(
"Ente preserves your memories, so they're always available to you, even if you lose your device ",
"ente preserves your memories, so they're always available to you, even if you lose your device.",
style: Theme.of(context).textTheme.caption,
),
const SizedBox(height: 4),
Text(
"Your family can be added to your plan as well. ",
style: Theme.of(context).textTheme.caption,
),
],
@ -107,15 +113,27 @@ class ValidityWidget extends StatelessWidget {
}
class SubFaqWidget extends StatelessWidget {
const SubFaqWidget({Key? key}) : super(key: key);
final bool isOnboarding;
const SubFaqWidget({Key? key, this.isOnboarding = false}) : super(key: key);
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.bottomCenter,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
final colorScheme = getEnteColorScheme(context);
return Padding(
padding: EdgeInsets.fromLTRB(16, 40, 16, isOnboarding ? 40 : 4),
child: MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "FAQs",
),
menuItemColor: colorScheme.fillFaint,
trailingWidget: Icon(
Icons.chevron_right_outlined,
color: colorScheme.strokeBase,
),
singleBorderRadius: 4,
alignCaptionedTextToLeft: true,
onTap: () async {
showModalBottomSheet<void>(
backgroundColor: Theme.of(context).colorScheme.bgColorForQuestions,
barrierColor: Colors.black87,
@ -125,17 +143,6 @@ class SubFaqWidget extends StatelessWidget {
},
);
},
child: Container(
padding: const EdgeInsets.all(40),
child: RichText(
text: TextSpan(
text: "FAQs",
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
decoration: TextDecoration.underline,
),
),
),
),
),
);
}

View file

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:io';
import "package:flutter/foundation.dart";
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:logging/logging.dart';
@ -12,8 +13,12 @@ import 'package:photos/models/subscription.dart';
import 'package:photos/models/user_details.dart';
import 'package:photos/services/billing_service.dart';
import 'package:photos/services/user_service.dart';
import "package:photos/theme/colors.dart";
import "package:photos/theme/ente_theme.dart";
import 'package:photos/ui/common/loading_widget.dart';
import 'package:photos/ui/common/progress_dialog.dart';
import "package:photos/ui/components/captioned_text_widget.dart";
import "package:photos/ui/components/menu_item_widget/menu_item_widget.dart";
import 'package:photos/ui/payment/child_subscription_widget.dart';
import 'package:photos/ui/payment/skip_subscription_widget.dart';
import 'package:photos/ui/payment/subscription_common_widgets.dart';
@ -48,6 +53,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
bool _hasLoadedData = false;
bool _isLoading = false;
late bool _isActiveStripeSubscriber;
EnteColorScheme colorScheme = darkScheme;
@override
void initState() {
@ -135,13 +141,16 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
@override
Widget build(BuildContext context) {
colorScheme = getEnteColorScheme(context);
if (!_isLoading) {
_isLoading = true;
_fetchSubData();
}
_dialog = createProgressDialog(context, "Please wait...");
final appBar = AppBar(
title: widget.isOnboarding ? null : const Text("Subscription"),
title: widget.isOnboarding
? null
: const Text("Subscription${kDebugMode ? ' Store' : ''}"),
);
return Scaffold(
appBar: appBar,
@ -149,6 +158,11 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
);
}
bool _isFreePlanUser() {
return _currentSubscription != null &&
freeProductID == _currentSubscription!.productID;
}
Future<void> _fetchSubData() async {
_userService.getUserDetailsV2(memoryCount: false).then((userDetails) async {
_userDetails = userDetails;
@ -212,98 +226,69 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
if (widget.isOnboarding) {
widgets.add(SkipSubscriptionWidget(freePlan: _freePlan));
}
widgets.add(const SubFaqWidget());
widgets.add(
SubFaqWidget(isOnboarding: widget.isOnboarding),
);
}
if (_hasActiveSubscription &&
_currentSubscription!.productID != freeProductID) {
widgets.addAll([
Align(
alignment: Alignment.center,
child: GestureDetector(
onTap: () {
final String paymentProvider =
_currentSubscription!.paymentProvider;
if (paymentProvider == appStore && !Platform.isAndroid) {
launchUrlString("https://apps.apple.com/account/billing");
} else if (paymentProvider == playStore && Platform.isAndroid) {
launchUrlString(
"https://play.google.com/store/account/subscriptions?sku=" +
_currentSubscription!.productID +
"&package=io.ente.photos",
);
} else if (paymentProvider == stripe) {
showErrorDialog(
context,
"Sorry",
"Visit web.ente.io to manage your subscription",
);
} else {
final String capitalizedWord = paymentProvider.isNotEmpty
? '${paymentProvider[0].toUpperCase()}${paymentProvider.substring(1).toLowerCase()}'
: '';
showErrorDialog(
context,
"Sorry",
"Please contact us at support@ente.io to manage your "
"$capitalizedWord subscription.",
);
}
},
child: Container(
padding: const EdgeInsets.fromLTRB(40, 80, 40, 20),
child: Column(
children: [
RichText(
text: TextSpan(
text: _isActiveStripeSubscriber
? "Visit web.ente.io to manage your subscription"
: "Payment details",
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontFamily: 'Inter-Medium',
fontSize: 14,
decoration: _isActiveStripeSubscriber
? TextDecoration.none
: TextDecoration.underline,
),
),
textAlign: TextAlign.center,
if (_isActiveStripeSubscriber) {
widgets.add(
Padding(
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),
child: Text(
"Visit web.ente.io to manage your subscription",
style: getEnteTextTheme(context).small.copyWith(
color: colorScheme.textMuted,
),
],
),
),
),
),
]);
);
} else {
widgets.add(
Padding(
padding: const EdgeInsets.fromLTRB(16, 40, 16, 4),
child: MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Payment details",
),
menuItemColor: colorScheme.fillFaint,
trailingWidget: Icon(
Icons.chevron_right_outlined,
color: colorScheme.strokeBase,
),
singleBorderRadius: 4,
alignCaptionedTextToLeft: true,
onTap: () async {
_onPlatformRestrictedPaymentDetailsClick();
},
),
),
);
}
}
if (!widget.isOnboarding) {
widgets.addAll([
Align(
alignment: Alignment.topCenter,
child: GestureDetector(
widgets.add(
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 80),
child: MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: _isFreePlanUser() ? "Family Plans" : "Manage Family",
),
menuItemColor: colorScheme.fillFaint,
trailingWidget: Icon(
Icons.chevron_right_outlined,
color: colorScheme.strokeBase,
),
singleBorderRadius: 4,
alignCaptionedTextToLeft: true,
onTap: () async {
_billingService.launchFamilyPortal(context, _userDetails);
},
child: Container(
padding: const EdgeInsets.fromLTRB(40, 0, 40, 80),
child: Column(
children: [
RichText(
text: TextSpan(
text: "Manage family",
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
decoration: TextDecoration.underline,
),
),
textAlign: TextAlign.center,
),
],
),
),
),
),
]);
);
}
return SingleChildScrollView(
child: Column(
@ -313,6 +298,35 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
);
}
void _onPlatformRestrictedPaymentDetailsClick() {
final String paymentProvider = _currentSubscription!.paymentProvider;
if (paymentProvider == appStore && !Platform.isAndroid) {
launchUrlString("https://apps.apple.com/account/billing");
} else if (paymentProvider == playStore && Platform.isAndroid) {
launchUrlString(
"https://play.google.com/store/account/subscriptions?sku=" +
_currentSubscription!.productID +
"&package=io.ente.photos",
);
} else if (paymentProvider == stripe) {
showErrorDialog(
context,
"Sorry",
"Visit web.ente.io to manage your subscription",
);
} else {
final String capitalizedWord = paymentProvider.isNotEmpty
? '${paymentProvider[0].toUpperCase()}${paymentProvider.substring(1).toLowerCase()}'
: '';
showErrorDialog(
context,
"Sorry",
"Please contact us at support@ente.io to manage your "
"$capitalizedWord subscription.",
);
}
}
List<Widget> _getStripePlanWidgets() {
final List<Widget> planWidgets = [];
bool foundActivePlan = false;

View file

@ -6,6 +6,7 @@ import 'package:photos/ui/advanced_settings_screen.dart';
import 'package:photos/ui/components/captioned_text_widget.dart';
import 'package:photos/ui/components/expandable_menu_item_widget.dart';
import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart';
import "package:photos/ui/growth/referral_screen.dart";
import 'package:photos/ui/payment/subscription.dart';
import 'package:photos/ui/settings/common_settings.dart';
import 'package:photos/utils/navigation_util.dart';
@ -51,6 +52,22 @@ class GeneralSectionWidget extends StatelessWidget {
},
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Referrals",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
routeToPage(
context,
const ReferralScreen(),
forceCustomPageRoute: true,
);
},
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Advanced",

View file

@ -2,12 +2,16 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import "package:flutter_animate/flutter_animate.dart";
import 'package:photos/core/configuration.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/events/opened_settings_event.dart';
import 'package:photos/services/feature_flag_service.dart';
import "package:photos/services/storage_bonus_service.dart";
import 'package:photos/theme/colors.dart';
import 'package:photos/theme/ente_theme.dart';
import "package:photos/ui/components/notification_widget.dart";
import "package:photos/ui/growth/referral_screen.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';
@ -21,9 +25,11 @@ import 'package:photos/ui/settings/social_section_widget.dart';
import 'package:photos/ui/settings/storage_card_widget.dart';
import 'package:photos/ui/settings/support_section_widget.dart';
import 'package:photos/ui/settings/theme_switch_widget.dart';
import "package:photos/utils/navigation_util.dart";
class SettingsPage extends StatelessWidget {
final ValueNotifier<String?> emailNotifier;
const SettingsPage({Key? key, required this.emailNotifier}) : super(key: key);
@override
@ -71,7 +77,28 @@ class SettingsPage extends StatelessWidget {
if (hasLoggedIn) {
contents.addAll([
const StorageCardWidget(),
const SizedBox(height: 12),
StorageBonusService.instance.shouldShowStorageBonus()
? RepaintBoundary(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: NotificationWidget(
startIcon: Icons.auto_awesome,
actionIcon: Icons.arrow_forward_outlined,
text: "Double your storage",
subText: "Refer friends and 2x your plan",
type: NotificationType.goldenBanner,
onTap: () async {
StorageBonusService.instance.markStorageBonusAsDone();
routeToPage(context, const ReferralScreen());
},
),
).animate(onPlay: (controller) => controller.repeat()).shimmer(
duration: 1000.ms,
delay: 3200.ms,
size: 0.6,
),
)
: const SizedBox(height: 12),
const BackupSectionWidget(),
sectionSpacing,
const AccountSectionWidget(),

View file

@ -19,6 +19,7 @@ import 'package:photos/theme/colors.dart';
import 'package:photos/ui/collections/section_title.dart';
import 'package:photos/ui/common/gradient_button.dart';
import 'package:photos/ui/common/loading_widget.dart';
import "package:photos/ui/new_shared_collections_gallery.dart";
import 'package:photos/ui/sharing/user_avator_widget.dart';
import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
import 'package:photos/ui/viewer/gallery/collection_page.dart';
@ -122,6 +123,10 @@ class _SharedCollectionGalleryState extends State<SharedCollectionGallery>
}),
builder: (context, snapshot) {
if (snapshot.hasData) {
if ((snapshot.data?.incoming.length ?? 0) == 0 &&
(snapshot.data?.outgoing.length ?? 0) == 0) {
return const Center(child: EmptyStateWidget());
}
return _getSharedCollectionsGallery(snapshot.data!);
} else if (snapshot.hasError) {
_logger.severe(

View file

@ -139,6 +139,8 @@ class _AlbumParticipantsPageState extends State<AlbumParticipantsPage> {
final currentUser = collaborators[listIndex];
final isSameAsLoggedInUser =
currentUserID == currentUser.id;
final isLastItem =
!isOwner && index == collaborators.length;
return Column(
children: [
MenuItemWidget(
@ -165,13 +167,15 @@ class _AlbumParticipantsPageState extends State<AlbumParticipantsPage> {
}
: null,
isTopBorderRadiusRemoved: listIndex > 0,
isBottomBorderRadiusRemoved: true,
isBottomBorderRadiusRemoved: !isLastItem,
singleBorderRadius: 8,
),
DividerWidget(
dividerType: DividerType.menu,
bgColor: getEnteColorScheme(context).blurStrokeFaint,
),
isLastItem
? const SizedBox.shrink()
: DividerWidget(
dividerType: DividerType.menu,
bgColor: getEnteColorScheme(context).fillFaint,
),
],
);
} else if (index == (1 + collaborators.length) && isOwner) {
@ -234,7 +238,7 @@ class _AlbumParticipantsPageState extends State<AlbumParticipantsPage> {
onTap: isOwner
? () async {
if (isOwner) {
await _navigateToManageUser(currentUser);
_navigateToManageUser(currentUser);
}
}
: null,

View file

@ -272,8 +272,10 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
}
Future<void> _updateUrlSettings(
BuildContext context, Map<String, dynamic> prop,
{bool showProgressDialog = true}) async {
BuildContext context,
Map<String, dynamic> prop, {
bool showProgressDialog = true,
}) async {
final dialog = showProgressDialog
? createProgressDialog(context, "Please wait...")
: null;

View file

@ -100,8 +100,9 @@ class _DeleteEmptyAlbumsState extends State<DeleteEmptyAlbums> {
"${collections.length}";
try {
await CollectionsService.instance.trashEmptyCollection(
collections[i].collection,
isBulkDelete: true);
collections[i].collection,
isBulkDelete: true,
);
} catch (_) {
failedCount++;
}

View file

@ -14,12 +14,12 @@ import 'package:photos/services/hidden_service.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/actions/collection/collection_file_actions.dart';
import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
import 'package:photos/ui/collection_action_sheet.dart';
import 'package:photos/ui/components/action_sheet_widget.dart';
import 'package:photos/ui/components/blur_menu_item_widget.dart';
import 'package:photos/ui/components/bottom_action_bar/expanded_menu_widget.dart';
import 'package:photos/ui/components/button_widget.dart';
import 'package:photos/ui/components/models/button_type.dart';
import 'package:photos/ui/create_collection_sheet.dart';
import 'package:photos/ui/sharing/manage_links_widget.dart';
import 'package:photos/utils/delete_file_util.dart';
import 'package:photos/utils/magic_util.dart';
@ -51,7 +51,7 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
late CollectionActions collectionActions;
late bool isCollectionOwner;
// _cachedCollectionForSharedLink is primarly used to avoid creating duplicate
// _cachedCollectionForSharedLink is primarily used to avoid creating duplicate
// links if user keeps on creating Create link button after selecting
// few files. This link is reset on any selection changed;
Collection? _cachedCollectionForSharedLink;
@ -283,10 +283,9 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
widget.selectedFiles
.unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true);
}
createCollectionSheet(
widget.selectedFiles,
null,
showCollectionActionSheet(
context,
selectedFiles: widget.selectedFiles,
actionType: CollectionActionType.moveFiles,
);
}
@ -296,11 +295,7 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
widget.selectedFiles
.unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true);
}
createCollectionSheet(
widget.selectedFiles,
null,
context,
);
showCollectionActionSheet(context, selectedFiles: widget.selectedFiles);
}
Future<void> _onDeleteClick() async {
@ -381,10 +376,9 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
widget.selectedFiles
.unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true);
}
createCollectionSheet(
widget.selectedFiles,
null,
showCollectionActionSheet(
context,
selectedFiles: widget.selectedFiles,
actionType: CollectionActionType.unHide,
);
}
@ -459,10 +453,9 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
}
void _restore() {
createCollectionSheet(
widget.selectedFiles,
null,
showCollectionActionSheet(
context,
selectedFiles: widget.selectedFiles,
actionType: CollectionActionType.restoreFiles,
);
}

View file

@ -5,9 +5,9 @@ import 'package:photos/models/gallery_type.dart';
import 'package:photos/models/magic_metadata.dart';
import 'package:photos/models/selected_files.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/collection_action_sheet.dart';
import 'package:photos/ui/components/bottom_action_bar/bottom_action_bar_widget.dart';
import 'package:photos/ui/components/icon_button_widget.dart';
import 'package:photos/ui/create_collection_sheet.dart';
import 'package:photos/ui/viewer/actions/file_selection_actions_widget.dart';
import 'package:photos/utils/delete_file_util.dart';
import 'package:photos/utils/magic_util.dart';
@ -85,10 +85,9 @@ class _FileSelectionOverlayBarState extends State<FileSelectionOverlayBar> {
iconButtonType: IconButtonType.primary,
iconColor: iconColor,
onTap: () {
createCollectionSheet(
widget.selectedFiles,
null,
showCollectionActionSheet(
context,
selectedFiles: widget.selectedFiles,
actionType: CollectionActionType.unHide,
);
},

View file

@ -0,0 +1,104 @@
import 'dart:convert';
import "package:chewie/chewie.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:logging/logging.dart";
import "package:media_extension/media_extension_action_types.dart";
import "package:photo_view/photo_view.dart";
import "package:photos/services/app_lifecycle_service.dart";
import "package:video_player/video_player.dart";
class FileViewer extends StatefulWidget {
const FileViewer({super.key});
@override
State<StatefulWidget> createState() {
return FileViewerState();
}
}
class FileViewerState extends State<FileViewer> {
final action = AppLifecycleService.instance.mediaExtensionAction;
ChewieController? controller;
VideoPlayerController? videoController;
final Logger _logger = Logger("FileViewer");
@override
void initState() {
super.initState();
if (action.type == MediaType.video) {
initController();
}
}
@override
void dispose() {
videoController?.dispose();
controller?.dispose();
super.dispose();
}
void initController() async {
videoController = VideoPlayerController.contentUri(
Uri.parse(action.data!),
);
controller = ChewieController(
videoPlayerController: videoController!,
autoInitialize: true,
aspectRatio: 16 / 9,
autoPlay: true,
looping: true,
showOptions: false,
materialProgressColors: ChewieProgressColors(
playedColor: const Color.fromRGBO(45, 194, 98, 1.0),
handleColor: Colors.white,
bufferedColor: Colors.white,
),
);
controller!.addListener(() {
if (!controller!.isFullScreen) {
SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp],
);
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
onPressed: () {
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
},
icon: const Icon(Icons.arrow_back),
),
),
body: Column(
children: [
Expanded(
child: Center(
child: (() {
if (action.type == MediaType.image) {
return PhotoView(
imageProvider: MemoryImage(base64Decode(action.data!)),
);
} else if (action.type == MediaType.video) {
return controller != null
? Chewie(controller: controller!)
: const CircularProgressIndicator();
} else {
_logger.severe('unsupported file type ${action.type}');
return const Icon(Icons.error);
}
})(),
),
),
],
),
);
}
}

View file

@ -124,6 +124,8 @@ class _DetailPageState extends State<DetailPage> {
_files![_selectedIndex],
_onEditFileRequested,
widget.config.mode == DetailPageMode.minimalistic,
onFileRemoved: _onFileRemoved,
userID: Configuration.instance.getUserID(),
key: _bottomBarKey,
),
],

View file

@ -14,6 +14,7 @@ import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/models/file.dart';
import 'package:photos/models/file_type.dart';
import 'package:photos/models/ignored_file.dart';
import "package:photos/models/magic_metadata.dart";
import 'package:photos/models/selected_files.dart';
import 'package:photos/models/trash_file.dart';
import 'package:photos/services/collections_service.dart';
@ -21,15 +22,12 @@ import 'package:photos/services/favorites_service.dart';
import 'package:photos/services/hidden_service.dart';
import 'package:photos/services/ignored_files_service.dart';
import 'package:photos/services/local_sync_service.dart';
import 'package:photos/ui/collection_action_sheet.dart';
import 'package:photos/ui/common/progress_dialog.dart';
import 'package:photos/ui/components/action_sheet_widget.dart';
import 'package:photos/ui/components/button_widget.dart';
import 'package:photos/ui/components/models/button_type.dart';
import 'package:photos/ui/create_collection_sheet.dart';
import 'package:photos/ui/viewer/file/custom_app_bar.dart';
import 'package:photos/utils/delete_file_util.dart';
import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/file_util.dart';
import "package:photos/utils/magic_util.dart";
import 'package:photos/utils/toast_util.dart';
class FadingAppBar extends StatefulWidget implements PreferredSizeWidget {
@ -148,22 +146,22 @@ class FadingAppBarState extends State<FadingAppBar> {
);
}
// options for files owned by the user
if (isOwnedByUser) {
if (isOwnedByUser && !isFileHidden) {
final bool isArchived =
widget.file.magicMetadata.visibility == visibilityArchive;
items.add(
PopupMenuItem(
value: 2,
child: Row(
children: [
Icon(
Platform.isAndroid
? Icons.delete_outline
: CupertinoIcons.delete,
isArchived ? Icons.unarchive : Icons.archive_outlined,
color: Theme.of(context).iconTheme.color,
),
const Padding(
padding: EdgeInsets.all(8),
),
const Text("Delete"),
Text(isArchived ? "Unarchive" : "Archive"),
],
),
),
@ -235,7 +233,7 @@ class FadingAppBarState extends State<FadingAppBar> {
if (value == 1) {
_download(widget.file);
} else if (value == 2) {
await _showSingleFileDeleteSheet(widget.file);
await _toggleFileArchiveStatus(widget.file);
} else if (value == 3) {
_setAs(widget.file);
} else if (value == 4) {
@ -269,12 +267,11 @@ class FadingAppBarState extends State<FadingAppBar> {
}
Future<void> _handleUnHideRequest(BuildContext context) async {
final s = SelectedFiles();
s.files.add(widget.file);
createCollectionSheet(
s,
null,
final selectedFiles = SelectedFiles();
selectedFiles.files.add(widget.file);
showCollectionActionSheet(
context,
selectedFiles: selectedFiles,
actionType: CollectionActionType.unHide,
);
}
@ -338,109 +335,16 @@ class FadingAppBarState extends State<FadingAppBar> {
);
}
Future<void> _showSingleFileDeleteSheet(File file) async {
final List<ButtonWidget> buttons = [];
final String fileType = file.fileType == FileType.video ? "video" : "photo";
final bool isBothLocalAndRemote =
file.uploadedFileID != null && file.localID != null;
final bool isLocalOnly =
file.uploadedFileID == null && file.localID != null;
final bool isRemoteOnly =
file.uploadedFileID != null && file.localID == null;
const String bodyHighlight = "It will be deleted from all albums.";
String body = "";
if (isBothLocalAndRemote) {
body = "This $fileType is in both ente and your device.";
} else if (isRemoteOnly) {
body = "This $fileType will be deleted from ente.";
} else if (isLocalOnly) {
body = "This $fileType will be deleted from your device.";
} else {
throw AssertionError("Unexpected state");
}
// Add option to delete from ente
if (isBothLocalAndRemote || isRemoteOnly) {
buttons.add(
ButtonWidget(
labelText: isBothLocalAndRemote ? "Delete from ente" : "Yes, delete",
buttonType: ButtonType.neutral,
buttonSize: ButtonSize.large,
shouldStickToDarkTheme: true,
buttonAction: ButtonAction.first,
shouldSurfaceExecutionStates: true,
isInAlert: true,
onTap: () async {
await deleteFilesFromRemoteOnly(context, [file]);
showShortToast(context, "Moved to trash");
if (isRemoteOnly) {
Navigator.of(context, rootNavigator: true).pop();
widget.onFileRemoved(file);
}
},
),
);
}
// Add option to delete from local
if (isBothLocalAndRemote || isLocalOnly) {
buttons.add(
ButtonWidget(
labelText:
isBothLocalAndRemote ? "Delete from device" : "Yes, delete",
buttonType: ButtonType.neutral,
buttonSize: ButtonSize.large,
shouldStickToDarkTheme: true,
buttonAction: ButtonAction.second,
shouldSurfaceExecutionStates: false,
isInAlert: true,
onTap: () async {
await deleteFilesOnDeviceOnly(context, [file]);
if (isLocalOnly) {
Navigator.of(context, rootNavigator: true).pop();
widget.onFileRemoved(file);
}
},
),
);
}
if (isBothLocalAndRemote) {
buttons.add(
ButtonWidget(
labelText: "Delete from both",
buttonType: ButtonType.neutral,
buttonSize: ButtonSize.large,
shouldStickToDarkTheme: true,
buttonAction: ButtonAction.third,
shouldSurfaceExecutionStates: true,
isInAlert: true,
onTap: () async {
await deleteFilesFromEverywhere(context, [file]);
Navigator.of(context, rootNavigator: true).pop();
widget.onFileRemoved(file);
},
),
);
}
buttons.add(
const ButtonWidget(
labelText: "Cancel",
buttonType: ButtonType.secondary,
buttonSize: ButtonSize.large,
shouldStickToDarkTheme: true,
buttonAction: ButtonAction.fourth,
isInAlert: true,
),
Future<void> _toggleFileArchiveStatus(File file) async {
final bool isArchived =
widget.file.magicMetadata.visibility == visibilityArchive;
await changeVisibility(
context,
[widget.file],
isArchived ? visibilityVisible : visibilityArchive,
);
final actionResult = await showActionSheet(
context: context,
buttons: buttons,
actionSheetType: ActionSheetType.defaultActionSheet,
body: body,
bodyHighlight: bodyHighlight,
);
if (actionResult?.action != null &&
actionResult!.action == ButtonAction.error) {
showGenericErrorDialog(context: context);
if (mounted) {
setState(() {});
}
}

View file

@ -2,31 +2,30 @@ import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/models/file.dart';
import 'package:photos/models/file_type.dart';
import 'package:photos/models/magic_metadata.dart';
import 'package:photos/models/selected_files.dart';
import 'package:photos/models/trash_file.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/theme/colors.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/create_collection_sheet.dart';
import 'package:photos/ui/viewer/file/file_info_widget.dart';
import "package:photos/ui/actions/file/file_actions.dart";
import 'package:photos/ui/collection_action_sheet.dart';
import 'package:photos/utils/delete_file_util.dart';
import 'package:photos/utils/magic_util.dart';
import 'package:photos/utils/share_util.dart';
class FadingBottomBar extends StatefulWidget {
final File file;
final Function(File) onEditRequested;
final Function(File) onFileRemoved;
final bool showOnlyInfoButton;
final int? userID;
const FadingBottomBar(
this.file,
this.onEditRequested,
this.showOnlyInfoButton, {
required this.onFileRemoved,
this.userID,
Key? key,
}) : super(key: key);
@ -63,6 +62,8 @@ class FadingBottomBarState extends State<FadingBottomBar> {
Widget _getBottomBar() {
final List<Widget> children = [];
final bool isOwnedByUser =
widget.file.ownerID == null || widget.file.ownerID == widget.userID;
children.add(
Tooltip(
message: "Info",
@ -88,15 +89,7 @@ class FadingBottomBarState extends State<FadingBottomBar> {
if (widget.file is TrashFile) {
_addTrashOptions(children);
}
final bool isUploadedByUser = widget.file.uploadedFileID != null &&
widget.file.ownerID == Configuration.instance.getUserID();
bool isFileHidden = false;
if (isUploadedByUser) {
isFileHidden = CollectionsService.instance
.getCollectionByID(widget.file.collectionID!)
?.isHidden() ??
false;
}
if (!widget.showOnlyInfoButton && widget.file is! TrashFile) {
if (widget.file.fileType == FileType.image ||
widget.file.fileType == FileType.livePhoto) {
@ -118,26 +111,21 @@ class FadingBottomBarState extends State<FadingBottomBar> {
),
);
}
if (isUploadedByUser && !isFileHidden) {
final bool isArchived =
widget.file.magicMetadata.visibility == visibilityArchive;
if (isOwnedByUser) {
children.add(
Tooltip(
message: isArchived ? "Unarchive" : "Archive",
message: "Delete",
child: Padding(
padding: const EdgeInsets.only(top: 12, bottom: 12),
child: IconButton(
icon: Icon(
isArchived ? Icons.unarchive : Icons.archive_outlined,
Platform.isAndroid
? Icons.delete_outline
: CupertinoIcons.delete,
color: Colors.white,
),
onPressed: () async {
await changeVisibility(
context,
[widget.file],
isArchived ? visibilityVisible : visibilityArchive,
);
safeRefresh();
await _showSingleFileDeleteSheet(widget.file);
},
),
),
@ -223,6 +211,14 @@ class FadingBottomBarState extends State<FadingBottomBar> {
);
}
Future<void> _showSingleFileDeleteSheet(File file) async {
await showSingleFileDeleteSheet(
context,
file,
onFileRemoved: widget.onFileRemoved,
);
}
void _addTrashOptions(List<Widget> children) {
children.add(
Tooltip(
@ -237,10 +233,9 @@ class FadingBottomBarState extends State<FadingBottomBar> {
onPressed: () {
final selectedFiles = SelectedFiles();
selectedFiles.toggleSelection(widget.file);
createCollectionSheet(
selectedFiles,
null,
showCollectionActionSheet(
context,
selectedFiles: selectedFiles,
actionType: CollectionActionType.restoreFiles,
);
},
@ -273,20 +268,6 @@ class FadingBottomBarState extends State<FadingBottomBar> {
}
Future<void> _displayInfo(File file) async {
final colorScheme = getEnteColorScheme(context);
return showBarModalBottomSheet(
topControl: const SizedBox.shrink(),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0)),
backgroundColor: colorScheme.backgroundElevated,
barrierColor: backdropFaintDark,
context: context,
builder: (BuildContext context) {
return Padding(
padding:
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: FileInfoWidget(file),
);
},
);
await showInfoSheet(context, file);
}
}

View file

@ -10,6 +10,7 @@ import "package:photos/ente_theme_data.dart";
import "package:photos/models/file.dart";
import "package:photos/models/file_type.dart";
import 'package:photos/services/collections_service.dart';
import "package:photos/services/feature_flag_service.dart";
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/components/divider_widget.dart';
import 'package:photos/ui/components/icon_button_widget.dart';
@ -159,7 +160,7 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
path.basenameWithoutExtension(file.displayName) +
path.extension(file.displayName).toUpperCase(),
),
subtitle: Row(
subtitle: Wrap(
children: [
showDimension
? Text(
@ -192,7 +193,7 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
horizontalTitleGap: 2,
leading: const Icon(Icons.camera_rounded),
title: Text(_exifData["takenOnDevice"] ?? "--"),
subtitle: Row(
subtitle: Wrap(
children: [
_exifData["fNumber"] != null
? Padding(
@ -236,14 +237,16 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
: DeviceFoldersListOfFileWidget(allDeviceFoldersOfFile),
),
),
SizedBox(
height: 62,
child: ListTile(
horizontalTitleGap: 0,
leading: const Icon(Icons.image_search),
title: ObjectTagsWidget(file),
),
),
FeatureFlagService.instance.isInternalUserOrDebugBuild()
? SizedBox(
height: 62,
child: ListTile(
horizontalTitleGap: 0,
leading: const Icon(Icons.image_search),
title: ObjectTagsWidget(file),
),
)
: null,
(file.uploadedFileID != null && file.updationTime != null)
? ListTile(
horizontalTitleGap: 2,

View file

@ -8,7 +8,9 @@ import 'package:photos/models/gallery_type.dart';
import 'package:photos/models/magic_metadata.dart';
import 'package:photos/models/selected_files.dart';
import 'package:photos/services/collections_service.dart';
import "package:photos/ui/components/album_horizontal_list_widget.dart";
import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.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_app_bar_widget.dart';
@ -61,6 +63,12 @@ class ArchivePage extends StatelessWidget {
tagPrefix: tagPrefix,
selectedFiles: _selectedFiles,
initialFiles: null,
emptyState: const EmptyState(
text: "You don't have any archived items.",
),
header: AlbumHorizontalListWidget(
CollectionsService.instance.getArchivedCollectionWithThumb,
),
);
return Scaffold(
appBar: PreferredSize(

View file

@ -16,13 +16,13 @@ import 'package:photos/ui/viewer/gallery/empty_state.dart';
import 'package:photos/ui/viewer/gallery/gallery.dart';
import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart';
class CollectionPage extends StatefulWidget {
class CollectionPage extends StatelessWidget {
final CollectionWithThumbnail c;
final String tagPrefix;
final GalleryType appBarType;
final bool hasVerifiedLock;
const CollectionPage(
CollectionPage(
this.c, {
this.tagPrefix = "collection",
this.appBarType = GalleryType.ownedCollection,
@ -30,42 +30,24 @@ class CollectionPage extends StatefulWidget {
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
Widget build(Object context) {
if (widget.hasVerifiedLock == false && widget.c.collection.isHidden()) {
if (hasVerifiedLock == false && c.collection.isHidden()) {
return const EmptyState();
}
final appBarTypeValue = _getGalleryType(widget.c.collection);
final appBarTypeValue = _getGalleryType(c.collection);
final List<File>? initialFiles =
widget.c.thumbnail != null ? [widget.c.thumbnail!] : null;
c.thumbnail != null ? [c.thumbnail!] : null;
final gallery = Gallery(
asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) async {
final FileLoadResult result =
await FilesDB.instance.getFilesInCollection(
widget.c.collection.id,
c.collection.id,
creationStartTime,
creationEndTime,
limit: limit,
@ -82,25 +64,25 @@ class _CollectionPageState extends State<CollectionPage> {
},
reloadEvent: Bus.instance
.on<CollectionUpdatedEvent>()
.where((event) => event.collectionID == widget.c.collection.id),
.where((event) => event.collectionID == c.collection.id),
removalEventTypes: const {
EventType.deletedFromRemote,
EventType.deletedFromEverywhere,
EventType.hide,
},
tagPrefix: widget.tagPrefix,
tagPrefix: tagPrefix,
selectedFiles: _selectedFiles,
initialFiles: initialFiles,
albumName: widget.c.collection.name,
albumName: c.collection.name,
);
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(50.0),
child: GalleryAppBarWidget(
appBarTypeValue,
widget.c.collection.name,
c.collection.name,
_selectedFiles,
collection: widget.c.collection,
collection: c.collection,
),
),
body: Stack(
@ -110,7 +92,7 @@ class _CollectionPageState extends State<CollectionPage> {
FileSelectionOverlayBar(
appBarTypeValue,
_selectedFiles,
collection: widget.c.collection,
collection: c.collection,
)
],
),
@ -129,12 +111,6 @@ class _CollectionPageState extends State<CollectionPage> {
} else if (c.type == CollectionType.favorites) {
return GalleryType.favorite;
}
return widget.appBarType;
}
_selectedFilesListener() {
_selectedFiles.files.isNotEmpty
? _bottomPosition.value = 0.0
: _bottomPosition.value = -150.0;
return appBarType;
}
}

View file

@ -14,7 +14,7 @@ import 'package:photos/ui/viewer/gallery/gallery.dart';
import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart';
import 'package:photos/utils/delete_file_util.dart';
class TrashPage extends StatefulWidget {
class TrashPage extends StatelessWidget {
final String tagPrefix;
final GalleryType appBarType;
final GalleryType overlayType;
@ -26,30 +26,9 @@ class TrashPage extends StatefulWidget {
Key? key,
}) : super(key: key);
@override
State<TrashPage> createState() => _TrashPageState();
}
class _TrashPageState extends State<TrashPage> {
late Function() _selectedFilesListener;
@override
void initState() {
_selectedFilesListener = () {
setState(() {});
};
widget._selectedFiles.addListener(_selectedFilesListener);
super.initState();
}
@override
void dispose() {
widget._selectedFiles.removeListener(_selectedFilesListener);
super.dispose();
}
@override
Widget build(Object context) {
final bool filesAreSelected = widget._selectedFiles.files.isNotEmpty;
final bool filesAreSelected = _selectedFiles.files.isNotEmpty;
final gallery = Gallery(
asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) {
@ -70,8 +49,8 @@ class _TrashPageState extends State<TrashPage> {
forceReloadEvents: [
Bus.instance.on<ForceReloadTrashPageEvent>(),
],
tagPrefix: widget.tagPrefix,
selectedFiles: widget._selectedFiles,
tagPrefix: tagPrefix,
selectedFiles: _selectedFiles,
header: _headerWidget(),
initialFiles: null,
);
@ -80,9 +59,9 @@ class _TrashPageState extends State<TrashPage> {
appBar: PreferredSize(
preferredSize: const Size.fromHeight(50.0),
child: GalleryAppBarWidget(
widget.appBarType,
appBarType,
"Trash",
widget._selectedFiles,
_selectedFiles,
),
),
body: Stack(
@ -109,7 +88,7 @@ class _TrashPageState extends State<TrashPage> {
),
),
),
FileSelectionOverlayBar(GalleryType.trash, widget._selectedFiles)
FileSelectionOverlayBar(GalleryType.trash, _selectedFiles)
],
),
);

View file

@ -35,6 +35,10 @@ num convertBytesToGBs(int bytes) {
return num.parse((bytes / (pow(1024, 3))).toStringAsFixed(1));
}
int convertBytesToAbsoluteGBs(int bytes) {
return (bytes / pow(1024, 3)).round();
}
int convertBytesToMBs(int bytes) {
return (bytes / pow(1024, 2)).round();
}

View file

@ -13,6 +13,7 @@ import 'package:photos/core/event_bus.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/events/collection_updated_event.dart';
import 'package:photos/events/files_updated_event.dart';
import "package:photos/events/force_reload_trash_page_event.dart";
import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/models/file.dart';
import 'package:photos/models/selected_files.dart';
@ -264,6 +265,9 @@ Future<bool> deleteFromTrash(BuildContext context, List<File> files) async {
source: "deleteFromTrash",
),
);
//the FilesUpdateEvent is not reloading trash on premanently removing
//files, so need to fire ForceReloadTrashPageEvent
Bus.instance.fire(ForceReloadTrashPageEvent());
} catch (e, s) {
_logger.info("failed to delete from trash", e, s);
rethrow;

View file

@ -1,6 +1,7 @@
import 'dart:math';
import 'package:confetti/confetti.dart';
import "package:dio/dio.dart";
import 'package:flutter/material.dart';
import 'package:photos/core/constants.dart';
import "package:photos/models/search/button_result.dart";
@ -38,6 +39,36 @@ Future<ButtonResult?> showErrorDialog(
);
}
Future<ButtonResult?> showErrorDialogForException({
required BuildContext context,
required Exception exception,
bool isDismissible = true,
String apiErrorPrefix = "It looks like something went wrong.",
}) async {
String errorMessage =
"It looks like something went wrong. Please retry after some time. If the error persists, please contact our support team.";
if (exception is DioError &&
exception.response != null &&
exception.response!.data["code"] != null) {
errorMessage =
"$apiErrorPrefix\n\nReason: " + exception.response!.data["code"];
}
return showDialogWidget(
context: context,
title: "Error",
icon: Icons.error_outline_outlined,
body: errorMessage,
isDismissible: isDismissible,
buttons: const [
ButtonWidget(
buttonType: ButtonType.secondary,
labelText: "OK",
isInAlert: true,
),
],
);
}
///Will return null if dismissed by tapping outside
Future<ButtonResult?> showGenericErrorDialog({
required BuildContext context,

Some files were not shown because too many files have changed in this diff Show more