Merge branch 'main' into memories_misc_improv

This commit is contained in:
laurenspriem
2025-08-01 12:01:30 +02:00
21 changed files with 430 additions and 1010 deletions

View File

@@ -12,6 +12,8 @@ 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
@@ -127,9 +129,6 @@ 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):
@@ -230,6 +229,8 @@ PODS:
- Flutter
- url_launcher_ios (0.0.1):
- Flutter
- vibration (1.7.5):
- Flutter
- video_player_avfoundation (0.0.1):
- Flutter
- FlutterMacOS
@@ -250,6 +251,7 @@ 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`)
@@ -269,7 +271,6 @@ 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`)
@@ -297,6 +298,7 @@ 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`)
@@ -304,7 +306,7 @@ DEPENDENCIES:
- workmanager (from `.symlinks/plugins/workmanager/ios`)
SPEC REPOS:
https://github.com/ente-io/ffmpeg-kit-custom-repo-ios:
https://github.com/ente-io/ffmpeg-kit-custom-repo-ios.git:
- ffmpeg_kit_custom
trunk:
- Firebase
@@ -339,6 +341,8 @@ 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:
@@ -377,8 +381,6 @@ 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:
@@ -433,6 +435,8 @@ 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:
@@ -445,83 +449,84 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/workmanager/ios"
SPEC CHECKSUMS:
app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7
battery_info: 83f3aae7be2fccefab1d2bf06b8aa96f11c8bcdd
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
dart_ui_isolate: 46f6714abe6891313267153ef6f9748d8ecfcab1
device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89
app_links: f3e17e4ee5e357b39d8b95290a9b2c299fca71c6
battery_info: b6c551049266af31556b93c9d9b9452cfec0219f
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
cupertino_http: 947a233f40cfea55167a49f2facc18434ea117ba
dart_ui_isolate: d5bcda83ca4b04f129d70eb90110b7a567aece14
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
emoji_picker_flutter: fe2e6151c5b548e975d546e6eeb567daf0962a58
ffmpeg_kit_custom: 682b4f2f1ff1f8abae5a92f6c3540f2441d5be99
ffmpeg_kit_flutter: 915b345acc97d4142e8a9a8549d177ff10f043f5
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
ffmpeg_kit_flutter: 9dce4803991478c78c6fb9f972703301101095fe
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf
firebase_core: 6cbed78b4f298ed103a9fd034e6dbc846320480f
firebase_messaging: 5e0adf2eb18b0ee59aa0c109314c091a0497ecac
firebase_core: 6e223dfa350b2edceb729cea505eaaef59330682
firebase_messaging: 07fde77ae28c08616a1d4d870450efc2b38cf40d
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629
FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917
FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
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
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
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
image_editor_common: 3de87e7c4804f4ae24c8f8a998362b98c105cac1
in_app_purchase_storekit: d1a48cb0f8b29dbf5f85f782f5dd79b21b90a5e6
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
launcher_icon_switcher: 84c218d233505aa7d8655d8fa61a3ba802c022da
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43
in_app_purchase_storekit: a1ce04056e23eecc666b086040239da7619cd783
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
launcher_icon_switcher: 8e0ad2131a20c51c1dd939896ee32e70cd845b37
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
local_auth_ios: f7a1841beef3151d140a967c2e46f30637cdf451
local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
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
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
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
native_video_player: 6809dec117e8997161dbfb42a6f90d6df71a504d
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
onnxruntime: f9b296392c96c42882be020a59dbeac6310d81b2
native_video_player: 29ab24a926804ac8c4a57eb6d744c7d927c2bc3e
objective_c: 77e887b5ba1827970907e10e832eec1683f3431d
onnxruntime: e7c2ae44385191eaad5ae64c935a72debaddc997
onnxruntime-c: a909204639a1f035f575127ac406f781ac797c9c
onnxruntime-objc: b6fab0f1787aa6f7190c2013f03037df4718bd8b
open_mail_app: 7314a609e88eed22d53671279e189af7a0ab0f11
open_mail_app: 70273c53f768beefdafbe310c3d9086e4da3cb02
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413
privacy_screen: 3159a541f5d3a31bea916cfd4e58f9dc722b3fd4
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
receive_sharing_intent: 79c848f5b045674ad60b9fea3bafea59962ad2c1
SDWebImage: f29024626962457f3470184232766516dee8dfea
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854
sentry_flutter: 27892878729f42701297c628eb90e7c6529f3684
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sentry_flutter: 2df8b0aab7e4aba81261c230cbea31c82a62dd1b
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1
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
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
PODFILE CHECKSUM: a8ef88ad74ba499756207e7592c6071a96756d18

View File

@@ -532,6 +532,7 @@
"${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",
@@ -548,7 +549,6 @@
"${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,6 +576,7 @@
"${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",
@@ -628,6 +629,7 @@
"${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",
@@ -644,7 +646,6 @@
"${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",
@@ -672,6 +673,7 @@
"${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",

View File

@@ -12495,6 +12495,118 @@ 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> {

View File

@@ -1808,5 +1808,24 @@
"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"
"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"
}
}
}
}

View File

@@ -142,7 +142,9 @@ class Collection {
bool canAutoAdd(int userID) {
final canEditCollection = isOwner(userID) ||
getRole(userID) == CollectionParticipantRole.collaborator;
return canEditCollection && !isDeleted;
final isFavoritesOrUncategorized = type == CollectionType.favorites ||
type == CollectionType.uncategorized;
return canEditCollection && !isDeleted && !isFavoritesOrUncategorized;
}
bool isDownloadEnabledForPublicLink() {

View File

@@ -187,11 +187,14 @@ extension SectionTypeExtensions on SectionType {
try {
final Collection c =
await CollectionsService.instance.createAlbum(text);
unawaited(
routeToPage(
context,
CollectionPage(CollectionWithThumbnail(c, null)),
),
// Close the dialog now so that it does not flash when leaving the album again.
Navigator.of(context).pop();
// ignore: unawaited_futures
await routeToPage(
context,
CollectionPage(CollectionWithThumbnail(c, null)),
);
} catch (e, s) {
Logger("CreateNewAlbumIcon")

View File

@@ -86,23 +86,35 @@ class SmartAlbumsService {
return;
}
_logger.info("Syncing Smart Albums");
final cachedConfigs = await getSmartConfigs();
final userId = Configuration.instance.getUserID()!;
for (final entry in cachedConfigs.entries) {
final collectionId = entry.key;
final config = entry.value;
if (config.personIDs.isEmpty) {
_logger.warning(
"Skipping sync for collection ($collectionId) as it has no person IDs",
);
continue;
}
final collection =
CollectionsService.instance.getCollectionByID(collectionId);
if (!(collection?.canAutoAdd(userId) ?? false)) {
if (collection == null || !collection.canAutoAdd(userId)) {
_logger.warning(
"Deleting collection config ($collectionId) as user does not have permission",
);
await _deleteEntry(
userId: userId,
collectionId: collectionId,
"For config ($collectionId) user does not have permission",
);
if (collection?.isDeleted ?? false) {
await _deleteEntry(
userId: userId,
collectionId: collectionId,
);
}
continue;
}
@@ -166,11 +178,14 @@ class SmartAlbumsService {
newConfig = newConfig.addFiles(updatedAtMap, pendingSyncFiles);
await saveConfig(newConfig);
} catch (_) {}
} catch (e, sT) {
_logger.warning(e, sT);
}
}
}
syncingCollection = null;
Bus.instance.fire(SmartAlbumSyncingEvent());
_logger.fine("Smart Albums sync completed");
}
Future<SmartAlbumConfig> addPeopleToSmartAlbum(
@@ -214,11 +229,6 @@ class SmartAlbumsService {
addWithCustomID: config.id == null,
userId: userId,
);
} else if (config.id != null) {
await _deleteEntry(
userId: userId,
collectionId: config.collectionId,
);
}
}
@@ -238,6 +248,7 @@ class SmartAlbumsService {
bool addWithCustomID = false,
required int userId,
}) async {
_logger.fine("Adding or updating entity for collection ($collectionId)");
final id = getId(collectionId: collectionId, userId: userId);
final result = await entityService.addOrUpdate(
type,
@@ -254,6 +265,7 @@ class SmartAlbumsService {
required int userId,
required int collectionId,
}) async {
_logger.fine("Deleting entry for collection ($collectionId)");
final id = getId(collectionId: collectionId, userId: userId);
await entityService.deleteEntry(id);
_lastCacheRefreshTime = 0; // Invalidate cache

View File

@@ -2,6 +2,7 @@ import "dart:async";
import "package:flutter/foundation.dart";
import 'package:flutter/material.dart';
import "package:logging/logging.dart";
import "package:photos/core/event_bus.dart";
import "package:photos/db/files_db.dart";
import "package:photos/events/collection_updated_event.dart";
@@ -36,6 +37,8 @@ class _SmartAlbumPeopleState extends State<SmartAlbumPeople> {
final _selectedPeople = SelectedPeople();
SmartAlbumConfig? currentConfig;
final _logger = Logger("SmartAlbumPeople");
@override
void initState() {
super.initState();
@@ -168,11 +171,16 @@ class _SmartAlbumPeopleState extends State<SmartAlbumPeople> {
await dialog.hide();
Navigator.pop(context);
} catch (e) {
} catch (error, stackTrace) {
_logger.severe(
"Error saving smart album config",
error,
stackTrace,
);
await dialog.hide();
await showGenericErrorDialog(
context: context,
error: e,
error: error,
);
}
}

View File

@@ -146,7 +146,7 @@ class _AlbumVerticalListWidgetState extends State<AlbumVerticalListWidget> {
},
showOnlyLoadingState: true,
textCapitalization: TextCapitalization.words,
popnavAfterSubmission: false,
popnavAfterSubmission: true,
);
if (result is Exception) {
await showGenericErrorDialog(

View File

@@ -310,9 +310,7 @@ class _CollectionActionSheetState extends State<CollectionActionSheet> {
if (result) {
showShortToast(
context,
"Added successfully to " +
_selectedCollections.length.toString() +
" albums",
S.of(context).addedToAlbums(_selectedCollections.length),
);
widget.selectedFiles?.clearAll();
}

View File

@@ -1,110 +0,0 @@
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;
}
}

View File

@@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import "package:flutter_svg/svg.dart";
import "package:photos/ente_theme_data.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/generated/l10n.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 {
@@ -43,7 +44,7 @@ class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget {
enableUndo ? close() : Navigator.of(context).pop();
},
child: Text(
'Cancel',
S.of(context).cancel,
style: getEnteTextTheme(context).body,
),
),
@@ -52,7 +53,7 @@ class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget {
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
tooltip: 'Undo',
tooltip: S.of(context).undo,
onPressed: () {
undo != null ? undo!() : null;
},
@@ -66,7 +67,7 @@ class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget {
),
const SizedBox(width: 12),
IconButton(
tooltip: 'Redo',
tooltip: S.of(context).redo,
onPressed: () {
redo != null ? redo!() : null;
},
@@ -88,7 +89,7 @@ class ImageEditorAppBar extends StatelessWidget implements PreferredSizeWidget {
key: ValueKey(isMainEditor ? 'save_copy' : 'done'),
onPressed: done,
child: Text(
isMainEditor ? 'Save Copy' : 'Done',
isMainEditor ? S.of(context).saveCopy : S.of(context).done,
style: getEnteTextTheme(context).body.copyWith(
color: isMainEditor
? (enableUndo

View File

@@ -1,5 +1,6 @@
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";
@@ -113,7 +114,7 @@ class _ImageEditorCropRotateBarState extends State<ImageEditorCropRotateBar>
children: [
CircularIconButton(
svgPath: "assets/image-editor/image-editor-crop-rotate.svg",
label: "Rotate",
label: S.of(context).rotate,
onTap: () {
widget.editor.rotate();
},
@@ -121,7 +122,7 @@ class _ImageEditorCropRotateBarState extends State<ImageEditorCropRotateBar>
const SizedBox(width: 6),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-flip.svg",
label: "Flip",
label: S.of(context).flip,
onTap: () {
widget.editor.flip();
},

View File

@@ -1,6 +1,7 @@
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";
@@ -90,7 +91,7 @@ class ImageEditorMainBottomBarState extends State<ImageEditorMainBottomBar>
children: <Widget>[
CircularIconButton(
svgPath: "assets/image-editor/image-editor-crop.svg",
label: "Crop",
label: S.of(context).crop,
onTap: () {
widget.editor.openCropRotateEditor();
},
@@ -98,21 +99,21 @@ class ImageEditorMainBottomBarState extends State<ImageEditorMainBottomBar>
CircularIconButton(
svgPath:
"assets/image-editor/image-editor-filter.svg",
label: "Filter",
label: S.of(context).filter,
onTap: () {
widget.editor.openFilterEditor();
},
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-tune.svg",
label: "Adjust",
label: S.of(context).adjust,
onTap: () {
widget.editor.openTuneEditor();
},
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-paint.svg",
label: "Draw",
label: S.of(context).draw,
onTap: () {
widget.editor.openPaintingEditor();
},
@@ -120,7 +121,7 @@ class ImageEditorMainBottomBarState extends State<ImageEditorMainBottomBar>
CircularIconButton(
svgPath:
"assets/image-editor/image-editor-sticker.svg",
label: "Sticker",
label: S.of(context).sticker,
onTap: () {
widget.editor.openEmojiEditor();
},

View File

@@ -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 NewImageEditor extends StatefulWidget {
class ImageEditorPage extends StatefulWidget {
final ente.EnteFile originalFile;
final File file;
final DetailPageConfiguration detailPageConfig;
const NewImageEditor({
const ImageEditorPage({
super.key,
required this.file,
required this.originalFile,
@@ -50,10 +50,10 @@ class NewImageEditor extends StatefulWidget {
});
@override
State<NewImageEditor> createState() => _NewImageEditorState();
State<ImageEditorPage> createState() => _ImageEditorPageState();
}
class _NewImageEditorState extends State<NewImageEditor> {
class _ImageEditorPageState extends State<ImageEditorPage> {
final _mainEditorBarKey = GlobalKey<ImageEditorMainBottomBarState>();
final editorKey = GlobalKey<ProImageEditorState>();
final _logger = Logger("ImageEditor");

View File

@@ -1,4 +1,5 @@
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";
@@ -63,7 +64,7 @@ class _ImageEditorPaintBarState extends State<ImageEditorPaintBar>
Padding(
padding: const EdgeInsets.only(left: 20.0),
child: Text(
"Brush Color",
S.of(context).brushColor,
style: getEnteTextTheme(context).body,
),
),

View File

@@ -1,5 +1,6 @@
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";
@@ -75,7 +76,7 @@ class _ImageEditorTextBarState extends State<ImageEditorTextBar>
children: [
CircularIconButton(
svgPath: "assets/image-editor/image-editor-text-color.svg",
label: "Color",
label: S.of(context).color,
isSelected: selectedActionIndex == 0,
onTap: () {
_selectAction(0);
@@ -83,7 +84,7 @@ class _ImageEditorTextBarState extends State<ImageEditorTextBar>
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-text-font.svg",
label: "Font",
label: S.of(context).font,
isSelected: selectedActionIndex == 1,
onTap: () {
_selectAction(1);
@@ -91,7 +92,7 @@ class _ImageEditorTextBarState extends State<ImageEditorTextBar>
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-text-background.svg",
label: "Background",
label: S.of(context).background,
isSelected: selectedActionIndex == 2,
onTap: () {
setState(() {
@@ -101,7 +102,7 @@ class _ImageEditorTextBarState extends State<ImageEditorTextBar>
),
CircularIconButton(
svgPath: "assets/image-editor/image-editor-text-align-left.svg",
label: "Align",
label: S.of(context).align,
isSelected: selectedActionIndex == 3,
onTap: () {
setState(() {

View File

@@ -1,553 +0,0 @@
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));
}
}
}

View File

@@ -19,8 +19,7 @@ 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_new.dart";
import 'package:photos/ui/tools/editor/image_editor_page.dart';
import "package:photos/ui/tools/editor/image_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";
@@ -64,16 +63,30 @@ class DetailPageConfiguration {
}
}
class DetailPage extends StatefulWidget {
class DetailPage extends StatelessWidget {
final DetailPageConfiguration config;
const DetailPage(this.config, {super.key});
@override
State<DetailPage> createState() => _DetailPageState();
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));
}
}
class _DetailPageState extends State<DetailPage> {
class _Body extends StatefulWidget {
final DetailPageConfiguration config;
const _Body(this.config);
@override
State<_Body> createState() => _BodyState();
}
class _BodyState extends State<_Body> {
final _logger = Logger("DetailPageState");
bool _shouldDisableScroll = false;
List<EnteFile>? _files;
@@ -137,102 +150,100 @@ class _DetailPageState extends State<DetailPage> {
_files!.length.toString() +
" files .",
);
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);
}
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);
}
},
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],
_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)
),
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)
.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),
),
icon: const Icon(
Icons.threesixty,
color: Colors.white,
size: 26,
),
onPressed: () async {
await openPanoramaViewerPage(
_files![selectedIndex],
);
},
);
},
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),
),
icon: const Icon(
Icons.threesixty,
color: Colors.white,
size: 26,
),
onPressed: () async {
await openPanoramaViewerPage(
_files![selectedIndex],
);
},
),
),
),
);
},
);
}
return const SizedBox();
},
),
],
),
),
);
},
);
}
return const SizedBox();
},
),
],
),
),
),
@@ -358,68 +369,6 @@ class _DetailPageState extends State<DetailPage> {
}
}
Future<void> _onNewImageEditor(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,
NewImageEditor(
originalFile: file,
file: ioFile,
detailPageConfig: widget.config.copyWith(
files: _files,
selectedIndex: _selectedIndexNotifier.value,
),
),
);
} catch (e) {
await dialog.hide();
_logger.warning("Failed to initiate edit", e);
}
}
Future<void> _onEditFileRequested(EnteFile file) async {
if (file.uploadedFileID != null &&
file.ownerID != Configuration.instance.getUserID()) {
@@ -438,6 +387,7 @@ class _DetailPageState extends State<DetailPage> {
}
final dialog = createProgressDialog(context, S.of(context).pleaseWait);
await dialog.show();
try {
final ioFile = await getFile(file);
if (ioFile == null) {
@@ -467,9 +417,9 @@ class _DetailPageState extends State<DetailPage> {
replacePage(
context,
ImageEditorPage(
imageProvider,
file,
widget.config.copyWith(
originalFile: file,
file: ioFile,
detailPageConfig: widget.config.copyWith(
files: _files,
selectedIndex: _selectedIndexNotifier.value,
),

View File

@@ -5,10 +5,10 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
url: "https://pub.dev"
source: hosted
version: "72.0.0"
version: "76.0.0"
_flutterfire_internals:
dependency: transitive
description:
@@ -21,7 +21,7 @@ packages:
dependency: transitive
description: dart
source: sdk
version: "0.3.2"
version: "0.3.3"
adaptive_theme:
dependency: "direct main"
description:
@@ -34,10 +34,10 @@ packages:
dependency: transitive
description:
name: analyzer
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
url: "https://pub.dev"
source: hosted
version: "6.7.0"
version: "6.11.0"
android_intent_plus:
dependency: "direct main"
description:
@@ -317,10 +317,10 @@ packages:
dependency: "direct main"
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.19.0"
computer:
dependency: "direct main"
description:
@@ -1271,38 +1271,6 @@ 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:
@@ -1424,18 +1392,18 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
url: "https://pub.dev"
source: hosted
version: "10.0.5"
version: "10.0.7"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
url: "https://pub.dev"
source: hosted
version: "3.0.5"
version: "3.0.8"
leak_tracker_testing:
dependency: transitive
description:
@@ -1544,10 +1512,10 @@ packages:
dependency: transitive
description:
name: macros
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
url: "https://pub.dev"
source: hosted
version: "0.1.2-main.4"
version: "0.1.3-main.0"
maps_launcher:
dependency: "direct main"
description:
@@ -1586,24 +1554,24 @@ packages:
description:
path: media_kit
ref: HEAD
resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1
url: "https://github.com/media-kit/media-kit"
source: git
version: "1.1.11"
version: "1.2.0"
media_kit_libs_android_video:
dependency: transitive
description:
name: media_kit_libs_android_video
sha256: "9dd8012572e4aff47516e55f2597998f0a378e3d588d0fad0ca1f11a53ae090c"
sha256: adff9b571b8ead0867f9f91070f8df39562078c0eb3371d88b9029a2d547d7b7
url: "https://pub.dev"
source: hosted
version: "1.3.6"
version: "1.3.7"
media_kit_libs_ios_video:
dependency: "direct main"
description:
path: "libs/ios/media_kit_libs_ios_video"
ref: HEAD
resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1
url: "https://github.com/media-kit/media-kit"
source: git
version: "1.1.4"
@@ -1611,10 +1579,10 @@ packages:
dependency: transitive
description:
name: media_kit_libs_linux
sha256: e186891c31daa6bedab4d74dcdb4e8adfccc7d786bfed6ad81fe24a3b3010310
sha256: "2b473399a49ec94452c4d4ae51cfc0f6585074398d74216092bf3d54aac37ecf"
url: "https://pub.dev"
source: hosted
version: "1.1.3"
version: "1.2.1"
media_kit_libs_macos_video:
dependency: transitive
description:
@@ -1628,27 +1596,27 @@ packages:
description:
path: "libs/universal/media_kit_libs_video"
ref: HEAD
resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1
url: "https://github.com/media-kit/media-kit"
source: git
version: "1.0.5"
version: "1.0.6"
media_kit_libs_windows_video:
dependency: transitive
description:
name: media_kit_libs_windows_video
sha256: "32654572167825c42c55466f5d08eee23ea11061c84aa91b09d0e0f69bdd0887"
sha256: dff76da2778729ab650229e6b4ec6ec111eb5151431002cbd7ea304ff1f112ab
url: "https://pub.dev"
source: hosted
version: "1.0.10"
version: "1.0.11"
media_kit_video:
dependency: "direct main"
description:
path: media_kit_video
ref: HEAD
resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1
url: "https://github.com/media-kit/media-kit"
source: git
version: "1.2.5"
version: "1.3.0"
meta:
dependency: transitive
description:
@@ -1712,7 +1680,7 @@ packages:
description:
path: "."
ref: HEAD
resolved-ref: "7814e2c61ee1fa74cef73b946eb08519c35bdaa5"
resolved-ref: "64e47a446bf3b64f012f2076481cebea51ca27cf"
url: "https://github.com/ente-io/motionphoto.git"
source: git
version: "0.0.1"
@@ -2325,7 +2293,7 @@ packages:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
version: "0.0.0"
source_gen:
dependency: transitive
description:
@@ -2450,10 +2418,10 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
url: "https://pub.dev"
source: hosted
version: "1.11.1"
version: "1.12.0"
step_progress_indicator:
dependency: "direct main"
description:
@@ -2482,10 +2450,10 @@ packages:
dependency: transitive
description:
name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.3.0"
styled_text:
dependency: "direct main"
description:
@@ -2546,26 +2514,26 @@ packages:
dependency: "direct dev"
description:
name: test
sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e"
sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f"
url: "https://pub.dev"
source: hosted
version: "1.25.7"
version: "1.25.8"
test_api:
dependency: transitive
description:
name: test_api
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
url: "https://pub.dev"
source: hosted
version: "0.7.2"
version: "0.7.3"
test_core:
dependency: transitive
description:
name: test_core
sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696"
sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d"
url: "https://pub.dev"
source: hosted
version: "0.6.4"
version: "0.6.5"
thermal:
dependency: "direct main"
description:
@@ -2845,10 +2813,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
url: "https://pub.dev"
source: hosted
version: "14.2.5"
version: "14.3.0"
volume_controller:
dependency: transitive
description:
@@ -2909,10 +2877,10 @@ packages:
dependency: transitive
description:
name: webdriver
sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e"
sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8"
url: "https://pub.dev"
source: hosted
version: "3.0.3"
version: "3.0.4"
webkit_inspection_protocol:
dependency: transitive
description:

View File

@@ -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+1120
version: 1.2.0+1200
publish_to: none
environment:
@@ -114,7 +114,6 @@ 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