Compare commits
14 Commits
refactor/g
...
release_mo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d90e37a00 | ||
|
|
f4605f86a4 | ||
|
|
eb9f5830a5 | ||
|
|
1f1cad181f | ||
|
|
0f8a8a7579 | ||
|
|
33703072eb | ||
|
|
d86f9d2ffa | ||
|
|
348ede2a03 | ||
|
|
5619b349b3 | ||
|
|
769adb75c5 | ||
|
|
1648f62da6 | ||
|
|
d7fdca78f7 | ||
|
|
63f24966ce | ||
|
|
1c14896fd6 |
@@ -12,8 +12,6 @@ PODS:
|
||||
- Flutter
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- emoji_picker_flutter (0.0.1):
|
||||
- Flutter
|
||||
- ffmpeg_kit_custom (6.0.3)
|
||||
- ffmpeg_kit_flutter (6.0.3):
|
||||
- ffmpeg_kit_custom
|
||||
@@ -129,6 +127,9 @@ PODS:
|
||||
- libwebp/sharpyuv (1.5.0)
|
||||
- libwebp/webp (1.5.0):
|
||||
- libwebp/sharpyuv
|
||||
- local_auth_darwin (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- local_auth_ios (0.0.1):
|
||||
- Flutter
|
||||
- Mantle (2.2.0):
|
||||
@@ -229,8 +230,6 @@ PODS:
|
||||
- Flutter
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- vibration (1.7.5):
|
||||
- Flutter
|
||||
- video_player_avfoundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@@ -251,7 +250,6 @@ DEPENDENCIES:
|
||||
- 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`)
|
||||
- emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`)
|
||||
- ffmpeg_kit_flutter (from `.symlinks/plugins/ffmpeg_kit_flutter/ios`)
|
||||
- file_saver (from `.symlinks/plugins/file_saver/ios`)
|
||||
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
|
||||
@@ -271,6 +269,7 @@ DEPENDENCIES:
|
||||
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
|
||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||
- launcher_icon_switcher (from `.symlinks/plugins/launcher_icon_switcher/ios`)
|
||||
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
||||
- local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`)
|
||||
- maps_launcher (from `.symlinks/plugins/maps_launcher/ios`)
|
||||
- media_extension (from `.symlinks/plugins/media_extension/ios`)
|
||||
@@ -298,7 +297,6 @@ DEPENDENCIES:
|
||||
- thermal (from `.symlinks/plugins/thermal/ios`)
|
||||
- ua_client_hints (from `.symlinks/plugins/ua_client_hints/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- vibration (from `.symlinks/plugins/vibration/ios`)
|
||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
|
||||
- video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`)
|
||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||
@@ -306,7 +304,7 @@ DEPENDENCIES:
|
||||
- workmanager (from `.symlinks/plugins/workmanager/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
https://github.com/ente-io/ffmpeg-kit-custom-repo-ios.git:
|
||||
https://github.com/ente-io/ffmpeg-kit-custom-repo-ios:
|
||||
- ffmpeg_kit_custom
|
||||
trunk:
|
||||
- Firebase
|
||||
@@ -341,8 +339,6 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/dart_ui_isolate/ios"
|
||||
device_info_plus:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
emoji_picker_flutter:
|
||||
:path: ".symlinks/plugins/emoji_picker_flutter/ios"
|
||||
ffmpeg_kit_flutter:
|
||||
:path: ".symlinks/plugins/ffmpeg_kit_flutter/ios"
|
||||
file_saver:
|
||||
@@ -381,6 +377,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/integration_test/ios"
|
||||
launcher_icon_switcher:
|
||||
:path: ".symlinks/plugins/launcher_icon_switcher/ios"
|
||||
local_auth_darwin:
|
||||
:path: ".symlinks/plugins/local_auth_darwin/darwin"
|
||||
local_auth_ios:
|
||||
:path: ".symlinks/plugins/local_auth_ios/ios"
|
||||
maps_launcher:
|
||||
@@ -435,8 +433,6 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/ua_client_hints/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
vibration:
|
||||
:path: ".symlinks/plugins/vibration/ios"
|
||||
video_player_avfoundation:
|
||||
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
|
||||
video_thumbnail:
|
||||
@@ -449,84 +445,83 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/workmanager/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: f3e17e4ee5e357b39d8b95290a9b2c299fca71c6
|
||||
battery_info: b6c551049266af31556b93c9d9b9452cfec0219f
|
||||
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
|
||||
cupertino_http: 947a233f40cfea55167a49f2facc18434ea117ba
|
||||
dart_ui_isolate: d5bcda83ca4b04f129d70eb90110b7a567aece14
|
||||
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
||||
emoji_picker_flutter: fe2e6151c5b548e975d546e6eeb567daf0962a58
|
||||
app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7
|
||||
battery_info: 83f3aae7be2fccefab1d2bf06b8aa96f11c8bcdd
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
|
||||
dart_ui_isolate: 46f6714abe6891313267153ef6f9748d8ecfcab1
|
||||
device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89
|
||||
ffmpeg_kit_custom: 682b4f2f1ff1f8abae5a92f6c3540f2441d5be99
|
||||
ffmpeg_kit_flutter: 9dce4803991478c78c6fb9f972703301101095fe
|
||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||
ffmpeg_kit_flutter: 915b345acc97d4142e8a9a8549d177ff10f043f5
|
||||
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
||||
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf
|
||||
firebase_core: 6e223dfa350b2edceb729cea505eaaef59330682
|
||||
firebase_messaging: 07fde77ae28c08616a1d4d870450efc2b38cf40d
|
||||
firebase_core: 6cbed78b4f298ed103a9fd034e6dbc846320480f
|
||||
firebase_messaging: 5e0adf2eb18b0ee59aa0c109314c091a0497ecac
|
||||
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d
|
||||
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629
|
||||
FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917
|
||||
FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_email_sender: e03bdda7637bcd3539bfe718fddd980e9508efaa
|
||||
flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e
|
||||
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
|
||||
flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f
|
||||
flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a
|
||||
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
|
||||
flutter_sodium: a00383520fc689c688b66fd3092984174712493e
|
||||
flutter_timezone: ac3da59ac941ff1c98a2e1f0293420e020120282
|
||||
fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
|
||||
flutter_email_sender: aa1e9772696691d02cd91fea829856c11efb8e58
|
||||
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
|
||||
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
||||
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
|
||||
flutter_native_splash: 6cad9122ea0fad137d23137dd14b937f3e90b145
|
||||
flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418
|
||||
flutter_sodium: 7e4621538491834eba53bd524547854bcbbd6987
|
||||
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
|
||||
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
|
||||
image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43
|
||||
in_app_purchase_storekit: a1ce04056e23eecc666b086040239da7619cd783
|
||||
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
|
||||
launcher_icon_switcher: 8e0ad2131a20c51c1dd939896ee32e70cd845b37
|
||||
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
||||
image_editor_common: 3de87e7c4804f4ae24c8f8a998362b98c105cac1
|
||||
in_app_purchase_storekit: d1a48cb0f8b29dbf5f85f782f5dd79b21b90a5e6
|
||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||
launcher_icon_switcher: 84c218d233505aa7d8655d8fa61a3ba802c022da
|
||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||
local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9
|
||||
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
|
||||
local_auth_ios: f7a1841beef3151d140a967c2e46f30637cdf451
|
||||
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
|
||||
maps_launcher: 2e5b6a2d664ec6c27f82ffa81b74228d770ab203
|
||||
media_extension: 6618f07abd762cdbfaadf1b0c56a287e820f0c84
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
motion_sensors: 03f55b7c637a7e365a0b5f9697a449f9059d5d91
|
||||
motionphoto: 8b65ce50c7d7ff3c767534fc3768b2eed9ac24e4
|
||||
move_to_background: cd3091014529ec7829e342ad2d75c0a11f4378a5
|
||||
maps_launcher: edf829809ba9e894d70e569bab11c16352dedb45
|
||||
media_extension: 671e2567880d96c95c65c9a82ccceed8f2e309fd
|
||||
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
||||
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||
motion_sensors: 741e702c17467b9569a92165dda8d4d88c6167f1
|
||||
motionphoto: 23e2aeb5c6380112f69468d71f970fa7438e5ed1
|
||||
move_to_background: 7e3467dd2a1d1013e98c9c1cb93fd53cd7ef9d84
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
native_video_player: 29ab24a926804ac8c4a57eb6d744c7d927c2bc3e
|
||||
objective_c: 77e887b5ba1827970907e10e832eec1683f3431d
|
||||
onnxruntime: e7c2ae44385191eaad5ae64c935a72debaddc997
|
||||
native_video_player: 6809dec117e8997161dbfb42a6f90d6df71a504d
|
||||
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
|
||||
onnxruntime: f9b296392c96c42882be020a59dbeac6310d81b2
|
||||
onnxruntime-c: a909204639a1f035f575127ac406f781ac797c9c
|
||||
onnxruntime-objc: b6fab0f1787aa6f7190c2013f03037df4718bd8b
|
||||
open_mail_app: 70273c53f768beefdafbe310c3d9086e4da3cb02
|
||||
open_mail_app: 7314a609e88eed22d53671279e189af7a0ab0f11
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
|
||||
privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413
|
||||
privacy_screen: 3159a541f5d3a31bea916cfd4e58f9dc722b3fd4
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
receive_sharing_intent: 79c848f5b045674ad60b9fea3bafea59962ad2c1
|
||||
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
||||
SDWebImage: f29024626962457f3470184232766516dee8dfea
|
||||
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
|
||||
Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854
|
||||
sentry_flutter: 2df8b0aab7e4aba81261c230cbea31c82a62dd1b
|
||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
sentry_flutter: 27892878729f42701297c628eb90e7c6529f3684
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1
|
||||
sqlite3_flutter_libs: 069c435986dd4b63461aecd68f4b30be4a9e9daa
|
||||
system_info_plus: 5393c8da281d899950d751713575fbf91c7709aa
|
||||
thermal: a9261044101ae8f532fa29cab4e8270b51b3f55c
|
||||
ua_client_hints: aeabd123262c087f0ce151ef96fa3ab77bfc8b38
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
vibration: 7d883d141656a1c1a6d8d238616b2042a51a1241
|
||||
video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
|
||||
video_thumbnail: 94ba6705afbaa120b77287080424930f23ea0c40
|
||||
volume_controller: 2e3de73d6e7e81a0067310d17fb70f2f86d71ac7
|
||||
wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56
|
||||
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
||||
sqlite3_flutter_libs: 3c323550ef3b928bc0aa9513c841e45a7d242832
|
||||
system_info_plus: 555ce7047fbbf29154726db942ae785c29211740
|
||||
thermal: d4c48be750d1ddbab36b0e2dcb2471531bc8df41
|
||||
ua_client_hints: 92fe0d139619b73ec9fcb46cc7e079a26178f586
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
|
||||
video_thumbnail: 584ccfa55d8fd2f3d5507218b0a18d84c839c620
|
||||
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
||||
wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
|
||||
workmanager: 01be2de7f184bd15de93a1812936a2b7f42ef07e
|
||||
|
||||
PODFILE CHECKSUM: a8ef88ad74ba499756207e7592c6071a96756d18
|
||||
|
||||
|
||||
@@ -532,7 +532,6 @@
|
||||
"${BUILT_PRODUCTS_DIR}/cupertino_http/cupertino_http.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/dart_ui_isolate/dart_ui_isolate.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/emoji_picker_flutter/emoji_picker_flutter.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/file_saver/file_saver.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/flutter_email_sender/flutter_email_sender.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/flutter_image_compress_common/flutter_image_compress_common.framework",
|
||||
@@ -549,6 +548,7 @@
|
||||
"${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/launcher_icon_switcher/launcher_icon_switcher.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/local_auth_darwin/local_auth_darwin.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/local_auth_ios/local_auth_ios.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/maps_launcher/maps_launcher.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/media_extension/media_extension.framework",
|
||||
@@ -576,7 +576,6 @@
|
||||
"${BUILT_PRODUCTS_DIR}/thermal/thermal.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/ua_client_hints/ua_client_hints.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/vibration/vibration.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/video_player_avfoundation/video_player_avfoundation.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/video_thumbnail/video_thumbnail.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/volume_controller/volume_controller.framework",
|
||||
@@ -629,7 +628,6 @@
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/cupertino_http.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/dart_ui_isolate.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/emoji_picker_flutter.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_saver.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_email_sender.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_image_compress_common.framework",
|
||||
@@ -646,6 +644,7 @@
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/launcher_icon_switcher.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth_darwin.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth_ios.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/maps_launcher.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_extension.framework",
|
||||
@@ -673,7 +672,6 @@
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/thermal.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ua_client_hints.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/vibration.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_player_avfoundation.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_thumbnail.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/volume_controller.framework",
|
||||
|
||||
@@ -27,7 +27,7 @@ const subGalleryMultiplier = 10;
|
||||
// used to identify which ente file are available in app cache
|
||||
const String sharedMediaIdentifier = 'ente-shared-media://';
|
||||
|
||||
const galleryThumbnailDiskLoadDeferDuration = Duration(milliseconds: 500);
|
||||
const galleryThumbnailDiskLoadDeferDuration = Duration(milliseconds: 80);
|
||||
const galleryThumbnailServerLoadDeferDuration = Duration(milliseconds: 80);
|
||||
|
||||
// 256 bit key maps to 24 words
|
||||
|
||||
112
mobile/apps/photos/lib/generated/l10n.dart
generated
112
mobile/apps/photos/lib/generated/l10n.dart
generated
@@ -12495,118 +12495,6 @@ class S {
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Undo`
|
||||
String get undo {
|
||||
return Intl.message(
|
||||
'Undo',
|
||||
name: 'undo',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Redo`
|
||||
String get redo {
|
||||
return Intl.message(
|
||||
'Redo',
|
||||
name: 'redo',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Filter`
|
||||
String get filter {
|
||||
return Intl.message(
|
||||
'Filter',
|
||||
name: 'filter',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Adjust`
|
||||
String get adjust {
|
||||
return Intl.message(
|
||||
'Adjust',
|
||||
name: 'adjust',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Draw`
|
||||
String get draw {
|
||||
return Intl.message(
|
||||
'Draw',
|
||||
name: 'draw',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Sticker`
|
||||
String get sticker {
|
||||
return Intl.message(
|
||||
'Sticker',
|
||||
name: 'sticker',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Brush Color`
|
||||
String get brushColor {
|
||||
return Intl.message(
|
||||
'Brush Color',
|
||||
name: 'brushColor',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Font`
|
||||
String get font {
|
||||
return Intl.message(
|
||||
'Font',
|
||||
name: 'font',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Background`
|
||||
String get background {
|
||||
return Intl.message(
|
||||
'Background',
|
||||
name: 'background',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Align`
|
||||
String get align {
|
||||
return Intl.message(
|
||||
'Align',
|
||||
name: 'align',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `{count, plural, =1{Added successfully to 1 album} other{Added successfully to {count} albums}}`
|
||||
String addedToAlbums(int count) {
|
||||
return Intl.plural(
|
||||
count,
|
||||
one: 'Added successfully to 1 album',
|
||||
other: 'Added successfully to $count albums',
|
||||
name: 'addedToAlbums',
|
||||
desc: 'Message shown when items are added to albums',
|
||||
args: [count],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<S> {
|
||||
|
||||
@@ -1808,24 +1808,5 @@
|
||||
"automaticallyAnalyzeAndSplitGrouping": "We will automatically analyze the grouping to determine if there are multiple people present, and separate them out again. This may take a few seconds.",
|
||||
"layout": "Layout",
|
||||
"day": "Day",
|
||||
"peopleAutoAddDesc": "Select the people you want to automatically add to the album",
|
||||
"undo": "Undo",
|
||||
"redo": "Redo",
|
||||
"filter": "Filter",
|
||||
"adjust": "Adjust",
|
||||
"draw": "Draw",
|
||||
"sticker": "Sticker",
|
||||
"brushColor": "Brush Color",
|
||||
"font": "Font",
|
||||
"background": "Background",
|
||||
"align": "Align",
|
||||
"addedToAlbums": "{count, plural, =1{Added successfully to 1 album} other{Added successfully to {count} albums}}",
|
||||
"@addedToAlbums": {
|
||||
"description": "Message shown when items are added to albums",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
}
|
||||
"peopleAutoAddDesc": "Select the people you want to automatically add to the album"
|
||||
}
|
||||
@@ -224,18 +224,20 @@ class GalleryGroups {
|
||||
int i = 0;
|
||||
while (!endOfListReached) {
|
||||
gridRowChildren.add(
|
||||
GalleryFileWidget(
|
||||
RepaintBoundary(
|
||||
key: ValueKey(
|
||||
tagPrefix +
|
||||
filesInGroup[firstIndexOfRowWrtFilesInGroup + i]
|
||||
.tag,
|
||||
),
|
||||
file: filesInGroup[firstIndexOfRowWrtFilesInGroup + i],
|
||||
selectedFiles: selectedFiles,
|
||||
limitSelectionToOne: limitSelectionToOne,
|
||||
tag: tagPrefix,
|
||||
photoGridSize: crossAxisCount,
|
||||
currentUserID: currentUserID,
|
||||
child: GalleryFileWidget(
|
||||
file: filesInGroup[firstIndexOfRowWrtFilesInGroup + i],
|
||||
selectedFiles: selectedFiles,
|
||||
limitSelectionToOne: limitSelectionToOne,
|
||||
tag: tagPrefix,
|
||||
photoGridSize: crossAxisCount,
|
||||
currentUserID: currentUserID,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -247,18 +249,20 @@ class GalleryGroups {
|
||||
} else {
|
||||
for (int i = 0; i < crossAxisCount; i++) {
|
||||
gridRowChildren.add(
|
||||
GalleryFileWidget(
|
||||
RepaintBoundary(
|
||||
key: ValueKey(
|
||||
tagPrefix +
|
||||
filesInGroup[firstIndexOfRowWrtFilesInGroup + i]
|
||||
.tag,
|
||||
),
|
||||
file: filesInGroup[firstIndexOfRowWrtFilesInGroup + i],
|
||||
selectedFiles: selectedFiles,
|
||||
limitSelectionToOne: limitSelectionToOne,
|
||||
tag: tagPrefix,
|
||||
photoGridSize: crossAxisCount,
|
||||
currentUserID: currentUserID,
|
||||
child: GalleryFileWidget(
|
||||
file: filesInGroup[firstIndexOfRowWrtFilesInGroup + i],
|
||||
selectedFiles: selectedFiles,
|
||||
limitSelectionToOne: limitSelectionToOne,
|
||||
tag: tagPrefix,
|
||||
photoGridSize: crossAxisCount,
|
||||
currentUserID: currentUserID,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -310,7 +310,9 @@ class _CollectionActionSheetState extends State<CollectionActionSheet> {
|
||||
if (result) {
|
||||
showShortToast(
|
||||
context,
|
||||
S.of(context).addedToAlbums(_selectedCollections.length),
|
||||
"Added successfully to " +
|
||||
_selectedCollections.length.toString() +
|
||||
" albums",
|
||||
);
|
||||
widget.selectedFiles?.clearAll();
|
||||
}
|
||||
|
||||
110
mobile/apps/photos/lib/ui/tools/editor/filtered_image.dart
Normal file
110
mobile/apps/photos/lib/ui/tools/editor/filtered_image.dart
Normal file
@@ -0,0 +1,110 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:image_editor/image_editor.dart';
|
||||
|
||||
class FilteredImage extends StatelessWidget {
|
||||
const FilteredImage({
|
||||
required this.child,
|
||||
this.brightness,
|
||||
this.saturation,
|
||||
this.hue,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final double? brightness, saturation, hue;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ColorFiltered(
|
||||
colorFilter: ColorFilter.matrix(
|
||||
ColorFilterGenerator.brightnessAdjustMatrix(
|
||||
value: brightness ?? 1,
|
||||
),
|
||||
),
|
||||
child: ColorFiltered(
|
||||
colorFilter: ColorFilter.matrix(
|
||||
ColorFilterGenerator.saturationAdjustMatrix(
|
||||
value: saturation ?? 1,
|
||||
),
|
||||
),
|
||||
child: ColorFiltered(
|
||||
colorFilter: ColorFilter.matrix(
|
||||
ColorFilterGenerator.hueAdjustMatrix(
|
||||
value: hue ?? 0,
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ColorFilterGenerator {
|
||||
static List<double> hueAdjustMatrix({double value = 1}) {
|
||||
value = value * pi;
|
||||
|
||||
if (value == 0) {
|
||||
return [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
];
|
||||
}
|
||||
final double cosVal = cos(value);
|
||||
final double sinVal = sin(value);
|
||||
const double lumR = 0.213;
|
||||
const double lumG = 0.715;
|
||||
const double lumB = 0.072;
|
||||
|
||||
return List<double>.from(<double>[
|
||||
(lumR + (cosVal * (1 - lumR))) + (sinVal * (-lumR)),
|
||||
(lumG + (cosVal * (-lumG))) + (sinVal * (-lumG)),
|
||||
(lumB + (cosVal * (-lumB))) + (sinVal * (1 - lumB)),
|
||||
0,
|
||||
0,
|
||||
(lumR + (cosVal * (-lumR))) + (sinVal * 0.143),
|
||||
(lumG + (cosVal * (1 - lumG))) + (sinVal * 0.14),
|
||||
(lumB + (cosVal * (-lumB))) + (sinVal * (-0.283)),
|
||||
0,
|
||||
0,
|
||||
(lumR + (cosVal * (-lumR))) + (sinVal * (-(1 - lumR))),
|
||||
(lumG + (cosVal * (-lumG))) + (sinVal * lumG),
|
||||
(lumB + (cosVal * (1 - lumB))) + (sinVal * lumB),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
]).map((i) => i.toDouble()).toList();
|
||||
}
|
||||
|
||||
static List<double> brightnessAdjustMatrix({double value = 1}) {
|
||||
return ColorOption.brightness(value).matrix;
|
||||
}
|
||||
|
||||
static List<double> saturationAdjustMatrix({double value = 1}) {
|
||||
return ColorOption.saturation(value).matrix;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:flutter_svg/svg.dart";
|
||||
import "package:photos/ente_theme_data.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart";
|
||||
|
||||
class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
@@ -44,7 +43,7 @@ class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
enableUndo ? close() : Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
S.of(context).cancel,
|
||||
'Cancel',
|
||||
style: getEnteTextTheme(context).body,
|
||||
),
|
||||
),
|
||||
@@ -53,7 +52,7 @@ class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: S.of(context).undo,
|
||||
tooltip: 'Undo',
|
||||
onPressed: () {
|
||||
undo != null ? undo!() : null;
|
||||
},
|
||||
@@ -67,7 +66,7 @@ class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
IconButton(
|
||||
tooltip: S.of(context).redo,
|
||||
tooltip: 'Redo',
|
||||
onPressed: () {
|
||||
redo != null ? redo!() : null;
|
||||
},
|
||||
@@ -89,7 +88,7 @@ class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
key: ValueKey(isMainEditor ? 'save_copy' : 'done'),
|
||||
onPressed: done,
|
||||
child: Text(
|
||||
isMainEditor ? S.of(context).saveCopy : S.of(context).done,
|
||||
isMainEditor ? 'Save Copy' : 'Done',
|
||||
style: getEnteTextTheme(context).body.copyWith(
|
||||
color: isMainEditor
|
||||
? (enableUndo
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:flutter_svg/svg.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/tools/editor/image_editor/circular_icon_button.dart";
|
||||
import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart";
|
||||
@@ -114,7 +113,7 @@ class _ImageEditorCropRotateBarState extends State<ImageEditorCropRotateBar>
|
||||
children: [
|
||||
CircularIconButton(
|
||||
svgPath: "assets/image-editor/image-editor-crop-rotate.svg",
|
||||
label: S.of(context).rotate,
|
||||
label: "Rotate",
|
||||
onTap: () {
|
||||
widget.editor.rotate();
|
||||
},
|
||||
@@ -122,7 +121,7 @@ class _ImageEditorCropRotateBarState extends State<ImageEditorCropRotateBar>
|
||||
const SizedBox(width: 6),
|
||||
CircularIconButton(
|
||||
svgPath: "assets/image-editor/image-editor-flip.svg",
|
||||
label: S.of(context).flip,
|
||||
label: "Flip",
|
||||
onTap: () {
|
||||
widget.editor.flip();
|
||||
},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/ui/tools/editor/image_editor/circular_icon_button.dart";
|
||||
import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart";
|
||||
import "package:photos/ui/tools/editor/image_editor/image_editor_constants.dart";
|
||||
@@ -91,7 +90,7 @@ class ImageEditorMainBottomBarState extends State<ImageEditorMainBottomBar>
|
||||
children: <Widget>[
|
||||
CircularIconButton(
|
||||
svgPath: "assets/image-editor/image-editor-crop.svg",
|
||||
label: S.of(context).crop,
|
||||
label: "Crop",
|
||||
onTap: () {
|
||||
widget.editor.openCropRotateEditor();
|
||||
},
|
||||
@@ -99,21 +98,21 @@ class ImageEditorMainBottomBarState extends State<ImageEditorMainBottomBar>
|
||||
CircularIconButton(
|
||||
svgPath:
|
||||
"assets/image-editor/image-editor-filter.svg",
|
||||
label: S.of(context).filter,
|
||||
label: "Filter",
|
||||
onTap: () {
|
||||
widget.editor.openFilterEditor();
|
||||
},
|
||||
),
|
||||
CircularIconButton(
|
||||
svgPath: "assets/image-editor/image-editor-tune.svg",
|
||||
label: S.of(context).adjust,
|
||||
label: "Adjust",
|
||||
onTap: () {
|
||||
widget.editor.openTuneEditor();
|
||||
},
|
||||
),
|
||||
CircularIconButton(
|
||||
svgPath: "assets/image-editor/image-editor-paint.svg",
|
||||
label: S.of(context).draw,
|
||||
label: "Draw",
|
||||
onTap: () {
|
||||
widget.editor.openPaintingEditor();
|
||||
},
|
||||
@@ -121,7 +120,7 @@ class ImageEditorMainBottomBarState extends State<ImageEditorMainBottomBar>
|
||||
CircularIconButton(
|
||||
svgPath:
|
||||
"assets/image-editor/image-editor-sticker.svg",
|
||||
label: S.of(context).sticker,
|
||||
label: "Sticker",
|
||||
onTap: () {
|
||||
widget.editor.openEmojiEditor();
|
||||
},
|
||||
|
||||
@@ -37,12 +37,12 @@ import "package:photos/utils/navigation_util.dart";
|
||||
import "package:pro_image_editor/models/editor_configs/main_editor_configs.dart";
|
||||
import 'package:pro_image_editor/pro_image_editor.dart';
|
||||
|
||||
class ImageEditorPage extends StatefulWidget {
|
||||
class NewImageEditor extends StatefulWidget {
|
||||
final ente.EnteFile originalFile;
|
||||
final File file;
|
||||
final DetailPageConfiguration detailPageConfig;
|
||||
|
||||
const ImageEditorPage({
|
||||
const NewImageEditor({
|
||||
super.key,
|
||||
required this.file,
|
||||
required this.originalFile,
|
||||
@@ -50,10 +50,10 @@ class ImageEditorPage extends StatefulWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
State<ImageEditorPage> createState() => _ImageEditorPageState();
|
||||
State<NewImageEditor> createState() => _NewImageEditorState();
|
||||
}
|
||||
|
||||
class _ImageEditorPageState extends State<ImageEditorPage> {
|
||||
class _NewImageEditorState extends State<NewImageEditor> {
|
||||
final _mainEditorBarKey = GlobalKey<ImageEditorMainBottomBarState>();
|
||||
final editorKey = GlobalKey<ProImageEditorState>();
|
||||
final _logger = Logger("ImageEditor");
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/tools/editor/image_editor/image_editor_color_picker.dart";
|
||||
import "package:photos/ui/tools/editor/image_editor/image_editor_configs_mixin.dart";
|
||||
@@ -64,7 +63,7 @@ class _ImageEditorPaintBarState extends State<ImageEditorPaintBar>
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 20.0),
|
||||
child: Text(
|
||||
S.of(context).brushColor,
|
||||
"Brush Color",
|
||||
style: getEnteTextTheme(context).body,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:flutter_svg/svg.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/tools/editor/image_editor/circular_icon_button.dart";
|
||||
import "package:photos/ui/tools/editor/image_editor/image_editor_color_picker.dart";
|
||||
@@ -76,7 +75,7 @@ class _ImageEditorTextBarState extends State<ImageEditorTextBar>
|
||||
children: [
|
||||
CircularIconButton(
|
||||
svgPath: "assets/image-editor/image-editor-text-color.svg",
|
||||
label: S.of(context).color,
|
||||
label: "Color",
|
||||
isSelected: selectedActionIndex == 0,
|
||||
onTap: () {
|
||||
_selectAction(0);
|
||||
@@ -84,7 +83,7 @@ class _ImageEditorTextBarState extends State<ImageEditorTextBar>
|
||||
),
|
||||
CircularIconButton(
|
||||
svgPath: "assets/image-editor/image-editor-text-font.svg",
|
||||
label: S.of(context).font,
|
||||
label: "Font",
|
||||
isSelected: selectedActionIndex == 1,
|
||||
onTap: () {
|
||||
_selectAction(1);
|
||||
@@ -92,7 +91,7 @@ class _ImageEditorTextBarState extends State<ImageEditorTextBar>
|
||||
),
|
||||
CircularIconButton(
|
||||
svgPath: "assets/image-editor/image-editor-text-background.svg",
|
||||
label: S.of(context).background,
|
||||
label: "Background",
|
||||
isSelected: selectedActionIndex == 2,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
@@ -102,7 +101,7 @@ class _ImageEditorTextBarState extends State<ImageEditorTextBar>
|
||||
),
|
||||
CircularIconButton(
|
||||
svgPath: "assets/image-editor/image-editor-text-align-left.svg",
|
||||
label: S.of(context).align,
|
||||
label: "Align",
|
||||
isSelected: selectedActionIndex == 3,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
|
||||
553
mobile/apps/photos/lib/ui/tools/editor/image_editor_page.dart
Normal file
553
mobile/apps/photos/lib/ui/tools/editor/image_editor_page.dart
Normal file
@@ -0,0 +1,553 @@
|
||||
import "dart:async";
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui show Image;
|
||||
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:flutter_image_compress/flutter_image_compress.dart";
|
||||
import 'package:image_editor/image_editor.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/db/files_db.dart';
|
||||
import 'package:photos/events/local_photos_updated_event.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/models/file/file.dart' as ente;
|
||||
import 'package:photos/models/location/location.dart';
|
||||
import 'package:photos/services/sync/sync_service.dart';
|
||||
import 'package:photos/ui/common/loading_widget.dart';
|
||||
import 'package:photos/ui/components/action_sheet_widget.dart';
|
||||
import 'package:photos/ui/components/buttons/button_widget.dart';
|
||||
import 'package:photos/ui/components/models/button_type.dart';
|
||||
import 'package:photos/ui/notification/toast.dart';
|
||||
import 'package:photos/ui/tools/editor/filtered_image.dart';
|
||||
import 'package:photos/ui/viewer/file/detail_page.dart';
|
||||
import 'package:photos/utils/dialog_util.dart';
|
||||
import 'package:photos/utils/navigation_util.dart';
|
||||
import 'package:syncfusion_flutter_core/theme.dart';
|
||||
import 'package:syncfusion_flutter_sliders/sliders.dart';
|
||||
|
||||
class ImageEditorPage extends StatefulWidget {
|
||||
final ImageProvider imageProvider;
|
||||
final DetailPageConfiguration detailPageConfig;
|
||||
final ente.EnteFile originalFile;
|
||||
|
||||
const ImageEditorPage(
|
||||
this.imageProvider,
|
||||
this.originalFile,
|
||||
this.detailPageConfig, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ImageEditorPage> createState() => _ImageEditorPageState();
|
||||
}
|
||||
|
||||
class _ImageEditorPageState extends State<ImageEditorPage> {
|
||||
static const double kBrightnessDefault = 1;
|
||||
static const double kBrightnessMin = 0;
|
||||
static const double kBrightnessMax = 2;
|
||||
static const double kSaturationDefault = 1;
|
||||
static const double kSaturationMin = 0;
|
||||
static const double kSaturationMax = 2;
|
||||
|
||||
final _logger = Logger("ImageEditor");
|
||||
final GlobalKey<ExtendedImageEditorState> editorKey =
|
||||
GlobalKey<ExtendedImageEditorState>();
|
||||
|
||||
double? _brightness = kBrightnessDefault;
|
||||
double? _saturation = kSaturationDefault;
|
||||
bool _hasEdited = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) async {
|
||||
if (_hasBeenEdited()) {
|
||||
await _showExitConfirmationDialog(context);
|
||||
} else {
|
||||
replacePage(context, DetailPage(widget.detailPageConfig));
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: const Color(0x00000000),
|
||||
elevation: 0,
|
||||
actions: _hasBeenEdited()
|
||||
? [
|
||||
IconButton(
|
||||
padding: const EdgeInsets.only(right: 16, left: 16),
|
||||
onPressed: () {
|
||||
editorKey.currentState!.reset();
|
||||
setState(() {
|
||||
_brightness = kBrightnessDefault;
|
||||
_saturation = kSaturationDefault;
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.history),
|
||||
),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(child: _buildImage()),
|
||||
const Padding(padding: EdgeInsets.all(4)),
|
||||
Column(
|
||||
children: [
|
||||
_buildBrightness(),
|
||||
_buildSat(),
|
||||
],
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(8)),
|
||||
SafeArea(child: _buildBottomBar()),
|
||||
Padding(padding: EdgeInsets.all(Platform.isIOS ? 16 : 6)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _hasBeenEdited() {
|
||||
return _hasEdited ||
|
||||
_saturation != kSaturationDefault ||
|
||||
_brightness != kBrightnessDefault;
|
||||
}
|
||||
|
||||
Widget _buildImage() {
|
||||
return Hero(
|
||||
tag: widget.detailPageConfig.tagPrefix + widget.originalFile.tag,
|
||||
child: ExtendedImage(
|
||||
image: widget.imageProvider,
|
||||
extendedImageEditorKey: editorKey,
|
||||
mode: ExtendedImageMode.editor,
|
||||
fit: BoxFit.contain,
|
||||
initEditorConfigHandler: (_) => EditorConfig(
|
||||
maxScale: 8.0,
|
||||
cropRectPadding: const EdgeInsets.all(20.0),
|
||||
hitTestSize: 20.0,
|
||||
cornerColor: const Color.fromRGBO(45, 150, 98, 1),
|
||||
editActionDetailsIsChanged: (_) {
|
||||
setState(() {
|
||||
_hasEdited = true;
|
||||
});
|
||||
},
|
||||
),
|
||||
loadStateChanged: (state) {
|
||||
if (state.extendedImageLoadState == LoadState.completed) {
|
||||
return FilteredImage(
|
||||
brightness: _brightness,
|
||||
saturation: _saturation,
|
||||
child: state.completedWidget,
|
||||
);
|
||||
}
|
||||
return const EnteLoadingWidget();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomBar() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildFlipButton(),
|
||||
_buildRotateLeftButton(),
|
||||
_buildRotateRightButton(),
|
||||
_buildSaveButton(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFlipButton() {
|
||||
final TextStyle subtitle2 = Theme.of(context).textTheme.titleSmall!;
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
flip();
|
||||
},
|
||||
child: SizedBox(
|
||||
width: 80,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: Icon(
|
||||
Icons.flip,
|
||||
color: Theme.of(context).iconTheme.color!.withOpacity(0.8),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(2)),
|
||||
Text(
|
||||
S.of(context).flip,
|
||||
style: subtitle2.copyWith(
|
||||
color: subtitle2.color!.withOpacity(0.8),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRotateLeftButton() {
|
||||
final TextStyle subtitle2 = Theme.of(context).textTheme.titleSmall!;
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
rotate(false);
|
||||
},
|
||||
child: SizedBox(
|
||||
width: 80,
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.rotate_left,
|
||||
color: Theme.of(context).iconTheme.color!.withOpacity(0.8),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(2)),
|
||||
Text(
|
||||
S.of(context).rotateLeft,
|
||||
style: subtitle2.copyWith(
|
||||
color: subtitle2.color!.withOpacity(0.8),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRotateRightButton() {
|
||||
final TextStyle subtitle2 = Theme.of(context).textTheme.titleSmall!;
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
rotate(true);
|
||||
},
|
||||
child: SizedBox(
|
||||
width: 80,
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.rotate_right,
|
||||
color: Theme.of(context).iconTheme.color!.withOpacity(0.8),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(2)),
|
||||
Text(
|
||||
S.of(context).rotateRight,
|
||||
style: subtitle2.copyWith(
|
||||
color: subtitle2.color!.withOpacity(0.8),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSaveButton() {
|
||||
final TextStyle subtitle2 = Theme.of(context).textTheme.titleSmall!;
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
_saveEdits();
|
||||
},
|
||||
child: SizedBox(
|
||||
width: 80,
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.save_alt_outlined,
|
||||
color: Theme.of(context).iconTheme.color!.withOpacity(0.8),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(2)),
|
||||
Text(
|
||||
S.of(context).saveCopy,
|
||||
style: subtitle2.copyWith(
|
||||
color: subtitle2.color!.withOpacity(0.8),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _saveEdits() async {
|
||||
final dialog = createProgressDialog(context, S.of(context).saving);
|
||||
await dialog.show();
|
||||
final ExtendedImageEditorState? state = editorKey.currentState;
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
final Rect? rect = state.getCropRect();
|
||||
if (rect == null) {
|
||||
return;
|
||||
}
|
||||
final EditActionDetails action = state.editAction!;
|
||||
final double radian = action.rotateAngle;
|
||||
|
||||
final bool flipHorizontal = action.flipY;
|
||||
final bool flipVertical = action.flipX;
|
||||
final Uint8List img = state.rawImageData;
|
||||
|
||||
// ignore: unnecessary_null_comparison
|
||||
if (img == null) {
|
||||
_logger.severe("null rawImageData");
|
||||
showToast(context, S.of(context).somethingWentWrong);
|
||||
return;
|
||||
}
|
||||
|
||||
final ImageEditorOption option = ImageEditorOption();
|
||||
|
||||
option.addOption(ClipOption.fromRect(rect));
|
||||
option.addOption(
|
||||
FlipOption(horizontal: flipHorizontal, vertical: flipVertical),
|
||||
);
|
||||
if (action.hasRotateAngle) {
|
||||
option.addOption(RotateOption(radian.toInt()));
|
||||
}
|
||||
|
||||
option.addOption(ColorOption.saturation(_saturation!));
|
||||
option.addOption(ColorOption.brightness(_brightness!));
|
||||
|
||||
option.outputFormat = const OutputFormat.jpeg(100);
|
||||
|
||||
final DateTime start = DateTime.now();
|
||||
Uint8List? result = await ImageEditor.editImage(
|
||||
image: img,
|
||||
imageEditorOption: option,
|
||||
);
|
||||
if (result == null) {
|
||||
_logger.severe("null result");
|
||||
showToast(context, S.of(context).somethingWentWrong);
|
||||
return;
|
||||
}
|
||||
_logger.info('Size before compression = ${result.length}');
|
||||
|
||||
final ui.Image decodedResult = await decodeImageFromList(result);
|
||||
result = await FlutterImageCompress.compressWithList(
|
||||
result,
|
||||
minWidth: decodedResult.width,
|
||||
minHeight: decodedResult.height,
|
||||
);
|
||||
_logger.info('Size after compression = ${result.length}');
|
||||
final Duration diff = DateTime.now().difference(start);
|
||||
_logger.info('image_editor time : $diff');
|
||||
|
||||
try {
|
||||
final fileName =
|
||||
path.basenameWithoutExtension(widget.originalFile.title!) +
|
||||
"_edited_" +
|
||||
DateTime.now().microsecondsSinceEpoch.toString() +
|
||||
".JPEG";
|
||||
//Disabling notifications for assets changing to insert the file into
|
||||
//files db before triggering a sync.
|
||||
await PhotoManager.stopChangeNotify();
|
||||
final AssetEntity newAsset =
|
||||
await (PhotoManager.editor.saveImage(result, filename: fileName));
|
||||
final newFile = await ente.EnteFile.fromAsset(
|
||||
widget.originalFile.deviceFolder ?? '',
|
||||
newAsset,
|
||||
);
|
||||
|
||||
newFile.creationTime = widget.originalFile.creationTime;
|
||||
newFile.collectionID = widget.originalFile.collectionID;
|
||||
newFile.location = widget.originalFile.location;
|
||||
if (!newFile.hasLocation && widget.originalFile.localID != null) {
|
||||
final assetEntity = await widget.originalFile.getAsset;
|
||||
if (assetEntity != null) {
|
||||
final latLong = await assetEntity.latlngAsync();
|
||||
newFile.location = Location(
|
||||
latitude: latLong.latitude,
|
||||
longitude: latLong.longitude,
|
||||
);
|
||||
}
|
||||
}
|
||||
newFile.generatedID = await FilesDB.instance.insertAndGetId(newFile);
|
||||
Bus.instance.fire(LocalPhotosUpdatedEvent([newFile], source: "editSave"));
|
||||
unawaited(SyncService.instance.sync());
|
||||
showShortToast(context, S.of(context).editsSaved);
|
||||
_logger.info("Original file " + widget.originalFile.toString());
|
||||
_logger.info("Saved edits to file " + newFile.toString());
|
||||
final files = widget.detailPageConfig.files;
|
||||
|
||||
// the index could be -1 if the files fetched doesn't contain the newly
|
||||
// edited files
|
||||
int selectionIndex =
|
||||
files.indexWhere((file) => file.generatedID == newFile.generatedID);
|
||||
if (selectionIndex == -1) {
|
||||
files.add(newFile);
|
||||
selectionIndex = files.length - 1;
|
||||
}
|
||||
await dialog.hide();
|
||||
replacePage(
|
||||
context,
|
||||
DetailPage(
|
||||
widget.detailPageConfig.copyWith(
|
||||
files: files,
|
||||
selectedIndex: min(selectionIndex, files.length - 1),
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e, s) {
|
||||
await dialog.hide();
|
||||
showToast(context, S.of(context).oopsCouldNotSaveEdits);
|
||||
_logger.severe(e, s);
|
||||
} finally {
|
||||
await PhotoManager.startChangeNotify();
|
||||
}
|
||||
}
|
||||
|
||||
void flip() {
|
||||
editorKey.currentState?.flip();
|
||||
}
|
||||
|
||||
void rotate(bool right) {
|
||||
editorKey.currentState?.rotate(right: right);
|
||||
}
|
||||
|
||||
Widget _buildSat() {
|
||||
final TextStyle subtitle2 = Theme.of(context).textTheme.titleSmall!;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 42,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
S.of(context).color,
|
||||
style: subtitle2.copyWith(
|
||||
color: subtitle2.color!.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SfSliderTheme(
|
||||
data: SfSliderThemeData(
|
||||
activeTrackHeight: 4,
|
||||
inactiveTrackHeight: 2,
|
||||
inactiveTrackColor: Colors.grey[900],
|
||||
activeTrackColor: const Color.fromRGBO(45, 150, 98, 1),
|
||||
thumbColor: const Color.fromRGBO(45, 150, 98, 1),
|
||||
thumbRadius: 10,
|
||||
tooltipBackgroundColor: Colors.grey[900],
|
||||
),
|
||||
child: SfSlider(
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_saturation = value;
|
||||
});
|
||||
},
|
||||
value: _saturation,
|
||||
enableTooltip: true,
|
||||
stepSize: 0.01,
|
||||
min: kSaturationMin,
|
||||
max: kSaturationMax,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBrightness() {
|
||||
final TextStyle subtitle2 = Theme.of(context).textTheme.titleSmall!;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 42,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
S.of(context).light,
|
||||
style: subtitle2.copyWith(
|
||||
color: subtitle2.color!.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SfSliderTheme(
|
||||
data: SfSliderThemeData(
|
||||
activeTrackHeight: 4,
|
||||
inactiveTrackHeight: 2,
|
||||
activeTrackColor: const Color.fromRGBO(45, 150, 98, 1),
|
||||
inactiveTrackColor: Colors.grey[900],
|
||||
thumbColor: const Color.fromRGBO(45, 150, 98, 1),
|
||||
thumbRadius: 10,
|
||||
tooltipBackgroundColor: Colors.grey[900],
|
||||
),
|
||||
child: SfSlider(
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_brightness = value;
|
||||
});
|
||||
},
|
||||
value: _brightness,
|
||||
enableTooltip: true,
|
||||
stepSize: 0.01,
|
||||
min: kBrightnessMin,
|
||||
max: kBrightnessMax,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showExitConfirmationDialog(BuildContext context) async {
|
||||
final actionResult = await showActionSheet(
|
||||
context: context,
|
||||
buttons: [
|
||||
ButtonWidget(
|
||||
labelText: S.of(context).yesDiscardChanges,
|
||||
buttonType: ButtonType.critical,
|
||||
buttonSize: ButtonSize.large,
|
||||
shouldStickToDarkTheme: true,
|
||||
buttonAction: ButtonAction.first,
|
||||
isInAlert: true,
|
||||
),
|
||||
ButtonWidget(
|
||||
labelText: S.of(context).no,
|
||||
buttonType: ButtonType.secondary,
|
||||
buttonSize: ButtonSize.large,
|
||||
buttonAction: ButtonAction.second,
|
||||
shouldStickToDarkTheme: true,
|
||||
isInAlert: true,
|
||||
),
|
||||
],
|
||||
body: S.of(context).doYouWantToDiscardTheEditsYouHaveMade,
|
||||
actionSheetType: ActionSheetType.defaultActionSheet,
|
||||
);
|
||||
if (actionResult?.action != null &&
|
||||
actionResult!.action == ButtonAction.first) {
|
||||
replacePage(context, DetailPage(widget.detailPageConfig));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,8 @@ import "package:photos/services/local_authentication_service.dart";
|
||||
import "package:photos/states/detail_page_state.dart";
|
||||
import "package:photos/ui/common/fast_scroll_physics.dart";
|
||||
import 'package:photos/ui/notification/toast.dart';
|
||||
import "package:photos/ui/tools/editor/image_editor/image_editor_page.dart";
|
||||
import "package:photos/ui/tools/editor/image_editor/image_editor_page_new.dart";
|
||||
import 'package:photos/ui/tools/editor/image_editor_page.dart';
|
||||
import "package:photos/ui/tools/editor/video_editor_page.dart";
|
||||
import "package:photos/ui/viewer/file/file_app_bar.dart";
|
||||
import "package:photos/ui/viewer/file/file_bottom_bar.dart";
|
||||
@@ -63,30 +64,16 @@ class DetailPageConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
class DetailPage extends StatelessWidget {
|
||||
class DetailPage extends StatefulWidget {
|
||||
final DetailPageConfiguration config;
|
||||
|
||||
const DetailPage(this.config, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Separating body to a different widget to avoid
|
||||
// unnecessary reinitialization of the InheritedDetailPageState
|
||||
// when the body is rebuilt, which can reset state stored in it.
|
||||
return InheritedDetailPageState(child: _Body(config));
|
||||
}
|
||||
State<DetailPage> createState() => _DetailPageState();
|
||||
}
|
||||
|
||||
class _Body extends StatefulWidget {
|
||||
final DetailPageConfiguration config;
|
||||
|
||||
const _Body(this.config);
|
||||
|
||||
@override
|
||||
State<_Body> createState() => _BodyState();
|
||||
}
|
||||
|
||||
class _BodyState extends State<_Body> {
|
||||
class _DetailPageState extends State<DetailPage> {
|
||||
final _logger = Logger("DetailPageState");
|
||||
bool _shouldDisableScroll = false;
|
||||
List<EnteFile>? _files;
|
||||
@@ -150,100 +137,102 @@ class _BodyState extends State<_Body> {
|
||||
_files!.length.toString() +
|
||||
" files .",
|
||||
);
|
||||
return PopScope(
|
||||
canPop: !isGuestView,
|
||||
onPopInvokedWithResult: (didPop, _) async {
|
||||
if (isGuestView) {
|
||||
final authenticated = await _requestAuthentication();
|
||||
if (authenticated) {
|
||||
Bus.instance.fire(GuestViewEvent(false, false));
|
||||
await localSettings.setOnGuestView(false);
|
||||
return InheritedDetailPageState(
|
||||
child: PopScope(
|
||||
canPop: !isGuestView,
|
||||
onPopInvokedWithResult: (didPop, _) async {
|
||||
if (isGuestView) {
|
||||
final authenticated = await _requestAuthentication();
|
||||
if (authenticated) {
|
||||
Bus.instance.fire(GuestViewEvent(false, false));
|
||||
await localSettings.setOnGuestView(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(80),
|
||||
child: ValueListenableBuilder(
|
||||
builder: (BuildContext context, int selectedIndex, _) {
|
||||
return FileAppBar(
|
||||
_files![selectedIndex],
|
||||
_onFileRemoved,
|
||||
widget.config.mode == DetailPageMode.full,
|
||||
enableFullScreenNotifier: InheritedDetailPageState.of(context)
|
||||
.enableFullScreenNotifier,
|
||||
);
|
||||
},
|
||||
valueListenable: _selectedIndexNotifier,
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(80),
|
||||
child: ValueListenableBuilder(
|
||||
builder: (BuildContext context, int selectedIndex, _) {
|
||||
return FileAppBar(
|
||||
_files![selectedIndex],
|
||||
_onFileRemoved,
|
||||
widget.config.mode == DetailPageMode.full,
|
||||
enableFullScreenNotifier: InheritedDetailPageState.of(context)
|
||||
.enableFullScreenNotifier,
|
||||
);
|
||||
},
|
||||
valueListenable: _selectedIndexNotifier,
|
||||
),
|
||||
),
|
||||
),
|
||||
extendBodyBehindAppBar: true,
|
||||
resizeToAvoidBottomInset: false,
|
||||
backgroundColor: Colors.black,
|
||||
body: Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildPageView(),
|
||||
ValueListenableBuilder(
|
||||
builder: (BuildContext context, int selectedIndex, _) {
|
||||
return FileBottomBar(
|
||||
_files![selectedIndex],
|
||||
_onEditFileRequested,
|
||||
widget.config.mode == DetailPageMode.minimalistic &&
|
||||
!isGuestView,
|
||||
onFileRemoved: _onFileRemoved,
|
||||
userID: Configuration.instance.getUserID(),
|
||||
enableFullScreenNotifier:
|
||||
InheritedDetailPageState.of(context)
|
||||
extendBodyBehindAppBar: true,
|
||||
resizeToAvoidBottomInset: false,
|
||||
backgroundColor: Colors.black,
|
||||
body: Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildPageView(),
|
||||
ValueListenableBuilder(
|
||||
builder: (BuildContext context, int selectedIndex, _) {
|
||||
return FileBottomBar(
|
||||
_files![selectedIndex],
|
||||
_onNewImageEditor,
|
||||
widget.config.mode == DetailPageMode.minimalistic &&
|
||||
!isGuestView,
|
||||
onFileRemoved: _onFileRemoved,
|
||||
userID: Configuration.instance.getUserID(),
|
||||
enableFullScreenNotifier:
|
||||
InheritedDetailPageState.of(context)
|
||||
.enableFullScreenNotifier,
|
||||
);
|
||||
},
|
||||
valueListenable: _selectedIndexNotifier,
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _selectedIndexNotifier,
|
||||
builder: (BuildContext context, int selectedIndex, _) {
|
||||
if (_files![selectedIndex].isPanorama() == true) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: InheritedDetailPageState.of(context)
|
||||
.enableFullScreenNotifier,
|
||||
);
|
||||
},
|
||||
valueListenable: _selectedIndexNotifier,
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _selectedIndexNotifier,
|
||||
builder: (BuildContext context, int selectedIndex, _) {
|
||||
if (_files![selectedIndex].isPanorama() == true) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: InheritedDetailPageState.of(context)
|
||||
.enableFullScreenNotifier,
|
||||
builder: (context, value, child) {
|
||||
return IgnorePointer(
|
||||
ignoring: value,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity: !value ? 1.0 : 0.0,
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Tooltip(
|
||||
message: S.of(context).panorama,
|
||||
child: IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: const Color(0xAA252525),
|
||||
fixedSize: const Size(44, 44),
|
||||
builder: (context, value, child) {
|
||||
return IgnorePointer(
|
||||
ignoring: value,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity: !value ? 1.0 : 0.0,
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Tooltip(
|
||||
message: S.of(context).panorama,
|
||||
child: IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: const Color(0xAA252525),
|
||||
fixedSize: const Size(44, 44),
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.threesixty,
|
||||
color: Colors.white,
|
||||
size: 26,
|
||||
),
|
||||
onPressed: () async {
|
||||
await openPanoramaViewerPage(
|
||||
_files![selectedIndex],
|
||||
);
|
||||
},
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.threesixty,
|
||||
color: Colors.white,
|
||||
size: 26,
|
||||
),
|
||||
onPressed: () async {
|
||||
await openPanoramaViewerPage(
|
||||
_files![selectedIndex],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -369,7 +358,7 @@ class _BodyState extends State<_Body> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onEditFileRequested(EnteFile file) async {
|
||||
Future<void> _onNewImageEditor(EnteFile file) async {
|
||||
if (file.uploadedFileID != null &&
|
||||
file.ownerID != Configuration.instance.getUserID()) {
|
||||
_logger.severe(
|
||||
@@ -416,7 +405,7 @@ class _BodyState extends State<_Body> {
|
||||
await dialog.hide();
|
||||
replacePage(
|
||||
context,
|
||||
ImageEditorPage(
|
||||
NewImageEditor(
|
||||
originalFile: file,
|
||||
file: ioFile,
|
||||
detailPageConfig: widget.config.copyWith(
|
||||
@@ -431,6 +420,67 @@ class _BodyState extends State<_Body> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onEditFileRequested(EnteFile file) async {
|
||||
if (file.uploadedFileID != null &&
|
||||
file.ownerID != Configuration.instance.getUserID()) {
|
||||
_logger.severe(
|
||||
"Attempt to edit unowned file",
|
||||
UnauthorizedEditError(),
|
||||
StackTrace.current,
|
||||
);
|
||||
// ignore: unawaited_futures
|
||||
showErrorDialog(
|
||||
context,
|
||||
S.of(context).sorry,
|
||||
S.of(context).weDontSupportEditingPhotosAndAlbumsThatYouDont,
|
||||
);
|
||||
return;
|
||||
}
|
||||
final dialog = createProgressDialog(context, S.of(context).pleaseWait);
|
||||
await dialog.show();
|
||||
try {
|
||||
final ioFile = await getFile(file);
|
||||
if (ioFile == null) {
|
||||
showShortToast(context, S.of(context).failedToFetchOriginalForEdit);
|
||||
await dialog.hide();
|
||||
return;
|
||||
}
|
||||
if (file.fileType == FileType.video) {
|
||||
await dialog.hide();
|
||||
replacePage(
|
||||
context,
|
||||
VideoEditorPage(
|
||||
file: file,
|
||||
ioFile: ioFile,
|
||||
detailPageConfig: widget.config.copyWith(
|
||||
files: _files,
|
||||
selectedIndex: _selectedIndexNotifier.value,
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final imageProvider =
|
||||
ExtendedFileImageProvider(ioFile, cacheRawData: true);
|
||||
await precacheImage(imageProvider, context);
|
||||
await dialog.hide();
|
||||
replacePage(
|
||||
context,
|
||||
ImageEditorPage(
|
||||
imageProvider,
|
||||
file,
|
||||
widget.config.copyWith(
|
||||
files: _files,
|
||||
selectedIndex: _selectedIndexNotifier.value,
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
await dialog.hide();
|
||||
_logger.warning("Failed to initiate edit", e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _requestAuthentication() async {
|
||||
return await LocalAuthenticationService.instance.requestLocalAuthentication(
|
||||
context,
|
||||
|
||||
@@ -151,13 +151,7 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
|
||||
Widget? image;
|
||||
if (_imageProvider != null) {
|
||||
image = Image(
|
||||
image: optimizedImageHeight != null || optimizedImageWidth != null
|
||||
? ResizeImage(
|
||||
_imageProvider!,
|
||||
width: optimizedImageWidth,
|
||||
height: optimizedImageHeight,
|
||||
)
|
||||
: _imageProvider!,
|
||||
image: _imageProvider!,
|
||||
fit: widget.fit,
|
||||
);
|
||||
}
|
||||
@@ -251,7 +245,11 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
|
||||
final cachedSmallThumbnail =
|
||||
ThumbnailInMemoryLruCache.get(widget.file, thumbnailSmallSize);
|
||||
if (cachedSmallThumbnail != null) {
|
||||
_imageProvider = Image.memory(cachedSmallThumbnail).image;
|
||||
_imageProvider = Image.memory(
|
||||
cachedSmallThumbnail,
|
||||
cacheHeight: optimizedImageHeight,
|
||||
cacheWidth: optimizedImageWidth,
|
||||
).image;
|
||||
_hasLoadedThumbnail = true;
|
||||
} else {
|
||||
if (widget.diskLoadDeferDuration != null) {
|
||||
@@ -296,7 +294,11 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
final imageProvider = Image.memory(thumbData).image;
|
||||
final imageProvider = Image.memory(
|
||||
thumbData,
|
||||
cacheHeight: optimizedImageHeight,
|
||||
cacheWidth: optimizedImageWidth,
|
||||
).image;
|
||||
_cacheAndRender(imageProvider);
|
||||
}
|
||||
ThumbnailInMemoryLruCache.put(
|
||||
@@ -381,10 +383,15 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
|
||||
_isLoadingRemoteThumbnail = true;
|
||||
final cachedThumbnail = ThumbnailInMemoryLruCache.get(widget.file);
|
||||
if (cachedThumbnail != null) {
|
||||
_imageProvider = Image.memory(cachedThumbnail).image;
|
||||
_imageProvider = Image.memory(
|
||||
cachedThumbnail,
|
||||
cacheHeight: optimizedImageHeight,
|
||||
cacheWidth: optimizedImageWidth,
|
||||
).image;
|
||||
_hasLoadedThumbnail = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (widget.serverLoadDeferDuration != null) {
|
||||
Future.delayed(widget.serverLoadDeferDuration!, () {
|
||||
if (mounted) {
|
||||
@@ -401,7 +408,11 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
|
||||
try {
|
||||
final thumbnail = await getThumbnailFromServer(widget.file);
|
||||
if (mounted) {
|
||||
final imageProvider = Image.memory(thumbnail).image;
|
||||
final imageProvider = Image.memory(
|
||||
thumbnail,
|
||||
cacheHeight: optimizedImageHeight,
|
||||
cacheWidth: optimizedImageWidth,
|
||||
).image;
|
||||
_cacheAndRender(imageProvider);
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -42,12 +42,6 @@ class SectionedListSliver<T> extends StatelessWidget {
|
||||
sectionLayouts: sectionLayouts,
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
//TODO:
|
||||
// This could be optimized by using a combination of
|
||||
//linear search and binary search depending on the index (use linear
|
||||
//if index is small) or keep track on lastIndex of section and
|
||||
//go to next section after the last index.
|
||||
// Check if the optimization is required.
|
||||
if (index >= childCount) return null;
|
||||
final sectionLayout = sectionLayouts
|
||||
.firstWhereOrNull((section) => section.hasChild(index));
|
||||
|
||||
@@ -600,6 +600,7 @@ class GalleryState extends State<Gallery> {
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: const ExponentialBouncingScrollPhysics(),
|
||||
controller: _scrollController,
|
||||
cacheExtent: galleryCacheExtent,
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: SizeChangedLayoutNotifier(
|
||||
@@ -637,6 +638,25 @@ class GalleryState extends State<Gallery> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double get galleryCacheExtent {
|
||||
final int photoGridSize = localSettings.getPhotoGridSize();
|
||||
switch (photoGridSize) {
|
||||
case 2:
|
||||
case 3:
|
||||
return 1000;
|
||||
case 4:
|
||||
return 850;
|
||||
case 5:
|
||||
return 600;
|
||||
case 6:
|
||||
return 300;
|
||||
default:
|
||||
throw StateError(
|
||||
'Invalid photo grid size configuration: $photoGridSize',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PinnedGroupHeader extends StatefulWidget {
|
||||
|
||||
@@ -5,10 +5,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
|
||||
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "76.0.0"
|
||||
version: "72.0.0"
|
||||
_flutterfire_internals:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -21,7 +21,7 @@ packages:
|
||||
dependency: transitive
|
||||
description: dart
|
||||
source: sdk
|
||||
version: "0.3.3"
|
||||
version: "0.3.2"
|
||||
adaptive_theme:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -34,10 +34,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
|
||||
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.11.0"
|
||||
version: "6.7.0"
|
||||
android_intent_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -317,10 +317,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.0"
|
||||
version: "1.18.0"
|
||||
computer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1271,6 +1271,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.3.0"
|
||||
image_editor:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_editor
|
||||
sha256: "38070067264fd9fea4328ca630d2ff7bd65ebe6aa4ed375d983b732d2ae7146b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
image_editor_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_editor_common
|
||||
sha256: "93d2f5c8b636f862775dd62a9ec20d09c8272598daa02f935955a4640e1844ee"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
image_editor_ohos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_editor_ohos
|
||||
sha256: "06756859586d5acefec6e3b4f356f9b1ce05ef09213bcb9a0ce1680ecea2d054"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.9"
|
||||
image_editor_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_editor_platform_interface
|
||||
sha256: "474517efc770464f7d99942472d8cfb369a3c378e95466ec17f74d2b80bd40de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
in_app_purchase:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1392,18 +1424,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
|
||||
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.7"
|
||||
version: "10.0.5"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
|
||||
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.8"
|
||||
version: "3.0.5"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1512,10 +1544,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: macros
|
||||
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
|
||||
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3-main.0"
|
||||
version: "0.1.2-main.4"
|
||||
maps_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1554,24 +1586,24 @@ packages:
|
||||
description:
|
||||
path: media_kit
|
||||
ref: HEAD
|
||||
resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1
|
||||
resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
|
||||
url: "https://github.com/media-kit/media-kit"
|
||||
source: git
|
||||
version: "1.2.0"
|
||||
version: "1.1.11"
|
||||
media_kit_libs_android_video:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_libs_android_video
|
||||
sha256: adff9b571b8ead0867f9f91070f8df39562078c0eb3371d88b9029a2d547d7b7
|
||||
sha256: "9dd8012572e4aff47516e55f2597998f0a378e3d588d0fad0ca1f11a53ae090c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.7"
|
||||
version: "1.3.6"
|
||||
media_kit_libs_ios_video:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "libs/ios/media_kit_libs_ios_video"
|
||||
ref: HEAD
|
||||
resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1
|
||||
resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
|
||||
url: "https://github.com/media-kit/media-kit"
|
||||
source: git
|
||||
version: "1.1.4"
|
||||
@@ -1579,10 +1611,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_libs_linux
|
||||
sha256: "2b473399a49ec94452c4d4ae51cfc0f6585074398d74216092bf3d54aac37ecf"
|
||||
sha256: e186891c31daa6bedab4d74dcdb4e8adfccc7d786bfed6ad81fe24a3b3010310
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
version: "1.1.3"
|
||||
media_kit_libs_macos_video:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1596,27 +1628,27 @@ packages:
|
||||
description:
|
||||
path: "libs/universal/media_kit_libs_video"
|
||||
ref: HEAD
|
||||
resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1
|
||||
resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
|
||||
url: "https://github.com/media-kit/media-kit"
|
||||
source: git
|
||||
version: "1.0.6"
|
||||
version: "1.0.5"
|
||||
media_kit_libs_windows_video:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_libs_windows_video
|
||||
sha256: dff76da2778729ab650229e6b4ec6ec111eb5151431002cbd7ea304ff1f112ab
|
||||
sha256: "32654572167825c42c55466f5d08eee23ea11061c84aa91b09d0e0f69bdd0887"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.11"
|
||||
version: "1.0.10"
|
||||
media_kit_video:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: media_kit_video
|
||||
ref: HEAD
|
||||
resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1
|
||||
resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
|
||||
url: "https://github.com/media-kit/media-kit"
|
||||
source: git
|
||||
version: "1.3.0"
|
||||
version: "1.2.5"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1680,7 +1712,7 @@ packages:
|
||||
description:
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: "64e47a446bf3b64f012f2076481cebea51ca27cf"
|
||||
resolved-ref: "7814e2c61ee1fa74cef73b946eb08519c35bdaa5"
|
||||
url: "https://github.com/ente-io/motionphoto.git"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
@@ -2293,7 +2325,7 @@ packages:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
version: "0.0.99"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2418,10 +2450,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.0"
|
||||
version: "1.11.1"
|
||||
step_progress_indicator:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2450,10 +2482,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.2.0"
|
||||
styled_text:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2514,26 +2546,26 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: test
|
||||
sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f"
|
||||
sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.25.8"
|
||||
version: "1.25.7"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
||||
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.3"
|
||||
version: "0.7.2"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d"
|
||||
sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.5"
|
||||
version: "0.6.4"
|
||||
thermal:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2813,10 +2845,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
|
||||
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.3.0"
|
||||
version: "14.2.5"
|
||||
volume_controller:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2877,10 +2909,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webdriver
|
||||
sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8"
|
||||
sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.4"
|
||||
version: "3.0.3"
|
||||
webkit_inspection_protocol:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -12,7 +12,7 @@ description: ente photos application
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
|
||||
version: 1.2.0+1200
|
||||
version: 1.2.0+1203
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
@@ -114,6 +114,7 @@ dependencies:
|
||||
html_unescape: ^2.0.0
|
||||
http: ^1.1.0
|
||||
image: ^4.0.17
|
||||
image_editor: ^1.6.0
|
||||
in_app_purchase: ^3.0.7
|
||||
intl: ^0.19.0
|
||||
latlong2: ^0.9.0
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import React from 'react';
|
||||
import { NavbarBase } from 'ente-base/components/Navbar';
|
||||
import { NormalNavbarContents, HiddenSectionNavbarContents } from './NavbarContents';
|
||||
import type { EnteFile } from 'ente-media/file';
|
||||
import type { SelectedState } from 'utils/file';
|
||||
|
||||
interface GalleryContainerProps {
|
||||
// State props
|
||||
filteredFiles: EnteFile[];
|
||||
selected: SelectedState;
|
||||
barMode: string;
|
||||
isInSearchMode: boolean;
|
||||
isFirstLoad: boolean;
|
||||
activeCollectionID: number | undefined;
|
||||
|
||||
// Event handlers
|
||||
onSidebar: () => void;
|
||||
onUpload: () => void;
|
||||
onSelectSearchOption: (option: unknown) => void;
|
||||
onSelectPeople: () => void;
|
||||
onSelectPerson: (personID: string) => void;
|
||||
onClearSelection: () => void;
|
||||
onChangeMode: (mode: string) => void;
|
||||
|
||||
// Children for flexible rendering
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified gallery container component that handles the basic layout
|
||||
*/
|
||||
export const GalleryContainer: React.FC<GalleryContainerProps> = ({
|
||||
selected,
|
||||
barMode,
|
||||
isInSearchMode,
|
||||
activeCollectionID,
|
||||
onSidebar,
|
||||
onUpload,
|
||||
onSelectSearchOption,
|
||||
onSelectPeople,
|
||||
onSelectPerson,
|
||||
onChangeMode,
|
||||
children,
|
||||
}) => {
|
||||
const showSelectionBar = selected.count > 0 && selected.collectionID === activeCollectionID;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Navigation Bar */}
|
||||
<NavbarBase
|
||||
sx={[
|
||||
{
|
||||
mb: "12px",
|
||||
px: "24px",
|
||||
"@media (width < 720px)": { px: "4px" },
|
||||
},
|
||||
showSelectionBar && { borderColor: "accent.main" },
|
||||
]}
|
||||
>
|
||||
{showSelectionBar ? (
|
||||
<div>Selection Bar Placeholder</div>
|
||||
) : barMode === "hidden-albums" ? (
|
||||
<HiddenSectionNavbarContents
|
||||
onBack={() => onChangeMode("albums")}
|
||||
/>
|
||||
) : (
|
||||
<NormalNavbarContents
|
||||
isInSearchMode={isInSearchMode}
|
||||
onSidebar={onSidebar}
|
||||
onUpload={onUpload}
|
||||
onShowSearchInput={() => {
|
||||
// TODO: Implement search mode toggle
|
||||
}}
|
||||
onSelectSearchOption={onSelectSearchOption}
|
||||
onSelectPeople={onSelectPeople}
|
||||
onSelectPerson={onSelectPerson}
|
||||
/>
|
||||
)}
|
||||
</NavbarBase>
|
||||
|
||||
{/* Main Content Area */}
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,65 +0,0 @@
|
||||
import React from 'react';
|
||||
import { GalleryEmptyState, PeopleEmptyState } from 'ente-new/photos/components/gallery';
|
||||
import { uploadManager } from 'services/upload-manager';
|
||||
import { PseudoCollectionID } from 'ente-new/photos/services/collection-summary';
|
||||
import type { EnteFile } from 'ente-media/file';
|
||||
import type { GalleryBarMode } from 'ente-new/photos/components/gallery/reducer';
|
||||
|
||||
interface GalleryContentProps {
|
||||
// Basic state
|
||||
filteredFiles: EnteFile[];
|
||||
isInSearchMode: boolean;
|
||||
isFirstLoad: boolean;
|
||||
barMode: GalleryBarMode;
|
||||
activeCollectionID: number | undefined;
|
||||
activePerson: unknown;
|
||||
|
||||
// Event handlers
|
||||
onUpload: () => void;
|
||||
|
||||
// Children for flexible content
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified main content area that handles empty states and delegates to children
|
||||
*/
|
||||
export const GalleryContent: React.FC<GalleryContentProps> = ({
|
||||
filteredFiles,
|
||||
isInSearchMode,
|
||||
isFirstLoad,
|
||||
barMode,
|
||||
activeCollectionID,
|
||||
activePerson,
|
||||
onUpload,
|
||||
children,
|
||||
}) => {
|
||||
// Show empty states for specific conditions
|
||||
const showGalleryEmptyState =
|
||||
!isInSearchMode &&
|
||||
!isFirstLoad &&
|
||||
!filteredFiles.length &&
|
||||
activeCollectionID === PseudoCollectionID.all;
|
||||
|
||||
const showPeopleEmptyState =
|
||||
!isInSearchMode &&
|
||||
!isFirstLoad &&
|
||||
barMode === "people" &&
|
||||
!activePerson;
|
||||
|
||||
if (showGalleryEmptyState) {
|
||||
return (
|
||||
<GalleryEmptyState
|
||||
isUploadInProgress={uploadManager.isUploadInProgress()}
|
||||
onUpload={onUpload}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (showPeopleEmptyState) {
|
||||
return <PeopleEmptyState />;
|
||||
}
|
||||
|
||||
// Render children (FileListWithViewer, GalleryBarAndListHeader, etc.)
|
||||
return <>{children}</>;
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
import React from 'react';
|
||||
import { PlanSelector } from 'ente-new/photos/components/PlanSelector';
|
||||
import { SingleInputDialog } from 'ente-base/components/SingleInputDialog';
|
||||
import { WhatsNew } from 'ente-new/photos/components/WhatsNew';
|
||||
import { t } from 'i18next';
|
||||
|
||||
interface GalleryModalsProps {
|
||||
// Modal states - using simplified visibility props
|
||||
planSelectorVisible: boolean;
|
||||
whatsNewVisible: boolean;
|
||||
albumNameInputVisible: boolean;
|
||||
|
||||
// Modal handlers
|
||||
onClosePlanSelector: () => void;
|
||||
onCloseWhatsNew: () => void;
|
||||
onCloseAlbumNameInput: () => void;
|
||||
onAlbumNameSubmit: (name: string) => Promise<void>;
|
||||
setLoading: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified container for essential gallery modals
|
||||
*/
|
||||
export const GalleryModals: React.FC<GalleryModalsProps> = ({
|
||||
planSelectorVisible,
|
||||
whatsNewVisible,
|
||||
albumNameInputVisible,
|
||||
onClosePlanSelector,
|
||||
onCloseWhatsNew,
|
||||
onCloseAlbumNameInput,
|
||||
onAlbumNameSubmit,
|
||||
setLoading,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{/* Plan Selector Modal */}
|
||||
{planSelectorVisible && (
|
||||
<PlanSelector
|
||||
open={planSelectorVisible}
|
||||
onClose={onClosePlanSelector}
|
||||
setLoading={setLoading}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* What's New Dialog */}
|
||||
{whatsNewVisible && (
|
||||
<WhatsNew
|
||||
open={whatsNewVisible}
|
||||
onClose={onCloseWhatsNew}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Album Name Input Dialog */}
|
||||
{albumNameInputVisible && (
|
||||
<SingleInputDialog
|
||||
open={albumNameInputVisible}
|
||||
onClose={onCloseAlbumNameInput}
|
||||
title={t("new_album")}
|
||||
label={t("album_name")}
|
||||
submitButtonTitle={t("create")}
|
||||
onSubmit={onAlbumNameSubmit}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
import { CenteredRow } from 'ente-base/components/containers';
|
||||
import { TranslucentLoadingOverlay } from 'ente-base/components/loaders';
|
||||
import { t } from 'i18next';
|
||||
|
||||
/**
|
||||
* Message shown during first load to inform users about potential delays
|
||||
*/
|
||||
export const FirstLoadMessage: React.FC = () => (
|
||||
<CenteredRow>
|
||||
<Typography variant="small" sx={{ color: "text.muted" }}>
|
||||
{t("initial_load_delay_warning")}
|
||||
</Typography>
|
||||
</CenteredRow>
|
||||
);
|
||||
|
||||
/**
|
||||
* Message shown when the app is offline
|
||||
*/
|
||||
export const OfflineMessage: React.FC = () => (
|
||||
<Typography
|
||||
variant="small"
|
||||
sx={{ bgcolor: "background.paper", p: 2, mb: 1, textAlign: "center" }}
|
||||
>
|
||||
{t("offline_message")}
|
||||
</Typography>
|
||||
);
|
||||
|
||||
/**
|
||||
* Blocking overlay shown during certain operations
|
||||
*/
|
||||
export const BlockingLoadOverlay: React.FC<{ show: boolean }> = ({ show }) =>
|
||||
show ? <TranslucentLoadingOverlay /> : null;
|
||||
@@ -1,80 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Stack, Typography, IconButton } from '@mui/material';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
import FileUploadOutlinedIcon from '@mui/icons-material/FileUploadOutlined';
|
||||
import { FocusVisibleButton } from 'ente-base/components/mui/FocusVisibleButton';
|
||||
import { useIsSmallWidth } from 'ente-base/components/utils/hooks';
|
||||
import type { ButtonishProps } from 'ente-base/components/mui';
|
||||
import { SearchBar, type SearchBarProps } from 'ente-new/photos/components/SearchBar';
|
||||
import { uploadManager } from 'services/upload-manager';
|
||||
import { t } from 'i18next';
|
||||
|
||||
interface NormalNavbarContentsProps extends SearchBarProps {
|
||||
onSidebar: () => void;
|
||||
onUpload: () => void;
|
||||
}
|
||||
|
||||
export const NormalNavbarContents: React.FC<NormalNavbarContentsProps> = ({
|
||||
onSidebar,
|
||||
onUpload,
|
||||
...props
|
||||
}) => (
|
||||
<>
|
||||
{!props.isInSearchMode && <SidebarButton onClick={onSidebar} />}
|
||||
<SearchBar {...props} />
|
||||
{!props.isInSearchMode && <UploadButton onClick={onUpload} />}
|
||||
</>
|
||||
);
|
||||
|
||||
const SidebarButton: React.FC<ButtonishProps> = ({ onClick }) => (
|
||||
<IconButton {...{ onClick }}>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
);
|
||||
|
||||
const UploadButton: React.FC<ButtonishProps> = ({ onClick }) => {
|
||||
const disabled = uploadManager.isUploadInProgress();
|
||||
const isSmallWidth = useIsSmallWidth();
|
||||
|
||||
const icon = <FileUploadOutlinedIcon />;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isSmallWidth ? (
|
||||
<IconButton {...{ onClick, disabled }}>{icon}</IconButton>
|
||||
) : (
|
||||
<FocusVisibleButton
|
||||
color="secondary"
|
||||
startIcon={icon}
|
||||
{...{ onClick, disabled }}
|
||||
>
|
||||
{t("upload")}
|
||||
</FocusVisibleButton>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface HiddenSectionNavbarContentsProps {
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export const HiddenSectionNavbarContents: React.FC<
|
||||
HiddenSectionNavbarContentsProps
|
||||
> = ({ onBack }) => (
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={(theme) => ({
|
||||
gap: "24px",
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
background: theme.vars.palette.background.default,
|
||||
})}
|
||||
>
|
||||
<IconButton onClick={onBack}>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
<Typography sx={{ flex: 1 }}>{t("section_hidden")}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
@@ -1,6 +0,0 @@
|
||||
// Gallery presentational components
|
||||
export { GalleryContainer } from './GalleryContainer';
|
||||
export { GalleryContent } from './GalleryContent';
|
||||
export { GalleryModals } from './GalleryModals';
|
||||
export { NormalNavbarContents, HiddenSectionNavbarContents } from './NavbarContents';
|
||||
export { FirstLoadMessage, OfflineMessage, BlockingLoadOverlay } from './MessageComponents';
|
||||
@@ -1,11 +0,0 @@
|
||||
// Gallery hooks for managing different aspects of the gallery component
|
||||
export { useGalleryData } from './useGalleryData';
|
||||
export { useFileOperations } from './useFileOperations';
|
||||
export { useSelection } from './useSelection';
|
||||
export { useCollectionOperations } from './useCollectionOperations';
|
||||
export { useModalManagement } from './useModalManagement';
|
||||
export { useGalleryInitialization } from './useGalleryInitialization';
|
||||
export { useGalleryNavigation } from './useGalleryNavigation';
|
||||
|
||||
// Re-export types for convenience
|
||||
export type { RemotePullOpts } from './useGalleryData';
|
||||
@@ -1,167 +0,0 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import type { Collection } from 'ente-media/collection';
|
||||
import type { EnteFile } from 'ente-media/file';
|
||||
import type { SelectedState } from 'utils/file';
|
||||
import type { CollectionOp } from 'ente-new/photos/components/SelectedFileOptions';
|
||||
import type { CollectionSelectorAttributes } from 'ente-new/photos/components/CollectionSelector';
|
||||
import { createAlbum } from 'ente-new/photos/services/collection';
|
||||
import { performCollectionOp } from 'ente-new/photos/components/gallery/helpers';
|
||||
import { getSelectedFiles } from 'utils/file';
|
||||
import { usePhotosAppContext } from 'ente-new/photos/types/context';
|
||||
import { useBaseContext } from 'ente-base/context';
|
||||
import { notifyOthersFilesDialogAttributes } from 'ente-new/photos/components/utils/dialog-attributes';
|
||||
|
||||
interface UseCollectionOperationsProps {
|
||||
user: { id: number } | null;
|
||||
filteredFiles: EnteFile[];
|
||||
selected: SelectedState;
|
||||
clearSelection: () => void;
|
||||
onRemotePull: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for managing collection operations
|
||||
*/
|
||||
export const useCollectionOperations = ({
|
||||
user,
|
||||
filteredFiles,
|
||||
selected,
|
||||
clearSelection,
|
||||
onRemotePull,
|
||||
}: UseCollectionOperationsProps) => {
|
||||
const { showLoadingBar, hideLoadingBar } = usePhotosAppContext();
|
||||
const { onGenericError, showMiniDialog } = useBaseContext();
|
||||
|
||||
// Collection selector state
|
||||
const [openCollectionSelector, setOpenCollectionSelector] = useState(false);
|
||||
const [collectionSelectorAttributes, setCollectionSelectorAttributes] =
|
||||
useState<CollectionSelectorAttributes | undefined>();
|
||||
|
||||
// Album creation state
|
||||
const [postCreateAlbumOp, setPostCreateAlbumOp] = useState<CollectionOp | undefined>();
|
||||
|
||||
/**
|
||||
* Create a handler for collection operations (add, move)
|
||||
*/
|
||||
const createOnSelectForCollectionOp = useCallback(
|
||||
(op: CollectionOp) => (selectedCollection: Collection) => {
|
||||
void (async () => {
|
||||
showLoadingBar();
|
||||
try {
|
||||
setOpenCollectionSelector(false);
|
||||
const selectedFiles = getSelectedFiles(selected, filteredFiles);
|
||||
const userFiles = selectedFiles.filter(
|
||||
(f) => f.ownerID === user!.id,
|
||||
);
|
||||
const sourceCollectionID = selected.collectionID;
|
||||
|
||||
if (userFiles.length > 0) {
|
||||
await performCollectionOp(
|
||||
op,
|
||||
selectedCollection,
|
||||
userFiles,
|
||||
sourceCollectionID,
|
||||
);
|
||||
}
|
||||
|
||||
// Notify if some files couldn't be processed
|
||||
if (userFiles.length !== selectedFiles.length) {
|
||||
showMiniDialog(notifyOthersFilesDialogAttributes());
|
||||
}
|
||||
|
||||
clearSelection();
|
||||
await onRemotePull();
|
||||
} catch (e) {
|
||||
onGenericError(e);
|
||||
} finally {
|
||||
hideLoadingBar();
|
||||
}
|
||||
})();
|
||||
},
|
||||
[
|
||||
showLoadingBar,
|
||||
hideLoadingBar,
|
||||
selected,
|
||||
filteredFiles,
|
||||
user,
|
||||
showMiniDialog,
|
||||
clearSelection,
|
||||
onRemotePull,
|
||||
onGenericError,
|
||||
],
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a handler for collection operations that need album creation
|
||||
*/
|
||||
const createOnCreateForCollectionOp = useCallback(
|
||||
(op: CollectionOp) => {
|
||||
setPostCreateAlbumOp(op);
|
||||
return () => {
|
||||
// This will be handled by the album name input dialog
|
||||
// The actual creation happens in handleAlbumNameSubmit
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle album name submission after creation
|
||||
*/
|
||||
const handleAlbumNameSubmit = useCallback(
|
||||
async (name: string) => {
|
||||
if (!postCreateAlbumOp) return;
|
||||
|
||||
try {
|
||||
const collection = await createAlbum(name);
|
||||
// Execute the deferred operation
|
||||
createOnSelectForCollectionOp(postCreateAlbumOp)(collection);
|
||||
setPostCreateAlbumOp(undefined);
|
||||
} catch (e) {
|
||||
onGenericError(e);
|
||||
setPostCreateAlbumOp(undefined);
|
||||
}
|
||||
},
|
||||
[postCreateAlbumOp, createOnSelectForCollectionOp, onGenericError],
|
||||
);
|
||||
|
||||
/**
|
||||
* Open collection selector with specific attributes
|
||||
*/
|
||||
const handleOpenCollectionSelector = useCallback(
|
||||
(attributes: CollectionSelectorAttributes) => {
|
||||
setCollectionSelectorAttributes(attributes);
|
||||
setOpenCollectionSelector(true);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
/**
|
||||
* Close collection selector
|
||||
*/
|
||||
const handleCloseCollectionSelector = useCallback(
|
||||
() => {
|
||||
setOpenCollectionSelector(false);
|
||||
setCollectionSelectorAttributes(undefined);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
// State
|
||||
openCollectionSelector,
|
||||
collectionSelectorAttributes,
|
||||
postCreateAlbumOp,
|
||||
|
||||
// Handlers
|
||||
createOnSelectForCollectionOp,
|
||||
createOnCreateForCollectionOp,
|
||||
handleAlbumNameSubmit,
|
||||
handleOpenCollectionSelector,
|
||||
handleCloseCollectionSelector,
|
||||
|
||||
// Setters for external control
|
||||
setOpenCollectionSelector,
|
||||
setCollectionSelectorAttributes,
|
||||
};
|
||||
};
|
||||
@@ -1,183 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import type { EnteFile } from 'ente-media/file';
|
||||
import type { ItemVisibility } from 'ente-media/file-metadata';
|
||||
import type { Collection } from 'ente-media/collection';
|
||||
import {
|
||||
addToFavoritesCollection,
|
||||
removeFromFavoritesCollection,
|
||||
removeFromCollection
|
||||
} from 'ente-new/photos/services/collection';
|
||||
import { updateFilesVisibility } from 'ente-new/photos/services/file';
|
||||
import { getSelectedFiles, type SelectedState } from 'utils/file';
|
||||
import type { FileOp } from 'ente-new/photos/components/SelectedFileOptions';
|
||||
import { usePhotosAppContext } from 'ente-new/photos/types/context';
|
||||
import { useBaseContext } from 'ente-base/context';
|
||||
import { notifyOthersFilesDialogAttributes } from 'ente-new/photos/components/utils/dialog-attributes';
|
||||
|
||||
interface UseFileOperationsProps {
|
||||
user: { id: number };
|
||||
filteredFiles: EnteFile[];
|
||||
selected: SelectedState;
|
||||
clearSelection: () => void;
|
||||
onRemotePull: () => Promise<void>;
|
||||
dispatch: (action: { type: string; [key: string]: unknown }) => void;
|
||||
favoriteFileIDs: Set<number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for handling file operations
|
||||
*/
|
||||
export const useFileOperations = ({
|
||||
user,
|
||||
filteredFiles,
|
||||
selected,
|
||||
clearSelection,
|
||||
onRemotePull,
|
||||
dispatch,
|
||||
favoriteFileIDs,
|
||||
}: UseFileOperationsProps) => {
|
||||
const { showLoadingBar, hideLoadingBar } = usePhotosAppContext();
|
||||
const { onGenericError, showMiniDialog } = useBaseContext();
|
||||
|
||||
/**
|
||||
* Toggle favorite status of a file
|
||||
*/
|
||||
const handleToggleFavorite = useCallback(
|
||||
async (file: EnteFile) => {
|
||||
const fileID = file.id;
|
||||
const isFavorite = favoriteFileIDs.has(fileID);
|
||||
|
||||
dispatch({ type: "addPendingFavoriteUpdate", fileID });
|
||||
try {
|
||||
const action = isFavorite
|
||||
? removeFromFavoritesCollection
|
||||
: addToFavoritesCollection;
|
||||
await action([file]);
|
||||
dispatch({
|
||||
type: "unsyncedFavoriteUpdate",
|
||||
fileID,
|
||||
isFavorite: !isFavorite,
|
||||
});
|
||||
} finally {
|
||||
dispatch({ type: "removePendingFavoriteUpdate", fileID });
|
||||
}
|
||||
},
|
||||
[favoriteFileIDs, dispatch],
|
||||
);
|
||||
|
||||
/**
|
||||
* Update file visibility
|
||||
*/
|
||||
const handleFileVisibilityUpdate = useCallback(
|
||||
async (file: EnteFile, visibility: ItemVisibility) => {
|
||||
const fileID = file.id;
|
||||
dispatch({ type: "addPendingVisibilityUpdate", fileID });
|
||||
try {
|
||||
await updateFilesVisibility([file], visibility);
|
||||
dispatch({
|
||||
type: "unsyncedPrivateMagicMetadataUpdate",
|
||||
fileID,
|
||||
privateMagicMetadata: {
|
||||
...file.magicMetadata,
|
||||
count: file.magicMetadata?.count ?? 0,
|
||||
version: (file.magicMetadata?.version ?? 0) + 1,
|
||||
data: { ...file.magicMetadata?.data, visibility },
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
dispatch({ type: "removePendingVisibilityUpdate", fileID });
|
||||
}
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
/**
|
||||
* Remove files from a collection
|
||||
*/
|
||||
const handleRemoveFilesFromCollection = useCallback(
|
||||
async (collection: Collection) => {
|
||||
showLoadingBar();
|
||||
let notifyOthersFiles = false;
|
||||
try {
|
||||
const selectedFiles = getSelectedFiles(selected, filteredFiles);
|
||||
const processedCount = await removeFromCollection(
|
||||
collection,
|
||||
selectedFiles,
|
||||
);
|
||||
notifyOthersFiles = processedCount !== selectedFiles.length;
|
||||
clearSelection();
|
||||
await onRemotePull();
|
||||
} catch (e) {
|
||||
onGenericError(e);
|
||||
} finally {
|
||||
hideLoadingBar();
|
||||
}
|
||||
|
||||
if (notifyOthersFiles) {
|
||||
showMiniDialog(notifyOthersFilesDialogAttributes());
|
||||
}
|
||||
},
|
||||
[
|
||||
showLoadingBar,
|
||||
hideLoadingBar,
|
||||
selected,
|
||||
filteredFiles,
|
||||
clearSelection,
|
||||
onRemotePull,
|
||||
onGenericError,
|
||||
showMiniDialog,
|
||||
],
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a file operation handler
|
||||
*/
|
||||
const createFileOpHandler = useCallback(
|
||||
(op: FileOp) => () => {
|
||||
void (async () => {
|
||||
showLoadingBar();
|
||||
try {
|
||||
const selectedFiles = getSelectedFiles(selected, filteredFiles);
|
||||
const toProcessFiles = selectedFiles.filter(
|
||||
(file) => file.ownerID === user.id,
|
||||
);
|
||||
|
||||
if (toProcessFiles.length > 0) {
|
||||
// TODO: Implement proper file operations with correct typing
|
||||
// await performFileOp(op, toProcessFiles, ...callbacks);
|
||||
console.log('File operation:', op, toProcessFiles.length, 'files');
|
||||
}
|
||||
|
||||
if (toProcessFiles.length !== selectedFiles.length) {
|
||||
showMiniDialog(notifyOthersFilesDialogAttributes());
|
||||
}
|
||||
clearSelection();
|
||||
await onRemotePull();
|
||||
} catch (e) {
|
||||
onGenericError(e);
|
||||
} finally {
|
||||
hideLoadingBar();
|
||||
}
|
||||
})();
|
||||
},
|
||||
[
|
||||
showLoadingBar,
|
||||
hideLoadingBar,
|
||||
selected,
|
||||
filteredFiles,
|
||||
user,
|
||||
dispatch,
|
||||
showMiniDialog,
|
||||
clearSelection,
|
||||
onRemotePull,
|
||||
onGenericError,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
handleToggleFavorite,
|
||||
handleFileVisibilityUpdate,
|
||||
handleRemoveFilesFromCollection,
|
||||
createFileOpHandler,
|
||||
};
|
||||
};
|
||||
@@ -1,132 +0,0 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useGalleryReducer } from 'ente-new/photos/components/gallery/reducer';
|
||||
import { PromiseQueue } from 'ente-utils/promise';
|
||||
import { usePhotosAppContext } from 'ente-new/photos/types/context';
|
||||
import { useBaseContext } from 'ente-base/context';
|
||||
import {
|
||||
savedCollections,
|
||||
savedCollectionFiles,
|
||||
savedTrashItems
|
||||
} from 'ente-new/photos/services/photos-fdb';
|
||||
import { pullFiles, prePullFiles, postPullFiles } from 'ente-new/photos/services/pull';
|
||||
import { ensureLocalUser } from 'ente-accounts/services/user';
|
||||
import { savedUserDetailsOrTriggerPull } from 'ente-new/photos/services/user-details';
|
||||
import { masterKeyFromSession, clearSessionStorage } from 'ente-base/session';
|
||||
import { isSessionInvalid } from 'ente-accounts/services/session';
|
||||
import log from 'ente-base/log';
|
||||
import exportService from 'ente-new/photos/services/export';
|
||||
|
||||
export interface RemotePullOpts {
|
||||
silent?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for managing gallery data fetching and state
|
||||
*/
|
||||
export const useGalleryData = () => {
|
||||
const { showLoadingBar, hideLoadingBar } = usePhotosAppContext();
|
||||
const { onGenericError } = useBaseContext();
|
||||
const router = useRouter();
|
||||
|
||||
const [state, dispatch] = useGalleryReducer();
|
||||
const [isFirstLoad, setIsFirstLoad] = useState(false);
|
||||
|
||||
// Queues for serializing remote operations
|
||||
const remoteFilesPullQueue = useRef(new PromiseQueue<void>());
|
||||
const remotePullQueue = useRef(new PromiseQueue<void>());
|
||||
|
||||
/**
|
||||
* Pull latest collections, collection files and trash items from remote
|
||||
*/
|
||||
const remoteFilesPull = useCallback(
|
||||
() =>
|
||||
remoteFilesPullQueue.current.add(() =>
|
||||
pullFiles({
|
||||
onSetCollections: (collections) =>
|
||||
dispatch({ type: "setCollections", collections }),
|
||||
onSetCollectionFiles: (collectionFiles) =>
|
||||
dispatch({
|
||||
type: "setCollectionFiles",
|
||||
collectionFiles,
|
||||
}),
|
||||
onSetTrashedItems: (trashItems) =>
|
||||
dispatch({ type: "setTrashItems", trashItems }),
|
||||
onDidUpdateCollectionFiles: () =>
|
||||
exportService.onLocalFilesUpdated(),
|
||||
}),
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
/**
|
||||
* Perform a full remote pull with error handling
|
||||
*/
|
||||
const remotePull = useCallback(
|
||||
async (opts?: RemotePullOpts) =>
|
||||
remotePullQueue.current.add(async () => {
|
||||
const { silent } = opts ?? {};
|
||||
|
||||
// Pre-flight checks
|
||||
if (!navigator.onLine) return;
|
||||
if (await isSessionInvalid()) {
|
||||
// Handle session expiry
|
||||
return;
|
||||
}
|
||||
if (!(await masterKeyFromSession())) {
|
||||
clearSessionStorage();
|
||||
void router.push("/credentials");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!silent) showLoadingBar();
|
||||
await prePullFiles();
|
||||
await remoteFilesPull();
|
||||
await postPullFiles();
|
||||
} catch (e) {
|
||||
log.error("Remote pull failed", e);
|
||||
} finally {
|
||||
dispatch({ type: "clearUnsyncedState" });
|
||||
if (!silent) hideLoadingBar();
|
||||
}
|
||||
}),
|
||||
[showLoadingBar, hideLoadingBar, router, remoteFilesPull],
|
||||
);
|
||||
|
||||
/**
|
||||
* Initialize the gallery on mount
|
||||
*/
|
||||
const initializeGallery = useCallback(async () => {
|
||||
try {
|
||||
dispatch({ type: "showAll" });
|
||||
|
||||
const user = ensureLocalUser();
|
||||
const userDetails = await savedUserDetailsOrTriggerPull();
|
||||
|
||||
dispatch({
|
||||
type: "mount",
|
||||
user,
|
||||
familyData: userDetails?.familyData,
|
||||
collections: await savedCollections(),
|
||||
collectionFiles: await savedCollectionFiles(),
|
||||
trashItems: await savedTrashItems(),
|
||||
});
|
||||
|
||||
await remotePull();
|
||||
setIsFirstLoad(false);
|
||||
} catch (error) {
|
||||
onGenericError(error);
|
||||
}
|
||||
}, [dispatch, remotePull, onGenericError]);
|
||||
|
||||
return {
|
||||
state,
|
||||
dispatch,
|
||||
isFirstLoad,
|
||||
setIsFirstLoad,
|
||||
remoteFilesPull,
|
||||
remotePull,
|
||||
initializeGallery,
|
||||
};
|
||||
};
|
||||
@@ -1,124 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { haveMasterKeyInSession } from 'ente-base/session';
|
||||
import { savedAuthToken } from 'ente-base/token';
|
||||
import { stashRedirect } from 'ente-accounts/services/redirect';
|
||||
import { validateKey } from 'ente-new/photos/components/gallery/helpers';
|
||||
import {
|
||||
getAndClearIsFirstLogin,
|
||||
getAndClearJustSignedUp
|
||||
} from 'ente-accounts/services/accounts-db';
|
||||
import { shouldShowWhatsNew } from 'ente-new/photos/services/changelog';
|
||||
import { initSettings } from 'ente-new/photos/services/settings';
|
||||
import { useBaseContext } from 'ente-base/context';
|
||||
|
||||
interface UseGalleryInitializationProps {
|
||||
onInitializeGallery: () => Promise<void>;
|
||||
onShowPlanSelector: () => void;
|
||||
onShowWhatsNew: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for handling gallery initialization and authentication checks
|
||||
*/
|
||||
export const useGalleryInitialization = ({
|
||||
onInitializeGallery,
|
||||
onShowPlanSelector,
|
||||
onShowWhatsNew,
|
||||
}: UseGalleryInitializationProps) => {
|
||||
const { logout } = useBaseContext();
|
||||
const router = useRouter();
|
||||
const [isFirstLoad, setIsFirstLoad] = useState(false);
|
||||
const [blockingLoad, setBlockingLoad] = useState(false);
|
||||
|
||||
/**
|
||||
* Preload all three variants of a responsive image
|
||||
*/
|
||||
const preloadImage = (imgBasePath: string) => {
|
||||
const srcset: string[] = [];
|
||||
for (let i = 1; i <= 3; i++) srcset.push(`${imgBasePath}/${i}x.png ${i}x`);
|
||||
new Image().srcset = srcset.join(",");
|
||||
};
|
||||
|
||||
/**
|
||||
* Set up keyboard shortcuts for select all
|
||||
*/
|
||||
const setupSelectAllKeyBoardShortcutHandler = () => {
|
||||
// This will be handled by the useSelection hook
|
||||
// Keeping this function for compatibility
|
||||
return () => {
|
||||
// Cleanup function - currently handled by useSelection hook
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const electron = globalThis.electron;
|
||||
let syncIntervalID: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
void (async () => {
|
||||
// Check authentication
|
||||
if (!haveMasterKeyInSession() || !(await savedAuthToken())) {
|
||||
stashRedirect("/gallery");
|
||||
void router.push("/");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate credentials
|
||||
if (!(await validateKey())) {
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
|
||||
// One-time initialization
|
||||
preloadImage("/images/subscription-card-background");
|
||||
initSettings();
|
||||
setupSelectAllKeyBoardShortcutHandler();
|
||||
|
||||
// Check if this is the user's first login
|
||||
setIsFirstLoad(getAndClearIsFirstLogin());
|
||||
|
||||
// Show plan selector for new users
|
||||
if (getAndClearJustSignedUp()) {
|
||||
onShowPlanSelector();
|
||||
}
|
||||
|
||||
// Initialize gallery data
|
||||
await onInitializeGallery();
|
||||
|
||||
// Clear first load state
|
||||
setIsFirstLoad(false);
|
||||
|
||||
// Start periodic sync
|
||||
syncIntervalID = setInterval(
|
||||
() => {
|
||||
// This should trigger a silent remote pull
|
||||
// Will be handled by the data management hook
|
||||
},
|
||||
5 * 60 * 1000 /* 5 minutes */,
|
||||
);
|
||||
|
||||
// Handle electron-specific features
|
||||
if (electron) {
|
||||
electron.onMainWindowFocus(() => {
|
||||
// Trigger silent remote pull on focus
|
||||
});
|
||||
if (await shouldShowWhatsNew(electron)) {
|
||||
onShowWhatsNew();
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
clearInterval(syncIntervalID);
|
||||
if (electron) {
|
||||
electron.onMainWindowFocus(undefined);
|
||||
}
|
||||
};
|
||||
}, [router, logout, onInitializeGallery, onShowPlanSelector, onShowWhatsNew]);
|
||||
|
||||
return {
|
||||
isFirstLoad,
|
||||
blockingLoad,
|
||||
setBlockingLoad,
|
||||
};
|
||||
};
|
||||
@@ -1,168 +0,0 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { PseudoCollectionID } from 'ente-new/photos/services/collection-summary';
|
||||
import type { SearchOption } from 'ente-new/photos/services/search/types';
|
||||
|
||||
interface UseGalleryNavigationProps {
|
||||
dispatch: (action: { type: string; [key: string]: unknown }) => void;
|
||||
barMode: string;
|
||||
activeCollectionID: number | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for managing gallery navigation and view changes
|
||||
*/
|
||||
export const useGalleryNavigation = ({
|
||||
dispatch,
|
||||
barMode,
|
||||
activeCollectionID,
|
||||
}: UseGalleryNavigationProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
/**
|
||||
* Grace period tracking for hidden section authentication
|
||||
*/
|
||||
const lastAuthenticationForHiddenTimestamp = useRef<number>(0);
|
||||
|
||||
/**
|
||||
* Handle collection summary selection with optional authentication
|
||||
*/
|
||||
const showCollectionSummary = useCallback(
|
||||
async (
|
||||
collectionSummaryID: number | undefined,
|
||||
isHiddenCollectionSummary: boolean | undefined,
|
||||
authenticateUser?: () => Promise<void>,
|
||||
) => {
|
||||
const lastAuthAt = lastAuthenticationForHiddenTimestamp.current;
|
||||
if (
|
||||
isHiddenCollectionSummary &&
|
||||
barMode !== "hidden-albums" &&
|
||||
Date.now() - lastAuthAt > 5 * 60 * 1e3 /* 5 minutes */ &&
|
||||
authenticateUser
|
||||
) {
|
||||
await authenticateUser();
|
||||
lastAuthenticationForHiddenTimestamp.current = Date.now();
|
||||
}
|
||||
|
||||
// Trigger a pull of the latest data when opening trash
|
||||
if (collectionSummaryID === PseudoCollectionID.trash) {
|
||||
// This should trigger a remote files pull
|
||||
// Will be handled by the calling component
|
||||
}
|
||||
|
||||
dispatch({ type: "showCollectionSummary", collectionSummaryID });
|
||||
},
|
||||
[dispatch, barMode],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle search option selection
|
||||
*/
|
||||
const handleSelectSearchOption = useCallback(
|
||||
(searchOption: SearchOption | undefined) => {
|
||||
if (searchOption) {
|
||||
const type = searchOption.suggestion.type;
|
||||
if (type === "collection") {
|
||||
dispatch({
|
||||
type: "showCollectionSummary",
|
||||
collectionSummaryID: searchOption.suggestion.collectionID,
|
||||
});
|
||||
} else if (type === "person") {
|
||||
dispatch({
|
||||
type: "showPerson",
|
||||
personID: searchOption.suggestion.person.id,
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: "enterSearchMode",
|
||||
searchSuggestion: searchOption.suggestion,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
dispatch({ type: "exitSearch" });
|
||||
}
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle gallery bar mode changes
|
||||
*/
|
||||
const handleChangeBarMode = useCallback(
|
||||
(mode: string) => {
|
||||
if (mode === "people") {
|
||||
dispatch({ type: "showPeople" });
|
||||
} else {
|
||||
dispatch({ type: "showAlbums" });
|
||||
}
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle collection selection
|
||||
*/
|
||||
const handleSelectCollection = useCallback(
|
||||
(collectionID: number) =>
|
||||
dispatch({
|
||||
type: "showCollectionSummary",
|
||||
collectionSummaryID: collectionID,
|
||||
}),
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle person selection
|
||||
*/
|
||||
const handleSelectPerson = useCallback(
|
||||
(personID: string) => dispatch({ type: "showPerson", personID }),
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle entering search mode
|
||||
*/
|
||||
const handleEnterSearchMode = useCallback(
|
||||
() => dispatch({ type: "enterSearchMode" }),
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle showing people view
|
||||
*/
|
||||
const handleShowPeople = useCallback(
|
||||
() => dispatch({ type: "showPeople" }),
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
/**
|
||||
* Update browser URL based on active collection
|
||||
*/
|
||||
const updateBrowserURL = useCallback(() => {
|
||||
if (typeof activeCollectionID === "undefined" || !router.isReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
let collectionURL = "";
|
||||
if (activeCollectionID !== PseudoCollectionID.all) {
|
||||
collectionURL = `?collection=${activeCollectionID}`;
|
||||
}
|
||||
const href = `/gallery${collectionURL}`;
|
||||
void router.push(href, undefined, { shallow: true });
|
||||
}, [activeCollectionID, router]);
|
||||
|
||||
return {
|
||||
// Navigation handlers
|
||||
showCollectionSummary,
|
||||
handleSelectSearchOption,
|
||||
handleChangeBarMode,
|
||||
handleSelectCollection,
|
||||
handleSelectPerson,
|
||||
handleEnterSearchMode,
|
||||
handleShowPeople,
|
||||
updateBrowserURL,
|
||||
|
||||
// State
|
||||
lastAuthenticationForHiddenTimestamp,
|
||||
};
|
||||
};
|
||||
@@ -1,121 +0,0 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import type { EnteFile } from 'ente-media/file';
|
||||
import { useModalVisibility } from 'ente-base/components/utils/modal';
|
||||
import type { UploadTypeSelectorIntent } from 'ente-gallery/components/Upload';
|
||||
|
||||
/**
|
||||
* Custom hook for managing all modal states and visibility
|
||||
*/
|
||||
export const useModalManagement = () => {
|
||||
// File drag and drop state
|
||||
const [dragAndDropFiles, setDragAndDropFiles] = useState<File[]>([]);
|
||||
const [shouldDisableDropzone, setShouldDisableDropzone] = useState(false);
|
||||
|
||||
// Upload modal state
|
||||
const [uploadTypeSelectorView, setUploadTypeSelectorView] = useState(false);
|
||||
const [uploadTypeSelectorIntent, setUploadTypeSelectorIntent] =
|
||||
useState<UploadTypeSelectorIntent>("upload");
|
||||
|
||||
// File viewer state
|
||||
const [isFileViewerOpen, setIsFileViewerOpen] = useState(false);
|
||||
|
||||
// Fix creation time dialog state
|
||||
const [fixCreationTimeFiles, setFixCreationTimeFiles] = useState<EnteFile[]>([]);
|
||||
|
||||
// Authentication callback for hidden section access
|
||||
const onAuthenticateCallback = useRef<(() => void) | undefined>(undefined);
|
||||
|
||||
// Modal visibility hooks
|
||||
const { show: showSidebar, props: sidebarVisibilityProps } = useModalVisibility();
|
||||
const { show: showPlanSelector, props: planSelectorVisibilityProps } = useModalVisibility();
|
||||
const { show: showWhatsNew, props: whatsNewVisibilityProps } = useModalVisibility();
|
||||
const { show: showFixCreationTime, props: fixCreationTimeVisibilityProps } = useModalVisibility();
|
||||
const { show: showExport, props: exportVisibilityProps } = useModalVisibility();
|
||||
const { show: showAuthenticateUser, props: authenticateUserVisibilityProps } = useModalVisibility();
|
||||
const { show: showAlbumNameInput, props: albumNameInputVisibilityProps } = useModalVisibility();
|
||||
|
||||
/**
|
||||
* Authenticate user for hidden section access
|
||||
*/
|
||||
const authenticateUser = useCallback(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
onAuthenticateCallback.current = resolve;
|
||||
showAuthenticateUser();
|
||||
}),
|
||||
[showAuthenticateUser],
|
||||
);
|
||||
|
||||
/**
|
||||
* Open upload type selector
|
||||
*/
|
||||
const openUploader = useCallback((intent?: UploadTypeSelectorIntent) => {
|
||||
setUploadTypeSelectorView(true);
|
||||
setUploadTypeSelectorIntent(intent ?? "upload");
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Close upload type selector
|
||||
*/
|
||||
const closeUploadTypeSelector = useCallback(() => {
|
||||
setUploadTypeSelectorView(false);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Check if any modal is currently open
|
||||
*/
|
||||
const isAnyModalOpen =
|
||||
uploadTypeSelectorView ||
|
||||
sidebarVisibilityProps.open ||
|
||||
planSelectorVisibilityProps.open ||
|
||||
fixCreationTimeVisibilityProps.open ||
|
||||
exportVisibilityProps.open ||
|
||||
authenticateUserVisibilityProps.open ||
|
||||
albumNameInputVisibilityProps.open ||
|
||||
isFileViewerOpen;
|
||||
|
||||
return {
|
||||
// State
|
||||
dragAndDropFiles,
|
||||
shouldDisableDropzone,
|
||||
uploadTypeSelectorView,
|
||||
uploadTypeSelectorIntent,
|
||||
isFileViewerOpen,
|
||||
fixCreationTimeFiles,
|
||||
isAnyModalOpen,
|
||||
|
||||
// Setters
|
||||
setDragAndDropFiles,
|
||||
setShouldDisableDropzone,
|
||||
setUploadTypeSelectorView,
|
||||
setUploadTypeSelectorIntent,
|
||||
setIsFileViewerOpen,
|
||||
setFixCreationTimeFiles,
|
||||
|
||||
// Modal visibility props
|
||||
sidebarVisibilityProps,
|
||||
planSelectorVisibilityProps,
|
||||
whatsNewVisibilityProps,
|
||||
fixCreationTimeVisibilityProps,
|
||||
exportVisibilityProps,
|
||||
authenticateUserVisibilityProps,
|
||||
albumNameInputVisibilityProps,
|
||||
|
||||
// Modal show functions
|
||||
showSidebar,
|
||||
showPlanSelector,
|
||||
showWhatsNew,
|
||||
showFixCreationTime,
|
||||
showExport,
|
||||
showAuthenticateUser,
|
||||
showAlbumNameInput,
|
||||
|
||||
// Actions
|
||||
authenticateUser,
|
||||
openUploader,
|
||||
closeUploadTypeSelector,
|
||||
|
||||
// Authentication callback
|
||||
onAuthenticateCallback,
|
||||
};
|
||||
};
|
||||
@@ -1,123 +0,0 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { SelectedState } from 'utils/file';
|
||||
import type { EnteFile } from 'ente-media/file';
|
||||
import { PseudoCollectionID } from 'ente-new/photos/services/collection-summary';
|
||||
|
||||
interface UseSelectionProps {
|
||||
user: { id: number } | null;
|
||||
filteredFiles: EnteFile[];
|
||||
activeCollectionID: number | undefined;
|
||||
barMode: string;
|
||||
activePersonID?: string;
|
||||
isAnyModalOpen: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for managing file selection state and keyboard shortcuts
|
||||
*/
|
||||
export const useSelection = ({
|
||||
user,
|
||||
filteredFiles,
|
||||
activeCollectionID,
|
||||
barMode,
|
||||
activePersonID,
|
||||
isAnyModalOpen,
|
||||
}: UseSelectionProps) => {
|
||||
const [selected, setSelected] = useState<SelectedState>({
|
||||
ownCount: 0,
|
||||
count: 0,
|
||||
collectionID: 0,
|
||||
context: { mode: "albums", collectionID: PseudoCollectionID.all },
|
||||
});
|
||||
|
||||
const selectAll = useCallback((e: KeyboardEvent) => {
|
||||
// Don't intercept Ctrl/Cmd + a if the user is typing in a text field
|
||||
if (
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
// Don't select all if conditions aren't met
|
||||
if (
|
||||
!user ||
|
||||
!filteredFiles.length ||
|
||||
isAnyModalOpen
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a selection with everything based on the current context
|
||||
const newSelected = {
|
||||
ownCount: 0,
|
||||
count: 0,
|
||||
collectionID: activeCollectionID,
|
||||
context:
|
||||
barMode === "people" && activePersonID
|
||||
? { mode: "people" as const, personID: activePersonID }
|
||||
: {
|
||||
mode: barMode as "albums" | "hidden-albums",
|
||||
collectionID: activeCollectionID!,
|
||||
},
|
||||
};
|
||||
|
||||
filteredFiles.forEach((item) => {
|
||||
if (item.ownerID === user.id) {
|
||||
newSelected.ownCount++;
|
||||
}
|
||||
newSelected.count++;
|
||||
// @ts-expect-error Selection code needs type fixing
|
||||
newSelected[item.id] = true;
|
||||
});
|
||||
setSelected(newSelected);
|
||||
}, [user, filteredFiles, activeCollectionID, barMode, activePersonID, isAnyModalOpen]);
|
||||
|
||||
const clearSelection = useCallback(() => {
|
||||
if (!selected.count) {
|
||||
return;
|
||||
}
|
||||
setSelected({
|
||||
ownCount: 0,
|
||||
count: 0,
|
||||
collectionID: 0,
|
||||
context: undefined,
|
||||
});
|
||||
}, [selected.count]);
|
||||
|
||||
const keyboardShortcutHandlerRef = useRef({ selectAll, clearSelection });
|
||||
|
||||
useEffect(() => {
|
||||
keyboardShortcutHandlerRef.current = { selectAll, clearSelection };
|
||||
}, [selectAll, clearSelection]);
|
||||
|
||||
// Set up keyboard shortcuts
|
||||
useEffect(() => {
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case "Escape":
|
||||
keyboardShortcutHandlerRef.current.clearSelection();
|
||||
break;
|
||||
case "a":
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
keyboardShortcutHandlerRef.current.selectAll(e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyUp);
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyUp);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
selected,
|
||||
setSelected,
|
||||
clearSelection,
|
||||
selectAll,
|
||||
};
|
||||
};
|
||||
@@ -1227,7 +1227,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7"
|
||||
integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==
|
||||
|
||||
"@types/react-dom@^19.1.6":
|
||||
"@types/react-dom@^19.1.1", "@types/react-dom@^19.1.6":
|
||||
version "19.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.1.6.tgz#4af629da0e9f9c0f506fc4d1caa610399c595d64"
|
||||
integrity sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==
|
||||
@@ -1244,7 +1244,7 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@^19.1.8":
|
||||
"@types/react@*", "@types/react@^19.1.0", "@types/react@^19.1.8":
|
||||
version "19.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.8.tgz#ff8395f2afb764597265ced15f8dddb0720ae1c3"
|
||||
integrity sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==
|
||||
|
||||
Reference in New Issue
Block a user