From 5a639a9c60c9cb3ea3ca6bb3ae97b76750cf29da Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 3 Feb 2025 15:09:52 +0530 Subject: [PATCH 001/796] [mob] Upgrade dio --- .../core/error-reporting/super_logging.dart | 2 +- mobile/lib/core/network/network.dart | 7 ++--- mobile/lib/gateways/cast_gw.dart | 2 +- mobile/lib/gateways/entity_gw.dart | 2 +- .../lib/module/upload/service/multipart.dart | 4 +-- mobile/lib/services/billing_service.dart | 10 +++---- mobile/lib/services/collections_service.dart | 24 +++++++-------- mobile/lib/services/file_magic_service.dart | 4 +-- mobile/lib/services/sync_service.dart | 10 +++---- mobile/lib/services/user_service.dart | 30 +++++++++---------- .../account/login_pwd_verification_page.dart | 8 +++-- .../lib/ui/account/verify_recovery_page.dart | 2 +- .../lib/ui/growth/referral_code_widget.dart | 2 +- mobile/lib/utils/dialog_util.dart | 19 +++++++----- mobile/lib/utils/file_uploader.dart | 12 ++++---- mobile/lib/utils/thumbnail_util.dart | 2 +- mobile/plugins/ente_cast/pubspec.yaml | 2 +- mobile/plugins/ente_feature_flag/pubspec.lock | 22 ++++++++++++-- mobile/plugins/ente_feature_flag/pubspec.yaml | 2 +- mobile/pubspec.lock | 12 ++++++-- mobile/pubspec.yaml | 2 +- 21 files changed, 104 insertions(+), 76 deletions(-) diff --git a/mobile/lib/core/error-reporting/super_logging.dart b/mobile/lib/core/error-reporting/super_logging.dart index 7c0bef58be..55e93c5947 100644 --- a/mobile/lib/core/error-reporting/super_logging.dart +++ b/mobile/lib/core/error-reporting/super_logging.dart @@ -236,7 +236,7 @@ class SuperLogging { } static _shouldSkipSentry(Object error) { - if (error is DioError) { + if (error is DioException) { return true; } final bool result = error is StorageLimitExceededError || diff --git a/mobile/lib/core/network/network.dart b/mobile/lib/core/network/network.dart index a55a1a807e..9722a40f24 100644 --- a/mobile/lib/core/network/network.dart +++ b/mobile/lib/core/network/network.dart @@ -8,18 +8,17 @@ import 'package:photos/core/network/ente_interceptor.dart'; import "package:photos/events/endpoint_updated_event.dart"; import "package:ua_client_hints/ua_client_hints.dart"; -int kConnectTimeout = 15000; - class NetworkClient { late Dio _dio; late Dio _enteDio; + static const kConnectTimeout = 15; Future init(PackageInfo packageInfo) async { final String ua = await userAgent(); final endpoint = Configuration.instance.getHttpEndpoint(); _dio = Dio( BaseOptions( - connectTimeout: kConnectTimeout, + connectTimeout: const Duration(seconds: kConnectTimeout), headers: { HttpHeaders.userAgentHeader: ua, 'X-Client-Version': packageInfo.version, @@ -30,7 +29,7 @@ class NetworkClient { _enteDio = Dio( BaseOptions( baseUrl: endpoint, - connectTimeout: kConnectTimeout, + connectTimeout: const Duration(seconds: kConnectTimeout), headers: { HttpHeaders.userAgentHeader: ua, 'X-Client-Version': packageInfo.version, diff --git a/mobile/lib/gateways/cast_gw.dart b/mobile/lib/gateways/cast_gw.dart index bc8af897ab..3db3894ad4 100644 --- a/mobile/lib/gateways/cast_gw.dart +++ b/mobile/lib/gateways/cast_gw.dart @@ -12,7 +12,7 @@ class CastGateway { ); return response.data["publicKey"]; } catch (e) { - if (e is DioError && e.response != null) { + if (e is DioException && e.response != null) { if (e.response!.statusCode == 404) { return null; } else if (e.response!.statusCode == 403) { diff --git a/mobile/lib/gateways/entity_gw.dart b/mobile/lib/gateways/entity_gw.dart index a88d31eeb9..0e4b8bb2bc 100644 --- a/mobile/lib/gateways/entity_gw.dart +++ b/mobile/lib/gateways/entity_gw.dart @@ -32,7 +32,7 @@ class EntityGateway { }, ); return EntityKey.fromMap(response.data); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null && (e.response!.statusCode ?? 0) == 404) { throw EntityKeyNotFound(); } else { diff --git a/mobile/lib/module/upload/service/multipart.dart b/mobile/lib/module/upload/service/multipart.dart index 163f055845..762d26301e 100644 --- a/mobile/lib/module/upload/service/multipart.dart +++ b/mobile/lib/module/upload/service/multipart.dart @@ -132,7 +132,7 @@ class MultiPartUploader { // upload individual parts and get their etags try { etags = await _uploadParts(multipartInfo, encryptedFile); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response?.statusCode == 404) { _logger.severe( "Multipart upload not found for key ${multipartInfo.urls.objectKey}", @@ -157,7 +157,7 @@ class MultiPartUploader { etags, multipartInfo.urls.completeURL, ); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response?.statusCode == 404) { _logger.severe( "Multipart upload not found for key ${multipartInfo.urls.objectKey}", diff --git a/mobile/lib/services/billing_service.dart b/mobile/lib/services/billing_service.dart index b26d4507a3..130a26241d 100644 --- a/mobile/lib/services/billing_service.dart +++ b/mobile/lib/services/billing_service.dart @@ -92,7 +92,7 @@ class BillingService { }, ); return Subscription.fromMap(response.data["subscription"]); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null && e.response!.statusCode == 409) { throw SubscriptionAlreadyClaimedError(); } else { @@ -109,7 +109,7 @@ class BillingService { final response = await _enteDio.get("/billing/subscription"); final subscription = Subscription.fromMap(response.data["subscription"]); return subscription; - } on DioError catch (e, s) { + } on DioException catch (e, s) { _logger.severe(e, s); rethrow; } @@ -121,7 +121,7 @@ class BillingService { await _enteDio.post("/billing/stripe/cancel-subscription"); final subscription = Subscription.fromMap(response.data["subscription"]); return subscription; - } on DioError catch (e, s) { + } on DioException catch (e, s) { _logger.severe(e, s); rethrow; } @@ -133,7 +133,7 @@ class BillingService { await _enteDio.post("/billing/stripe/activate-subscription"); final subscription = Subscription.fromMap(response.data["subscription"]); return subscription; - } on DioError catch (e, s) { + } on DioException catch (e, s) { _logger.severe(e, s); rethrow; } @@ -150,7 +150,7 @@ class BillingService { }, ); return response.data["url"]; - } on DioError catch (e, s) { + } on DioException catch (e, s) { _logger.severe(e, s); rethrow; } diff --git a/mobile/lib/services/collections_service.dart b/mobile/lib/services/collections_service.dart index 7c5de3985b..3bac9fe978 100644 --- a/mobile/lib/services/collections_service.dart +++ b/mobile/lib/services/collections_service.dart @@ -553,7 +553,7 @@ class CollectionsService { unawaited(_db.insert([_collectionIDToCollections[collectionID]!])); RemoteSyncService.instance.sync(silently: true).ignore(); return sharees; - } on DioError catch (e) { + } on DioException catch (e) { if (e.response?.statusCode == 402) { throw SharingNotPermittedForFreeAccountsError(); } @@ -641,7 +641,7 @@ class CollectionsService { } else { await _handleCollectionDeletion(collection); } - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null) { debugPrint("Error " + e.response!.toString()); } @@ -808,7 +808,7 @@ class CollectionsService { // trigger sync to fetch the latest collection state from server sync().ignore(); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null && e.response?.statusCode == 409) { _logger.severe('collection magic data out of sync'); sync().ignore(); @@ -867,7 +867,7 @@ class CollectionsService { _cacheLocalPathAndCollection(collection); // trigger sync to fetch the latest collection state from server sync().ignore(); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null && e.response?.statusCode == 409) { _logger.severe('collection magic data out of sync'); sync().ignore(); @@ -927,7 +927,7 @@ class CollectionsService { _cacheLocalPathAndCollection(collection); // trigger sync to fetch the latest collection state from server sync().ignore(); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null && e.response?.statusCode == 409) { _logger.severe('collection magic data out of sync'); sync().ignore(); @@ -958,7 +958,7 @@ class CollectionsService { Bus.instance.fire( CollectionUpdatedEvent(collection.id, [], "shareUrL"), ); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response?.statusCode == 402) { throw SharingNotPermittedForFreeAccountsError(); } @@ -987,7 +987,7 @@ class CollectionsService { Bus.instance.fire( CollectionUpdatedEvent(collection.id, [], "updateUrl"), ); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response?.statusCode == 402) { throw SharingNotPermittedForFreeAccountsError(); } @@ -1013,7 +1013,7 @@ class CollectionsService { "disableShareUrl", ), ); - } on DioError catch (e) { + } on DioException catch (e) { _logger.info(e); rethrow; } @@ -1038,7 +1038,7 @@ class CollectionsService { return collections; } catch (e, s) { _logger.warning(e, s); - if (e is DioError && e.response?.statusCode == 401) { + if (e is DioException && e.response?.statusCode == 401) { throw UnauthorizedError(); } rethrow; @@ -1091,7 +1091,7 @@ class CollectionsService { } catch (e, s) { _logger.warning(e, s); _logger.severe("Failed to fetch public collection"); - if (e is DioError && e.response?.statusCode == 410) { + if (e is DioException && e.response?.statusCode == 410) { await showInfoDialog( context, title: S.of(context).linkExpired, @@ -1100,7 +1100,7 @@ class CollectionsService { throw UnauthorizedError(); } await showGenericErrorDialog(context: context, error: e); - if (e is DioError && e.response?.statusCode == 401) { + if (e is DioException && e.response?.statusCode == 401) { throw UnauthorizedError(); } rethrow; @@ -1300,7 +1300,7 @@ class CollectionsService { _cacheLocalPathAndCollection(collection); return collection; } catch (e) { - if (e is DioError && e.response?.statusCode == 401) { + if (e is DioException && e.response?.statusCode == 401) { throw UnauthorizedError(); } _logger.severe('failed to fetch collection: $collectionID', e); diff --git a/mobile/lib/services/file_magic_service.dart b/mobile/lib/services/file_magic_service.dart index 9fbbfb9760..30e74d9c73 100644 --- a/mobile/lib/services/file_magic_service.dart +++ b/mobile/lib/services/file_magic_service.dart @@ -117,7 +117,7 @@ class FileMagicService { // should be eventually synced after remote sync has completed await _filesDB.insertMultiple(files); RemoteSyncService.instance.sync(silently: true).ignore(); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null && e.response!.statusCode == 409) { RemoteSyncService.instance.sync(silently: true).ignore(); } @@ -185,7 +185,7 @@ class FileMagicService { // update the state of the selected file. Same file in other collection // should be eventually synced after remote sync has completed RemoteSyncService.instance.sync(silently: true).ignore(); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null && e.response!.statusCode == 409) { RemoteSyncService.instance.sync(silently: true).ignore(); } diff --git a/mobile/lib/services/sync_service.dart b/mobile/lib/services/sync_service.dart index 873270f349..bf42fecf26 100644 --- a/mobile/lib/services/sync_service.dart +++ b/mobile/lib/services/sync_service.dart @@ -131,11 +131,11 @@ class SyncService { ), ); } catch (e) { - if (e is DioError) { - if (e.type == DioErrorType.connectTimeout || - e.type == DioErrorType.sendTimeout || - e.type == DioErrorType.receiveTimeout || - e.type == DioErrorType.other) { + if (e is DioException) { + if (e.type == DioExceptionType.connectionTimeout || + e.type == DioExceptionType.sendTimeout || + e.type == DioExceptionType.receiveTimeout || + e.type == DioExceptionType.unknown) { Bus.instance.fire( SyncStatusUpdate( SyncStatus.paused, diff --git a/mobile/lib/services/user_service.dart b/mobile/lib/services/user_service.dart index 80d0cf1caa..7e6223476d 100644 --- a/mobile/lib/services/user_service.dart +++ b/mobile/lib/services/user_service.dart @@ -124,7 +124,7 @@ class UserService { } else { throw Exception("send-ott action failed, non-200"); } - } on DioError catch (e) { + } on DioException catch (e) { await dialog.hide(); _logger.info(e); final String? enteErrCode = e.response?.data["code"]; @@ -185,7 +185,7 @@ class UserService { ); final publicKey = response.data["publicKey"]; return publicKey; - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null && e.response?.statusCode == 404) { return null; } @@ -221,7 +221,7 @@ class UserService { } } return userDetails; - } on DioError catch (e) { + } on DioException catch (e) { _logger.info(e); rethrow; } @@ -231,7 +231,7 @@ class UserService { try { final response = await _enteDio.get("/users/sessions"); return Sessions.fromMap(response.data); - } on DioError catch (e) { + } on DioException catch (e) { _logger.info(e); rethrow; } @@ -245,7 +245,7 @@ class UserService { "token": token, }, ); - } on DioError catch (e) { + } on DioException catch (e) { _logger.info(e); rethrow; } @@ -254,7 +254,7 @@ class UserService { Future leaveFamilyPlan() async { try { await _enteDio.delete("/family/leave"); - } on DioError catch (e) { + } on DioException catch (e) { _logger.warning('failed to leave family plan', e); rethrow; } @@ -271,7 +271,7 @@ class UserService { } } catch (e) { // check if token is already invalid - if (e is DioError && e.response?.statusCode == 401) { + if (e is DioException && e.response?.statusCode == 401) { await Configuration.instance.logout(); Navigator.of(context).popUntil((route) => route.isFirst); return; @@ -342,7 +342,7 @@ class UserService { }, ); return response.data; - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null) { if (e.response!.statusCode == 404 || e.response!.statusCode == 410) { throw PassKeySessionExpiredError(); @@ -460,7 +460,7 @@ class UserService { // should never reach here throw Exception("unexpected response during email verification"); } - } on DioError catch (e) { + } on DioException catch (e) { _logger.info(e); await dialog.hide(); if (e.response != null && e.response!.statusCode == 410) { @@ -532,7 +532,7 @@ class UserService { S.of(context).oops, S.of(context).verificationFailedPleaseTryAgain, ); - } on DioError catch (e) { + } on DioException catch (e) { await dialog.hide(); if (e.response != null && e.response!.statusCode == 403) { // ignore: unawaited_futures @@ -592,7 +592,7 @@ class UserService { } else { throw Exception("get-srp-attributes action failed"); } - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null && e.response!.statusCode == 404) { throw SrpSetupNotCompleteError(); } @@ -865,7 +865,7 @@ class UserService { (route) => route.isFirst, ); } - } on DioError catch (e) { + } on DioException catch (e) { await dialog.hide(); _logger.severe(e); if (e.response != null && e.response!.statusCode == 404) { @@ -932,7 +932,7 @@ class UserService { (route) => route.isFirst, ); } - } on DioError catch (e) { + } on DioException catch (e) { await dialog.hide(); _logger.severe('error while recovery 2fa', e); if (e.response != null && e.response!.statusCode == 404) { @@ -1031,7 +1031,7 @@ class UserService { (route) => route.isFirst, ); } - } on DioError catch (e) { + } on DioException catch (e) { await dialog.hide(); _logger.severe("error during recovery", e); if (e.response != null && e.response!.statusCode == 404) { @@ -1126,7 +1126,7 @@ class UserService { } catch (e, s) { await dialog.hide(); _logger.severe(e, s); - if (e is DioError) { + if (e is DioException) { if (e.response != null && e.response!.statusCode == 401) { // ignore: unawaited_futures showErrorDialog( diff --git a/mobile/lib/ui/account/login_pwd_verification_page.dart b/mobile/lib/ui/account/login_pwd_verification_page.dart index ac974c20da..2d5e9237ce 100644 --- a/mobile/lib/ui/account/login_pwd_verification_page.dart +++ b/mobile/lib/ui/account/login_pwd_verification_page.dart @@ -113,7 +113,7 @@ class _LoginPasswordVerificationPageState password, dialog, ); - } on DioError catch (e, s) { + } on DioException catch (e, s) { await dialog.hide(); if (e.response != null && e.response!.statusCode == 401) { _logger.severe('server reject, failed verify SRP login', e, s); @@ -123,8 +123,10 @@ class _LoginPasswordVerificationPageState S.of(context).pleaseTryAgain, ); } else { - _logger.severe('API failure during SRP login', e, s); - if (e.type == DioErrorType.other) { + _logger.severe('API failure during SRP login ${e.type}', e, s); + if (e.type == DioExceptionType.connectionTimeout || + e.type == DioExceptionType.receiveTimeout || + e.type == DioExceptionType.sendTimeout) { await _showContactSupportDialog( context, S.of(context).noInternetConnection, diff --git a/mobile/lib/ui/account/verify_recovery_page.dart b/mobile/lib/ui/account/verify_recovery_page.dart index 54c8595977..46585a734d 100644 --- a/mobile/lib/ui/account/verify_recovery_page.dart +++ b/mobile/lib/ui/account/verify_recovery_page.dart @@ -43,7 +43,7 @@ class _VerifyRecoveryPageState extends State { await userRemoteFlagService.markRecoveryVerificationAsDone(); } catch (e) { await dialog.hide(); - if (e is DioError && e.type == DioErrorType.other) { + if (e is DioException && e.type == DioExceptionType.connectionError) { await showErrorDialog( context, S.of(context).noInternetConnection, diff --git a/mobile/lib/ui/growth/referral_code_widget.dart b/mobile/lib/ui/growth/referral_code_widget.dart index cb139c7263..7620aff22d 100644 --- a/mobile/lib/ui/growth/referral_code_widget.dart +++ b/mobile/lib/ui/growth/referral_code_widget.dart @@ -108,7 +108,7 @@ class ReferralCodeWidget extends StatelessWidget { notifyParent?.call(); } catch (e, s) { Logger("ReferralCodeWidget").severe("Failed to update code", e, s); - if (e is DioError) { + if (e is DioException) { if (e.response?.statusCode == 400) { await showInfoDialog( context, diff --git a/mobile/lib/utils/dialog_util.dart b/mobile/lib/utils/dialog_util.dart index 8d3f65e438..9fcadb2763 100644 --- a/mobile/lib/utils/dialog_util.dart +++ b/mobile/lib/utils/dialog_util.dart @@ -76,7 +76,7 @@ Future showErrorDialogForException({ }) async { String errorMessage = message ?? S.of(context).tempErrorContactSupportIfPersists; - if (exception is DioError && + if (exception is DioException && exception.response != null && exception.response!.data["code"] != null) { errorMessage = @@ -107,9 +107,12 @@ String parseErrorForUI( if (error == null) { return genericError; } - if (error is DioError) { - final DioError dioError = error; - if (dioError.type == DioErrorType.other) { + if (error is DioException) { + final DioException dioError = error; + if (dioError.type == DioExceptionType.receiveTimeout || + dioError.type == DioExceptionType.connectionError || + dioError.type == DioExceptionType.sendTimeout || + dioError.type == DioExceptionType.cancel) { if (dioError.error.toString().contains('Failed host lookup')) { return S.of(context).networkHostLookUpErr; } else if (dioError.error.toString().contains('SocketException')) { @@ -122,15 +125,15 @@ String parseErrorForUI( return genericError; } String errorInfo = ""; - if (error is DioError) { - final DioError dioError = error; - if (dioError.type == DioErrorType.response) { + if (error is DioException) { + final DioException dioError = error; + if (dioError.type == DioExceptionType.badResponse) { if (dioError.response?.data["code"] != null) { errorInfo = "Reason: " + dioError.response!.data["code"]; } else { errorInfo = "Reason: " + dioError.response!.data.toString(); } - } else if (dioError.type == DioErrorType.other) { + } else if (dioError.type == DioExceptionType.badCertificate) { errorInfo = "Reason: " + dioError.error.toString(); } else { errorInfo = "Reason: " + dioError.type.toString(); diff --git a/mobile/lib/utils/file_uploader.dart b/mobile/lib/utils/file_uploader.dart index 049c0f6b8f..22f00f2f12 100644 --- a/mobile/lib/utils/file_uploader.dart +++ b/mobile/lib/utils/file_uploader.dart @@ -858,7 +858,7 @@ class FileUploader { } bool isPutOrUpdateFileError(Object e) { - if (e is DioError) { + if (e is DioException) { return e.requestOptions.path.contains("/files") || e.requestOptions.path.contains("/files/update"); } @@ -1153,7 +1153,7 @@ class FileUploader { file.thumbnailDecryptionHeader = thumbnailDecryptionHeader; file.metadataDecryptionHeader = metadataDecryptionHeader; return file; - } on DioError catch (e) { + } on DioException catch (e) { if (e.response?.statusCode == 413) { throw FileTooLargeForPlanError(); } else if (e.response?.statusCode == 426) { @@ -1221,7 +1221,7 @@ class FileUploader { file.thumbnailDecryptionHeader = thumbnailDecryptionHeader; file.metadataDecryptionHeader = metadataDecryptionHeader; return file; - } on DioError catch (e) { + } on DioException catch (e) { if (e.response?.statusCode == 426) { _onStorageLimitExceeded(); } else if (attempt < kMaximumUploadAttempts) { @@ -1277,7 +1277,7 @@ class FileUploader { .map((e) => UploadURL.fromMap(e)) .toList(); _uploadURLs.addAll(urls); - } on DioError catch (e, s) { + } on DioException catch (e, s) { if (e.response != null) { if (e.response!.statusCode == 402) { final error = NoActiveSubscriptionError(); @@ -1327,8 +1327,8 @@ class FileUploader { ); return uploadURL.objectKey; - } on DioError catch (e) { - if (e.message.startsWith("HttpException: Content size")) { + } on DioException catch (e) { + if (e.message?.startsWith("HttpException: Content size") ?? false) { rethrow; } else if (attempt < kMaximumUploadAttempts) { _logger.info("Upload failed for $fileName, retrying"); diff --git a/mobile/lib/utils/thumbnail_util.dart b/mobile/lib/utils/thumbnail_util.dart index c8db830735..7769976888 100644 --- a/mobile/lib/utils/thumbnail_util.dart +++ b/mobile/lib/utils/thumbnail_util.dart @@ -192,7 +192,7 @@ Future _downloadAndDecryptThumbnail(FileDownloadItem item) async { .data; } } catch (e) { - if (e is DioError && CancelToken.isCancel(e)) { + if (e is DioException && CancelToken.isCancel(e)) { return; } rethrow; diff --git a/mobile/plugins/ente_cast/pubspec.yaml b/mobile/plugins/ente_cast/pubspec.yaml index 967e147e91..6cc08a0849 100644 --- a/mobile/plugins/ente_cast/pubspec.yaml +++ b/mobile/plugins/ente_cast/pubspec.yaml @@ -7,7 +7,7 @@ environment: dependencies: collection: - dio: ^4.0.6 + dio: ^5.8.0+1 flutter: sdk: flutter shared_preferences: ^2.0.5 diff --git a/mobile/plugins/ente_feature_flag/pubspec.lock b/mobile/plugins/ente_feature_flag/pubspec.lock index 4f2db8cd08..e58ff1231c 100644 --- a/mobile/plugins/ente_feature_flag/pubspec.lock +++ b/mobile/plugins/ente_feature_flag/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + async: + dependency: transitive + description: + name: async + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + url: "https://pub.dev" + source: hosted + version: "2.12.0" characters: dependency: transitive description: @@ -21,10 +29,18 @@ packages: dependency: "direct main" description: name: dio - sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "5.8.0+1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a + url: "https://pub.dev" + source: hosted + version: "2.1.0" ffi: dependency: transitive description: @@ -273,5 +289,5 @@ packages: source: hosted version: "1.0.4" sdks: - dart: ">=3.3.0 <4.0.0" + dart: ">=3.4.0 <4.0.0" flutter: ">=3.19.0" diff --git a/mobile/plugins/ente_feature_flag/pubspec.yaml b/mobile/plugins/ente_feature_flag/pubspec.yaml index 7507d61f1c..6ad2c7626d 100644 --- a/mobile/plugins/ente_feature_flag/pubspec.yaml +++ b/mobile/plugins/ente_feature_flag/pubspec.yaml @@ -7,7 +7,7 @@ environment: dependencies: collection: - dio: ^4.0.6 + dio: ^5.8.0+1 flutter: sdk: flutter shared_preferences: ^2.0.5 diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 28219c70d9..c5e9d6a0a1 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -413,10 +413,18 @@ packages: dependency: "direct main" description: name: dio - sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "5.8.0+1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a + url: "https://pub.dev" + source: hosted + version: "2.1.0" dots_indicator: dependency: "direct main" description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 07b2938512..6caa8a8a0f 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -42,7 +42,7 @@ dependencies: dart_ui_isolate: ^1.1.1 defer_pointer: ^0.0.2 device_info_plus: ^9.0.3 - dio: ^4.0.6 + dio: ^5.8.0+1 dots_indicator: ^2.0.0 dotted_border: ^2.1.0 dropdown_button2: ^2.0.0 From 1396ca57dbd4becd3e2a5e01a2315f215beef67b Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 3 Feb 2025 15:21:46 +0530 Subject: [PATCH 002/796] [mob] Use native dio adapter --- mobile/lib/core/network/network.dart | 5 +++ mobile/pubspec.lock | 48 ++++++++++++++++++++++++++++ mobile/pubspec.yaml | 1 + 3 files changed, 54 insertions(+) diff --git a/mobile/lib/core/network/network.dart b/mobile/lib/core/network/network.dart index 9722a40f24..ba2540b401 100644 --- a/mobile/lib/core/network/network.dart +++ b/mobile/lib/core/network/network.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:dio/dio.dart'; +import 'package:native_dio_adapter/native_dio_adapter.dart'; import 'package:package_info_plus/package_info_plus.dart'; import "package:photos/core/configuration.dart"; import "package:photos/core/event_bus.dart"; @@ -37,6 +38,10 @@ class NetworkClient { }, ), ); + + _dio.httpClientAdapter = NativeAdapter(); + _enteDio.httpClientAdapter = NativeAdapter(); + _setupInterceptors(endpoint); Bus.instance.on().listen((event) { diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index c5e9d6a0a1..c0b535c7ea 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -329,6 +329,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.2" + cronet_http: + dependency: transitive + description: + name: cronet_http + sha256: "3af9c4d57bf07ef4b307e77b22be4ad61bea19ee6ff65e62184863f3a09f1415" + url: "https://pub.dev" + source: hosted + version: "1.3.2" cross_file: dependency: "direct main" description: @@ -353,6 +361,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + cupertino_http: + dependency: transitive + description: + name: cupertino_http + sha256: "6fcf79586ad872ddcd6004d55c8c2aab3cdf0337436e8f99837b1b6c30665d0c" + url: "https://pub.dev" + source: hosted + version: "2.0.2" cupertino_icons: dependency: "direct main" description: @@ -1165,6 +1181,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + http_profile: + dependency: transitive + description: + name: http_profile + sha256: "7e679e355b09aaee2ab5010915c932cce3f2d1c11c3b2dc177891687014ffa78" + url: "https://pub.dev" + source: hosted + version: "0.1.0" image: dependency: "direct main" description: @@ -1330,6 +1354,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + jni: + dependency: transitive + description: + name: jni + sha256: f377c585ea9c08d48b427dc2e03780af2889d1bb094440da853c6883c1acba4b + url: "https://pub.dev" + source: hosted + version: "0.10.1" js: dependency: transitive description: @@ -1694,6 +1726,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + native_dio_adapter: + dependency: "direct main" + description: + name: native_dio_adapter + sha256: "7420bc9517b2abe09810199a19924617b45690a44ecfb0616ac9babc11875c03" + url: "https://pub.dev" + source: hosted + version: "1.4.0" native_video_player: dependency: "direct main" description: @@ -1727,6 +1767,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "62e79ab8c3ed6f6a340ea50dd48d65898f5d70425d404f0d99411f6e56e04584" + url: "https://pub.dev" + source: hosted + version: "4.1.0" octo_image: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 6caa8a8a0f..d43492da27 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -128,6 +128,7 @@ dependencies: git: "https://github.com/ente-io/motionphoto.git" move_to_background: ^1.0.2 nanoid: ^1.0.0 + native_dio_adapter: ^1.4.0 native_video_player: git: url: https://github.com/ashilkn/native_video_player.git From 701b7b8f37924a0434e911c4f014bc05f53f03d7 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Tue, 4 Feb 2025 11:13:09 +0530 Subject: [PATCH 003/796] [mob] Set cronetHttpNoPlay=true while building apk for droid --- .github/workflows/mobile-internal-release.yml | 2 +- .github/workflows/mobile-release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mobile-internal-release.yml b/.github/workflows/mobile-internal-release.yml index 6752fb1308..ae5535f655 100644 --- a/.github/workflows/mobile-internal-release.yml +++ b/.github/workflows/mobile-internal-release.yml @@ -40,7 +40,7 @@ jobs: - name: Build PlayStore AAB run: | - flutter build appbundle --release --flavor playstore + flutter build appbundle --dart-define=cronetHttpNoPlay=true --release --flavor playstore env: SIGNING_KEY_PATH: "/home/runner/work/_temp/keystore/ente_photos_key.jks" SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS_PHOTOS }} diff --git a/.github/workflows/mobile-release.yml b/.github/workflows/mobile-release.yml index ecf2c6d769..8997f0afbc 100644 --- a/.github/workflows/mobile-release.yml +++ b/.github/workflows/mobile-release.yml @@ -45,7 +45,7 @@ jobs: - name: Build independent APK run: | - flutter build apk --release --flavor independent + flutter build apk --dart-define=cronetHttpNoPlay=true --release --flavor independent mv build/app/outputs/flutter-apk/app-independent-release.apk build/app/outputs/flutter-apk/ente-${{ github.ref_name }}.apk env: SIGNING_KEY_PATH: "/home/runner/work/_temp/keystore/ente_photos_key.jks" From 36c06d5501b183eef0fc88ae85227c331d0fdfc6 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Tue, 4 Feb 2025 11:24:02 +0530 Subject: [PATCH 004/796] [mob] keep keep class org.chromium.net in droid proguard --- mobile/android/app/proguard-rules.pro | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mobile/android/app/proguard-rules.pro b/mobile/android/app/proguard-rules.pro index f35c179f30..066c0d1874 100644 --- a/mobile/android/app/proguard-rules.pro +++ b/mobile/android/app/proguard-rules.pro @@ -2,3 +2,5 @@ # To ensure that stack traces is unambiguous # https://developer.android.com/studio/build/shrink-code#decode-stack-trace -keepattributes LineNumberTable,SourceFile + +-keep class org.chromium.net.** { *; } From e54027c5ddea0896a389f6531dd94246bb8f6769 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Tue, 4 Feb 2025 11:30:47 +0530 Subject: [PATCH 005/796] [mob][photos] Basic structure --- mobile/lib/services/search_service.dart | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index 7366ede9cc..e56229a0af 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -1189,11 +1189,32 @@ class SearchService { return searchResults; } + Future> getTripsResults( + BuildContext context, + int? limit, + ) async { + final List searchResults = []; + final allFiles = await getAllFilesForSearch(); + if (allFiles.isEmpty) return []; + + // Identify base locations + + + // Identify trip locations + + + // Check if there are any trips to surface + } + Future> onThisDayOrWeekResults( BuildContext context, int? limit, ) async { final List searchResults = []; + final trips = await getTripsResults(context, limit); + if (trips.isNotEmpty) { + searchResults.addAll(trips); + } final allFiles = await getAllFilesForSearch(); if (allFiles.isEmpty) return []; From 3478720cb3b49ef6b268276aa8ceaf858580ce24 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Wed, 5 Feb 2025 14:17:09 +0530 Subject: [PATCH 006/796] [mob][photos] Test trips selection --- mobile/lib/models/base/id.dart | 4 + mobile/lib/services/search_service.dart | 222 +++++++++++++++++++++++- 2 files changed, 224 insertions(+), 2 deletions(-) diff --git a/mobile/lib/models/base/id.dart b/mobile/lib/models/base/id.dart index 2b095e0274..3fbae25e41 100644 --- a/mobile/lib/models/base/id.dart +++ b/mobile/lib/models/base/id.dart @@ -15,3 +15,7 @@ String newID(String prefix) { String newIsolateTaskID(String task) { return "${task}_${customAlphabet(enteWhiteListedAlphabet, clusterIDLength)}"; } + +String newAutoLocationID() { + return "cluster_${customAlphabet(enteWhiteListedAlphabet, clusterIDLength)}"; +} diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index 4bf0929fa8..0b67a23f1b 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -15,6 +15,7 @@ import "package:photos/db/ml/db.dart"; import 'package:photos/events/local_photos_updated_event.dart'; import "package:photos/extensions/user_extension.dart"; import "package:photos/models/api/collection/user.dart"; +import "package:photos/models/base/id.dart"; import 'package:photos/models/collection/collection.dart'; import 'package:photos/models/collection/collection_items.dart'; import "package:photos/models/file/extensions/file_props.dart"; @@ -1195,15 +1196,232 @@ class SearchService { ) async { final List searchResults = []; final allFiles = await getAllFilesForSearch(); + final Iterable> locationTagEntities = + (await locationService.getLocationTags()); if (allFiles.isEmpty) return []; - // Identify base locations + final Map, List> tagToItemsMap = {}; + for (int i = 0; i < locationTagEntities.length; i++) { + tagToItemsMap[locationTagEntities.elementAt(i)] = []; + } + final Map, Location)> smallRadiusClusters = {}; + final Map, Location)> wideRadiusClusters = {}; + // Go through all files and cluster the ones not inside any location tag + for (EnteFile file in allFiles) { + if (file.hasLocation && file.uploadedFileID != null) { + // Check if the file is inside any location tag + bool hasLocationTag = false; + for (LocalEntity tag in tagToItemsMap.keys) { + if (isFileInsideLocationTag( + tag.item.centerPoint, + file.location!, + tag.item.radius, + )) { + hasLocationTag = true; + tagToItemsMap[tag]!.add(file); + } + } + // Cluster the files not inside any location tag (incremental clustering) + if (!hasLocationTag) { + // Small radius clustering for base locations + bool foundSmallCluster = false; + for (final clusterID in smallRadiusClusters.keys) { + final clusterLocation = smallRadiusClusters[clusterID]!.$2; + if (isFileInsideLocationTag( + clusterLocation, + file.location!, + 1.0, + )) { + smallRadiusClusters[clusterID]!.$1.add(file); + foundSmallCluster = true; + break; + } + } + if (!foundSmallCluster) { + smallRadiusClusters[newAutoLocationID()] = ( + [ + file, + ], + file.location! + ); + } + // Wide radius clustering for trip locations + bool foundWideCluster = false; + for (final clusterID in wideRadiusClusters.keys) { + final clusterLocation = wideRadiusClusters[clusterID]!.$2; + if (isFileInsideLocationTag( + clusterLocation, + file.location!, + 50.0, + )) { + wideRadiusClusters[clusterID]!.$1.add(file); + foundWideCluster = true; + break; + } + if (!foundWideCluster) { + wideRadiusClusters[newAutoLocationID()] = ( + [ + file, + ], + file.location! + ); + } + } + } + } + } + // Identify base locations + final Map, Location, bool)> baseLocations = {}; + for (final clusterID in smallRadiusClusters.keys) { + final files = smallRadiusClusters[clusterID]!.$1; + final location = smallRadiusClusters[clusterID]!.$2; + // Check that the photos are distributed over a longer time range (3+ months) + final creationTimes = []; + for (final file in files) { + if (file.creationTime != null) { + creationTimes.add(file.creationTime!); + } + } + creationTimes.sort(); + if (creationTimes.length < 2) continue; + final firstCreationTime = DateTime.fromMicrosecondsSinceEpoch( + creationTimes.first, + ); + final lastCreationTime = DateTime.fromMicrosecondsSinceEpoch( + creationTimes.last, + ); + if (lastCreationTime.difference(firstCreationTime).inDays < 90) { + continue; + } + // Check if it's a current or old base location + final bool current = lastCreationTime.isAfter( + DateTime.now().subtract( + const Duration(days: 90), + ), + ); + baseLocations[clusterID] = (files, location, current); + } // Identify trip locations + final Map, Location, int, int)> tripLocations = {}; + for (final clusterID in wideRadiusClusters.keys) { + final files = wideRadiusClusters[clusterID]!.$1; + final location = wideRadiusClusters[clusterID]!.$2; + // Check that the photos are distributed over a short time range (2-30 days) + final creationTimes = []; + for (final file in files) { + if (file.creationTime != null) { + creationTimes.add(file.creationTime!); + } + } + creationTimes.sort(); + if (creationTimes.length < 2) continue; + final firstCreationTime = DateTime.fromMicrosecondsSinceEpoch( + creationTimes.first, + ); + final lastCreationTime = DateTime.fromMicrosecondsSinceEpoch( + creationTimes.last, + ); + final days = lastCreationTime.difference(firstCreationTime).inDays; + if (days < 2 || days > 30) { + continue; + } + // Check that it's at least 10km away from any base or tag location + bool tooClose = false; + for (final baseLocation in baseLocations.values) { + if (isFileInsideLocationTag( + baseLocation.$2, + location, + 10.0, + )) { + tooClose = true; + break; + } + } + if (tooClose) continue; + for (final tag in tagToItemsMap.keys) { + if (isFileInsideLocationTag( + tag.item.centerPoint, + location, + 10.0, + )) { + tooClose = true; + break; + } + } + if (tooClose) continue; + tripLocations[clusterID] = ( + files, + location, + firstCreationTime.microsecondsSinceEpoch, + lastCreationTime.microsecondsSinceEpoch + ); + } + // Check if any trip locations should be merged + // bool merged = false; + // String? mergeID; + // for (final tripLocation in tripLocations.values) { + // final otherTripFirstTime = DateTime.fromMicrosecondsSinceEpoch( + // tripLocation.$3, + // ); + // final otherTripLastTime = DateTime.fromMicrosecondsSinceEpoch( + // tripLocation.$4, + // ); + // final bool tripsMatch = firstCreationTime.isBefore( + // otherTripLastTime.add( + // const Duration(days: 1), + // ), + // ) || + // lastCreationTime.isAfter( + // otherTripFirstTime.subtract( + // const Duration(days: 1), + // ), + // ); - // Check if there are any trips to surface + // TODO: lau: Check if there are any trips to surface + // For now for testing let's just surface all base and trip locations + for (final baseLocation in baseLocations.values) { + final files = baseLocation.$1; // TODO: lau: take best selection only + final location = baseLocation.$2; + final current = baseLocation.$3; + final name = "Base (${current ? 'current' : 'old'})"; + searchResults.add( + GenericSearchResult( + ResultType.event, + name, + files, + hierarchicalSearchFilter: TopLevelGenericFilter( + filterName: name, + occurrence: kMostRelevantFilter, + filterResultType: ResultType.event, + matchedUploadedIDs: filesToUploadedFileIDs(files), + filterIcon: Icons.event_outlined, + ), + ), + ); + } + for (final tripLocation in tripLocations.values) { + final files = tripLocation.$1; // TODO: lau: take best selection only + // final location = tripLocation.$2; + const name = "Trip!"; + searchResults.add( + GenericSearchResult( + ResultType.event, + name, + files, + hierarchicalSearchFilter: TopLevelGenericFilter( + filterName: name, + occurrence: kMostRelevantFilter, + filterResultType: ResultType.event, + matchedUploadedIDs: filesToUploadedFileIDs(files), + filterIcon: Icons.event_outlined, + ), + ), + ); + } + return searchResults; // TODO: lau: take [limit] into account } Future> onThisDayOrWeekResults( From 252ae8169d4411a283ae3bad0019c3af3410a0f5 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Wed, 5 Feb 2025 15:15:35 +0530 Subject: [PATCH 007/796] [mob][photos] Improve base locations --- mobile/lib/services/search_service.dart | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index 0b67a23f1b..4e73b2570a 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -1230,7 +1230,7 @@ class SearchService { if (isFileInsideLocationTag( clusterLocation, file.location!, - 1.0, + 0.6, )) { smallRadiusClusters[clusterID]!.$1.add(file); foundSmallCluster = true; @@ -1278,13 +1278,18 @@ class SearchService { final location = smallRadiusClusters[clusterID]!.$2; // Check that the photos are distributed over a longer time range (3+ months) final creationTimes = []; + final Set uniqueDays = {}; for (final file in files) { if (file.creationTime != null) { creationTimes.add(file.creationTime!); + final date = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); + final dayStamp = + DateTime(date.year, date.month, date.day).microsecondsSinceEpoch; + uniqueDays.add(dayStamp); } } creationTimes.sort(); - if (creationTimes.length < 2) continue; + if (creationTimes.length < 10) continue; final firstCreationTime = DateTime.fromMicrosecondsSinceEpoch( creationTimes.first, ); @@ -1294,6 +1299,9 @@ class SearchService { if (lastCreationTime.difference(firstCreationTime).inDays < 90) { continue; } + // Check for a minimum average number of days photos are clicked in range + final daysRange = lastCreationTime.difference(firstCreationTime).inDays; + if (uniqueDays.length < daysRange * 0.1) continue; // Check if it's a current or old base location final bool current = lastCreationTime.isAfter( DateTime.now().subtract( From 99e5bc5050306cbed2f57f29b012884b5941392b Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Wed, 5 Feb 2025 16:00:43 +0530 Subject: [PATCH 008/796] [mob][photos] Simplify --- mobile/lib/services/search_service.dart | 95 +++++++++++++------------ 1 file changed, 48 insertions(+), 47 deletions(-) diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index 4e73b2570a..02096fccbb 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -1208,65 +1208,66 @@ class SearchService { final Map, Location)> wideRadiusClusters = {}; // Go through all files and cluster the ones not inside any location tag for (EnteFile file in allFiles) { - if (file.hasLocation && file.uploadedFileID != null) { - // Check if the file is inside any location tag - bool hasLocationTag = false; - for (LocalEntity tag in tagToItemsMap.keys) { + if (!file.hasLocation || file.uploadedFileID == null || !file.isOwner) { + continue; + } + // Check if the file is inside any location tag + bool hasLocationTag = false; + for (LocalEntity tag in tagToItemsMap.keys) { + if (isFileInsideLocationTag( + tag.item.centerPoint, + file.location!, + tag.item.radius, + )) { + hasLocationTag = true; + tagToItemsMap[tag]!.add(file); + } + } + // Cluster the files not inside any location tag (incremental clustering) + if (!hasLocationTag) { + // Small radius clustering for base locations + bool foundSmallCluster = false; + for (final clusterID in smallRadiusClusters.keys) { + final clusterLocation = smallRadiusClusters[clusterID]!.$2; if (isFileInsideLocationTag( - tag.item.centerPoint, + clusterLocation, file.location!, - tag.item.radius, + 0.6, )) { - hasLocationTag = true; - tagToItemsMap[tag]!.add(file); + smallRadiusClusters[clusterID]!.$1.add(file); + foundSmallCluster = true; + break; } } - // Cluster the files not inside any location tag (incremental clustering) - if (!hasLocationTag) { - // Small radius clustering for base locations - bool foundSmallCluster = false; - for (final clusterID in smallRadiusClusters.keys) { - final clusterLocation = smallRadiusClusters[clusterID]!.$2; - if (isFileInsideLocationTag( - clusterLocation, - file.location!, - 0.6, - )) { - smallRadiusClusters[clusterID]!.$1.add(file); - foundSmallCluster = true; - break; - } + if (!foundSmallCluster) { + smallRadiusClusters[newAutoLocationID()] = ( + [ + file, + ], + file.location! + ); + } + // Wide radius clustering for trip locations + bool foundWideCluster = false; + for (final clusterID in wideRadiusClusters.keys) { + final clusterLocation = wideRadiusClusters[clusterID]!.$2; + if (isFileInsideLocationTag( + clusterLocation, + file.location!, + 50.0, + )) { + wideRadiusClusters[clusterID]!.$1.add(file); + foundWideCluster = true; + break; } - if (!foundSmallCluster) { - smallRadiusClusters[newAutoLocationID()] = ( + if (!foundWideCluster) { + wideRadiusClusters[newAutoLocationID()] = ( [ file, ], file.location! ); } - // Wide radius clustering for trip locations - bool foundWideCluster = false; - for (final clusterID in wideRadiusClusters.keys) { - final clusterLocation = wideRadiusClusters[clusterID]!.$2; - if (isFileInsideLocationTag( - clusterLocation, - file.location!, - 50.0, - )) { - wideRadiusClusters[clusterID]!.$1.add(file); - foundWideCluster = true; - break; - } - if (!foundWideCluster) { - wideRadiusClusters[newAutoLocationID()] = ( - [ - file, - ], - file.location! - ); - } - } } } } From edfd86628a12b3eef8d689d893fd6405d24e7161 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Wed, 5 Feb 2025 16:29:01 +0530 Subject: [PATCH 009/796] [mob][photos] Basic trips --- mobile/lib/services/search_service.dart | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index 02096fccbb..16d881b3e2 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -1260,14 +1260,14 @@ class SearchService { foundWideCluster = true; break; } - if (!foundWideCluster) { - wideRadiusClusters[newAutoLocationID()] = ( - [ - file, - ], - file.location! - ); - } + } + if (!foundWideCluster) { + wideRadiusClusters[newAutoLocationID()] = ( + [ + file, + ], + file.location! + ); } } } @@ -1393,7 +1393,6 @@ class SearchService { // For now for testing let's just surface all base and trip locations for (final baseLocation in baseLocations.values) { final files = baseLocation.$1; // TODO: lau: take best selection only - final location = baseLocation.$2; final current = baseLocation.$3; final name = "Base (${current ? 'current' : 'old'})"; searchResults.add( From e3833044e98dc04e57202d8026809a6f9ffe80f1 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:44:55 +0530 Subject: [PATCH 010/796] [mob] Avoid reloading all files from DB on Upload events --- mobile/lib/ui/viewer/gallery/gallery.dart | 50 ++++++++++++++++++++--- mobile/lib/utils/file_uploader.dart | 14 +++---- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/mobile/lib/ui/viewer/gallery/gallery.dart b/mobile/lib/ui/viewer/gallery/gallery.dart index 1788866697..a07f6a8c09 100644 --- a/mobile/lib/ui/viewer/gallery/gallery.dart +++ b/mobile/lib/ui/viewer/gallery/gallery.dart @@ -18,6 +18,7 @@ import 'package:photos/ui/viewer/gallery/empty_state.dart'; import "package:photos/ui/viewer/gallery/state/gallery_context_state.dart"; import "package:photos/ui/viewer/gallery/state/gallery_files_inherited_widget.dart"; import "package:photos/ui/viewer/gallery/state/inherited_search_filter_data.dart"; +import "package:photos/utils/date_time_util.dart"; import "package:photos/utils/debouncer.dart"; import "package:photos/utils/hierarchical_search_util.dart"; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; @@ -130,19 +131,57 @@ class GalleryState extends State { _itemScroller = ItemScrollController(); if (widget.reloadEvent != null) { _reloadEventSubscription = widget.reloadEvent!.listen((event) async { + bool shouldReloadFromDB = true; + if (event.source == 'uploadCompleted') { + final Map genIDToUploadedFiles = {}; + for (int i = 0; i < event.updatedFiles.length; i++) { + if (event.updatedFiles[i].generatedID == null) { + shouldReloadFromDB = true; + break; + } + genIDToUploadedFiles[event.updatedFiles[i].generatedID!] = + event.updatedFiles[i]; + } + for (int i = 0; i < _allGalleryFiles.length; i++) { + final file = _allGalleryFiles[i]; + if (file.generatedID == null) { + continue; + } + final updateFile = genIDToUploadedFiles[file.generatedID!]; + if (updateFile != null && + updateFile.localID == file.localID && + areFromSameDay( + updateFile.creationTime ?? 0, + file.creationTime ?? 0, + )) { + _allGalleryFiles[i] = updateFile; + genIDToUploadedFiles.remove(file.generatedID!); + } + } + shouldReloadFromDB = genIDToUploadedFiles.isNotEmpty; + } + if (!shouldReloadFromDB) { + final bool hasCalledSetState = _onFilesLoaded(_allGalleryFiles); + _logger.info( + 'Skip softRefresh from DB, processed updated in memory with setStateReload $hasCalledSetState', + ); + return; + } + _debouncer.run(() async { // In soft refresh, setState is called for entire gallery only when // number of child change _logger.finest("Soft refresh all files on ${event.reason} "); final result = await _loadFiles(); - final bool hasReloaded = _onFilesLoaded(result.files); - if (hasReloaded && kDebugMode) { + final bool hasTriggeredSetState = _onFilesLoaded(result.files); + if (hasTriggeredSetState && kDebugMode) { _logger.finest( "Reloaded gallery on soft refresh all files on ${event.reason}", ); } - - setState(() {}); + if (!hasTriggeredSetState && mounted) { + setState(() {}); + } }); }); } @@ -207,8 +246,9 @@ class GalleryState extends State { _hasLoadedFiles = true; currentGroupedFiles = updatedGroupedFiles; }); + return true; } - return true; + return false; } else { currentGroupedFiles = updatedGroupedFiles; return false; diff --git a/mobile/lib/utils/file_uploader.dart b/mobile/lib/utils/file_uploader.dart index 05bf94a947..75b7cb08b8 100644 --- a/mobile/lib/utils/file_uploader.dart +++ b/mobile/lib/utils/file_uploader.dart @@ -823,14 +823,12 @@ class FileUploader { } await UploadLocksDB.instance.deleteMultipartTrack(lockKey); - if (!_isBackground) { - Bus.instance.fire( - LocalPhotosUpdatedEvent( - [remoteFile], - source: "downloadComplete", - ), - ); - } + Bus.instance.fire( + LocalPhotosUpdatedEvent( + [remoteFile], + source: "uploadCompleted", + ), + ); _logger.info("File upload complete for " + remoteFile.toString()); uploadCompleted = true; Bus.instance.fire(FileUploadedEvent(remoteFile)); From f11803fd1f5db7eb7e3cd3f5608ca00527fe4262 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 5 Feb 2025 16:58:09 +0530 Subject: [PATCH 011/796] [mob] Lint fix --- mobile/lib/utils/file_uploader.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/mobile/lib/utils/file_uploader.dart b/mobile/lib/utils/file_uploader.dart index 75b7cb08b8..ad2f7a1062 100644 --- a/mobile/lib/utils/file_uploader.dart +++ b/mobile/lib/utils/file_uploader.dart @@ -82,7 +82,6 @@ class FileUploader { int _uploadCounter = 0; int _videoUploadCounter = 0; late ProcessType _processType; - late bool _isBackground; late SharedPreferences _prefs; // _hasInitiatedForceUpload is used to track if user attempted force upload @@ -104,7 +103,6 @@ class FileUploader { Future init(SharedPreferences preferences, bool isBackground) async { _prefs = preferences; - _isBackground = isBackground; _processType = isBackground ? ProcessType.background : ProcessType.foreground; final currentTime = DateTime.now().microsecondsSinceEpoch; From 8922d7e663c03ad66e90bfc87a4ea60881b4b0be Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Thu, 6 Feb 2025 14:38:05 +0530 Subject: [PATCH 012/796] [mob][photos] Merge trips --- mobile/lib/services/search_service.dart | 71 +++++++++++++++++-------- 1 file changed, 48 insertions(+), 23 deletions(-) diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index 16d881b3e2..421570dd59 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -1369,25 +1369,50 @@ class SearchService { } // Check if any trip locations should be merged - // bool merged = false; - // String? mergeID; - // for (final tripLocation in tripLocations.values) { - // final otherTripFirstTime = DateTime.fromMicrosecondsSinceEpoch( - // tripLocation.$3, - // ); - // final otherTripLastTime = DateTime.fromMicrosecondsSinceEpoch( - // tripLocation.$4, - // ); - // final bool tripsMatch = firstCreationTime.isBefore( - // otherTripLastTime.add( - // const Duration(days: 1), - // ), - // ) || - // lastCreationTime.isAfter( - // otherTripFirstTime.subtract( - // const Duration(days: 1), - // ), - // ); + final Map, int, int)> mergedTrips = {}; + for (final tripID in tripLocations.keys) { + final trip = tripLocations[tripID]!; + final tripFirstTime = DateTime.fromMicrosecondsSinceEpoch( + trip.$3, + ); + final tripLastTime = DateTime.fromMicrosecondsSinceEpoch( + trip.$4, + ); + bool merged = false; + for (final otherTripID in mergedTrips.keys) { + final otherTrip = mergedTrips[otherTripID]!; + final otherTripFirstTime = DateTime.fromMicrosecondsSinceEpoch( + otherTrip.$2, + ); + final otherTripLastTime = DateTime.fromMicrosecondsSinceEpoch( + otherTrip.$3, + ); + final bool overlapBeginning = tripFirstTime.isBefore( + otherTripLastTime.add( + const Duration(days: 1), + ), + ) && + tripFirstTime.isAfter(otherTripFirstTime); + final bool overlapEnd = tripLastTime.isAfter( + otherTripFirstTime.subtract( + const Duration(days: 1), + ), + ) && + tripLastTime.isBefore(otherTripLastTime); + if (overlapBeginning || overlapEnd) { + mergedTrips[otherTripID] = ( + otherTrip.$1 + trip.$1, + min(otherTrip.$2, trip.$3), + max(otherTrip.$3, trip.$4), + ); + _logger.info('Merged two trip locations'); + merged = true; + break; + } + } + if (merged) continue; + mergedTrips[tripID] = (trip.$1, trip.$3, trip.$4); + } // TODO: lau: Check if there are any trips to surface // For now for testing let's just surface all base and trip locations @@ -1410,10 +1435,10 @@ class SearchService { ), ); } - for (final tripLocation in tripLocations.values) { - final files = tripLocation.$1; // TODO: lau: take best selection only - // final location = tripLocation.$2; - const name = "Trip!"; + for (final finalTrip in mergedTrips.values) { + final files = finalTrip.$1; // TODO: lau: take best selection only + final year = DateTime.fromMicrosecondsSinceEpoch((finalTrip.$2 + finalTrip.$2) ~/ 2).year; + final name = "Trip! ($year)"; searchResults.add( GenericSearchResult( ResultType.event, From d363f3759293f29a95b2fec62607d7e27696a371 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 7 Feb 2025 12:26:52 +0530 Subject: [PATCH 013/796] [mob] Fix creationTime parsing --- .../viewer/file_details/creation_time_item_widget.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mobile/lib/ui/viewer/file_details/creation_time_item_widget.dart b/mobile/lib/ui/viewer/file_details/creation_time_item_widget.dart index 90b6ab9d58..13c8800fa8 100644 --- a/mobile/lib/ui/viewer/file_details/creation_time_item_widget.dart +++ b/mobile/lib/ui/viewer/file_details/creation_time_item_widget.dart @@ -21,15 +21,15 @@ class CreationTimeItem extends StatefulWidget { class _CreationTimeItemState extends State { @override Widget build(BuildContext context) { - final dateTime = - DateTime.fromMicrosecondsSinceEpoch(widget.file.creationTime!); + final dateTime = DateTime.fromMicrosecondsSinceEpoch( + widget.file.creationTime!, + isUtc: true, + ).toLocal(); return InfoItemWidget( key: const ValueKey("Creation time"), leadingIcon: Icons.calendar_today_outlined, title: DateFormat.yMMMEd(Localizations.localeOf(context).languageCode) - .format( - DateTime.fromMicrosecondsSinceEpoch(widget.file.creationTime!), - ), + .format(dateTime), subtitleSection: Future.value([ Text( getTimeIn12hrFormat(dateTime) + " " + dateTime.timeZoneName, From 10079d4cb0edcf09a829f626eb64ffe541ae272b Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 7 Feb 2025 12:35:15 +0530 Subject: [PATCH 014/796] [mob] Fix handling of timezone --- mobile/lib/utils/exif_util.dart | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/mobile/lib/utils/exif_util.dart b/mobile/lib/utils/exif_util.dart index 652336ebee..45741f07b6 100644 --- a/mobile/lib/utils/exif_util.dart +++ b/mobile/lib/utils/exif_util.dart @@ -154,24 +154,22 @@ Future getCreationTimeFromEXIF( } DateTime getDateTimeInDeviceTimezone(String exifTime, String? offsetString) { - final DateTime result = DateFormat(kExifDateTimePattern).parse(exifTime); + final shouldParseDateInUTCTimeZone = offsetString != null; + final DateTime result = DateFormat(kExifDateTimePattern) + .parse(exifTime, shouldParseDateInUTCTimeZone); if (offsetString == null) { return result; } try { final List splitHHMM = offsetString.split(":"); - // Parse the offset from the photo's time zone final int offsetHours = int.parse(splitHHMM[0]); final int offsetMinutes = int.parse(splitHHMM[1]) * (offsetHours.isNegative ? -1 : 1); // Adjust the date for the offset to get the photo's correct UTC time final photoUtcDate = result.add(Duration(hours: -offsetHours, minutes: -offsetMinutes)); - // Getting the current device's time zone offset from UTC - final now = DateTime.now(); - final localOffset = now.timeZoneOffset; - // Adjusting the photo's UTC time to the device's local time - final deviceLocalTime = photoUtcDate.add(localOffset); + // Convert the UTC time to the device's local time + final deviceLocalTime = photoUtcDate.toLocal(); return deviceLocalTime; } catch (e, s) { _logger.severe("tz offset adjust failed $offsetString", e, s); From 602881ee2619bb8b45c7cb1d1070920ad1b5f96d Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 7 Feb 2025 13:54:44 +0530 Subject: [PATCH 015/796] [mob] Refactor --- mobile/lib/models/file/file.dart | 6 ++-- mobile/lib/utils/exif_util.dart | 55 +++++++++++++++++++++----------- mobile/lib/utils/share_util.dart | 6 ++-- 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/mobile/lib/models/file/file.dart b/mobile/lib/models/file/file.dart index 937bee0cb1..9f3d561315 100644 --- a/mobile/lib/models/file/file.dart +++ b/mobile/lib/models/file/file.dart @@ -175,10 +175,10 @@ class EnteFile { final exifData = await getExifFromSourceFile(mediaUploadData.sourceFile!); if (exifData != null) { if (fileType == FileType.image) { - final exifTime = await getCreationTimeFromEXIF(null, exifData); - if (exifTime != null) { + final dateResult = await tryParseExifDateTime(null, exifData); + if (dateResult != null && dateResult.time != null) { hasExifTime = true; - creationTime = exifTime.microsecondsSinceEpoch; + creationTime = dateResult.time!.microsecondsSinceEpoch; } mediaUploadData.isPanorama = checkPanoramaFromEXIF(null, exifData); diff --git a/mobile/lib/utils/exif_util.dart b/mobile/lib/utils/exif_util.dart index 45741f07b6..940ffed644 100644 --- a/mobile/lib/utils/exif_util.dart +++ b/mobile/lib/utils/exif_util.dart @@ -125,7 +125,14 @@ bool? checkPanoramaFromEXIF(File? file, Map? exifData) { return element?.printable == "6"; } -Future getCreationTimeFromEXIF( +class ParsedExifDateTime { + final DateTime? time; + final String? dateTime; + final String? offsetTime; + ParsedExifDateTime(this.time, this.dateTime, this.offsetTime); +} + +Future tryParseExifDateTime( File? file, Map? exifData, ) async { @@ -153,28 +160,38 @@ Future getCreationTimeFromEXIF( return null; } -DateTime getDateTimeInDeviceTimezone(String exifTime, String? offsetString) { +ParsedExifDateTime getDateTimeInDeviceTimezone( + String exifTime, + String? offsetString, +) { final shouldParseDateInUTCTimeZone = offsetString != null; final DateTime result = DateFormat(kExifDateTimePattern) .parse(exifTime, shouldParseDateInUTCTimeZone); - if (offsetString == null) { - return result; + if (offsetString != null && (offsetString ?? '').toUpperCase() != "Z") { + try { + final List splitHHMM = offsetString.split(":"); + final int offsetHours = int.parse(splitHHMM[0]); + final int offsetMinutes = + int.parse(splitHHMM[1]) * (offsetHours.isNegative ? -1 : 1); + // Adjust the date for the offset to get the photo's correct UTC time + final photoUtcDate = + result.add(Duration(hours: -offsetHours, minutes: -offsetMinutes)); + // Convert the UTC time to the device's local time + final deviceLocalTime = photoUtcDate.toLocal(); + return ParsedExifDateTime( + deviceLocalTime, + result.toIso8601String(), + offsetString, + ); + } catch (e, s) { + _logger.severe("tz offset adjust failed $offsetString", e, s); + } } - try { - final List splitHHMM = offsetString.split(":"); - final int offsetHours = int.parse(splitHHMM[0]); - final int offsetMinutes = - int.parse(splitHHMM[1]) * (offsetHours.isNegative ? -1 : 1); - // Adjust the date for the offset to get the photo's correct UTC time - final photoUtcDate = - result.add(Duration(hours: -offsetHours, minutes: -offsetMinutes)); - // Convert the UTC time to the device's local time - final deviceLocalTime = photoUtcDate.toLocal(); - return deviceLocalTime; - } catch (e, s) { - _logger.severe("tz offset adjust failed $offsetString", e, s); - } - return result; + return ParsedExifDateTime( + result, + result.toIso8601String(), + (offsetString ?? '').toUpperCase() == 'Z' ? 'Z' : null, + ); } Location? locationFromExif(Map exif) { diff --git a/mobile/lib/utils/share_util.dart b/mobile/lib/utils/share_util.dart index c39d1733b9..3ec147798d 100644 --- a/mobile/lib/utils/share_util.dart +++ b/mobile/lib/utils/share_util.dart @@ -165,9 +165,9 @@ Future> convertIncomingSharedMediaToFile( enteFile.fileType = media.type == SharedMediaType.image ? FileType.image : FileType.video; if (enteFile.fileType == FileType.image) { - final exifTime = await getCreationTimeFromEXIF(ioFile, null); - if (exifTime != null) { - enteFile.creationTime = exifTime.microsecondsSinceEpoch; + final dateResult = await tryParseExifDateTime(ioFile, null); + if (dateResult != null && dateResult.time != null) { + enteFile.creationTime = dateResult.time!.microsecondsSinceEpoch; } } else if (enteFile.fileType == FileType.video) { enteFile.duration = (media.duration ?? 0) ~/ 1000; From 0a9e706b509e505743b2ea95619b4dd184409742 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 7 Feb 2025 14:36:16 +0530 Subject: [PATCH 016/796] [mob] Refactor --- mobile/lib/utils/exif_util.dart | 48 ++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/mobile/lib/utils/exif_util.dart b/mobile/lib/utils/exif_util.dart index 940ffed644..beee7f61ef 100644 --- a/mobile/lib/utils/exif_util.dart +++ b/mobile/lib/utils/exif_util.dart @@ -126,10 +126,21 @@ bool? checkPanoramaFromEXIF(File? file, Map? exifData) { } class ParsedExifDateTime { - final DateTime? time; - final String? dateTime; - final String? offsetTime; - ParsedExifDateTime(this.time, this.dateTime, this.offsetTime); + late final DateTime? time; + late final String? dateTime; + late final String? offsetTime; + ParsedExifDateTime(DateTime this.time, String? dateTime, this.offsetTime) { + if (dateTime != null && dateTime.endsWith('Z')) { + this.dateTime = dateTime.substring(0, dateTime.length - 1); + } else { + this.dateTime = dateTime; + } + } + + @override + String toString() { + return "ParsedExifDateTime{time: $time, dateTime: $dateTime, offsetTime: $offsetTime}"; + } } Future tryParseExifDateTime( @@ -144,16 +155,17 @@ Future tryParseExifDateTime( : exif.containsKey(kImageDateTime) ? exif[kImageDateTime]!.printable : null; - if (exifTime != null && exifTime != kEmptyExifDateTime) { - String? exifOffsetTime; - for (final key in kExifOffSetKeys) { - if (exif.containsKey(key)) { - exifOffsetTime = exif[key]!.printable; - break; - } - } - return getDateTimeInDeviceTimezone(exifTime, exifOffsetTime); + if (exifTime == null || exifTime == kEmptyExifDateTime) { + return null; } + String? exifOffsetTime; + for (final key in kExifOffSetKeys) { + if (exif.containsKey(key)) { + exifOffsetTime = exif[key]!.printable; + break; + } + } + return getDateTimeInDeviceTimezone(exifTime, exifOffsetTime); } catch (e) { _logger.severe("failed to getCreationTimeFromEXIF", e); } @@ -164,10 +176,10 @@ ParsedExifDateTime getDateTimeInDeviceTimezone( String exifTime, String? offsetString, ) { - final shouldParseDateInUTCTimeZone = offsetString != null; - final DateTime result = DateFormat(kExifDateTimePattern) - .parse(exifTime, shouldParseDateInUTCTimeZone); - if (offsetString != null && (offsetString ?? '').toUpperCase() != "Z") { + final hasOffset = (offsetString ?? '') != ''; + final DateTime result = + DateFormat(kExifDateTimePattern).parse(exifTime, hasOffset); + if (hasOffset && offsetString!.toUpperCase() != "Z") { try { final List splitHHMM = offsetString.split(":"); final int offsetHours = int.parse(splitHHMM[0]); @@ -184,7 +196,7 @@ ParsedExifDateTime getDateTimeInDeviceTimezone( offsetString, ); } catch (e, s) { - _logger.severe("tz offset adjust failed $offsetString", e, s); + _logger.severe("offset parsing failed $exifTime && $offsetString", e, s); } } return ParsedExifDateTime( From 5522121cf6ab36a7bfe547069c7f4f7d458657c7 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 7 Feb 2025 14:40:16 +0530 Subject: [PATCH 017/796] [mob] iOS podlock changes --- mobile/ios/Podfile.lock | 110 ++++++++++++++++++++-------------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 4fa0953533..87a17625ce 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -440,82 +440,82 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/wakelock_plus/ios" SPEC CHECKSUMS: - background_fetch: 94b36ee293e82972852dba8ede1fbcd3bd3d9d57 - battery_info: a06b00c06a39bc94c92beebf600f1810cb6c8c87 - connectivity_plus: 2256d3e20624a7749ed21653aafe291a46446fee - dart_ui_isolate: 46f6714abe6891313267153ef6f9748d8ecfcab1 - device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89 + background_fetch: 39f11371c0dce04b001c4bfd5e782bcccb0a85e2 + battery_info: 09f5c9ee65394f2291c8c6227bedff345b8a730c + connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695 + dart_ui_isolate: d5bcda83ca4b04f129d70eb90110b7a567aece14 + device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 ffmpeg-kit-ios-full-gpl: 80adc341962e55ef709e36baa8ed9a70cf4ea62b - ffmpeg_kit_flutter_full_gpl: ce18b888487c05c46ed252cd2e7956812f2e3bd1 - file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6 + ffmpeg_kit_flutter_full_gpl: 8d15c14c0c3aba616fac04fe44b3d27d02e3c330 + file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 Firebase: 374a441a91ead896215703a674d58cdb3e9d772b - firebase_core: 2337982fb78ee4d8d91e608b0a3d4f44346a93c8 - firebase_messaging: f3bddfa28c2cad70b3341bf461e987a24efd28d6 + firebase_core: feb37e79f775c2bd08dd35e02d83678291317e10 + firebase_messaging: e2f0ba891b1509668c07f5099761518a5af8fe3c FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_email_sender: cd533cdc7ea5eda6fabb2c7f78521c71207778a4 - flutter_image_compress: 4b058288a81f76e5e80340af37c709abafff34c4 - flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 - flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100 - flutter_native_splash: 6cad9122ea0fad137d23137dd14b937f3e90b145 - flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418 - flutter_sodium: 152647449ba89a157fd48d7e293dcd6d29c6ab0e - fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038 + flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b + flutter_image_compress: 5a5e9aee05b6553048b8df1c3bc456d0afaac433 + flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4 + flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 + flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a + flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be + flutter_sodium: c84426b4de738514b5b66cfdeb8a06634e72fe0b + fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d - home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f - image_editor_common: 3de87e7c4804f4ae24c8f8a998362b98c105cac1 - image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a - in_app_purchase_storekit: e126ef1b89e4a9fdf07e28f005f82632b4609437 - integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e + home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57 + image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43 + image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 + in_app_purchase_storekit: 8c3b0b3eb1b0f04efbff401c3de6266d4258d433 + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 - local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 - local_auth_ios: f7a1841beef3151d140a967c2e46f30637cdf451 + local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3 + local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9 Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d - maps_launcher: edf829809ba9e894d70e569bab11c16352dedb45 - media_extension: a1fec16ee9c8241a6aef9613578ebf097d6c5e64 - media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 - media_kit_native_event_loop: 5fba1a849a6c87a34985f1e178a0de5bd444a0cf - media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 - motion_sensors: 741e702c17467b9569a92165dda8d4d88c6167f1 - motionphoto: 584b43031ead3060225cdff08fa49818879801d2 - move_to_background: 155f7bfbd34d43ad847cb630d2d2d87c17199710 + maps_launcher: 2e5b6a2d664ec6c27f82ffa81b74228d770ab203 + media_extension: 6d30dc1431ebaa63f43c397c37917b1a0a597a4c + media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 + media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a + media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e + motion_sensors: 03f55b7c637a7e365a0b5f9697a449f9059d5d91 + motionphoto: d4a432b8c8f22fb3ad966258597c0103c9c5ff16 + move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - native_video_player: b65c58951ede2f93d103a25366bdebca95081265 - onnxruntime: f9b296392c96c42882be020a59dbeac6310d81b2 + native_video_player: d12af78a1a4a8cf09775a5177d5b392def6fd23c + onnxruntime: e7c2ae44385191eaad5ae64c935a72debaddc997 onnxruntime-c: a909204639a1f035f575127ac406f781ac797c9c onnxruntime-objc: b6fab0f1787aa6f7190c2013f03037df4718bd8b - open_mail_app: 06d5a4162866388a92b1df3deb96e56be20cf45c + open_mail_app: 794172f6a22cd16319d3ddaf45e945b2f74952b0 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 - package_info_plus: 566e1b7a2f3900e4b0020914ad3fc051dcc95596 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 - permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d - photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413 - privacy_screen: 3159a541f5d3a31bea916cfd4e58f9dc722b3fd4 + package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a + privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 - receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00 - screen_brightness_ios: 5ed898fa50fa82a26171c086ca5e28228f932576 + receive_sharing_intent: 79c848f5b045674ad60b9fea3bafea59962ad2c1 + screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8 SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 Sentry: 38ed8bf38eab5812787274bf591e528074c19e02 - sentry_flutter: a72ca0eb6e78335db7c4ddcddd1b9f6c8ed5b764 - share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + sentry_flutter: 7d1f1df30f3768c411603ed449519bbb90a7d87b + share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d sqlite3: 7559e33dae4c78538df563795af3a86fc887ee71 - sqlite3_flutter_libs: 5235ce0546528db87927a3ef1baff8b7d5107f0e - system_info_plus: 555ce7047fbbf29154726db942ae785c29211740 + sqlite3_flutter_libs: 58ae36c0dd086395d066b4fe4de9cdca83e717b3 + system_info_plus: 5393c8da281d899950d751713575fbf91c7709aa Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e - ua_client_hints: 92fe0d139619b73ec9fcb46cc7e079a26178f586 - uni_links: ed8c961e47ed9ce42b6d91e1de8049e38a4b3152 - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d - video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b - video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140 - volume_controller: ca1cde542ee70fad77d388f82e9616488110942b - wakelock_plus: 8c239121a007daa1d6759c6acdc507860273dd2f + ua_client_hints: aeabd123262c087f0ce151ef96fa3ab77bfc8b38 + uni_links: d97da20c7701486ba192624d99bffaaffcfc298a + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 + video_thumbnail: c4e2a3c539e247d4de13cd545344fd2d26ffafd1 + volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 + wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 PODFILE CHECKSUM: 20e086e6008977d43a3d40260f3f9bffcac748dd From c20dcdae76a632ef9e8ff88bb13a09e6f61919d6 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 7 Feb 2025 14:59:52 +0530 Subject: [PATCH 018/796] [mob] Add support for parsing dateTime & offsetTime from pubMagicMetadata --- mobile/lib/models/metadata/file_magic.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/mobile/lib/models/metadata/file_magic.dart b/mobile/lib/models/metadata/file_magic.dart index 7599b7c82f..02f6188a9d 100644 --- a/mobile/lib/models/metadata/file_magic.dart +++ b/mobile/lib/models/metadata/file_magic.dart @@ -14,6 +14,8 @@ const latKey = "lat"; const longKey = "long"; const motionVideoIndexKey = "mvi"; const noThumbKey = "noThumb"; +const dateTimeKey = 'dateTime'; +const offsetTimeKey = 'offsetTime'; class MagicMetadata { // 0 -> visible @@ -46,6 +48,11 @@ class PubMagicMetadata { double? lat; double? long; + // ISO 8601 datetime without timezone. This contains the date and time of the photo in the original tz + // where the photo was taken. + String? dateTime; + String? offsetTime; + // Motion Video Index. Positive value (>0) indicates that the file is a motion // photo int? mvi; @@ -74,6 +81,8 @@ class PubMagicMetadata { this.mvi, this.noThumb, this.mediaType, + this.dateTime, + this.offsetTime, }); factory PubMagicMetadata.fromEncodedJson(String encodedJson) => @@ -96,6 +105,8 @@ class PubMagicMetadata { mvi: map[motionVideoIndexKey], noThumb: map[noThumbKey], mediaType: map[mediaTypeKey], + dateTime: map[dateTimeKey], + offsetTime: map[offsetTimeKey], ); } From b6b724f64fd09324c8f85a4bb046349db22eb055 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 7 Feb 2025 15:38:49 +0530 Subject: [PATCH 019/796] [mob] Parse exif as part of MediaUploadData --- mobile/lib/models/file/file.dart | 2 +- mobile/lib/utils/exif_util.dart | 2 +- mobile/lib/utils/file_uploader.dart | 2 +- mobile/lib/utils/file_uploader_util.dart | 28 +++++++++++++++++++----- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/mobile/lib/models/file/file.dart b/mobile/lib/models/file/file.dart index 9f3d561315..44572632e1 100644 --- a/mobile/lib/models/file/file.dart +++ b/mobile/lib/models/file/file.dart @@ -172,7 +172,7 @@ class EnteFile { bool hasExifTime = false; if ((fileType == FileType.image || fileType == FileType.video) && mediaUploadData.sourceFile != null) { - final exifData = await getExifFromSourceFile(mediaUploadData.sourceFile!); + final exifData = await tryExifFromFile(mediaUploadData.sourceFile!); if (exifData != null) { if (fileType == FileType.image) { final dateResult = await tryParseExifDateTime(null, exifData); diff --git a/mobile/lib/utils/exif_util.dart b/mobile/lib/utils/exif_util.dart index beee7f61ef..a94e02bece 100644 --- a/mobile/lib/utils/exif_util.dart +++ b/mobile/lib/utils/exif_util.dart @@ -47,7 +47,7 @@ Future> getExif(EnteFile file) async { } } -Future?> getExifFromSourceFile(File originFile) async { +Future?> tryExifFromFile(File originFile) async { try { final exif = await readExifAsync(originFile); return exif; diff --git a/mobile/lib/utils/file_uploader.dart b/mobile/lib/utils/file_uploader.dart index 05bf94a947..c9d36643aa 100644 --- a/mobile/lib/utils/file_uploader.dart +++ b/mobile/lib/utils/file_uploader.dart @@ -538,7 +538,7 @@ class FileUploader { MediaUploadData? mediaUploadData; try { - mediaUploadData = await getUploadDataFromEnteFile(file); + mediaUploadData = await getUploadDataFromEnteFile(file, parseExif: true); } catch (e) { // This additional try catch block is added because for resumable upload, // we need to compute the hash before the next step. Previously, this diff --git a/mobile/lib/utils/file_uploader_util.dart b/mobile/lib/utils/file_uploader_util.dart index 6ad6622f5f..63764a3066 100644 --- a/mobile/lib/utils/file_uploader_util.dart +++ b/mobile/lib/utils/file_uploader_util.dart @@ -6,6 +6,7 @@ import 'dart:ui' as ui; import "package:archive/archive_io.dart"; import "package:computer/computer.dart"; +import "package:exif/exif.dart"; import 'package:logging/logging.dart'; import "package:motion_photos/motion_photos.dart"; import 'package:motionphoto/motionphoto.dart'; @@ -44,6 +45,8 @@ class MediaUploadData { // For iOS, this value will be always null. final int? motionPhotoStartIndex; + final Map? exifData; + bool? isPanorama; MediaUploadData( @@ -55,6 +58,7 @@ class MediaUploadData { this.width, this.motionPhotoStartIndex, this.isPanorama, + this.exifData, }); } @@ -69,20 +73,27 @@ class FileHashData { FileHashData(this.fileHash, {this.zipHash}); } -Future getUploadDataFromEnteFile(EnteFile file) async { +Future getUploadDataFromEnteFile( + EnteFile file, { + bool parseExif = false, +}) async { if (file.isSharedMediaToAppSandbox) { - return await _getMediaUploadDataFromAppCache(file); + return await _getMediaUploadDataFromAppCache(file, parseExif); } else { - return await _getMediaUploadDataFromAssetFile(file); + return await _getMediaUploadDataFromAssetFile(file, parseExif); } } -Future _getMediaUploadDataFromAssetFile(EnteFile file) async { +Future _getMediaUploadDataFromAssetFile( + EnteFile file, + bool parseExif, +) async { File? sourceFile; Uint8List? thumbnailData; bool isDeleted; String? zipHash; String fileHash; + Map? exifData; // The timeouts are to safeguard against https://github.com/CaiJingLong/flutter_photo_manager/issues/467 final asset = await file.getAsset @@ -115,6 +126,9 @@ Future _getMediaUploadDataFromAssetFile(EnteFile file) async { InvalidReason.sourceFileMissing, ); } + if (parseExif) { + exifData = await tryExifFromFile(sourceFile); + } // h4ck to fetch location data if missing (thank you Android Q+) lazily only during uploads await _decorateEnteFileData(file, asset, sourceFile); fileHash = CryptoUtil.bin2base64(await CryptoUtil.getHash(sourceFile)); @@ -177,6 +191,7 @@ Future _getMediaUploadDataFromAssetFile(EnteFile file) async { height: h, width: w, motionPhotoStartIndex: motionPhotoStartingIndex, + exifData: exifData, ); } @@ -330,9 +345,11 @@ Future getPubMetadataRequest( ); } -Future _getMediaUploadDataFromAppCache(EnteFile file) async { +Future _getMediaUploadDataFromAppCache( + EnteFile file, bool parseExif) async { File sourceFile; Uint8List? thumbnailData; + Map? exifData; const bool isDeleted = false; final localPath = getSharedMediaFilePath(file); sourceFile = File(localPath); @@ -350,6 +367,7 @@ Future _getMediaUploadDataFromAppCache(EnteFile file) async { Map? dimensions; if (file.fileType == FileType.image) { dimensions = await getImageHeightAndWith(imagePath: localPath); + exifData = await tryExifFromFile(sourceFile); } else if (thumbnailData != null) { // the thumbnail null check is to ensure that we are able to generate thum // for video, we need to use the thumbnail data with any max width/height From 8559dd8364c5ff584fae9bb4fa0fc2a81b286eb9 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 7 Feb 2025 16:09:57 +0530 Subject: [PATCH 020/796] [mob] Refactor --- mobile/lib/models/file/file.dart | 47 +++++++++--------------- mobile/lib/utils/file_uploader.dart | 8 +++- mobile/lib/utils/file_uploader_util.dart | 10 ++++- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/mobile/lib/models/file/file.dart b/mobile/lib/models/file/file.dart index 44572632e1..705d740e82 100644 --- a/mobile/lib/models/file/file.dart +++ b/mobile/lib/models/file/file.dart @@ -160,6 +160,7 @@ class EnteFile { Future> getMetadataForUpload( MediaUploadData mediaUploadData, + ParsedExifDateTime? exifTime, ) async { final asset = await getAsset; // asset can be null for files shared to app @@ -170,36 +171,24 @@ class EnteFile { } } bool hasExifTime = false; - if ((fileType == FileType.image || fileType == FileType.video) && - mediaUploadData.sourceFile != null) { - final exifData = await tryExifFromFile(mediaUploadData.sourceFile!); - if (exifData != null) { - if (fileType == FileType.image) { - final dateResult = await tryParseExifDateTime(null, exifData); - if (dateResult != null && dateResult.time != null) { - hasExifTime = true; - creationTime = dateResult.time!.microsecondsSinceEpoch; - } - mediaUploadData.isPanorama = checkPanoramaFromEXIF(null, exifData); - - if (mediaUploadData.isPanorama != true) { - try { - final xmpData = await getXmp(mediaUploadData.sourceFile!); - mediaUploadData.isPanorama = checkPanoramaFromXMP(xmpData); - } catch (_) {} - - mediaUploadData.isPanorama ??= false; - } - } - if (Platform.isAndroid) { - //Fix for missing location data in lower android versions. - final Location? exifLocation = locationFromExif(exifData); - if (Location.isValidLocation(exifLocation)) { - location = exifLocation; - } - } - } + if (exifTime != null && exifTime.time != null) { + hasExifTime = true; + creationTime = exifTime.time!.microsecondsSinceEpoch; } + if (mediaUploadData.exifData != null) { + mediaUploadData.isPanorama = + checkPanoramaFromEXIF(null, mediaUploadData.exifData); + } + if (mediaUploadData.isPanorama != true && + fileType == FileType.image && + mediaUploadData.sourceFile != null) { + try { + final xmpData = await getXmp(mediaUploadData.sourceFile!); + mediaUploadData.isPanorama = checkPanoramaFromXMP(xmpData); + } catch (_) {} + mediaUploadData.isPanorama ??= false; + } + // Try to get the timestamp from fileName. In case of iOS, file names are // generic IMG_XXXX, so only parse it on Android devices if (!hasExifTime && Platform.isAndroid && title != null) { diff --git a/mobile/lib/utils/file_uploader.dart b/mobile/lib/utils/file_uploader.dart index c9d36643aa..4ed4497191 100644 --- a/mobile/lib/utils/file_uploader.dart +++ b/mobile/lib/utils/file_uploader.dart @@ -41,6 +41,7 @@ import 'package:photos/services/sync_service.dart'; import "package:photos/services/user_service.dart"; import 'package:photos/utils/crypto_util.dart'; import 'package:photos/utils/data_util.dart'; +import "package:photos/utils/exif_util.dart"; import "package:photos/utils/file_key.dart"; import 'package:photos/utils/file_uploader_util.dart'; import "package:photos/utils/file_util.dart"; @@ -730,8 +731,13 @@ class FileUploader { encThumbSize, ); } + final ParsedExifDateTime? exifTime = await tryParseExifDateTime( + null, + mediaUploadData.exifData, + ); + final metadata = + await file.getMetadataForUpload(mediaUploadData, exifTime); - final metadata = await file.getMetadataForUpload(mediaUploadData); final encryptedMetadataResult = await CryptoUtil.encryptChaCha( utf8.encode(jsonEncode(metadata)), fileAttributes.key!, diff --git a/mobile/lib/utils/file_uploader_util.dart b/mobile/lib/utils/file_uploader_util.dart index 63764a3066..4c9abab322 100644 --- a/mobile/lib/utils/file_uploader_util.dart +++ b/mobile/lib/utils/file_uploader_util.dart @@ -130,7 +130,7 @@ Future _getMediaUploadDataFromAssetFile( exifData = await tryExifFromFile(sourceFile); } // h4ck to fetch location data if missing (thank you Android Q+) lazily only during uploads - await _decorateEnteFileData(file, asset, sourceFile); + await _decorateEnteFileData(file, asset, sourceFile, exifData); fileHash = CryptoUtil.bin2base64(await CryptoUtil.getHash(sourceFile)); if (file.fileType == FileType.livePhoto && Platform.isIOS) { @@ -299,6 +299,7 @@ Future _decorateEnteFileData( EnteFile file, AssetEntity asset, File sourceFile, + Map? exifData, ) async { // h4ck to fetch location data if missing (thank you Android Q+) lazily only during uploads if (file.location == null || @@ -313,6 +314,13 @@ Future _decorateEnteFileData( file.location = props.location; } } + if (Platform.isAndroid && exifData != null) { + //Fix for missing location data in lower android versions. + final Location? exifLocation = locationFromExif(exifData); + if (Location.isValidLocation(exifLocation)) { + file.location = exifLocation; + } + } if (file.title == null || file.title!.isEmpty) { _logger.warning("Title was missing ${file.tag}"); file.title = await asset.titleAsync; From 5d0a15e9e5314b27dd49956283a9f2c49aa8281c Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 7 Feb 2025 16:32:02 +0530 Subject: [PATCH 021/796] [mob] Fill dateTime and offsetTime during upload --- mobile/lib/utils/file_uploader.dart | 45 +++++++++++++++++++---------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/mobile/lib/utils/file_uploader.dart b/mobile/lib/utils/file_uploader.dart index 4ed4497191..1d21e51c72 100644 --- a/mobile/lib/utils/file_uploader.dart +++ b/mobile/lib/utils/file_uploader.dart @@ -779,22 +779,9 @@ class FileUploader { CryptoUtil.bin2base64(encryptedFileKeyData.encryptedData!); final keyDecryptionNonce = CryptoUtil.bin2base64(encryptedFileKeyData.nonce!); - final Map pubMetadata = {}; + final Map pubMetadata = + _buildPublicMagicData(mediaUploadData, exifTime); MetadataRequest? pubMetadataRequest; - if ((mediaUploadData.height ?? 0) != 0 && - (mediaUploadData.width ?? 0) != 0) { - pubMetadata[heightKey] = mediaUploadData.height; - pubMetadata[widthKey] = mediaUploadData.width; - pubMetadata[mediaTypeKey] = - mediaUploadData.isPanorama == true ? 1 : 0; - } - if (mediaUploadData.motionPhotoStartIndex != null) { - pubMetadata[motionVideoIndexKey] = - mediaUploadData.motionPhotoStartIndex; - } - if (mediaUploadData.thumbnail == null) { - pubMetadata[noThumbKey] = true; - } if (pubMetadata.isNotEmpty) { pubMetadataRequest = await getPubMetadataRequest( file, @@ -878,6 +865,34 @@ class FileUploader { } } + Map _buildPublicMagicData( + MediaUploadData mediaUploadData, + ParsedExifDateTime? exifTime, + ) { + final Map pubMetadata = {}; + if ((mediaUploadData.height ?? 0) != 0 && + (mediaUploadData.width ?? 0) != 0) { + pubMetadata[heightKey] = mediaUploadData.height; + pubMetadata[widthKey] = mediaUploadData.width; + pubMetadata[mediaTypeKey] = mediaUploadData.isPanorama == true ? 1 : 0; + } + if (mediaUploadData.motionPhotoStartIndex != null) { + pubMetadata[motionVideoIndexKey] = mediaUploadData.motionPhotoStartIndex; + } + if (mediaUploadData.thumbnail == null) { + pubMetadata[noThumbKey] = true; + } + if (exifTime != null) { + if (exifTime.dateTime != null) { + pubMetadata[dateTimeKey] = exifTime.dateTime; + } + if (exifTime.offsetTime != null) { + pubMetadata[offsetTimeKey] = exifTime.offsetTime; + } + } + return pubMetadata; + } + bool isPutOrUpdateFileError(Object e) { if (e is DioError) { return e.requestOptions.path.contains("/files") || From 726c6dc8e6f1ce9e58fc219f6bef01a701b186d4 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Mon, 10 Feb 2025 11:33:11 +0530 Subject: [PATCH 022/796] [mob][photos] Increase trip distance threshold --- mobile/lib/services/search_service.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index 421570dd59..f99713a8f2 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -1254,7 +1254,7 @@ class SearchService { if (isFileInsideLocationTag( clusterLocation, file.location!, - 50.0, + 100.0, )) { wideRadiusClusters[clusterID]!.$1.add(file); foundWideCluster = true; @@ -1437,7 +1437,9 @@ class SearchService { } for (final finalTrip in mergedTrips.values) { final files = finalTrip.$1; // TODO: lau: take best selection only - final year = DateTime.fromMicrosecondsSinceEpoch((finalTrip.$2 + finalTrip.$2) ~/ 2).year; + final year = DateTime.fromMicrosecondsSinceEpoch( + (finalTrip.$2 + finalTrip.$2) ~/ 2, + ).year; final name = "Trip! ($year)"; searchResults.add( GenericSearchResult( From 12c472ef01e5fad088f2b701e13eb31952aa15c7 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 10 Feb 2025 11:34:45 +0530 Subject: [PATCH 023/796] [mob] Fix lint & missing exif for files shared to ente --- mobile/lib/utils/file_uploader_util.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mobile/lib/utils/file_uploader_util.dart b/mobile/lib/utils/file_uploader_util.dart index 4c9abab322..6f37236b8e 100644 --- a/mobile/lib/utils/file_uploader_util.dart +++ b/mobile/lib/utils/file_uploader_util.dart @@ -354,7 +354,9 @@ Future getPubMetadataRequest( } Future _getMediaUploadDataFromAppCache( - EnteFile file, bool parseExif) async { + EnteFile file, + bool parseExif, +) async { File sourceFile; Uint8List? thumbnailData; Map? exifData; @@ -394,6 +396,7 @@ Future _getMediaUploadDataFromAppCache( FileHashData(fileHash), height: dimensions?['height'], width: dimensions?['width'], + exifData: exifData, ); } catch (e, s) { _logger.warning("failed to generate thumbnail", e, s); From b453ffef85ed493b9045676b8204d59abed63176 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 10 Feb 2025 13:29:17 +0530 Subject: [PATCH 024/796] [server] Speed up file deletion --- server/pkg/controller/file.go | 36 ++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/server/pkg/controller/file.go b/server/pkg/controller/file.go index 7f871cc098..0b2568ff5c 100644 --- a/server/pkg/controller/file.go +++ b/server/pkg/controller/file.go @@ -6,11 +6,13 @@ import ( "encoding/json" "errors" "fmt" - "github.com/ente-io/museum/pkg/controller/discord" - "github.com/ente-io/museum/pkg/utils/network" "runtime/debug" "strconv" "strings" + "sync" + + "github.com/ente-io/museum/pkg/controller/discord" + "github.com/ente-io/museum/pkg/utils/network" "github.com/ente-io/museum/pkg/controller/email" "github.com/ente-io/museum/pkg/controller/lock" @@ -695,14 +697,38 @@ func (c *FileController) CleanupDeletedFiles() { defer func() { c.LockController.ReleaseLock(DeletedObjectQueueLock) }() - items, err := c.QueueRepo.GetItemsReadyForDeletion(repo.DeleteObjectQueue, 1500) + items, err := c.QueueRepo.GetItemsReadyForDeletion(repo.DeleteObjectQueue, 2500) if err != nil { log.WithError(err).Error("Failed to fetch items from queue") return } - for _, i := range items { - c.cleanupDeletedFile(i) + var wg sync.WaitGroup + itemChan := make(chan repo.QueueItem, len(items)) + + // Start worker goroutines + for w := 0; w < 4; w++ { + wg.Add(1) + go func() { + defer wg.Done() + for item := range itemChan { + func(item repo.QueueItem) { + defer func() { + if r := recover(); r != nil { + log.WithField("item", item.Item).Errorf("Recovered from panic: %v", r) + } + }() + c.cleanupDeletedFile(item) + }(item) + } + }() } + // Send items to the channel + for _, item := range items { + itemChan <- item + } + close(itemChan) + // Wait for all workers to finish + wg.Wait() } func (c *FileController) GetTotalFileCount() (int64, error) { From caf601b49bf20936cadfa45115f69ab3930f27a7 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Mon, 10 Feb 2025 13:55:59 +0530 Subject: [PATCH 025/796] [mob][photos] Switch order --- mobile/lib/services/search_service.dart | 43 +++++++++++++------------ 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index f99713a8f2..0b6c4980d6 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -1317,25 +1317,7 @@ class SearchService { for (final clusterID in wideRadiusClusters.keys) { final files = wideRadiusClusters[clusterID]!.$1; final location = wideRadiusClusters[clusterID]!.$2; - // Check that the photos are distributed over a short time range (2-30 days) - final creationTimes = []; - for (final file in files) { - if (file.creationTime != null) { - creationTimes.add(file.creationTime!); - } - } - creationTimes.sort(); - if (creationTimes.length < 2) continue; - final firstCreationTime = DateTime.fromMicrosecondsSinceEpoch( - creationTimes.first, - ); - final lastCreationTime = DateTime.fromMicrosecondsSinceEpoch( - creationTimes.last, - ); - final days = lastCreationTime.difference(firstCreationTime).inDays; - if (days < 2 || days > 30) { - continue; - } + // Check that it's at least 10km away from any base or tag location bool tooClose = false; for (final baseLocation in baseLocations.values) { @@ -1348,7 +1330,6 @@ class SearchService { break; } } - if (tooClose) continue; for (final tag in tagToItemsMap.keys) { if (isFileInsideLocationTag( tag.item.centerPoint, @@ -1360,6 +1341,28 @@ class SearchService { } } if (tooClose) continue; + + // Check that the photos are distributed over a short time range (2-30 days) + final creationTimes = []; + for (final file in files) { + if (file.creationTime != null) { + creationTimes.add(file.creationTime!); + } + } + if (creationTimes.length < 2) continue; + creationTimes.sort(); + + final firstCreationTime = DateTime.fromMicrosecondsSinceEpoch( + creationTimes.first, + ); + final lastCreationTime = DateTime.fromMicrosecondsSinceEpoch( + creationTimes.last, + ); + final days = lastCreationTime.difference(firstCreationTime).inDays; + if (days < 2 || days > 30) { + continue; + } + tripLocations[clusterID] = ( files, location, From d99d08e8ae62c86c458218c0c2c947241721136e Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Mon, 10 Feb 2025 14:09:56 +0530 Subject: [PATCH 026/796] [mob][photos] creationTime check --- mobile/lib/services/search_service.dart | 28 ++++++++++++------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index 0b6c4980d6..4cddec7ba8 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -1208,7 +1208,10 @@ class SearchService { final Map, Location)> wideRadiusClusters = {}; // Go through all files and cluster the ones not inside any location tag for (EnteFile file in allFiles) { - if (!file.hasLocation || file.uploadedFileID == null || !file.isOwner) { + if (!file.hasLocation || + file.uploadedFileID == null || + !file.isOwner || + file.creationTime == null) { continue; } // Check if the file is inside any location tag @@ -1281,13 +1284,11 @@ class SearchService { final creationTimes = []; final Set uniqueDays = {}; for (final file in files) { - if (file.creationTime != null) { - creationTimes.add(file.creationTime!); - final date = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); - final dayStamp = - DateTime(date.year, date.month, date.day).microsecondsSinceEpoch; - uniqueDays.add(dayStamp); - } + creationTimes.add(file.creationTime!); + final date = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); + final dayStamp = + DateTime(date.year, date.month, date.day).microsecondsSinceEpoch; + uniqueDays.add(dayStamp); } creationTimes.sort(); if (creationTimes.length < 10) continue; @@ -1341,17 +1342,14 @@ class SearchService { } } if (tooClose) continue; - - // Check that the photos are distributed over a short time range (2-30 days) + + // Check that the photos are distributed over a short time range (2-30 days) or multiple short time ranges only final creationTimes = []; for (final file in files) { - if (file.creationTime != null) { - creationTimes.add(file.creationTime!); - } + creationTimes.add(file.creationTime!); } if (creationTimes.length < 2) continue; creationTimes.sort(); - final firstCreationTime = DateTime.fromMicrosecondsSinceEpoch( creationTimes.first, ); @@ -1362,7 +1360,7 @@ class SearchService { if (days < 2 || days > 30) { continue; } - + tripLocations[clusterID] = ( files, location, From b0966e0cca0279e826a6b25f111f4c23b615bc76 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 10 Feb 2025 15:19:51 +0530 Subject: [PATCH 027/796] [server] Delete more items in single run --- server/pkg/controller/file.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/pkg/controller/file.go b/server/pkg/controller/file.go index 0b2568ff5c..be9a553756 100644 --- a/server/pkg/controller/file.go +++ b/server/pkg/controller/file.go @@ -697,7 +697,7 @@ func (c *FileController) CleanupDeletedFiles() { defer func() { c.LockController.ReleaseLock(DeletedObjectQueueLock) }() - items, err := c.QueueRepo.GetItemsReadyForDeletion(repo.DeleteObjectQueue, 2500) + items, err := c.QueueRepo.GetItemsReadyForDeletion(repo.DeleteObjectQueue, 5000) if err != nil { log.WithError(err).Error("Failed to fetch items from queue") return From 7509abd1a90fedc121e37503783ebb3c58ab655b Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 10 Feb 2025 15:21:31 +0530 Subject: [PATCH 028/796] [server] Increase cron freq --- server/cmd/museum/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index e1a3622011..9ddb958596 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -943,7 +943,7 @@ func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, publicCollectionR } }) - schedule(c, "@every 10m", func() { + schedule(c, "@every 8m", func() { fileController.CleanupDeletedFiles() }) schedule(c, "@every 101s", func() { From 8d7950afea9f2047400aeaab3387395848856bdc Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Mon, 10 Feb 2025 15:49:02 +0530 Subject: [PATCH 029/796] [mob][photos] Change logic for repeating trips --- mobile/lib/services/search_service.dart | 70 +++++++++++++++++-------- 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index 4cddec7ba8..e35f347392 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -1315,6 +1315,7 @@ class SearchService { // Identify trip locations final Map, Location, int, int)> tripLocations = {}; + clusteredLocations: for (final clusterID in wideRadiusClusters.keys) { final files = wideRadiusClusters[clusterID]!.$1; final location = wideRadiusClusters[clusterID]!.$2; @@ -1341,32 +1342,55 @@ class SearchService { break; } } - if (tooClose) continue; + if (tooClose) continue clusteredLocations; // Check that the photos are distributed over a short time range (2-30 days) or multiple short time ranges only - final creationTimes = []; - for (final file in files) { - creationTimes.add(file.creationTime!); - } - if (creationTimes.length < 2) continue; - creationTimes.sort(); - final firstCreationTime = DateTime.fromMicrosecondsSinceEpoch( - creationTimes.first, - ); - final lastCreationTime = DateTime.fromMicrosecondsSinceEpoch( - creationTimes.last, - ); - final days = lastCreationTime.difference(firstCreationTime).inDays; - if (days < 2 || days > 30) { - continue; - } + files.sort((a, b) => a.creationTime!.compareTo(b.creationTime!)); + // Find distinct time blocks (potential trips) + List currentBlockFiles = [files.first]; + int blockStart = files.first.creationTime!; + int lastTime = files.first.creationTime!; + DateTime lastDateTime = DateTime.fromMicrosecondsSinceEpoch(lastTime); - tripLocations[clusterID] = ( - files, - location, - firstCreationTime.microsecondsSinceEpoch, - lastCreationTime.microsecondsSinceEpoch - ); + for (int i = 1; i < files.length; i++) { + final currentFile = files[i]; + final currentTime = currentFile.creationTime!; + final gap = DateTime.fromMicrosecondsSinceEpoch(currentTime) + .difference(lastDateTime) + .inDays; + + // If gap is too large, end current block and check if it's a valid trip + if (gap > 15) { + // 10 days gap to separate trips. If gap is small, it's likely not a trip + if (gap < 90) continue clusteredLocations; + + final blockDuration = lastDateTime + .difference(DateTime.fromMicrosecondsSinceEpoch(blockStart)) + .inDays; + + // Check if current block is a valid trip (2-30 days) + if (blockDuration >= 2 && blockDuration <= 30) { + tripLocations[newAutoLocationID()] = + (List.from(currentBlockFiles), location, blockStart, lastTime); + } + + // Start new block + currentBlockFiles = []; + blockStart = currentTime; + } + + currentBlockFiles.add(currentFile); + lastTime = currentTime; + lastDateTime = DateTime.fromMicrosecondsSinceEpoch(lastTime); + } + // Check final block + final lastBlockDuration = lastDateTime + .difference(DateTime.fromMicrosecondsSinceEpoch(blockStart)) + .inDays; + if (lastBlockDuration >= 2 && lastBlockDuration <= 30) { + tripLocations[newAutoLocationID()] = + (List.from(currentBlockFiles), location, blockStart, lastTime); + } } // Check if any trip locations should be merged From d607d8a8516e24a40fa6f43bea1b04a5054492d3 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Mon, 10 Feb 2025 16:38:46 +0530 Subject: [PATCH 030/796] [mob][photos] Merge locations better --- mobile/lib/services/search_service.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index e35f347392..09fd29fbfd 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -1414,13 +1414,13 @@ class SearchService { ); final bool overlapBeginning = tripFirstTime.isBefore( otherTripLastTime.add( - const Duration(days: 1), + const Duration(days: 3), ), ) && tripFirstTime.isAfter(otherTripFirstTime); final bool overlapEnd = tripLastTime.isAfter( otherTripFirstTime.subtract( - const Duration(days: 1), + const Duration(days: 3), ), ) && tripLastTime.isBefore(otherTripLastTime); From 5a0d2ba922d9d4bc106f5754afc883b616c3ebf7 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Mon, 10 Feb 2025 16:51:15 +0530 Subject: [PATCH 031/796] [mob][photos] Remove too small trips --- mobile/lib/services/search_service.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index 09fd29fbfd..3227f4b77b 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -1439,6 +1439,14 @@ class SearchService { mergedTrips[tripID] = (trip.$1, trip.$3, trip.$4); } + // Remove too small trips + for (final tripID in mergedTrips.keys.toList()) { + final filesAmount = mergedTrips[tripID]!.$1.length; + if (filesAmount < 20) { + mergedTrips.remove(tripID); + } + } + // TODO: lau: Check if there are any trips to surface // For now for testing let's just surface all base and trip locations for (final baseLocation in baseLocations.values) { From 1358087ee794e6b6cbaa33b26f6dafd50f8c6f88 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 10 Feb 2025 19:22:04 +0530 Subject: [PATCH 032/796] photosd-v1.7.9 --- desktop/CHANGELOG.md | 3 +-- desktop/package.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/desktop/CHANGELOG.md b/desktop/CHANGELOG.md index aa647dd8e6..768f7f8afe 100644 --- a/desktop/CHANGELOG.md +++ b/desktop/CHANGELOG.md @@ -1,11 +1,10 @@ # CHANGELOG -## v1.7.9 (Unreleased) +## v1.7.9 - Light mode. - Faster and more stable thumbnail generation. - Support `.supplemental-metadata` JSON files in Google Takeout. -- . ## v1.7.8 diff --git a/desktop/package.json b/desktop/package.json index 3859892503..13d8f80938 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "ente", - "version": "1.7.9-beta", + "version": "1.7.9", "private": true, "description": "Desktop client for Ente Photos", "repository": "github:ente-io/photos-desktop", From dda46c063984a89d0b00cc94822974d566bfea9a Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 10 Feb 2025 19:29:49 +0530 Subject: [PATCH 033/796] [desktop] next --- desktop/CHANGELOG.md | 4 ++++ desktop/package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/desktop/CHANGELOG.md b/desktop/CHANGELOG.md index 768f7f8afe..4d7450cec9 100644 --- a/desktop/CHANGELOG.md +++ b/desktop/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## v1.7.10 (Unreleased) + +- . + ## v1.7.9 - Light mode. diff --git a/desktop/package.json b/desktop/package.json index 13d8f80938..ccf8eff772 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "ente", - "version": "1.7.9", + "version": "1.7.10-beta", "private": true, "description": "Desktop client for Ente Photos", "repository": "github:ente-io/photos-desktop", From 59e26779b956d8e7247bf2e8908d76ad57e8e8b8 Mon Sep 17 00:00:00 2001 From: mngshm Date: Mon, 10 Feb 2025 19:36:40 +0530 Subject: [PATCH 034/796] [server][WIP] functionality for modifying users storage limit --- server/cmd/museum/main.go | 1 + server/pkg/api/family.go | 15 +++++++++++++ server/pkg/controller/family/admin.go | 31 +++++++++++++++++++++++++++ server/pkg/repo/family.go | 10 +++++++++ 4 files changed, 57 insertions(+) diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index e1a3622011..62944cd004 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -621,6 +621,7 @@ func main() { familiesJwtAuthAPI.GET("/family/members", familyHandler.FetchMembers) familiesJwtAuthAPI.DELETE("/family/remove-member/:id", familyHandler.RemoveMember) familiesJwtAuthAPI.DELETE("/family/revoke-invite/:id", familyHandler.RevokeInvite) + familiesJwtAuthAPI.POST("/family/modify-storage", familyHandler.ModifyStorageLimit) emergencyHandler := &api.EmergencyHandler{ Controller: emergencyCtrl, diff --git a/server/pkg/api/family.go b/server/pkg/api/family.go index e4ca403ac4..82e352b80a 100644 --- a/server/pkg/api/family.go +++ b/server/pkg/api/family.go @@ -120,6 +120,21 @@ func (h *FamilyHandler) AcceptInvite(c *gin.Context) { c.JSON(http.StatusOK, response) } +// ModifyStorageLimit allows adminUser to Modify the storage for a member in the Family. +func (h *FamilyHandler) ModifyStorageLimit(c *gin.Context) { + var request ente.FamilyMember + if err := c.ShouldBindJSON(&request); err != nil { + handler.Error(c, stacktrace.Propagate(err, "Could not bind request params")) + return + } + + err := h.Controller.ModifyMemberStorage(c, auth.GetUserID(c.Request.Header), request.ID, request.StorageLimit) + if err != nil { + handler.Error(c, stacktrace.Propagate(err, "")) + } + c.JSON(http.StatusOK, nil) +} + // GetInviteInfo returns basic information about invitor/admin as long as the invite is valid func (h *FamilyHandler) GetInviteInfo(c *gin.Context) { inviteToken := c.Param("token") diff --git a/server/pkg/controller/family/admin.go b/server/pkg/controller/family/admin.go index fbb3506b13..36caffbae8 100644 --- a/server/pkg/controller/family/admin.go +++ b/server/pkg/controller/family/admin.go @@ -190,6 +190,37 @@ func (c *Controller) CloseFamily(ctx context.Context, adminID int64) error { return nil } +// ModifyMemberStorage allows admin user to update the storageLimit for a member in the family +func (c *Controller) ModifyMemberStorage(ctx context.Context, adminID int64, id uuid.UUID, storageLimit *int64) error { + familyAdminID, err := c.UserRepo.GetFamilyAdminID(adminID) + if err != nil { + stacktrace.Propagate(err, "Could not get family admin ID") + } + + member, err := c.FamilyRepo.GetMemberById(ctx, id) + if err != nil { + stacktrace.Propagate(err, "Couldn't fetch Family Member") + } + + // get admin subscription in order to get the size of total storage quota + // available in that family. and let the user chose storage of outw of the whole + // storage quota or maximum limit. + activeSub, err := c.BillingCtrl.GetActiveSubscription(adminID) + if err != nil { + stacktrace.Propagate(err, "couldn't get active subscription") + } + + if adminID == *familyAdminID && id == member.ID { + if storageLimit != nil && *storageLimit > activeSub.Storage { + err := c.FamilyRepo.ModifyMemberStorage(ctx, adminID, member.ID, storageLimit) + if err != nil { + stacktrace.Propagate(err, "Couldn't Modify Members Storage") + } + } + } + return nil +} + func (c *Controller) sendNotification(ctx context.Context, adminUserID int64, memberUserID int64, newStatus ente.MemberStatus, inviteToken *string) error { adminUser, err := c.UserRepo.Get(adminUserID) if err != nil { diff --git a/server/pkg/repo/family.go b/server/pkg/repo/family.go index 8043ebab1b..f3130a8f74 100644 --- a/server/pkg/repo/family.go +++ b/server/pkg/repo/family.go @@ -196,6 +196,16 @@ func (repo *FamilyRepository) RemoveMember(ctx context.Context, adminID int64, m return stacktrace.Propagate(tx.Commit(), "failed to commit") } +// UpdateStorage is used to set Pre-existing Members Storage Limit. +func (repo *FamilyRepository) ModifyMemberStorage(ctx context.Context, adminID int64, id uuid.UUID, storageLimit *int64) error { + _, err := repo.DB.Exec(`UPDATE families SET storage=$1 where token=$2`, *storageLimit, id) + if err != nil { + return stacktrace.Propagate(err, "Could not update Members Storage Limit") + } + + return stacktrace.Propagate(err, "Failed to Modify Members Storage Limit") +} + // RevokeInvite revokes the invitation invite func (repo *FamilyRepository) RevokeInvite(ctx context.Context, adminID int64, memberID int64) error { tx, err := repo.DB.BeginTx(ctx, nil) From 77be0a18d43ddae07b690d5877eb11af6fc6bf99 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 10 Feb 2025 20:04:15 +0530 Subject: [PATCH 035/796] [docs] Update logs menu location --- .../docs/photos/troubleshooting/sharing-logs.md | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/docs/docs/photos/troubleshooting/sharing-logs.md b/docs/docs/photos/troubleshooting/sharing-logs.md index cd9417fac3..0de78748a4 100644 --- a/docs/docs/photos/troubleshooting/sharing-logs.md +++ b/docs/docs/photos/troubleshooting/sharing-logs.md @@ -20,21 +20,14 @@ the logs just make the process a bit faster and easier. - Select for the option to _Report a Bug_. - Tap on _Report a bug_. -## Desktop - -- Click on _Help_ menu at the top of your screen, and select the _View logs_ - option. -- Open settings (click on the three horizontal lines button located at the top - left corner of the screen). -- Click on _Support_. This will open your email client where you can attach the - logs in the email and describe the issue. - -## Web +## Desktop and Web - Open settings (click on the three horizontal lines button located at the top left corner of the screen). -- Click on _Debug Logs_ towards the bottom of settings. -- Click on _Download logs_ +- Click on the _Help_ option towards the bottom of settings. +- Click on _View logs_. This will show you the location of the logs on your + system (desktop), or download them from the browser onto your computer (web). +- Go back to settings. - Click on _Support_. This will open your email client where you can attach the logs in the email and describe the issue. From 2947ca2e3c14c07b3b065867d740e011b1ca704d Mon Sep 17 00:00:00 2001 From: mngshm Date: Tue, 11 Feb 2025 11:27:21 +0530 Subject: [PATCH 036/796] fix storagelimit column name in DB.Exec --- server/pkg/repo/family.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/pkg/repo/family.go b/server/pkg/repo/family.go index f3130a8f74..79692a6122 100644 --- a/server/pkg/repo/family.go +++ b/server/pkg/repo/family.go @@ -198,7 +198,7 @@ func (repo *FamilyRepository) RemoveMember(ctx context.Context, adminID int64, m // UpdateStorage is used to set Pre-existing Members Storage Limit. func (repo *FamilyRepository) ModifyMemberStorage(ctx context.Context, adminID int64, id uuid.UUID, storageLimit *int64) error { - _, err := repo.DB.Exec(`UPDATE families SET storage=$1 where token=$2`, *storageLimit, id) + _, err := repo.DB.Exec(`UPDATE families SET storage_limit=$1 where token=$2`, *storageLimit, id) if err != nil { return stacktrace.Propagate(err, "Could not update Members Storage Limit") } From 8da160b834f3158876d64019484cdfe13506bea1 Mon Sep 17 00:00:00 2001 From: mngshm Date: Tue, 11 Feb 2025 11:51:47 +0530 Subject: [PATCH 037/796] minor fix for db column names in DB.Exec --- server/pkg/repo/family.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/pkg/repo/family.go b/server/pkg/repo/family.go index 79692a6122..ee0fe20b16 100644 --- a/server/pkg/repo/family.go +++ b/server/pkg/repo/family.go @@ -198,7 +198,7 @@ func (repo *FamilyRepository) RemoveMember(ctx context.Context, adminID int64, m // UpdateStorage is used to set Pre-existing Members Storage Limit. func (repo *FamilyRepository) ModifyMemberStorage(ctx context.Context, adminID int64, id uuid.UUID, storageLimit *int64) error { - _, err := repo.DB.Exec(`UPDATE families SET storage_limit=$1 where token=$2`, *storageLimit, id) + _, err := repo.DB.Exec(`UPDATE families SET storage_limi=$1 where id=$2`, *storageLimit, id) if err != nil { return stacktrace.Propagate(err, "Could not update Members Storage Limit") } From 01aa6796989517238a178f3b3eca1ea2f28e3807 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Tue, 11 Feb 2025 14:17:48 +0530 Subject: [PATCH 038/796] [mob][photos] Better merge --- mobile/lib/services/search_service.dart | 30 +++++++++---------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index 3227f4b77b..dcb272aaa1 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -1406,31 +1406,21 @@ class SearchService { bool merged = false; for (final otherTripID in mergedTrips.keys) { final otherTrip = mergedTrips[otherTripID]!; - final otherTripFirstTime = DateTime.fromMicrosecondsSinceEpoch( - otherTrip.$2, - ); - final otherTripLastTime = DateTime.fromMicrosecondsSinceEpoch( - otherTrip.$3, - ); - final bool overlapBeginning = tripFirstTime.isBefore( - otherTripLastTime.add( - const Duration(days: 3), - ), - ) && - tripFirstTime.isAfter(otherTripFirstTime); - final bool overlapEnd = tripLastTime.isAfter( - otherTripFirstTime.subtract( - const Duration(days: 3), - ), - ) && - tripLastTime.isBefore(otherTripLastTime); - if (overlapBeginning || overlapEnd) { + final otherTripFirstTime = + DateTime.fromMicrosecondsSinceEpoch(otherTrip.$2); + final otherTripLastTime = + DateTime.fromMicrosecondsSinceEpoch(otherTrip.$3); + if (tripFirstTime + .isBefore(otherTripLastTime.add(const Duration(days: 3))) && + tripLastTime.isAfter( + otherTripFirstTime.subtract(const Duration(days: 3)), + )) { mergedTrips[otherTripID] = ( otherTrip.$1 + trip.$1, min(otherTrip.$2, trip.$3), max(otherTrip.$3, trip.$4), ); - _logger.info('Merged two trip locations'); + _logger.finest('Merged two trip locations'); merged = true; break; } From 45f15490795fb3d26211651cae6bfb1ba0c57ca0 Mon Sep 17 00:00:00 2001 From: ashilkn Date: Tue, 11 Feb 2025 15:49:58 +0530 Subject: [PATCH 039/796] [mob][photos] Fix: FileAppbar buttons not working on some screens --- mobile/lib/ui/viewer/file/custom_app_bar.dart | 25 --------- mobile/lib/ui/viewer/file/detail_page.dart | 1 - mobile/lib/ui/viewer/file/file_app_bar.dart | 56 +++++++++---------- 3 files changed, 26 insertions(+), 56 deletions(-) delete mode 100644 mobile/lib/ui/viewer/file/custom_app_bar.dart diff --git a/mobile/lib/ui/viewer/file/custom_app_bar.dart b/mobile/lib/ui/viewer/file/custom_app_bar.dart deleted file mode 100644 index 1003e56af9..0000000000 --- a/mobile/lib/ui/viewer/file/custom_app_bar.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter/material.dart'; - -class CustomAppBar extends PreferredSize { - @override - final Widget child; - @override - final Size preferredSize; - final double height; - - const CustomAppBar( - this.child, - this.preferredSize, { - Key? key, - this.height = kToolbarHeight, - }) : super(key: key, child: child, preferredSize: preferredSize); - - @override - Widget build(BuildContext context) { - return Container( - height: preferredSize.height, - alignment: Alignment.center, - child: child, - ); - } -} diff --git a/mobile/lib/ui/viewer/file/detail_page.dart b/mobile/lib/ui/viewer/file/detail_page.dart index 7e8795c978..3751e327f6 100644 --- a/mobile/lib/ui/viewer/file/detail_page.dart +++ b/mobile/lib/ui/viewer/file/detail_page.dart @@ -156,7 +156,6 @@ class _DetailPageState extends State { return FileAppBar( _files![selectedIndex], _onFileRemoved, - 100, widget.config.mode == DetailPageMode.full, enableFullScreenNotifier: _enableFullScreenNotifier, ); diff --git a/mobile/lib/ui/viewer/file/file_app_bar.dart b/mobile/lib/ui/viewer/file/file_app_bar.dart index e763a174cc..46adbad5d4 100644 --- a/mobile/lib/ui/viewer/file/file_app_bar.dart +++ b/mobile/lib/ui/viewer/file/file_app_bar.dart @@ -23,7 +23,6 @@ import "package:photos/services/local_authentication_service.dart"; import "package:photos/services/preview_video_store.dart"; import "package:photos/theme/ente_theme.dart"; import 'package:photos/ui/collections/collection_action_sheet.dart'; -import 'package:photos/ui/viewer/file/custom_app_bar.dart'; import "package:photos/ui/viewer/file_details/favorite_widget.dart"; import "package:photos/ui/viewer/file_details/upload_icon_widget.dart"; import 'package:photos/utils/dialog_util.dart'; @@ -35,14 +34,12 @@ import 'package:photos/utils/toast_util.dart'; class FileAppBar extends StatefulWidget { final EnteFile file; final Function(EnteFile) onFileRemoved; - final double height; final bool shouldShowActions; final ValueNotifier enableFullScreenNotifier; const FileAppBar( this.file, this.onFileRemoved, - this.height, this.shouldShowActions, { required this.enableFullScreenNotifier, super.key, @@ -98,8 +95,9 @@ class FileAppBarState extends State { final isTrashedFile = widget.file is TrashFile; final shouldShowActions = widget.shouldShowActions && !isTrashedFile; - return CustomAppBar( - ValueListenableBuilder( + return PreferredSize( + preferredSize: const Size.fromHeight(kToolbarHeight), + child: ValueListenableBuilder( valueListenable: widget.enableFullScreenNotifier, builder: (context, bool isFullScreen, child) { return IgnorePointer( @@ -124,32 +122,33 @@ class FileAppBarState extends State { stops: const [0, 0.2, 1], ), ), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 250), - switchInCurve: Curves.easeInOut, - switchOutCurve: Curves.easeInOut, - child: AppBar( - clipBehavior: Clip.none, - key: ValueKey(isGuestView), - iconTheme: const IconThemeData( - color: Colors.white, - ), //same for both themes - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () { - isGuestView - ? _requestAuthentication() - : Navigator.of(context).pop(); - }, + child: SafeArea( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + switchInCurve: Curves.easeInOut, + switchOutCurve: Curves.easeInOut, + child: AppBar( + clipBehavior: Clip.none, + key: ValueKey(isGuestView), + iconTheme: const IconThemeData( + color: Colors.white, + ), //same for both themes + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + isGuestView + ? _requestAuthentication() + : Navigator.of(context).pop(); + }, + ), + actions: shouldShowActions && !isGuestView ? _actions : [], + elevation: 0, + backgroundColor: const Color(0x00000000), ), - actions: shouldShowActions && !isGuestView ? _actions : [], - elevation: 0, - backgroundColor: const Color(0x00000000), ), ), ), ), - Size.fromHeight(Platform.isAndroid ? 84 : 96), ); } @@ -179,10 +178,7 @@ class FileAppBarState extends State { } if (!isFileHidden && isFileUploaded) { _actions.add( - Padding( - padding: const EdgeInsets.all(8), - child: FavoriteWidget(widget.file), - ), + Center(child: FavoriteWidget(widget.file)), ); } if (!isFileUploaded) { From 2282db780014fbcb72921655a368ce11aa6ed98b Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Tue, 11 Feb 2025 16:06:00 +0530 Subject: [PATCH 040/796] [mob] Build changes --- mobile/ios/Podfile.lock | 13 +++++++++ mobile/pubspec.lock | 60 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 4a76000080..6769a1fcc8 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -6,6 +6,9 @@ PODS: - connectivity_plus (0.0.1): - Flutter - FlutterMacOS + - cupertino_http (0.0.1): + - Flutter + - FlutterMacOS - dart_ui_isolate (0.0.1): - Flutter - device_info_plus (0.0.1): @@ -158,6 +161,8 @@ PODS: - nanopb/encode (3.30910.0) - native_video_player (1.0.0): - Flutter + - objective_c (0.0.1): + - Flutter - onnxruntime (0.0.1): - Flutter - onnxruntime-objc (= 1.18.0) @@ -247,6 +252,7 @@ DEPENDENCIES: - background_fetch (from `.symlinks/plugins/background_fetch/ios`) - battery_info (from `.symlinks/plugins/battery_info/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) + - cupertino_http (from `.symlinks/plugins/cupertino_http/darwin`) - dart_ui_isolate (from `.symlinks/plugins/dart_ui_isolate/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - ffmpeg_kit_flutter_full_gpl (from `.symlinks/plugins/ffmpeg_kit_flutter_full_gpl/ios`) @@ -278,6 +284,7 @@ DEPENDENCIES: - motionphoto (from `.symlinks/plugins/motionphoto/ios`) - move_to_background (from `.symlinks/plugins/move_to_background/ios`) - native_video_player (from `.symlinks/plugins/native_video_player/ios`) + - objective_c (from `.symlinks/plugins/objective_c/ios`) - onnxruntime (from `.symlinks/plugins/onnxruntime/ios`) - open_mail_app (from `.symlinks/plugins/open_mail_app/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) @@ -331,6 +338,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/battery_info/ios" connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/darwin" + cupertino_http: + :path: ".symlinks/plugins/cupertino_http/darwin" dart_ui_isolate: :path: ".symlinks/plugins/dart_ui_isolate/ios" device_info_plus: @@ -393,6 +402,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/move_to_background/ios" native_video_player: :path: ".symlinks/plugins/native_video_player/ios" + objective_c: + :path: ".symlinks/plugins/objective_c/ios" onnxruntime: :path: ".symlinks/plugins/onnxruntime/ios" open_mail_app: @@ -442,6 +453,7 @@ SPEC CHECKSUMS: background_fetch: 39f11371c0dce04b001c4bfd5e782bcccb0a85e2 battery_info: 09f5c9ee65394f2291c8c6227bedff345b8a730c connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db + cupertino_http: 947a233f40cfea55167a49f2facc18434ea117ba dart_ui_isolate: d5bcda83ca4b04f129d70eb90110b7a567aece14 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 ffmpeg-kit-ios-full-gpl: 80adc341962e55ef709e36baa8ed9a70cf4ea62b @@ -484,6 +496,7 @@ SPEC CHECKSUMS: move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 native_video_player: d12af78a1a4a8cf09775a5177d5b392def6fd23c + objective_c: 77e887b5ba1827970907e10e832eec1683f3431d onnxruntime: e7c2ae44385191eaad5ae64c935a72debaddc997 onnxruntime-c: a909204639a1f035f575127ac406f781ac797c9c onnxruntime-objc: b6fab0f1787aa6f7190c2013f03037df4718bd8b diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index e6776987fb..ab9c4cd2a4 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -337,6 +337,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.2" + cronet_http: + dependency: transitive + description: + name: cronet_http + sha256: "3af9c4d57bf07ef4b307e77b22be4ad61bea19ee6ff65e62184863f3a09f1415" + url: "https://pub.dev" + source: hosted + version: "1.3.2" cross_file: dependency: "direct main" description: @@ -361,6 +369,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + cupertino_http: + dependency: transitive + description: + name: cupertino_http + sha256: "6fcf79586ad872ddcd6004d55c8c2aab3cdf0337436e8f99837b1b6c30665d0c" + url: "https://pub.dev" + source: hosted + version: "2.0.2" cupertino_icons: dependency: "direct main" description: @@ -421,10 +437,18 @@ packages: dependency: "direct main" description: name: dio - sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "5.8.0+1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a + url: "https://pub.dev" + source: hosted + version: "2.1.0" dots_indicator: dependency: "direct main" description: @@ -1173,6 +1197,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + http_profile: + dependency: transitive + description: + name: http_profile + sha256: "7e679e355b09aaee2ab5010915c932cce3f2d1c11c3b2dc177891687014ffa78" + url: "https://pub.dev" + source: hosted + version: "0.1.0" image: dependency: "direct main" description: @@ -1338,6 +1370,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + jni: + dependency: transitive + description: + name: jni + sha256: f377c585ea9c08d48b427dc2e03780af2889d1bb094440da853c6883c1acba4b + url: "https://pub.dev" + source: hosted + version: "0.10.1" js: dependency: transitive description: @@ -1702,6 +1742,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + native_dio_adapter: + dependency: "direct main" + description: + name: native_dio_adapter + sha256: "7420bc9517b2abe09810199a19924617b45690a44ecfb0616ac9babc11875c03" + url: "https://pub.dev" + source: hosted + version: "1.4.0" native_video_player: dependency: "direct main" description: @@ -1735,6 +1783,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "62e79ab8c3ed6f6a340ea50dd48d65898f5d70425d404f0d99411f6e56e04584" + url: "https://pub.dev" + source: hosted + version: "4.1.0" octo_image: dependency: transitive description: From 4a9bc84375bdd5169738728f95f2cde99db13329 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Tue, 11 Feb 2025 16:08:01 +0530 Subject: [PATCH 041/796] [mob][photos] Surface only relevant trips --- mobile/lib/services/search_service.dart | 124 +++++++++++++++++++----- 1 file changed, 102 insertions(+), 22 deletions(-) diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index dcb272aaa1..8c5e6fa5f7 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -1199,6 +1199,9 @@ class SearchService { final Iterable> locationTagEntities = (await locationService.getLocationTags()); if (allFiles.isEmpty) return []; + final currentTime = DateTime.now().toLocal(); + final currentMonth = currentTime.month; + final cutOffTime = currentTime.subtract(const Duration(days: 365)); final Map, List> tagToItemsMap = {}; for (int i = 0; i < locationTagEntities.length; i++) { @@ -1211,6 +1214,7 @@ class SearchService { if (!file.hasLocation || file.uploadedFileID == null || !file.isOwner || + file.creationTime! > cutOffTime.microsecondsSinceEpoch || file.creationTime == null) { continue; } @@ -1437,10 +1441,9 @@ class SearchService { } } - // TODO: lau: Check if there are any trips to surface - // For now for testing let's just surface all base and trip locations + // For now for testing let's just surface all base locations for (final baseLocation in baseLocations.values) { - final files = baseLocation.$1; // TODO: lau: take best selection only + final files = baseLocation.$1; final current = baseLocation.$3; final name = "Base (${current ? 'current' : 'old'})"; searchResults.add( @@ -1458,26 +1461,103 @@ class SearchService { ), ); } - for (final finalTrip in mergedTrips.values) { - final files = finalTrip.$1; // TODO: lau: take best selection only - final year = DateTime.fromMicrosecondsSinceEpoch( - (finalTrip.$2 + finalTrip.$2) ~/ 2, - ).year; - final name = "Trip! ($year)"; - searchResults.add( - GenericSearchResult( - ResultType.event, - name, - files, - hierarchicalSearchFilter: TopLevelGenericFilter( - filterName: name, - occurrence: kMostRelevantFilter, - filterResultType: ResultType.event, - matchedUploadedIDs: filesToUploadedFileIDs(files), - filterIcon: Icons.event_outlined, + + // For now we surface the two most recent trips of current month, and if none, the earliest upcoming redundant trip + // Group the trips per month and then year + final Map, int, int)>>> + tripsByMonthYear = {}; + for (final trip in mergedTrips.values) { + final int avgMicros = (trip.$2 + trip.$3) ~/ 2; + final tripDate = DateTime.fromMicrosecondsSinceEpoch(avgMicros); + tripsByMonthYear + .putIfAbsent(tripDate.month, () => {}) + .putIfAbsent(tripDate.year, () => []) + .add(trip); + } + + // Flatten trips for the current month and annotate with their average date. + final List<(List, int, int, DateTime)> currentMonthTrips = []; + if (tripsByMonthYear.containsKey(currentMonth)) { + for (final trips in tripsByMonthYear[currentMonth]!.values) { + for (final trip in trips) { + final int avgMicros = (trip.$2 + trip.$3) ~/ 2; + final tripDate = DateTime.fromMicrosecondsSinceEpoch(avgMicros); + currentMonthTrips.add((trip.$1, trip.$2, trip.$3, tripDate)); + } + } + } + + // If there are past trips this month, show the one or two most recent ones. + if (currentMonthTrips.isNotEmpty) { + currentMonthTrips.sort((a, b) => b.$4.compareTo(a.$4)); + final tripsToShow = currentMonthTrips.take(2); + for (final trip in tripsToShow) { + final year = trip.$4.year; + String name = "Trip in $year!"; + if (year == currentTime.year - 1) { + name = "Last year's trip!"; + } + final photoSelection = await _bestSelection(trip.$1); + searchResults.add( + GenericSearchResult( + ResultType.event, + name, + photoSelection, + hierarchicalSearchFilter: TopLevelGenericFilter( + filterName: name, + occurrence: kMostRelevantFilter, + filterResultType: ResultType.event, + matchedUploadedIDs: filesToUploadedFileIDs(trip.$1), + filterIcon: Icons.event_outlined, + ), ), - ), - ); + ); + } + } + // Otherwise, if no trips happened in the current month, + // look for the earliest upcoming trip in another month that has 3+ trips. + else { + final sortedUpcomingMonths = + List.generate(12, (i) => ((currentMonth + i) % 12) + 1); + checkUpcomingMonths: + for (final month in sortedUpcomingMonths) { + if (tripsByMonthYear.containsKey(month)) { + final List<(List, int, int, DateTime)> thatMonthTrips = []; + for (final trips in tripsByMonthYear[month]!.values) { + for (final trip in trips) { + final int avgMicros = (trip.$2 + trip.$3) ~/ 2; + final tripDate = DateTime.fromMicrosecondsSinceEpoch(avgMicros); + thatMonthTrips.add((trip.$1, trip.$2, trip.$3, tripDate)); + } + } + if (thatMonthTrips.length >= 3) { + // take and use the third earliest trip + thatMonthTrips.sort((a, b) => a.$4.compareTo(b.$4)); + final trip = thatMonthTrips[2]; + final year = trip.$4.year; + String name = "Trip in $year!"; + if (year == currentTime.year - 1) { + name = "Last year's trip!"; + } + final photoSelection = await _bestSelection(trip.$1); + searchResults.add( + GenericSearchResult( + ResultType.event, + name, + photoSelection, + hierarchicalSearchFilter: TopLevelGenericFilter( + filterName: name, + occurrence: kMostRelevantFilter, + filterResultType: ResultType.event, + matchedUploadedIDs: filesToUploadedFileIDs(trip.$1), + filterIcon: Icons.event_outlined, + ), + ), + ); + break checkUpcomingMonths; + } + } + } } return searchResults; // TODO: lau: take [limit] into account } From 218c652ed116603032e56b1459d11fcdbed79123 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Tue, 11 Feb 2025 16:57:54 +0530 Subject: [PATCH 042/796] [server] Make new links joinable by default --- server/ente/public_collection.go | 2 +- server/pkg/repo/public_collection.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/ente/public_collection.go b/server/ente/public_collection.go index ef22302b89..eb1bd8c385 100644 --- a/server/ente/public_collection.go +++ b/server/ente/public_collection.go @@ -12,7 +12,7 @@ import ( type CreatePublicAccessTokenRequest struct { CollectionID int64 `json:"collectionID" binding:"required"` EnableCollect bool `json:"enableCollect"` - // defaults to false + // defaults to true EnableJoin *bool `json:"enableJoin"` ValidTill int64 `json:"validTill"` DeviceLimit int `json:"deviceLimit"` diff --git a/server/pkg/repo/public_collection.go b/server/pkg/repo/public_collection.go index 3b5aa6dbce..f5ae8f2d72 100644 --- a/server/pkg/repo/public_collection.go +++ b/server/pkg/repo/public_collection.go @@ -38,7 +38,7 @@ func (pcr *PublicCollectionRepository) GetAlbumUrl(token string) string { func (pcr *PublicCollectionRepository) Insert(ctx context.Context, cID int64, token string, validTill int64, deviceLimit int, enableCollect bool, enableJoin *bool) error { // default value for enableJoin is true - join := false + join := true if enableJoin != nil { join = *enableJoin } From dc3f074588eed1b9ce27437e6a04ce1f70031ced Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Tue, 11 Feb 2025 17:14:20 +0530 Subject: [PATCH 043/796] fix: don't index unowned files --- mobile/lib/db/files_db.dart | 2 ++ mobile/lib/services/preview_video_store.dart | 1 + 2 files changed, 3 insertions(+) diff --git a/mobile/lib/db/files_db.dart b/mobile/lib/db/files_db.dart index 4452f12097..c734044c4c 100644 --- a/mobile/lib/db/files_db.dart +++ b/mobile/lib/db/files_db.dart @@ -1733,6 +1733,7 @@ class FilesDB { Future> getAllFilesAfterDate({ required FileType fileType, required DateTime beginDate, + required int userID, }) async { final db = await instance.sqliteAsyncDB; final results = await db.getAll( @@ -1741,6 +1742,7 @@ class FilesDB { WHERE $columnFileType = ? AND $columnCreationTime > ? AND $columnUploadedFileID != -1 + AND $columnOwnerID = $userID ORDER BY $columnCreationTime DESC ''', [getInt(fileType), beginDate.microsecondsSinceEpoch], diff --git a/mobile/lib/services/preview_video_store.dart b/mobile/lib/services/preview_video_store.dart index 37baf778d6..b14fc8c707 100644 --- a/mobile/lib/services/preview_video_store.dart +++ b/mobile/lib/services/preview_video_store.dart @@ -601,6 +601,7 @@ class PreviewVideoStore { final files = await FilesDB.instance.getAllFilesAfterDate( fileType: FileType.video, beginDate: cutoff, + userID: Configuration.instance.getUserID()!, ); final previewIds = FileDataService.instance.previewIds; From bf4807da5bfdfd6fe58262915584642ebd6fa3e9 Mon Sep 17 00:00:00 2001 From: mngshm Date: Tue, 11 Feb 2025 17:20:04 +0530 Subject: [PATCH 044/796] [server] use custom request struct for modifying functionality --- server/ente/family.go | 5 +++++ server/pkg/api/family.go | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/server/ente/family.go b/server/ente/family.go index 004067007c..2911ca0b72 100644 --- a/server/ente/family.go +++ b/server/ente/family.go @@ -49,6 +49,11 @@ type FamilyMember struct { AdminUserID int64 `json:"-"` // for internal use only, ignore from json response } +type ModifyMemberStorage struct { + ID uuid.UUID `json:"id" binding:"required"` + StorageLimit *int64 `json:"storageLimit" binding:"required"` +} + type FamilyMemberResponse struct { Members []FamilyMember `json:"members" binding:"required"` // Family admin subscription storage capacity. This excludes add-on and any other bonus storage diff --git a/server/pkg/api/family.go b/server/pkg/api/family.go index 82e352b80a..0e27f0d999 100644 --- a/server/pkg/api/family.go +++ b/server/pkg/api/family.go @@ -122,7 +122,7 @@ func (h *FamilyHandler) AcceptInvite(c *gin.Context) { // ModifyStorageLimit allows adminUser to Modify the storage for a member in the Family. func (h *FamilyHandler) ModifyStorageLimit(c *gin.Context) { - var request ente.FamilyMember + var request ente.ModifyMemberStorage if err := c.ShouldBindJSON(&request); err != nil { handler.Error(c, stacktrace.Propagate(err, "Could not bind request params")) return From cea9fa84a11be90eec9fe043bded6c450a83dff1 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Tue, 11 Feb 2025 17:26:06 +0530 Subject: [PATCH 045/796] [mob][photos] Limit --- mobile/lib/services/search_service.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index 8c5e6fa5f7..0f2427fd6e 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -1512,6 +1512,7 @@ class SearchService { ), ), ); + if (limit != null && searchResults.length >= limit) return searchResults; } } // Otherwise, if no trips happened in the current month, @@ -1559,7 +1560,7 @@ class SearchService { } } } - return searchResults; // TODO: lau: take [limit] into account + return searchResults; } Future> onThisDayOrWeekResults( From 38a35696a34750238a3134999cf63d136c84bb0e Mon Sep 17 00:00:00 2001 From: mngshm Date: Tue, 11 Feb 2025 17:28:30 +0530 Subject: [PATCH 046/796] fix column names in DB & include UsageCtrl in controllers --- server/pkg/controller/family/family.go | 2 ++ server/pkg/repo/family.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/server/pkg/controller/family/family.go b/server/pkg/controller/family/family.go index 9cf2a768f0..c519e50692 100644 --- a/server/pkg/controller/family/family.go +++ b/server/pkg/controller/family/family.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "github.com/ente-io/museum/pkg/controller/usercache" "github.com/ente-io/museum/pkg/utils/time" @@ -26,6 +27,7 @@ type Controller struct { UserRepo *repo.UserRepository FamilyRepo *repo.FamilyRepository UserCacheCtrl *usercache.Controller + UsageRepo *repo.UsageRepository } // FetchMembers return list of members who are part of a family plan diff --git a/server/pkg/repo/family.go b/server/pkg/repo/family.go index ee0fe20b16..c9e2d96673 100644 --- a/server/pkg/repo/family.go +++ b/server/pkg/repo/family.go @@ -198,7 +198,7 @@ func (repo *FamilyRepository) RemoveMember(ctx context.Context, adminID int64, m // UpdateStorage is used to set Pre-existing Members Storage Limit. func (repo *FamilyRepository) ModifyMemberStorage(ctx context.Context, adminID int64, id uuid.UUID, storageLimit *int64) error { - _, err := repo.DB.Exec(`UPDATE families SET storage_limi=$1 where id=$2`, *storageLimit, id) + _, err := repo.DB.Exec(`UPDATE families SET storage_limit=$1 where id=$2`, *storageLimit, id) if err != nil { return stacktrace.Propagate(err, "Could not update Members Storage Limit") } From 782688c1f74cd6891de905de8cd592e49925deea Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 7 Feb 2025 08:01:13 +0530 Subject: [PATCH 047/796] Scaffold --- web/apps/photos/src/pages/g2.tsx | 1 + web/packages/new/photos/pages/g2.tsx | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 web/apps/photos/src/pages/g2.tsx create mode 100644 web/packages/new/photos/pages/g2.tsx diff --git a/web/apps/photos/src/pages/g2.tsx b/web/apps/photos/src/pages/g2.tsx new file mode 100644 index 0000000000..4a7e2042f1 --- /dev/null +++ b/web/apps/photos/src/pages/g2.tsx @@ -0,0 +1 @@ +export { default } from "@/new/photos/pages/g2"; diff --git a/web/packages/new/photos/pages/g2.tsx b/web/packages/new/photos/pages/g2.tsx new file mode 100644 index 0000000000..f19704a60c --- /dev/null +++ b/web/packages/new/photos/pages/g2.tsx @@ -0,0 +1,7 @@ +// TODO(PS): WIP gallery using upstream photoswipe +// +// Needs yarn workspace gallery add photoswipe@^5.4.4 (not committed yet). + +export const Page: React.FC = () => { + return
Hello
; +}; From b87b68e9d4793a74c05198f53d880432253ab508 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 7 Feb 2025 09:25:27 +0530 Subject: [PATCH 048/796] Scaffold differently --- web/apps/photos/src/components/PhotoFrame.tsx | 2 ++ web/apps/photos/src/pages/g2.tsx | 1 - web/packages/new/photos/components/FileViewer5.tsx | 13 +++++++++++++ web/packages/new/photos/components/PhotoViewer.tsx | 11 +++++++++++ web/packages/new/photos/pages/g2.tsx | 7 ------- 5 files changed, 26 insertions(+), 8 deletions(-) delete mode 100644 web/apps/photos/src/pages/g2.tsx create mode 100644 web/packages/new/photos/components/FileViewer5.tsx delete mode 100644 web/packages/new/photos/pages/g2.tsx diff --git a/web/apps/photos/src/components/PhotoFrame.tsx b/web/apps/photos/src/components/PhotoFrame.tsx index f197512807..af3b5aa8e7 100644 --- a/web/apps/photos/src/components/PhotoFrame.tsx +++ b/web/apps/photos/src/components/PhotoFrame.tsx @@ -7,6 +7,7 @@ import { } from "@/gallery/services/download"; import { EnteFile } from "@/media/file"; import { FileType } from "@/media/file-type"; +import { FileViewer } from "@/new/photos/components/FileViewer"; import type { GalleryBarMode } from "@/new/photos/components/gallery/reducer"; import { TRASH_SECTION } from "@/new/photos/services/collection"; import { styled } from "@mui/material"; @@ -517,6 +518,7 @@ const PhotoFrame = ({ /> )} + {process.env.NEXT_ENTE_WIP_PS5 && } { + return ( +
+ Hello +
+ ); +}; diff --git a/web/packages/new/photos/components/PhotoViewer.tsx b/web/packages/new/photos/components/PhotoViewer.tsx index 6734d9d900..82dd388e7b 100644 --- a/web/packages/new/photos/components/PhotoViewer.tsx +++ b/web/packages/new/photos/components/PhotoViewer.tsx @@ -9,6 +9,17 @@ import { t } from "i18next"; import { useState } from "react"; import { aboveGalleryContentZ } from "./utils/z-index"; +// TODO(PS) +import dynamic from "next/dynamic"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +const FV5 = dynamic(() => import("./FileViewer5")); +const FVD = () => <>; + +export const FileViewer: React.FC = (props) => { + return process.env.NEXT_ENTE_WIP_PS5 ? : ; +}; + type ConfirmDeleteFileDialogProps = ModalVisibilityProps & { /** * Called when the user confirms the deletion. diff --git a/web/packages/new/photos/pages/g2.tsx b/web/packages/new/photos/pages/g2.tsx deleted file mode 100644 index f19704a60c..0000000000 --- a/web/packages/new/photos/pages/g2.tsx +++ /dev/null @@ -1,7 +0,0 @@ -// TODO(PS): WIP gallery using upstream photoswipe -// -// Needs yarn workspace gallery add photoswipe@^5.4.4 (not committed yet). - -export const Page: React.FC = () => { - return
Hello
; -}; From d322f5e1bcd2c778d7a7fe28055408ddff303b89 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 7 Feb 2025 09:31:30 +0530 Subject: [PATCH 049/796] Take 2 --- web/apps/photos/src/components/PhotoFrame.tsx | 2 +- web/packages/new/photos/components/FileViewer5.tsx | 10 +++++++++- web/packages/new/photos/components/PhotoViewer.tsx | 4 ++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/web/apps/photos/src/components/PhotoFrame.tsx b/web/apps/photos/src/components/PhotoFrame.tsx index af3b5aa8e7..49985f404a 100644 --- a/web/apps/photos/src/components/PhotoFrame.tsx +++ b/web/apps/photos/src/components/PhotoFrame.tsx @@ -518,7 +518,7 @@ const PhotoFrame = ({ /> )} - {process.env.NEXT_ENTE_WIP_PS5 && } + {process.env.NEXT_PUBLIC_ENTE_WIP_PS5 && } { +const FileViewer: React.FC = () => { return (
Hello
); }; + +export default FileViewer; diff --git a/web/packages/new/photos/components/PhotoViewer.tsx b/web/packages/new/photos/components/PhotoViewer.tsx index 82dd388e7b..1f6806696e 100644 --- a/web/packages/new/photos/components/PhotoViewer.tsx +++ b/web/packages/new/photos/components/PhotoViewer.tsx @@ -13,11 +13,11 @@ import { aboveGalleryContentZ } from "./utils/z-index"; import dynamic from "next/dynamic"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore -const FV5 = dynamic(() => import("./FileViewer5")); +const FV5 = dynamic(() => import("./FileViewer5"), { ssr: false }); const FVD = () => <>; export const FileViewer: React.FC = (props) => { - return process.env.NEXT_ENTE_WIP_PS5 ? : ; + return process.env.NEXT_PUBLIC_ENTE_WIP_PS5 ? : ; }; type ConfirmDeleteFileDialogProps = ModalVisibilityProps & { From 4881f0879036d93ac6f27582bf7726b00c0ab86e Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 7 Feb 2025 09:49:48 +0530 Subject: [PATCH 050/796] Try import --- web/packages/new/photos/components/.gitignore | 2 ++ web/packages/new/photos/components/FileViewer5.tsx | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 web/packages/new/photos/components/.gitignore diff --git a/web/packages/new/photos/components/.gitignore b/web/packages/new/photos/components/.gitignore new file mode 100644 index 0000000000..cbf1262ae4 --- /dev/null +++ b/web/packages/new/photos/components/.gitignore @@ -0,0 +1,2 @@ +# TODO(PS): Remove me once PS5 is integrated +ps5 diff --git a/web/packages/new/photos/components/FileViewer5.tsx b/web/packages/new/photos/components/FileViewer5.tsx index f2cca4a5f7..ad2a3c4ef1 100644 --- a/web/packages/new/photos/components/FileViewer5.tsx +++ b/web/packages/new/photos/components/FileViewer5.tsx @@ -1,6 +1,8 @@ // TODO(PS): WIP gallery using upstream photoswipe // -// Needs yarn workspace gallery add photoswipe@^5.4.4 (not committed yet). +// Needs (not committed yet): +// yarn workspace gallery add photoswipe@^5.4.4 +// mv node_modules/photoswipe packages/new/photos/components/ps5 if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) { console.warn("Using WIP upstream photoswipe"); @@ -9,8 +11,12 @@ if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) { } import { Button } from "@mui/material"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import PhotoSwipeLightBox from "./ps5/dist/photoswipe-lightbox.esm.js"; const FileViewer: React.FC = () => { + console.log(PhotoSwipeLightBox); return (
Hello From 97bdc9362a475e5819658f20b54c50123ca6059c Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 10 Feb 2025 14:57:47 +0530 Subject: [PATCH 051/796] Tinker --- web/apps/photos/src/components/PhotoFrame.tsx | 2 +- .../new/photos/components/FileViewer5.tsx | 49 ++++++++++++++++--- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/web/apps/photos/src/components/PhotoFrame.tsx b/web/apps/photos/src/components/PhotoFrame.tsx index 49985f404a..1cd1bf5661 100644 --- a/web/apps/photos/src/components/PhotoFrame.tsx +++ b/web/apps/photos/src/components/PhotoFrame.tsx @@ -503,6 +503,7 @@ const PhotoFrame = ({ return ( + {process.env.NEXT_PUBLIC_ENTE_WIP_PS5 && } {({ height, width }) => ( )} - {process.env.NEXT_PUBLIC_ENTE_WIP_PS5 && } { console.log(PhotoSwipeLightBox); + useEffect(() => { + const lightbox = new PhotoSwipeLightBox({ + gallery: "#test-gallery", + mainClass: "our-extra-pswp-main-class", + pswpModule: PhotoSwipe, + }); + lightbox.init(); + + return () => { + lightbox.destroy(); + }; + }, []); return ( -
- Hello -
+ + + + ); }; +const Container = styled("div")` + border: 1px solid red; + + #test-gallery { + border: 1px solid red; + min-height: 10px; + } +`; + export default FileViewer; From 970da9f29c8bb6122da668ab233744f26c84baee Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 10 Feb 2025 15:41:01 +0530 Subject: [PATCH 052/796] Direct --- web/packages/new/photos/components/FileViewer5.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/web/packages/new/photos/components/FileViewer5.tsx b/web/packages/new/photos/components/FileViewer5.tsx index 81c0c87c5d..5d0c64b7bb 100644 --- a/web/packages/new/photos/components/FileViewer5.tsx +++ b/web/packages/new/photos/components/FileViewer5.tsx @@ -19,17 +19,18 @@ import PhotoSwipeLightBox from "./ps5/dist/photoswipe-lightbox.esm.js"; import PhotoSwipe from "./ps5/dist/photoswipe.esm.js"; const FileViewer: React.FC = () => { + const pswpRef = useRef(); console.log(PhotoSwipeLightBox); useEffect(() => { - const lightbox = new PhotoSwipeLightBox({ - gallery: "#test-gallery", - mainClass: "our-extra-pswp-main-class", - pswpModule: PhotoSwipe, + const pswp = new PhotoSwipe({ + // mainClass: "our-extra-pswp-main-class", }); - lightbox.init(); + pswp.init(); + pswpRef.current = pswp; return () => { - lightbox.destroy(); + pswp.destroy(); + pswpRef.current = undefined; }; }, []); return ( From 3bbfa71824c9423d9fcb3b3653f48d749d5691de Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 10 Feb 2025 15:56:11 +0530 Subject: [PATCH 053/796] Doc --- .../new/photos/components/FileViewer5.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/web/packages/new/photos/components/FileViewer5.tsx b/web/packages/new/photos/components/FileViewer5.tsx index 5d0c64b7bb..e9cd961572 100644 --- a/web/packages/new/photos/components/FileViewer5.tsx +++ b/web/packages/new/photos/components/FileViewer5.tsx @@ -18,6 +18,26 @@ import { useEffect } from "react"; import PhotoSwipeLightBox from "./ps5/dist/photoswipe-lightbox.esm.js"; import PhotoSwipe from "./ps5/dist/photoswipe.esm.js"; +/** + * The {@link FileViewer} is our PhotoSwipe based image and video viewer. + * + * --- + * + * [Note: PhotoSwipe] + * + * PhotoSwipe is a library that behaves similarly to the OG "lightbox" image + * gallery JavaScript component from the middle ages. + * + * We don't need the lightbox functionality since we already have our own + * thumbnail list (the "gallery"), so we only use the "Core" PhotoSwipe module + * as our image viewer component. + * + * When the user clicks on one of the thumbnails in our gallery, we make the + * root PhotoSwipe component visible. Within the DOM this is a dialog that takes + * up the entire viewport, and shows the image etc, and various controls. + * + * The documentation for PhotoSwipe is at https://photoswipe.com/. + */ const FileViewer: React.FC = () => { const pswpRef = useRef(); console.log(PhotoSwipeLightBox); From 44c64c06a78c9dcca3021135dd5803134b6e400e Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 10 Feb 2025 16:17:56 +0530 Subject: [PATCH 054/796] idata --- .../new/photos/components/FileViewer5.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/web/packages/new/photos/components/FileViewer5.tsx b/web/packages/new/photos/components/FileViewer5.tsx index e9cd961572..7327eb9884 100644 --- a/web/packages/new/photos/components/FileViewer5.tsx +++ b/web/packages/new/photos/components/FileViewer5.tsx @@ -14,7 +14,7 @@ if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) { } import { Button, styled } from "@mui/material"; -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; import PhotoSwipeLightBox from "./ps5/dist/photoswipe-lightbox.esm.js"; import PhotoSwipe from "./ps5/dist/photoswipe.esm.js"; @@ -45,6 +45,19 @@ const FileViewer: React.FC = () => { const pswp = new PhotoSwipe({ // mainClass: "our-extra-pswp-main-class", }); + // Provide data about slides to PhotoSwipe via callbacks + // https://photoswipe.com/data-sources/#dynamically-generated-data + pswp.addFilter("numItems", () => { + return 2; + }); + pswp.addFilter("itemData", (itemData, index) => { + console.log({ itemData, index }); + return { + src: `https://dummyimage.com/100/777/fff/?text=i${index}`, + width: 100, + height: 100, + }; + }); pswp.init(); pswpRef.current = pswp; From 5c16ce345914cb016168d76047525e7d41ed69da Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 10 Feb 2025 16:43:39 +0530 Subject: [PATCH 055/796] Prop --- web/apps/photos/src/components/PhotoFrame.tsx | 7 +++- .../new/photos/components/FileViewer5.tsx | 38 +++++++++++++++++-- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/web/apps/photos/src/components/PhotoFrame.tsx b/web/apps/photos/src/components/PhotoFrame.tsx index 1cd1bf5661..8052fcb36c 100644 --- a/web/apps/photos/src/components/PhotoFrame.tsx +++ b/web/apps/photos/src/components/PhotoFrame.tsx @@ -123,7 +123,7 @@ export interface PhotoFrameProps { } /** - * TODO: Rename me to FileListWithViewer + * TODO: Rename me to FileListWithViewer (or Gallery?) */ const PhotoFrame = ({ mode, @@ -503,7 +503,10 @@ const PhotoFrame = ({ return ( - {process.env.NEXT_PUBLIC_ENTE_WIP_PS5 && } + {process.env.NEXT_PUBLIC_ENTE_WIP_PS5 && ( + /* @ts-expect-error TODO(PS): test */ + + )} {({ height, width }) => ( { +const FileViewer: React.FC = ({ files, index }) => { const pswpRef = useRef(); - console.log(PhotoSwipeLightBox); + useEffect(() => { const pswp = new PhotoSwipe({ - // mainClass: "our-extra-pswp-main-class", + // Opaque background. + bgOpacity: 1, + // TODO(PS): padding option? for handling custom title bar. + // TODO(PS): will we need this? + mainClass: "our-extra-pswp-main-class", }); // Provide data about slides to PhotoSwipe via callbacks // https://photoswipe.com/data-sources/#dynamically-generated-data @@ -58,6 +86,7 @@ const FileViewer: React.FC = () => { height: 100, }; }); + // initializing PhotoSwipe Core adds it to the DOM. pswp.init(); pswpRef.current = pswp; @@ -66,6 +95,7 @@ const FileViewer: React.FC = () => { pswpRef.current = undefined; }; }, []); + return ( From a57232c34b1e6b335bdfd8ea9a6541bd91b536f6 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 11 Feb 2025 08:03:45 +0530 Subject: [PATCH 056/796] Link --- web/apps/photos/src/components/PhotoFrame.tsx | 20 +++++++++++++---- .../new/photos/components/FileViewer5.tsx | 22 +++++++++++++++---- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/web/apps/photos/src/components/PhotoFrame.tsx b/web/apps/photos/src/components/PhotoFrame.tsx index 8052fcb36c..62a70d5a22 100644 --- a/web/apps/photos/src/components/PhotoFrame.tsx +++ b/web/apps/photos/src/components/PhotoFrame.tsx @@ -1,3 +1,4 @@ +import { useModalVisibility } from "@/base/components/utils/modal"; import log from "@/base/log"; import { downloadManager, @@ -159,6 +160,9 @@ const PhotoFrame = ({ const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false); const router = useRouter(); + const { show: showPhotoSwipe, props: photoSwipeVisibilityProps } = + useModalVisibility(); + const [displayFiles, setDisplayFiles] = useState( undefined, ); @@ -262,8 +266,12 @@ const PhotoFrame = ({ const onThumbnailClick = (index: number) => () => { setCurrentIndex(index); - setOpen(true); - setIsPhotoSwipeOpen?.(true); + if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) { + showPhotoSwipe(); + } else { + setOpen(true); + setIsPhotoSwipeOpen?.(true); + } }; const handleSelect = handleSelectCreator( @@ -504,8 +512,12 @@ const PhotoFrame = ({ return ( {process.env.NEXT_PUBLIC_ENTE_WIP_PS5 && ( - /* @ts-expect-error TODO(PS): test */ - + )} {({ height, width }) => ( diff --git a/web/packages/new/photos/components/FileViewer5.tsx b/web/packages/new/photos/components/FileViewer5.tsx index db5a68004a..84b51da3cc 100644 --- a/web/packages/new/photos/components/FileViewer5.tsx +++ b/web/packages/new/photos/components/FileViewer5.tsx @@ -62,7 +62,7 @@ interface FileViewerProps { * * The documentation for PhotoSwipe is at https://photoswipe.com/. */ -const FileViewer: React.FC = ({ files, index }) => { +const FileViewer: React.FC = ({ open, files, index }) => { const pswpRef = useRef(); useEffect(() => { @@ -86,16 +86,30 @@ const FileViewer: React.FC = ({ files, index }) => { height: 100, }; }); - // initializing PhotoSwipe Core adds it to the DOM. - pswp.init(); pswpRef.current = pswp; return () => { - pswp.destroy(); + if (pswpRef.current.isOpen) pswpRef.current.destroy(); pswpRef.current = undefined; }; }, []); + useEffect(() => { + const pswp = pswpRef.current!; + if (open) { + if (!pswp.isOpen) { + // Initializing PhotoSwipe adds it to the DOM as a "dialog" div + // with the class "pswp" (and others). + pswp.init(); + } + } else { + if (pswp.isOpen) { + // Closing PhotoSwipe removes it from the DOM. + pswp.close(); + } + } + }, [open]); + return ( From f3d9595953d24c7bcb3e19f3d9a4f7f1afe98715 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 11 Feb 2025 08:29:27 +0530 Subject: [PATCH 057/796] diversion: pswp doesn't reset isOpen --- web/apps/photos/src/components/PhotoFrame.tsx | 22 ++++++++++++------- .../new/photos/components/FileViewer5.tsx | 18 ++++++++++++++- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/web/apps/photos/src/components/PhotoFrame.tsx b/web/apps/photos/src/components/PhotoFrame.tsx index 62a70d5a22..61300576c3 100644 --- a/web/apps/photos/src/components/PhotoFrame.tsx +++ b/web/apps/photos/src/components/PhotoFrame.tsx @@ -1,4 +1,3 @@ -import { useModalVisibility } from "@/base/components/utils/modal"; import log from "@/base/log"; import { downloadManager, @@ -160,8 +159,9 @@ const PhotoFrame = ({ const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false); const router = useRouter(); - const { show: showPhotoSwipe, props: photoSwipeVisibilityProps } = - useModalVisibility(); + // const { show: showPhotoSwipe, props: photoSwipeVisibilityProps } = + // useModalVisibility(); + const [open5, setOpen5] = useState(false); const [displayFiles, setDisplayFiles] = useState( undefined, @@ -259,15 +259,20 @@ const PhotoFrame = ({ }; const handleClose = (needUpdate) => { - setOpen(false); - needUpdate && syncWithRemote(); - setIsPhotoSwipeOpen?.(false); + if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) { + setOpen5(false); + } else { + setOpen(false); + needUpdate && syncWithRemote(); + setIsPhotoSwipeOpen?.(false); + } }; const onThumbnailClick = (index: number) => () => { setCurrentIndex(index); if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) { - showPhotoSwipe(); + // showPhotoSwipe(); + setOpen5(true); } else { setOpen(true); setIsPhotoSwipeOpen?.(true); @@ -513,8 +518,9 @@ const PhotoFrame = ({ {process.env.NEXT_PUBLIC_ENTE_WIP_PS5 && ( diff --git a/web/packages/new/photos/components/FileViewer5.tsx b/web/packages/new/photos/components/FileViewer5.tsx index 84b51da3cc..c121cee102 100644 --- a/web/packages/new/photos/components/FileViewer5.tsx +++ b/web/packages/new/photos/components/FileViewer5.tsx @@ -62,7 +62,12 @@ interface FileViewerProps { * * The documentation for PhotoSwipe is at https://photoswipe.com/. */ -const FileViewer: React.FC = ({ open, files, index }) => { +const FileViewer: React.FC = ({ + open, + onClose, + files, + index, +}) => { const pswpRef = useRef(); useEffect(() => { @@ -86,6 +91,10 @@ const FileViewer: React.FC = ({ open, files, index }) => { height: 100, }; }); + pswp.on("close", () => { + console.log("pswp.on(close)") + onClose(); + }); pswpRef.current = pswp; return () => { @@ -94,6 +103,8 @@ const FileViewer: React.FC = ({ open, files, index }) => { }; }, []); + console.log({ open, pswpIsOpen: pswpRef.current?.isOpen }); + useEffect(() => { const pswp = pswpRef.current!; if (open) { @@ -105,11 +116,16 @@ const FileViewer: React.FC = ({ open, files, index }) => { } else { if (pswp.isOpen) { // Closing PhotoSwipe removes it from the DOM. + // + // We get to this particular point if we're being closed + // externally. pswp.close(); } } }, [open]); + const handleClose = () => {}; + return ( From e952aa80a5b63a55130945bec54e78d552ecbf36 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 11 Feb 2025 08:35:56 +0530 Subject: [PATCH 058/796] Don't reuse (see prev diversion) --- .../new/photos/components/FileViewer5.tsx | 35 ++++++------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/web/packages/new/photos/components/FileViewer5.tsx b/web/packages/new/photos/components/FileViewer5.tsx index c121cee102..6a9962e4c9 100644 --- a/web/packages/new/photos/components/FileViewer5.tsx +++ b/web/packages/new/photos/components/FileViewer5.tsx @@ -71,6 +71,8 @@ const FileViewer: React.FC = ({ const pswpRef = useRef(); useEffect(() => { + if (!open) return; + const pswp = new PhotoSwipe({ // Opaque background. bgOpacity: 1, @@ -92,39 +94,24 @@ const FileViewer: React.FC = ({ }; }); pswp.on("close", () => { - console.log("pswp.on(close)") + console.log("pswp.on(close)"); onClose(); }); pswpRef.current = pswp; + // Initializing PhotoSwipe adds it to the DOM as a "dialog" div + // with the class "pswp" (and others). + pswp.init(); return () => { - if (pswpRef.current.isOpen) pswpRef.current.destroy(); + // Closing PhotoSwipe removes it from the DOM. + // + // This will only have an effect if we're being closed externally. + pswpRef.current?.close(); pswpRef.current = undefined; }; - }, []); - - console.log({ open, pswpIsOpen: pswpRef.current?.isOpen }); - - useEffect(() => { - const pswp = pswpRef.current!; - if (open) { - if (!pswp.isOpen) { - // Initializing PhotoSwipe adds it to the DOM as a "dialog" div - // with the class "pswp" (and others). - pswp.init(); - } - } else { - if (pswp.isOpen) { - // Closing PhotoSwipe removes it from the DOM. - // - // We get to this particular point if we're being closed - // externally. - pswp.close(); - } - } }, [open]); - const handleClose = () => {}; + console.log({ open, pswpIsOpen: pswpRef.current?.isOpen }); return ( From f30e05389b22ec4eff270566e3c13edad0ce6895 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 11 Feb 2025 09:16:24 +0530 Subject: [PATCH 059/796] Validate --- web/apps/photos/src/components/PhotoFrame.tsx | 16 ++++++++++ .../new/photos/components/FileViewer5.tsx | 31 ++++++++++++++++--- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/web/apps/photos/src/components/PhotoFrame.tsx b/web/apps/photos/src/components/PhotoFrame.tsx index 61300576c3..efbef014d4 100644 --- a/web/apps/photos/src/components/PhotoFrame.tsx +++ b/web/apps/photos/src/components/PhotoFrame.tsx @@ -235,6 +235,22 @@ const PhotoFrame = ({ } }, [selected]); + console.log({ t: "PhotoFrame", open: open }); + + useEffect(() => { + if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) { + console.log({ t: "PhotoFrame UE", open: open5 }); + + setTimeout(() => { + setOpen5(false); + }, 5000); + } + return () => { + if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) + console.log({ t: "PhotoFrame UE cleanup", open: open5 }); + }; + }, [open5]); + if (!displayFiles) { return
; } diff --git a/web/packages/new/photos/components/FileViewer5.tsx b/web/packages/new/photos/components/FileViewer5.tsx index 6a9962e4c9..223c5108c8 100644 --- a/web/packages/new/photos/components/FileViewer5.tsx +++ b/web/packages/new/photos/components/FileViewer5.tsx @@ -94,19 +94,42 @@ const FileViewer: React.FC = ({ }; }); pswp.on("close", () => { - console.log("pswp.on(close)"); + // The user did some action within the image viewer to close it. Let + // our parent component know that the image viewer is closing. onClose(); }); pswpRef.current = pswp; - // Initializing PhotoSwipe adds it to the DOM as a "dialog" div - // with the class "pswp" (and others). + // Initializing PhotoSwipe adds it to the DOM as a dialog-like div with + // the class "pswp". pswp.init(); return () => { // Closing PhotoSwipe removes it from the DOM. // - // This will only have an effect if we're being closed externally. + // This will only have an effect if we're being closed externally + // (e.g. if the user selects an album in the file info). If this + // cleanup function is running because we were closed internally, + // then the PhotoSwipe code will no-op this extra close. pswpRef.current?.close(); + console.log({ + "pswpRef.current": pswpRef.current, + "pswpRef.current.isOpen": pswpRef.current.isOpen, + "pswpRef.current.opener": pswpRef.current.opener, + "pswpRef.current.opener.isOpen": pswpRef.current.opener.isOpen, + "pswpRef.current.opener.isClosed": + pswpRef.current.opener.isClosed, + }); + setTimeout(() => { + console.log({ + "pswpRef.current": pswpRef.current, + "pswpRef.current.isOpen": pswpRef.current.isOpen, + "pswpRef.current.opener": pswpRef.current.opener, + "pswpRef.current.opener.isOpen": + pswpRef.current.opener.isOpen, + "pswpRef.current.opener.isClosed": + pswpRef.current.opener.isClosed, + }); + }, 1000); pswpRef.current = undefined; }; }, [open]); From 825a9df9faf52bea1c1002cf7dfda4ca85030f5b Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 11 Feb 2025 09:18:28 +0530 Subject: [PATCH 060/796] Cleanup up scaffold --- web/apps/photos/src/components/PhotoFrame.tsx | 16 ------------- .../new/photos/components/FileViewer5.tsx | 23 +------------------ 2 files changed, 1 insertion(+), 38 deletions(-) diff --git a/web/apps/photos/src/components/PhotoFrame.tsx b/web/apps/photos/src/components/PhotoFrame.tsx index efbef014d4..61300576c3 100644 --- a/web/apps/photos/src/components/PhotoFrame.tsx +++ b/web/apps/photos/src/components/PhotoFrame.tsx @@ -235,22 +235,6 @@ const PhotoFrame = ({ } }, [selected]); - console.log({ t: "PhotoFrame", open: open }); - - useEffect(() => { - if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) { - console.log({ t: "PhotoFrame UE", open: open5 }); - - setTimeout(() => { - setOpen5(false); - }, 5000); - } - return () => { - if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) - console.log({ t: "PhotoFrame UE cleanup", open: open5 }); - }; - }, [open5]); - if (!displayFiles) { return
; } diff --git a/web/packages/new/photos/components/FileViewer5.tsx b/web/packages/new/photos/components/FileViewer5.tsx index 223c5108c8..8b7444931d 100644 --- a/web/packages/new/photos/components/FileViewer5.tsx +++ b/web/packages/new/photos/components/FileViewer5.tsx @@ -110,32 +110,11 @@ const FileViewer: React.FC = ({ // (e.g. if the user selects an album in the file info). If this // cleanup function is running because we were closed internally, // then the PhotoSwipe code will no-op this extra close. - pswpRef.current?.close(); - console.log({ - "pswpRef.current": pswpRef.current, - "pswpRef.current.isOpen": pswpRef.current.isOpen, - "pswpRef.current.opener": pswpRef.current.opener, - "pswpRef.current.opener.isOpen": pswpRef.current.opener.isOpen, - "pswpRef.current.opener.isClosed": - pswpRef.current.opener.isClosed, - }); - setTimeout(() => { - console.log({ - "pswpRef.current": pswpRef.current, - "pswpRef.current.isOpen": pswpRef.current.isOpen, - "pswpRef.current.opener": pswpRef.current.opener, - "pswpRef.current.opener.isOpen": - pswpRef.current.opener.isOpen, - "pswpRef.current.opener.isClosed": - pswpRef.current.opener.isClosed, - }); - }, 1000); + pswpRef.current.close(); pswpRef.current = undefined; }; }, [open]); - console.log({ open, pswpIsOpen: pswpRef.current?.isOpen }); - return ( From 2e52efb15fe95e3d55d3bf07f340fd6a59fd0c7b Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 11 Feb 2025 10:13:11 +0530 Subject: [PATCH 061/796] Class --- .../new/photos/components/FileViewer5.tsx | 48 ++------ .../photos/components/FileViewerPhotoSwipe.ts | 111 ++++++++++++++++++ 2 files changed, 122 insertions(+), 37 deletions(-) create mode 100644 web/packages/new/photos/components/FileViewerPhotoSwipe.ts diff --git a/web/packages/new/photos/components/FileViewer5.tsx b/web/packages/new/photos/components/FileViewer5.tsx index 8b7444931d..0ea5332034 100644 --- a/web/packages/new/photos/components/FileViewer5.tsx +++ b/web/packages/new/photos/components/FileViewer5.tsx @@ -16,7 +16,7 @@ if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) { import type { EnteFile } from "@/media/file.js"; import { Button, styled } from "@mui/material"; import { useEffect, useRef } from "react"; -import PhotoSwipe from "./ps5/dist/photoswipe.esm.js"; +import { FileViewerPhotoSwipe } from "./FileViewerPhotoSwipe"; interface FileViewerProps { /** @@ -68,49 +68,23 @@ const FileViewer: React.FC = ({ files, index, }) => { - const pswpRef = useRef(); + const pswpRef = useRef(); useEffect(() => { - if (!open) return; + if (!open) { + // The close state will be handled by the cleanup function. + return; + } - const pswp = new PhotoSwipe({ - // Opaque background. - bgOpacity: 1, - // TODO(PS): padding option? for handling custom title bar. - // TODO(PS): will we need this? - mainClass: "our-extra-pswp-main-class", - }); - // Provide data about slides to PhotoSwipe via callbacks - // https://photoswipe.com/data-sources/#dynamically-generated-data - pswp.addFilter("numItems", () => { - return 2; - }); - pswp.addFilter("itemData", (itemData, index) => { - console.log({ itemData, index }); - return { - src: `https://dummyimage.com/100/777/fff/?text=i${index}`, - width: 100, - height: 100, - }; - }); - pswp.on("close", () => { - // The user did some action within the image viewer to close it. Let - // our parent component know that the image viewer is closing. - onClose(); + const pswp = new FileViewerPhotoSwipe({ + files, + initialIndex: index, + onClose, }); pswpRef.current = pswp; - // Initializing PhotoSwipe adds it to the DOM as a dialog-like div with - // the class "pswp". - pswp.init(); return () => { - // Closing PhotoSwipe removes it from the DOM. - // - // This will only have an effect if we're being closed externally - // (e.g. if the user selects an album in the file info). If this - // cleanup function is running because we were closed internally, - // then the PhotoSwipe code will no-op this extra close. - pswpRef.current.close(); + pswpRef.current?.closeIfNeeded(); pswpRef.current = undefined; }; }, [open]); diff --git a/web/packages/new/photos/components/FileViewerPhotoSwipe.ts b/web/packages/new/photos/components/FileViewerPhotoSwipe.ts new file mode 100644 index 0000000000..7183333079 --- /dev/null +++ b/web/packages/new/photos/components/FileViewerPhotoSwipe.ts @@ -0,0 +1,111 @@ +/* eslint-disable */ +// @ts-nocheck + +import type { EnteFile } from "@/media/file"; + +// TODO(PS): WIP gallery using upstream photoswipe +// +// Needs (not committed yet): +// yarn workspace gallery add photoswipe@^5.4.4 +// mv node_modules/photoswipe packages/new/photos/components/ps5 + +if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) { + console.warn("Using WIP upstream photoswipe"); +} else { + throw new Error("Whoa"); +} + +let PhotoSwipe; +if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) { + PhotoSwipe = require("./ps5/dist/photoswipe.esm.js").default; +} + +interface FileViewerPhotoSwipeOptions { + files: EnteFile[]; + initialIndex: number; + onClose: () => void; +} + +/** + * A wrapper over {@link PhotoSwipe} to tailor its interface for use by our file + * viewer. + * + * This is somewhat akin to the {@link PhotoSwipeLightbox}, except this doesn't + * have any UI of its own, it only modifies PhotoSwipe Core's behaviour. + * + * [Note: PhotoSwipe] + * + * PhotoSwipe is a library that behaves similarly to the OG "lightbox" image + * gallery JavaScript component from the middle ages. + * + * We don't need the lightbox functionality since we already have our own + * thumbnail list (the "gallery"), so we only use the "Core" PhotoSwipe module + * as our image viewer component. + * + * When the user clicks on one of the thumbnails in our gallery, we make the + * root PhotoSwipe component visible. Within the DOM this is a dialog-like div + * that takes up the entire viewport, shows the image, various controls etc. + * + * Documentation: https://photoswipe.com/. + */ +export class FileViewerPhotoSwipe { + pswp: PhotoSwipe; + + constructor({ files, initialIndex, onClose }: FileViewerPhotoSwipeOptions) { + this.files = files; + + const pswp = new PhotoSwipe({ + // Opaque background. + bgOpacity: 1, + // Set the index within files that we should open to. Subsequent + // updates to the index will be tracked by PhotoSwipe internally. + index: initialIndex, + // TODO(PS): padding option? for handling custom title bar. + // TODO(PS): will we need this? + mainClass: "our-extra-pswp-main-class", + }); + // Provide data about slides to PhotoSwipe via callbacks + // https://photoswipe.com/data-sources/#dynamically-generated-data + pswp.addFilter("numItems", () => { + return files.length; + }); + // const enqueueUpdates = index; + pswp.addFilter("itemData", (itemData, index) => { + const file = files[index]; + console.log({ itemData, index, file }); + + return { + src: `https://dummyimage.com/100/777/fff/?text=i${index}`, + width: 100, + height: 100, + }; + }); + pswp.on("close", () => { + // The user did some action within the file viewer to close it. Let + // our parent know that we have been closed. + onClose(); + }); + // Initializing PhotoSwipe adds it to the DOM as a dialog-like div with + // the class "pswp". + pswp.init(); + + this.pswp = pswp; + } + + closeIfNeeded() { + // Closing PhotoSwipe removes it from the DOM. + // + // This will only have an effect if we're being closed externally (e.g. + // if the user selects an album in the file info). + // + // If this cleanup function is running in the sequence where we were + // closed internally (e.g. the user activated the close button within + // the file viewer), then PhotoSwipe will ignore this extra close. + this.pswp.close(); + this.pswp = undefined; + } + + updateFiles(files: EnteFile[]) { + // TODO(PS) + } +} From 42ac508fe7abfbe855650fee034f62400a713ed9 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 11 Feb 2025 10:39:41 +0530 Subject: [PATCH 062/796] enqueue 1 --- .../photos/components/FileViewerPhotoSwipe.ts | 86 +++++++++++++++++-- 1 file changed, 81 insertions(+), 5 deletions(-) diff --git a/web/packages/new/photos/components/FileViewerPhotoSwipe.ts b/web/packages/new/photos/components/FileViewerPhotoSwipe.ts index 7183333079..d58d9d5b8e 100644 --- a/web/packages/new/photos/components/FileViewerPhotoSwipe.ts +++ b/web/packages/new/photos/components/FileViewerPhotoSwipe.ts @@ -1,6 +1,9 @@ /* eslint-disable */ // @ts-nocheck +import { assertionFailed } from "@/base/assert"; +import log from "@/base/log"; +import { downloadManager } from "@/gallery/services/download"; import type { EnteFile } from "@/media/file"; // TODO(PS): WIP gallery using upstream photoswipe @@ -19,6 +22,58 @@ let PhotoSwipe; if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) { PhotoSwipe = require("./ps5/dist/photoswipe.esm.js").default; } +// TODO(PS): +//import { type SlideData } from "./ps5/dist/types/slide/" +type SlideData = { + /** + * thumbnail element + */ + element?: HTMLElement | undefined; + /** + * image URL + */ + src?: string | undefined; + /** + * image srcset + */ + srcset?: string | undefined; + /** + * image width (deprecated) + */ + w?: number | undefined; + /** + * image height (deprecated) + */ + h?: number | undefined; + /** + * image width + */ + width?: number | undefined; + /** + * image height + */ + height?: number | undefined; + /** + * placeholder image URL that's displayed before large image is loaded + */ + msrc?: string | undefined; + /** + * image alt text + */ + alt?: string | undefined; + /** + * whether thumbnail is cropped client-side or not + */ + thumbCropped?: boolean | undefined; + /** + * html content of a slide + */ + html?: string | undefined; + /** + * slide type + */ + type?: string | undefined; +}; interface FileViewerPhotoSwipeOptions { files: EnteFile[]; @@ -49,7 +104,8 @@ interface FileViewerPhotoSwipeOptions { * Documentation: https://photoswipe.com/. */ export class FileViewerPhotoSwipe { - pswp: PhotoSwipe; + private pswp: PhotoSwipe; + private itemDataByFileID: Map = new Map(); constructor({ files, initialIndex, onClose }: FileViewerPhotoSwipeOptions) { this.files = files; @@ -67,13 +123,22 @@ export class FileViewerPhotoSwipe { // Provide data about slides to PhotoSwipe via callbacks // https://photoswipe.com/data-sources/#dynamically-generated-data pswp.addFilter("numItems", () => { - return files.length; + return this.files.length; }); // const enqueueUpdates = index; - pswp.addFilter("itemData", (itemData, index) => { + pswp.addFilter("itemData", (_, index) => { const file = files[index]; - console.log({ itemData, index, file }); + let itemData: SlideData | undefined; + if (file) { + itemData = this.itemDataByFileID.get(file.id); + if (!itemData) this.enqueueUpdates(index, file); + } + + log.debug(() => ["[ps]", { itemData, index, file, itemData }]); + if (!file) assertionFailed(); + + if (itemData) return itemData; return { src: `https://dummyimage.com/100/777/fff/?text=i${index}`, width: 100, @@ -92,6 +157,12 @@ export class FileViewerPhotoSwipe { this.pswp = pswp; } + /** + * Close this instance of {@link FileViewerPhotoSwipe} if it hasn't itself + * initiated the close. + * + * This instance **cannot** be used after this function has been called. + */ closeIfNeeded() { // Closing PhotoSwipe removes it from the DOM. // @@ -102,10 +173,15 @@ export class FileViewerPhotoSwipe { // closed internally (e.g. the user activated the close button within // the file viewer), then PhotoSwipe will ignore this extra close. this.pswp.close(); - this.pswp = undefined; } updateFiles(files: EnteFile[]) { // TODO(PS) } + + async enqueueUpdates(index: number, file: EnteFile) { + const thumbnailURL = await downloadManager.renderableThumbnailURL(file); + this.itemDataByFileID.set(file.id, { src: thumbnailURL }); + this.pswp.refreshSlideContent(index); + } } From 8c68af7772c553e2f9d1a387c74f83c96d1655f2 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 11 Feb 2025 10:53:22 +0530 Subject: [PATCH 063/796] Empty seems to work --- .../photos/components/FileViewerPhotoSwipe.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/web/packages/new/photos/components/FileViewerPhotoSwipe.ts b/web/packages/new/photos/components/FileViewerPhotoSwipe.ts index d58d9d5b8e..53cc0bad2d 100644 --- a/web/packages/new/photos/components/FileViewerPhotoSwipe.ts +++ b/web/packages/new/photos/components/FileViewerPhotoSwipe.ts @@ -139,11 +139,13 @@ export class FileViewerPhotoSwipe { if (!file) assertionFailed(); if (itemData) return itemData; - return { - src: `https://dummyimage.com/100/777/fff/?text=i${index}`, - width: 100, - height: 100, - }; + + // We don't have anything to show immediately, though in most cases + // a cached renderable thumbnail URL will be available shortly. + // Meanwhile return empty slide data: PhotoSwipe will not show + // anything in the image area but will otherwise render the + // surrounding UI properly. + return {}; }); pswp.on("close", () => { // The user did some action within the file viewer to close it. Let @@ -181,6 +183,10 @@ export class FileViewerPhotoSwipe { async enqueueUpdates(index: number, file: EnteFile) { const thumbnailURL = await downloadManager.renderableThumbnailURL(file); + // We don't have the dimensions of the thumbnail. We could try to deduce + // something from the file's aspect ratio etc, but that's not needed: + // PhotoSwipe already correctly (for our purposes) handles just a source + // URL being present. this.itemDataByFileID.set(file.id, { src: thumbnailURL }); this.pswp.refreshSlideContent(index); } From ca31a422fa336cf4baed7138bb6d91d79a859e86 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 11 Feb 2025 11:12:22 +0530 Subject: [PATCH 064/796] Multiple --- .../photos/components/FileViewerPhotoSwipe.ts | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/web/packages/new/photos/components/FileViewerPhotoSwipe.ts b/web/packages/new/photos/components/FileViewerPhotoSwipe.ts index 53cc0bad2d..1637b05cd9 100644 --- a/web/packages/new/photos/components/FileViewerPhotoSwipe.ts +++ b/web/packages/new/photos/components/FileViewerPhotoSwipe.ts @@ -5,6 +5,7 @@ import { assertionFailed } from "@/base/assert"; import log from "@/base/log"; import { downloadManager } from "@/gallery/services/download"; import type { EnteFile } from "@/media/file"; +import { FileType } from "@/media/file-type"; // TODO(PS): WIP gallery using upstream photoswipe // @@ -132,20 +133,29 @@ export class FileViewerPhotoSwipe { let itemData: SlideData | undefined; if (file) { itemData = this.itemDataByFileID.get(file.id); - if (!itemData) this.enqueueUpdates(index, file); + if (!itemData) { + // We don't have anything to show immediately, though in + // most cases a cached renderable thumbnail URL will be + // available shortly. + // + // Meanwhile, + // + // 1. Return empty slide data; PhotoSwipe will not show + // anything in the image area but will otherwise render + // the surrounding UI properly. + // + // 2. Insert empty data so that we don't enqueue multiple + // updates. + itemData = {}; + this.itemDataByFileID.set(file.id, itemData); + this.enqueueUpdates(index, file); + } } log.debug(() => ["[ps]", { itemData, index, file, itemData }]); if (!file) assertionFailed(); - if (itemData) return itemData; - - // We don't have anything to show immediately, though in most cases - // a cached renderable thumbnail URL will be available shortly. - // Meanwhile return empty slide data: PhotoSwipe will not show - // anything in the image area but will otherwise render the - // surrounding UI properly. - return {}; + return itemData ?? {}; }); pswp.on("close", () => { // The user did some action within the file viewer to close it. Let @@ -189,5 +199,13 @@ export class FileViewerPhotoSwipe { // URL being present. this.itemDataByFileID.set(file.id, { src: thumbnailURL }); this.pswp.refreshSlideContent(index); + + switch (file.metadata.fileType) { + case FileType.image: + break; + + default: + break; + } } } From 6f0deba3edf35b283f8fefce97152649d0fc1166 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 11 Feb 2025 11:33:55 +0530 Subject: [PATCH 065/796] full --- .../photos/components/FileViewerPhotoSwipe.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/web/packages/new/photos/components/FileViewerPhotoSwipe.ts b/web/packages/new/photos/components/FileViewerPhotoSwipe.ts index 1637b05cd9..91d3cd3ccf 100644 --- a/web/packages/new/photos/components/FileViewerPhotoSwipe.ts +++ b/web/packages/new/photos/components/FileViewerPhotoSwipe.ts @@ -192,17 +192,29 @@ export class FileViewerPhotoSwipe { } async enqueueUpdates(index: number, file: EnteFile) { + const update = (itemData: SlideData) => { + this.itemDataByFileID.set(file.id, itemData); + this.pswp.refreshSlideContent(index); + }; + const thumbnailURL = await downloadManager.renderableThumbnailURL(file); // We don't have the dimensions of the thumbnail. We could try to deduce // something from the file's aspect ratio etc, but that's not needed: // PhotoSwipe already correctly (for our purposes) handles just a source // URL being present. - this.itemDataByFileID.set(file.id, { src: thumbnailURL }); - this.pswp.refreshSlideContent(index); + update({ src: thumbnailURL }); switch (file.metadata.fileType) { - case FileType.image: + case FileType.image: { + const sourceURLs = + await downloadManager.renderableSourceURLs(file); + update({ + src: sourceURLs.url, + width: file.pubMagicMetadata?.data?.w, + height: file.pubMagicMetadata?.data?.h, + }); break; + } default: break; From 96937041f184478ed570951a63492a0b2c00156e Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 11 Feb 2025 11:39:23 +0530 Subject: [PATCH 066/796] vid 1 --- .../photos/components/FileViewerPhotoSwipe.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/web/packages/new/photos/components/FileViewerPhotoSwipe.ts b/web/packages/new/photos/components/FileViewerPhotoSwipe.ts index 91d3cd3ccf..67156baece 100644 --- a/web/packages/new/photos/components/FileViewerPhotoSwipe.ts +++ b/web/packages/new/photos/components/FileViewerPhotoSwipe.ts @@ -3,7 +3,10 @@ import { assertionFailed } from "@/base/assert"; import log from "@/base/log"; -import { downloadManager } from "@/gallery/services/download"; +import { + downloadManager, + type RenderableSourceURLs, +} from "@/gallery/services/download"; import type { EnteFile } from "@/media/file"; import { FileType } from "@/media/file-type"; @@ -216,8 +219,25 @@ export class FileViewerPhotoSwipe { break; } + case FileType.video: + { + const sourceURLs = + await downloadManager.renderableSourceURLs(file); + update({ + html: videoHTML(sourceURLs), + }); + break; + } + break; + default: break; } } } + +const videoHTML = ({ url }: RenderableSourceURLs) => ` + +`; From 5b1130ab2475be257bfce5a1c06390e85725ccb0 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 11 Feb 2025 11:57:23 +0530 Subject: [PATCH 067/796] dd --- .../photos/components/FileViewerPhotoSwipe.ts | 47 +++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/web/packages/new/photos/components/FileViewerPhotoSwipe.ts b/web/packages/new/photos/components/FileViewerPhotoSwipe.ts index 67156baece..da1da8ceb0 100644 --- a/web/packages/new/photos/components/FileViewerPhotoSwipe.ts +++ b/web/packages/new/photos/components/FileViewerPhotoSwipe.ts @@ -80,9 +80,26 @@ type SlideData = { }; interface FileViewerPhotoSwipeOptions { + /** + * The files that are being displayed in the context in which the file + * viewer was invoked. + */ files: EnteFile[]; + /** + * The index of the file that should be initially shown. + * + * Subsequently the user may navigate between files by using the controls + * provided within the file viewer itself. + */ initialIndex: number; + /** + * Called when the file viewer is closed. + */ onClose: () => void; + /** + * If true, then controls for downloading the file are not shown. + */ + disableDownload?: boolean; } /** @@ -108,11 +125,31 @@ interface FileViewerPhotoSwipeOptions { * Documentation: https://photoswipe.com/. */ export class FileViewerPhotoSwipe { + /** + * The PhotoSwipe instance which we wrap. + */ private pswp: PhotoSwipe; + /** + * The options with which we were initialized. + */ + private opts: Pick; + /** + * The best available SlideData for rendering the file with the given ID. + * + * If an entry does not exist for a particular fileID, then it is lazily + * added on demand. The same entry might get updated multiple times, as we + * start with the thumbnail but then also update this with the original etc. + */ private itemDataByFileID: Map = new Map(); - constructor({ files, initialIndex, onClose }: FileViewerPhotoSwipeOptions) { + constructor({ + files, + initialIndex, + onClose, + disableDownload, + }: FileViewerPhotoSwipeOptions) { this.files = files; + this.opts = { disableDownload }; const pswp = new PhotoSwipe({ // Opaque background. @@ -223,8 +260,9 @@ export class FileViewerPhotoSwipe { { const sourceURLs = await downloadManager.renderableSourceURLs(file); + const disableDownload = !!this.opts.disableDownload; update({ - html: videoHTML(sourceURLs), + html: videoHTML(sourceURLs, disableDownload), }); break; } @@ -236,8 +274,9 @@ export class FileViewerPhotoSwipe { } } -const videoHTML = ({ url }: RenderableSourceURLs) => ` -