diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml
index e49cf5b8daa91..ac57884eef34e 100644
--- a/mobile/android/app/src/main/AndroidManifest.xml
+++ b/mobile/android/app/src/main/AndroidManifest.xml
@@ -43,7 +43,7 @@
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/notification_icon" />
-
@@ -61,6 +61,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -96,4 +124,4 @@
-
+
\ No newline at end of file
diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json
index 1ff40b3566e00..6c3f92c53d825 100644
--- a/mobile/assets/i18n/en-US.json
+++ b/mobile/assets/i18n/en-US.json
@@ -657,5 +657,15 @@
"viewer_stack_use_as_main_asset": "Use as Main Asset",
"viewer_unstack": "Un-Stack",
"wifi_name": "WiFi Name",
- "your_wifi_name": "Your WiFi name"
-}
\ No newline at end of file
+ "your_wifi_name": "Your WiFi name",
+ "upload": "Upload",
+ "uploading": "Uploading",
+ "shared_intent_upload_button_progress_text": "{} / {} Uploaded",
+ "enqueued": "Enqueued",
+ "not_selected": "Not selected",
+ "completed": "Completed",
+ "failed": "Failed",
+ "paused": "Paused",
+ "canceled": "Canceled",
+ "upload_to_immich": "Upload to Immich ({})"
+}
diff --git a/mobile/ios/Podfile b/mobile/ios/Podfile
index b048c0bb0c87b..a98032db2009d 100644
--- a/mobile/ios/Podfile
+++ b/mobile/ios/Podfile
@@ -32,6 +32,13 @@ target 'Runner' do
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
+
+ # share_handler addition start
+ target 'ShareExtension' do
+ inherit! :search_paths
+ pod "share_handler_ios_models", :path => ".symlinks/plugins/share_handler_ios/ios/Models"
+ end
+ # share_handler addition end
end
post_install do |installer|
diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock
index bc65bd4b7f919..00a63be8d7b28 100644
--- a/mobile/ios/Podfile.lock
+++ b/mobile/ios/Podfile.lock
@@ -82,9 +82,17 @@ PODS:
- Flutter
- FlutterMacOS
- SAMKeychain (1.5.3)
- - SDWebImage (5.19.4):
- - SDWebImage/Core (= 5.19.4)
- - SDWebImage/Core (5.19.4)
+ - SDWebImage (5.20.0):
+ - SDWebImage/Core (= 5.20.0)
+ - SDWebImage/Core (5.20.0)
+ - share_handler_ios (0.0.14):
+ - Flutter
+ - share_handler_ios/share_handler_ios_models (= 0.0.14)
+ - share_handler_ios_models
+ - share_handler_ios/share_handler_ios_models (0.0.14):
+ - Flutter
+ - share_handler_ios_models
+ - share_handler_ios_models (0.0.9)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
@@ -94,7 +102,7 @@ PODS:
- Flutter
- FlutterMacOS
- SwiftyGif (5.4.5)
- - Toast (4.0.0)
+ - Toast (4.1.1)
- url_launcher_ios (0.0.1):
- Flutter
- wakelock_plus (0.0.1):
@@ -123,6 +131,8 @@ DEPENDENCIES:
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
+ - share_handler_ios (from `.symlinks/plugins/share_handler_ios/ios`)
+ - share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
@@ -184,6 +194,10 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/permission_handler_apple/ios"
photo_manager:
:path: ".symlinks/plugins/photo_manager/ios"
+ share_handler_ios:
+ :path: ".symlinks/plugins/share_handler_ios/ios"
+ share_handler_ios_models:
+ :path: ".symlinks/plugins/share_handler_ios/ios/Models"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
@@ -222,15 +236,17 @@ SPEC CHECKSUMS:
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
- SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d
+ SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8
+ share_handler_ios: 6dd3a4ac5ca0d955274aec712ba0ecdcaf583e7c
+ share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
- Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
+ Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
-PODFILE CHECKSUM: 2282844f7aed70427ae663932332dad1225156c8
+PODFILE CHECKSUM: 03b7eead4ee77b9e778179eeb0f3b5513617451c
COCOAPODS: 1.15.2
diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj
index b3da30f108ab2..10133cc330549 100644
--- a/mobile/ios/Runner.xcodeproj/project.pbxproj
+++ b/mobile/ios/Runner.xcodeproj/project.pbxproj
@@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+ 3B6A31FED0FC846D6BD69BBC /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */; };
65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */; };
65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
@@ -16,8 +17,21 @@
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */; };
+ FAC6F89B2D287C890078CB2F /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = FAC6F8902D287C890078CB2F /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ FAC6F8B72D287F120078CB2F /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6F8B52D287F120078CB2F /* ShareViewController.swift */; };
+ FAC6F8B92D287F120078CB2F /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FAC6F8B32D287F120078CB2F /* MainInterface.storyboard */; };
/* End PBXBuildFile section */
+/* Begin PBXContainerItemProxy section */
+ FAC6F8982D287C890078CB2F /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 97C146E61CF9000F007C117D /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = FAC6F88F2D287C890078CB2F;
+ remoteInfo = ShareExtension;
+ };
+/* End PBXContainerItemProxy section */
+
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
@@ -29,13 +43,26 @@
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
+ FAC6F89A2D287C890078CB2F /* Embed Foundation Extensions */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 13;
+ files = (
+ FAC6F89B2D287C890078CB2F /* ShareExtension.appex in Embed Foundation Extensions */,
+ );
+ name = "Embed Foundation Extensions";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
2E3441B73560D0F6FD25E04F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+ 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 571EAA93D77181C7C98C2EA6 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = ""; };
65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundServicePlugin.swift; sourceTree = ""; };
65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundSyncWorker.swift; sourceTree = ""; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
@@ -49,9 +76,16 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = ""; };
E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
F7101BB0391A314774615E89 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
+ F8A35EA3C3E01BD66AFDE0E5 /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = ""; };
FA9973382CF6DF4B000EF859 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; };
+ FAC6F8902D287C890078CB2F /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
+ FAC6F8B12D287F120078CB2F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ FAC6F8B22D287F120078CB2F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; };
+ FAC6F8B42D287F120078CB2F /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = ""; };
+ FAC6F8B52D287F120078CB2F /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; };
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = ""; };
/* End PBXFileReference section */
@@ -64,6 +98,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ FAC6F88D2D287C890078CB2F /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 3B6A31FED0FC846D6BD69BBC /* Pods_ShareExtension.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -73,6 +115,9 @@
2E3441B73560D0F6FD25E04F /* Pods-Runner.debug.xcconfig */,
E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */,
F7101BB0391A314774615E89 /* Pods-Runner.profile.xcconfig */,
+ F8A35EA3C3E01BD66AFDE0E5 /* Pods-ShareExtension.debug.xcconfig */,
+ 571EAA93D77181C7C98C2EA6 /* Pods-ShareExtension.release.xcconfig */,
+ B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */,
);
path = Pods;
sourceTree = "";
@@ -81,6 +126,7 @@
isa = PBXGroup;
children = (
886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */,
+ 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */,
);
name = Frameworks;
sourceTree = "";
@@ -110,6 +156,7 @@
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
+ FAC6F8B62D287F120078CB2F /* ShareExtension */,
97C146EF1CF9000F007C117D /* Products */,
0FB772A5B9601143383626CA /* Pods */,
1754452DD81DA6620E279E51 /* Frameworks */,
@@ -120,6 +167,7 @@
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Immich-Debug.app */,
+ FAC6F8902D287C890078CB2F /* ShareExtension.appex */,
);
name = Products;
sourceTree = "";
@@ -142,6 +190,17 @@
path = Runner;
sourceTree = "";
};
+ FAC6F8B62D287F120078CB2F /* ShareExtension */ = {
+ isa = PBXGroup;
+ children = (
+ FAC6F8B12D287F120078CB2F /* Info.plist */,
+ FAC6F8B32D287F120078CB2F /* MainInterface.storyboard */,
+ FAC6F8B42D287F120078CB2F /* ShareExtension.entitlements */,
+ FAC6F8B52D287F120078CB2F /* ShareViewController.swift */,
+ );
+ path = ShareExtension;
+ sourceTree = "";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -155,6 +214,7 @@
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
+ FAC6F89A2D287C890078CB2F /* Embed Foundation Extensions */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
D218A34AEE62BC1EF119F5B0 /* [CP] Embed Pods Frameworks */,
6724EEB7D74949FA08581154 /* [CP] Copy Pods Resources */,
@@ -162,12 +222,31 @@
buildRules = (
);
dependencies = (
+ FAC6F8992D287C890078CB2F /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Immich-Debug.app */;
productType = "com.apple.product-type.application";
};
+ FAC6F88F2D287C890078CB2F /* ShareExtension */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = FAC6F8A02D287C890078CB2F /* Build configuration list for PBXNativeTarget "ShareExtension" */;
+ buildPhases = (
+ 3BEF3D71D97E337D921C0EB5 /* [CP] Check Pods Manifest.lock */,
+ FAC6F88C2D287C890078CB2F /* Sources */,
+ FAC6F88D2D287C890078CB2F /* Frameworks */,
+ FAC6F88E2D287C890078CB2F /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = ShareExtension;
+ productName = ShareExtension;
+ productReference = FAC6F8902D287C890078CB2F /* ShareExtension.appex */;
+ productType = "com.apple.product-type.app-extension";
+ };
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -175,6 +254,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
+ LastSwiftUpdateCheck = 1600;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
@@ -182,6 +262,9 @@
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
+ FAC6F88F2D287C890078CB2F = {
+ CreatedOnToolsVersion = 16.0;
+ };
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
@@ -198,6 +281,7 @@
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
+ FAC6F88F2D287C890078CB2F /* ShareExtension */,
);
};
/* End PBXProject section */
@@ -214,6 +298,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ FAC6F88E2D287C890078CB2F /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ FAC6F8B92D287F120078CB2F /* MainInterface.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
@@ -233,6 +325,28 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
+ 3BEF3D71D97E337D921C0EB5 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-ShareExtension-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
4044AF030EF7D8721844FFBA /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -318,8 +432,24 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ FAC6F88C2D287C890078CB2F /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ FAC6F8B72D287F120078CB2F /* ShareViewController.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXSourcesBuildPhase section */
+/* Begin PBXTargetDependency section */
+ FAC6F8992D287C890078CB2F /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = FAC6F88F2D287C890078CB2F /* ShareExtension */;
+ targetProxy = FAC6F8982D287C890078CB2F /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
@@ -337,6 +467,14 @@
name = LaunchScreen.storyboard;
sourceTree = "";
};
+ FAC6F8B32D287F120078CB2F /* MainInterface.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ FAC6F8B22D287F120078CB2F /* Base */,
+ );
+ name = MainInterface.storyboard;
+ sourceTree = "";
+ };
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
@@ -404,6 +542,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 187;
+ CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -547,6 +686,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 187;
+ CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -576,6 +716,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 187;
+ CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -594,6 +735,129 @@
};
name = Release;
};
+ FAC6F89C2D287C890078CB2F /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = F8A35EA3C3E01BD66AFDE0E5 /* Pods-ShareExtension.debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ CUSTOM_GROUP_ID = group.app.immich.share;
+ DEVELOPMENT_TEAM = 2F67MQ8R79;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = ShareExtension/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.vdebug.ShareExtension;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ FAC6F89D2D287C890078CB2F /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 571EAA93D77181C7C98C2EA6 /* Pods-ShareExtension.release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ CUSTOM_GROUP_ID = group.app.immich.share;
+ DEVELOPMENT_TEAM = 2F67MQ8R79;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = ShareExtension/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.ShareExtension;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ FAC6F89E2D287C890078CB2F /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ CUSTOM_GROUP_ID = group.app.immich.share;
+ DEVELOPMENT_TEAM = 2F67MQ8R79;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = ShareExtension/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.profile.ShareExtension;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Profile;
+ };
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -617,6 +881,16 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
+ FAC6F8A02D287C890078CB2F /* Build configuration list for PBXNativeTarget "ShareExtension" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ FAC6F89C2D287C890078CB2F /* Debug */,
+ FAC6F89D2D287C890078CB2F /* Release */,
+ FAC6F89E2D287C890078CB2F /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist
index f3aed115b25bc..be5ec5d9d7069 100644
--- a/mobile/ios/Runner/Info.plist
+++ b/mobile/ios/Runner/Info.plist
@@ -2,6 +2,8 @@
+ AppGroupId
+ $(CUSTOM_GROUP_ID)
BGTaskSchedulerPermittedIdentifiers
app.alextran.immich.backgroundFetch
@@ -13,6 +15,24 @@
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
${PRODUCT_NAME}
+ CFBundleDocumentTypes
+
+
+ CFBundleTypeName
+ ShareHandler
+ LSHandlerRank
+ Alternate
+ LSItemContentTypes
+
+ public.file-url
+ public.image
+ public.text
+ public.movie
+ public.url
+ public.data
+
+
+
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
@@ -61,6 +81,17 @@
1.124.0
CFBundleSignature
????
+ CFBundleURLTypes
+
+
+ CFBundleTypeRole
+ Editor
+ CFBundleURLSchemes
+
+ ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)
+
+
+
CFBundleVersion
187
FLTEnableImpeller
@@ -73,6 +104,8 @@
LSRequiresIPhoneOS
+ LSSupportsOpeningDocumentsInPlace
+ No
MGLMapboxMetricsEnabledSettingShownInApp
NSAppTransportSecurity
@@ -94,6 +127,10 @@
We need to manage backup your photos album
NSPhotoLibraryUsageDescription
We need to manage backup your photos album
+ NSUserActivityTypes
+
+ INSendMessageIntent
+
UIApplicationSupportsIndirectInputEvents
UIBackgroundModes
diff --git a/mobile/ios/Runner/Runner.entitlements b/mobile/ios/Runner/Runner.entitlements
index ba21fbdaf2902..d558e35e0a036 100644
--- a/mobile/ios/Runner/Runner.entitlements
+++ b/mobile/ios/Runner/Runner.entitlements
@@ -4,5 +4,9 @@
com.apple.developer.networking.wifi-info
+ com.apple.security.application-groups
+
+ group.app.immich.share
+
diff --git a/mobile/ios/Runner/RunnerProfile.entitlements b/mobile/ios/Runner/RunnerProfile.entitlements
index 75e36a143e4f0..d44633db4087c 100644
--- a/mobile/ios/Runner/RunnerProfile.entitlements
+++ b/mobile/ios/Runner/RunnerProfile.entitlements
@@ -6,5 +6,9 @@
development
com.apple.developer.networking.wifi-info
+ com.apple.security.application-groups
+
+ group.app.immich.share
+
diff --git a/mobile/ios/ShareExtension/Base.lproj/MainInterface.storyboard b/mobile/ios/ShareExtension/Base.lproj/MainInterface.storyboard
new file mode 100644
index 0000000000000..286a50894d878
--- /dev/null
+++ b/mobile/ios/ShareExtension/Base.lproj/MainInterface.storyboard
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/ios/ShareExtension/Info.plist b/mobile/ios/ShareExtension/Info.plist
new file mode 100644
index 0000000000000..0f52fbffdf9c7
--- /dev/null
+++ b/mobile/ios/ShareExtension/Info.plist
@@ -0,0 +1,35 @@
+
+
+
+
+ AppGroupId
+ $(CUSTOM_GROUP_ID)
+ NSExtension
+
+ NSExtensionAttributes
+
+ IntentsSupported
+
+ INSendMessageIntent
+
+ NSExtensionActivationRule
+ SUBQUERY ( extensionItems, $extensionItem, SUBQUERY ( $extensionItem.attachments,
+ $attachment, ( ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.file-url"
+ || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image" || ANY
+ $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.text" || ANY
+ $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.movie" || ANY
+ $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" ) ).@count > 0
+ ).@count > 0
+ PHSupportedMediaTypes
+
+ Video
+ Image
+
+
+ NSExtensionMainStoryboard
+ MainInterface
+ NSExtensionPointIdentifier
+ com.apple.share-services
+
+
+
\ No newline at end of file
diff --git a/mobile/ios/ShareExtension/ShareExtension.entitlements b/mobile/ios/ShareExtension/ShareExtension.entitlements
new file mode 100644
index 0000000000000..4ad1a257d8b72
--- /dev/null
+++ b/mobile/ios/ShareExtension/ShareExtension.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.application-groups
+
+ group.app.immich.share
+
+
+
diff --git a/mobile/ios/ShareExtension/ShareViewController.swift b/mobile/ios/ShareExtension/ShareViewController.swift
new file mode 100644
index 0000000000000..b1b38efc79eae
--- /dev/null
+++ b/mobile/ios/ShareExtension/ShareViewController.swift
@@ -0,0 +1,3 @@
+import share_handler_ios_models
+
+class ShareViewController: ShareHandlerIosViewController {}
\ No newline at end of file
diff --git a/mobile/lib/interfaces/share_handler.interface.dart b/mobile/lib/interfaces/share_handler.interface.dart
new file mode 100644
index 0000000000000..6d0eb9170c412
--- /dev/null
+++ b/mobile/lib/interfaces/share_handler.interface.dart
@@ -0,0 +1,7 @@
+import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
+
+abstract interface class IShareHandlerRepository {
+ void Function(List)? onSharedMedia;
+
+ Future init();
+}
diff --git a/mobile/lib/interfaces/upload.interface.dart b/mobile/lib/interfaces/upload.interface.dart
new file mode 100644
index 0000000000000..d4b2298a148e8
--- /dev/null
+++ b/mobile/lib/interfaces/upload.interface.dart
@@ -0,0 +1,11 @@
+import 'package:background_downloader/background_downloader.dart';
+
+abstract interface class IUploadRepository {
+ void Function(TaskStatusUpdate)? onUploadStatus;
+ void Function(TaskProgressUpdate)? onTaskProgress;
+
+ Future upload(UploadTask task);
+ Future cancel(String id);
+ Future deleteAllTrackingRecords();
+ Future deleteRecordsWithIds(List id);
+}
diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart
index 807212fc655eb..e90d2d7b55958 100644
--- a/mobile/lib/main.dart
+++ b/mobile/lib/main.dart
@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:background_downloader/background_downloader.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:easy_localization/easy_localization.dart';
+import 'package:immich_mobile/services/share_intent_service.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:timezone/data/latest.dart';
import 'package:isar/isar.dart';
@@ -107,10 +108,12 @@ Future initApp() async {
progressBar: true,
);
- FileDownloader().trackTasksInGroup(
+ await FileDownloader().trackTasksInGroup(
downloadGroupLivePhoto,
markDownloadedComplete: false,
);
+
+ await FileDownloader().trackTasks();
}
Future loadDb() async {
@@ -208,6 +211,8 @@ class ImmichAppState extends ConsumerState
// needs to be delayed so that EasyLocalization is working
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
});
+
+ ref.read(shareIntentServiceProvider).init();
}
@override
diff --git a/mobile/lib/models/upload/share_intent_attachment.model.dart b/mobile/lib/models/upload/share_intent_attachment.model.dart
new file mode 100644
index 0000000000000..bd9cee55cdb0e
--- /dev/null
+++ b/mobile/lib/models/upload/share_intent_attachment.model.dart
@@ -0,0 +1,112 @@
+// ignore_for_file: public_member_api_docs, sort_constructors_first
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:immich_mobile/utils/bytes_units.dart';
+import 'package:path/path.dart';
+
+enum ShareIntentAttachmentType {
+ image,
+ video,
+}
+
+enum UploadStatus {
+ enqueued,
+ running,
+ complete,
+ notFound,
+ failed,
+ canceled,
+ waitingtoRetry,
+ paused,
+}
+
+class ShareIntentAttachment {
+ final String path;
+
+ // enum
+ final ShareIntentAttachmentType type;
+
+ // enum
+ final UploadStatus status;
+
+ final double uploadProgress;
+
+ final int fileLength;
+
+ ShareIntentAttachment({
+ required this.path,
+ required this.type,
+ required this.status,
+ this.uploadProgress = 0,
+ this.fileLength = 0,
+ });
+
+ int get id => hash(path);
+
+ File get file => File(path);
+
+ String get fileName => basename(file.path);
+
+ bool get isImage => type == ShareIntentAttachmentType.image;
+
+ bool get isVideo => type == ShareIntentAttachmentType.video;
+
+ String get fileSize => formatHumanReadableBytes(fileLength, 2);
+
+ ShareIntentAttachment copyWith({
+ String? path,
+ ShareIntentAttachmentType? type,
+ UploadStatus? status,
+ double? uploadProgress,
+ }) {
+ return ShareIntentAttachment(
+ path: path ?? this.path,
+ type: type ?? this.type,
+ status: status ?? this.status,
+ uploadProgress: uploadProgress ?? this.uploadProgress,
+ );
+ }
+
+ Map toMap() {
+ return {
+ 'path': path,
+ 'type': type.index,
+ 'status': status.index,
+ 'uploadProgress': uploadProgress,
+ };
+ }
+
+ factory ShareIntentAttachment.fromMap(Map map) {
+ return ShareIntentAttachment(
+ path: map['path'] as String,
+ type: ShareIntentAttachmentType.values[map['type'] as int],
+ status: UploadStatus.values[map['status'] as int],
+ uploadProgress: map['uploadProgress'] as double,
+ );
+ }
+
+ String toJson() => json.encode(toMap());
+
+ factory ShareIntentAttachment.fromJson(String source) =>
+ ShareIntentAttachment.fromMap(
+ json.decode(source) as Map,
+ );
+
+ @override
+ String toString() {
+ return 'ShareIntentAttachment(path: $path, type: $type, status: $status, uploadProgress: $uploadProgress)';
+ }
+
+ @override
+ bool operator ==(covariant ShareIntentAttachment other) {
+ if (identical(this, other)) return true;
+
+ return other.path == path && other.type == type;
+ }
+
+ @override
+ int get hashCode {
+ return path.hashCode ^ type.hashCode;
+ }
+}
diff --git a/mobile/lib/pages/album/album_shared_user_icons.dart b/mobile/lib/pages/album/album_shared_user_icons.dart
index 4cb9804e25bc3..f417f9fb38c3a 100644
--- a/mobile/lib/pages/album/album_shared_user_icons.dart
+++ b/mobile/lib/pages/album/album_shared_user_icons.dart
@@ -32,7 +32,7 @@ class AlbumSharedUserIcons extends HookConsumerWidget {
}
return GestureDetector(
- onTap: () => context.pushRoute(AlbumOptionsRoute()),
+ onTap: () => context.pushRoute(const AlbumOptionsRoute()),
child: SizedBox(
height: 50,
child: ListView.builder(
diff --git a/mobile/lib/pages/common/large_leading_tile.dart b/mobile/lib/pages/common/large_leading_tile.dart
index c6bbeb2e7df24..f2cb9f19aea32 100644
--- a/mobile/lib/pages/common/large_leading_tile.dart
+++ b/mobile/lib/pages/common/large_leading_tile.dart
@@ -13,6 +13,9 @@ class LargeLeadingTile extends StatelessWidget {
horizontal: 16.0,
),
this.borderRadius = 20.0,
+ this.trailing,
+ this.selected = false,
+ this.disabled = false,
});
final Widget leading;
@@ -21,30 +24,43 @@ class LargeLeadingTile extends StatelessWidget {
final Widget? subtitle;
final EdgeInsetsGeometry leadingPadding;
final double borderRadius;
-
+ final Widget? trailing;
+ final bool selected;
+ final bool disabled;
@override
Widget build(BuildContext context) {
return InkWell(
borderRadius: BorderRadius.circular(borderRadius),
- onTap: onTap,
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- Padding(
- padding: leadingPadding,
- child: leading,
- ),
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- SizedBox(
- width: context.width * 0.6,
- child: title,
+ onTap: disabled ? null : onTap,
+ child: Container(
+ decoration: BoxDecoration(
+ color: selected
+ ? Theme.of(context).primaryColor.withAlpha(30)
+ : Colors.transparent,
+ borderRadius: BorderRadius.circular(borderRadius),
+ ),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Padding(
+ padding: leadingPadding,
+ child: leading,
+ ),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SizedBox(
+ width: context.width * 0.6,
+ child: title,
+ ),
+ subtitle ?? const SizedBox.shrink(),
+ ],
),
- subtitle ?? const SizedBox.shrink(),
- ],
- ),
- ],
+ ),
+ if (trailing != null) trailing!,
+ ],
+ ),
),
);
}
diff --git a/mobile/lib/pages/photos/photos.page.dart b/mobile/lib/pages/photos/photos.page.dart
index 30fe1ab3f2939..9e15b0193e749 100644
--- a/mobile/lib/pages/photos/photos.page.dart
+++ b/mobile/lib/pages/photos/photos.page.dart
@@ -34,10 +34,12 @@ class PhotosPage extends HookConsumerWidget {
Future(() => ref.read(assetProvider.notifier).getAllAsset());
Future(() => ref.read(albumProvider.notifier).refreshRemoteAlbums());
ref.read(serverInfoProvider.notifier).getServerInfo();
+
return;
},
[],
);
+
Widget buildLoadingIndicator() {
Timer(const Duration(seconds: 2), () => tipOneOpacity.value = 1);
diff --git a/mobile/lib/pages/share_intent/share_intent.page.dart b/mobile/lib/pages/share_intent/share_intent.page.dart
new file mode 100644
index 0000000000000..77249cd486d75
--- /dev/null
+++ b/mobile/lib/pages/share_intent/share_intent.page.dart
@@ -0,0 +1,281 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
+
+import 'package:immich_mobile/pages/common/large_leading_tile.dart';
+import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
+import 'package:immich_mobile/entities/store.entity.dart' as db_store;
+
+@RoutePage()
+class ShareIntentPage extends HookConsumerWidget {
+ const ShareIntentPage({super.key, required this.attachments});
+
+ final List attachments;
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final currentEndpoint =
+ db_store.Store.get(db_store.StoreKey.serverEndpoint);
+ final candidates = ref.watch(shareIntentUploadProvider);
+ final isUploaded = useState(false);
+
+ useEffect(
+ () {
+ Future.microtask(() {
+ ref
+ .read(shareIntentUploadProvider.notifier)
+ .addAttachments(attachments);
+ });
+ return () {};
+ },
+ const [],
+ );
+
+ void removeAttachment(ShareIntentAttachment attachment) {
+ ref.read(shareIntentUploadProvider.notifier).removeAttachment(attachment);
+ }
+
+ void addAttachments(List attachments) {
+ ref.read(shareIntentUploadProvider.notifier).addAttachments(attachments);
+ }
+
+ void upload() async {
+ for (final attachment in candidates) {
+ await ref
+ .read(shareIntentUploadProvider.notifier)
+ .upload(attachment.file);
+ }
+
+ isUploaded.value = true;
+ }
+
+ bool isSelected(ShareIntentAttachment attachment) {
+ return candidates.contains(attachment);
+ }
+
+ void toggleSelection(ShareIntentAttachment attachment) {
+ if (isSelected(attachment)) {
+ removeAttachment(attachment);
+ } else {
+ addAttachments([attachment]);
+ }
+ }
+
+ return Scaffold(
+ appBar: AppBar(
+ title: Column(
+ children: [
+ const Text('upload_to_immich').tr(
+ args: [
+ candidates.length.toString(),
+ ],
+ ),
+ Text(
+ currentEndpoint,
+ style: context.textTheme.labelMedium?.copyWith(
+ color: context.colorScheme.onSurface.withAlpha(200),
+ ),
+ ),
+ ],
+ ),
+ ),
+ body: ListView.builder(
+ itemCount: attachments.length,
+ itemBuilder: (context, index) {
+ final attachment = attachments[index];
+ final target = candidates.firstWhere(
+ (element) => element.id == attachment.id,
+ orElse: () => attachment,
+ );
+
+ return Padding(
+ padding: const EdgeInsets.symmetric(
+ vertical: 4.0,
+ horizontal: 16,
+ ),
+ child: LargeLeadingTile(
+ onTap: () => toggleSelection(attachment),
+ disabled: isUploaded.value,
+ selected: isSelected(attachment),
+ leading: Stack(
+ children: [
+ ClipRRect(
+ borderRadius: BorderRadius.circular(16),
+ child: attachment.isImage
+ ? Image.file(
+ attachment.file,
+ width: 64,
+ height: 64,
+ fit: BoxFit.cover,
+ )
+ : const SizedBox(
+ width: 64,
+ height: 64,
+ child: Center(
+ child: Icon(
+ Icons.videocam,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ ),
+ if (attachment.isImage)
+ const Positioned(
+ top: 8,
+ right: 8,
+ child: Icon(
+ Icons.image,
+ color: Colors.white,
+ size: 20,
+ shadows: [
+ Shadow(
+ offset: Offset(0, 0),
+ blurRadius: 8.0,
+ color: Colors.black45,
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ title: Text(
+ attachment.fileName,
+ style: context.textTheme.titleSmall,
+ ),
+ subtitle: Text(
+ attachment.fileSize,
+ style: context.textTheme.labelLarge,
+ ),
+ trailing: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0),
+ child: UploadStatusIcon(
+ selected: isSelected(attachment),
+ status: target.status,
+ progress: target.uploadProgress,
+ ),
+ ),
+ ),
+ );
+ },
+ ),
+ bottomNavigationBar: SafeArea(
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: SizedBox(
+ height: 48,
+ child: ElevatedButton(
+ onPressed: isUploaded.value ? null : upload,
+ child: isUploaded.value
+ ? UploadingText(candidates: candidates)
+ : const Text('upload').tr(),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class UploadingText extends StatelessWidget {
+ const UploadingText({super.key, required this.candidates});
+ final List candidates;
+
+ @override
+ Widget build(BuildContext context) {
+ final uploadedCount = candidates.where((element) {
+ return element.status == UploadStatus.complete;
+ }).length;
+
+ return const Text("shared_intent_upload_button_progress_text")
+ .tr(args: [uploadedCount.toString(), candidates.length.toString()]);
+ }
+}
+
+class UploadStatusIcon extends StatelessWidget {
+ const UploadStatusIcon({
+ super.key,
+ required this.status,
+ required this.selected,
+ this.progress = 0,
+ });
+
+ final UploadStatus status;
+ final double progress;
+ final bool selected;
+
+ @override
+ Widget build(BuildContext context) {
+ if (!selected) {
+ return Icon(
+ Icons.check_circle_outline_rounded,
+ color: context.colorScheme.onSurface.withAlpha(100),
+ semanticLabel: 'not_selected'.tr(),
+ );
+ }
+
+ switch (status) {
+ case UploadStatus.enqueued:
+ return Icon(
+ Icons.check_circle_rounded,
+ color: context.primaryColor,
+ semanticLabel: 'enqueued'.tr(),
+ );
+ case UploadStatus.running:
+ return Stack(
+ alignment: AlignmentDirectional.center,
+ children: [
+ SizedBox(
+ width: 40,
+ height: 40,
+ child: TweenAnimationBuilder(
+ tween: Tween(begin: 0.0, end: progress),
+ duration: const Duration(milliseconds: 500),
+ builder: (context, value, _) => CircularProgressIndicator(
+ backgroundColor: context.colorScheme.surfaceContainerLow,
+ strokeWidth: 3,
+ value: value,
+ semanticsLabel: 'uploading'.tr(),
+ ),
+ ),
+ ),
+ Text(
+ (progress * 100).toStringAsFixed(0),
+ style: context.textTheme.labelSmall?.copyWith(
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
+ );
+ case UploadStatus.complete:
+ return Icon(
+ Icons.check_circle_rounded,
+ color: Colors.green,
+ semanticLabel: 'completed'.tr(),
+ );
+ case UploadStatus.notFound:
+ case UploadStatus.failed:
+ return Icon(
+ Icons.error_rounded,
+ color: Colors.red,
+ semanticLabel: 'failed'.tr(),
+ );
+ case UploadStatus.canceled:
+ return Icon(
+ Icons.cancel_rounded,
+ color: Colors.red,
+ semanticLabel: 'canceled'.tr(),
+ );
+ case UploadStatus.waitingtoRetry:
+ case UploadStatus.paused:
+ return Icon(
+ Icons.pause_circle_rounded,
+ color: context.primaryColor,
+ semanticLabel: 'paused'.tr(),
+ );
+ }
+ }
+}
diff --git a/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart b/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart
new file mode 100644
index 0000000000000..49392928c0fd1
--- /dev/null
+++ b/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart
@@ -0,0 +1,108 @@
+import 'dart:io';
+
+import 'package:background_downloader/background_downloader.dart';
+import 'package:flutter/foundation.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/string_extensions.dart';
+import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
+import 'package:immich_mobile/services/upload.service.dart';
+
+final shareIntentUploadProvider = StateNotifierProvider.autoDispose<
+ ShareIntentUploadStateNotifier, List>(
+ ((ref) => ShareIntentUploadStateNotifier(
+ ref.watch(uploadServiceProvider),
+ )),
+);
+
+class ShareIntentUploadStateNotifier
+ extends StateNotifier> {
+ final UploadService _uploadService;
+
+ ShareIntentUploadStateNotifier(
+ this._uploadService,
+ ) : super([]) {
+ _uploadService.onUploadStatus = _uploadStatusCallback;
+ _uploadService.onTaskProgress = _taskProgressCallback;
+ }
+
+ void addAttachments(List attachments) {
+ state = [...state, ...attachments];
+ }
+
+ void removeAttachment(ShareIntentAttachment attachment) {
+ state = state.where((element) => element != attachment).toList();
+ }
+
+ void clearAttachments() {
+ state = [];
+ }
+
+ void _updateUploadStatus(TaskStatusUpdate task, TaskStatus status) async {
+ if (status == TaskStatus.canceled) {
+ return;
+ }
+
+ final taskId = task.task.taskId;
+ final uploadStatus = switch (task.status) {
+ TaskStatus.complete => UploadStatus.complete,
+ TaskStatus.failed => UploadStatus.failed,
+ TaskStatus.canceled => UploadStatus.canceled,
+ TaskStatus.enqueued => UploadStatus.enqueued,
+ TaskStatus.running => UploadStatus.running,
+ TaskStatus.paused => UploadStatus.paused,
+ TaskStatus.notFound => UploadStatus.notFound,
+ TaskStatus.waitingToRetry => UploadStatus.waitingtoRetry
+ };
+
+ state = [
+ for (final attachment in state)
+ if (attachment.id == taskId.toInt())
+ attachment.copyWith(status: uploadStatus)
+ else
+ attachment,
+ ];
+ }
+
+ void _uploadStatusCallback(TaskStatusUpdate update) {
+ _updateUploadStatus(update, update.status);
+
+ switch (update.status) {
+ case TaskStatus.complete:
+ if (update.responseStatusCode == 200) {
+ kDebugMode
+ ? debugPrint("[COMPLETE] ${update.task.taskId} - DUPLICATE")
+ : null;
+ } else {
+ kDebugMode ? debugPrint("[COMPLETE] ${update.task.taskId}") : null;
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ void _taskProgressCallback(TaskProgressUpdate update) {
+ // Ignore if the task is cancled or completed
+ if (update.progress == -2 || update.progress == -1) {
+ return;
+ }
+
+ final taskId = update.task.taskId;
+ state = [
+ for (final attachment in state)
+ if (attachment.id == taskId.toInt())
+ attachment.copyWith(uploadProgress: update.progress)
+ else
+ attachment,
+ ];
+ }
+
+ Future upload(File file) {
+ return _uploadService.upload(file);
+ }
+
+ Future cancelUpload(String id) {
+ return _uploadService.cancelUpload(id);
+ }
+}
diff --git a/mobile/lib/repositories/share_handler.repository.dart b/mobile/lib/repositories/share_handler.repository.dart
new file mode 100644
index 0000000000000..4c07b662a8ea9
--- /dev/null
+++ b/mobile/lib/repositories/share_handler.repository.dart
@@ -0,0 +1,63 @@
+import 'dart:io';
+
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/interfaces/share_handler.interface.dart';
+import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
+import 'package:share_handler/share_handler.dart';
+
+final shareHandlerRepositoryProvider = Provider(
+ (ref) => ShareHandlerRepository(),
+);
+
+class ShareHandlerRepository implements IShareHandlerRepository {
+ ShareHandlerRepository();
+
+ @override
+ void Function(List attachments)? onSharedMedia;
+
+ @override
+ Future init() async {
+ final handler = ShareHandlerPlatform.instance;
+ final media = await handler.getInitialSharedMedia();
+
+ if (media != null && media.attachments != null) {
+ onSharedMedia?.call(_buildPayload(media.attachments!));
+ }
+
+ handler.sharedMediaStream.listen((SharedMedia media) {
+ if (media.attachments != null) {
+ onSharedMedia?.call(_buildPayload(media.attachments!));
+ }
+ });
+ }
+
+ List _buildPayload(
+ List attachments,
+ ) {
+ final payload = [];
+
+ for (final attachment in attachments) {
+ if (attachment == null) {
+ continue;
+ }
+
+ final type = attachment.type == SharedAttachmentType.image
+ ? ShareIntentAttachmentType.image
+ : ShareIntentAttachmentType.video;
+
+ final fileLength = File(attachment.path).lengthSync();
+
+ payload.add(
+ ShareIntentAttachment(
+ path: attachment.path,
+ type: type,
+ status: UploadStatus.enqueued,
+ uploadProgress: 0.0,
+ fileLength: fileLength,
+ ),
+ );
+ }
+
+ return payload;
+ }
+}
diff --git a/mobile/lib/repositories/upload.repository.dart b/mobile/lib/repositories/upload.repository.dart
new file mode 100644
index 0000000000000..6445d144f627c
--- /dev/null
+++ b/mobile/lib/repositories/upload.repository.dart
@@ -0,0 +1,42 @@
+import 'package:background_downloader/background_downloader.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/interfaces/upload.interface.dart';
+import 'package:immich_mobile/utils/upload.dart';
+
+final uploadRepositoryProvider = Provider((ref) => UploadRepository());
+
+class UploadRepository implements IUploadRepository {
+ @override
+ void Function(TaskStatusUpdate)? onUploadStatus;
+
+ @override
+ void Function(TaskProgressUpdate)? onTaskProgress;
+
+ UploadRepository() {
+ FileDownloader().registerCallbacks(
+ group: uploadGroup,
+ taskStatusCallback: (update) => onUploadStatus?.call(update),
+ taskProgressCallback: (update) => onTaskProgress?.call(update),
+ );
+ }
+
+ @override
+ Future upload(UploadTask task) {
+ return FileDownloader().enqueue(task);
+ }
+
+ @override
+ Future deleteAllTrackingRecords() {
+ return FileDownloader().database.deleteAllRecords();
+ }
+
+ @override
+ Future cancel(String id) {
+ return FileDownloader().cancelTaskWithId(id);
+ }
+
+ @override
+ Future deleteRecordsWithIds(List ids) {
+ return FileDownloader().database.deleteRecordsWithIds(ids);
+ }
+}
diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart
index 5adfeb4061db2..3078a0dc1a714 100644
--- a/mobile/lib/routing/router.dart
+++ b/mobile/lib/routing/router.dart
@@ -8,6 +8,7 @@ import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/models/memories/memory.model.dart';
import 'package:immich_mobile/models/search/search_filter.model.dart';
import 'package:immich_mobile/models/shared_link/shared_link.model.dart';
+import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
import 'package:immich_mobile/pages/backup/album_preview.page.dart';
import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart';
import 'package:immich_mobile/pages/backup/backup_controller.page.dart';
@@ -57,6 +58,7 @@ import 'package:immich_mobile/pages/library/partner/partner.page.dart';
import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart';
import 'package:immich_mobile/pages/library/shared_link/shared_link.page.dart';
import 'package:immich_mobile/pages/library/shared_link/shared_link_edit.page.dart';
+import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/routing/auth_guard.dart';
@@ -277,6 +279,10 @@ class AppRouter extends RootStackRouter {
page: NativeVideoViewerRoute.page,
guards: [_authGuard, _duplicateGuard],
),
+ AutoRoute(
+ page: ShareIntentRoute.page,
+ guards: [_authGuard, _duplicateGuard],
+ ),
];
}
diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart
index 3bd89661753f9..48528fdfe2ffd 100644
--- a/mobile/lib/routing/router.gr.dart
+++ b/mobile/lib/routing/router.gr.dart
@@ -136,15 +136,10 @@ class AlbumAssetSelectionRouteArgs {
/// generated route for
/// [AlbumOptionsPage]
-class AlbumOptionsRoute extends PageRouteInfo {
- AlbumOptionsRoute({
- Key? key,
- List? children,
- }) : super(
+class AlbumOptionsRoute extends PageRouteInfo {
+ const AlbumOptionsRoute({List? children})
+ : super(
AlbumOptionsRoute.name,
- args: AlbumOptionsRouteArgs(
- key: key,
- ),
initialChildren: children,
);
@@ -153,25 +148,11 @@ class AlbumOptionsRoute extends PageRouteInfo {
static PageInfo page = PageInfo(
name,
builder: (data) {
- final args = data.argsAs();
- return AlbumOptionsPage(
- key: args.key,
- );
+ return const AlbumOptionsPage();
},
);
}
-class AlbumOptionsRouteArgs {
- const AlbumOptionsRouteArgs({this.key});
-
- final Key? key;
-
- @override
- String toString() {
- return 'AlbumOptionsRouteArgs{key: $key}';
- }
-}
-
/// generated route for
/// [AlbumPreviewPage]
class AlbumPreviewRoute extends PageRouteInfo {
@@ -1453,6 +1434,52 @@ class SettingsSubRouteArgs {
}
}
+/// generated route for
+/// [ShareIntentPage]
+class ShareIntentRoute extends PageRouteInfo {
+ ShareIntentRoute({
+ Key? key,
+ required List attachments,
+ List? children,
+ }) : super(
+ ShareIntentRoute.name,
+ args: ShareIntentRouteArgs(
+ key: key,
+ attachments: attachments,
+ ),
+ initialChildren: children,
+ );
+
+ static const String name = 'ShareIntentRoute';
+
+ static PageInfo page = PageInfo(
+ name,
+ builder: (data) {
+ final args = data.argsAs();
+ return ShareIntentPage(
+ key: args.key,
+ attachments: args.attachments,
+ );
+ },
+ );
+}
+
+class ShareIntentRouteArgs {
+ const ShareIntentRouteArgs({
+ this.key,
+ required this.attachments,
+ });
+
+ final Key? key;
+
+ final List attachments;
+
+ @override
+ String toString() {
+ return 'ShareIntentRouteArgs{key: $key, attachments: $attachments}';
+ }
+}
+
/// generated route for
/// [SharedLinkEditPage]
class SharedLinkEditRoute extends PageRouteInfo {
diff --git a/mobile/lib/services/share_intent_service.dart b/mobile/lib/services/share_intent_service.dart
new file mode 100644
index 0000000000000..31158de49eda4
--- /dev/null
+++ b/mobile/lib/services/share_intent_service.dart
@@ -0,0 +1,27 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
+import 'package:immich_mobile/repositories/share_handler.repository.dart';
+import 'package:immich_mobile/routing/router.dart';
+
+final shareIntentServiceProvider = Provider(
+ (ref) => ShareIntentService(
+ ref.watch(appRouterProvider),
+ ref.watch(shareHandlerRepositoryProvider),
+ ),
+);
+
+class ShareIntentService {
+ final AppRouter router;
+ final ShareHandlerRepository shareHandlerRepository;
+
+ ShareIntentService(this.router, this.shareHandlerRepository);
+
+ Future init() {
+ shareHandlerRepository.onSharedMedia = navigateToShareIntentRoute;
+ return shareHandlerRepository.init();
+ }
+
+ void navigateToShareIntentRoute(List attachments) {
+ router.push(ShareIntentRoute(attachments: attachments));
+ }
+}
diff --git a/mobile/lib/services/upload.service.dart b/mobile/lib/services/upload.service.dart
new file mode 100644
index 0000000000000..78f143658763c
--- /dev/null
+++ b/mobile/lib/services/upload.service.dart
@@ -0,0 +1,94 @@
+import 'dart:io';
+
+import 'package:background_downloader/background_downloader.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/store.entity.dart';
+import 'package:immich_mobile/interfaces/upload.interface.dart';
+import 'package:immich_mobile/repositories/upload.repository.dart';
+import 'package:immich_mobile/services/api.service.dart';
+import 'package:immich_mobile/utils/upload.dart';
+import 'package:path/path.dart';
+// import 'package:logging/logging.dart';
+
+final uploadServiceProvider = Provider(
+ (ref) => UploadService(
+ ref.watch(uploadRepositoryProvider),
+ ),
+);
+
+class UploadService {
+ final IUploadRepository _uploadRepository;
+ // final Logger _log = Logger("UploadService");
+ void Function(TaskStatusUpdate)? onUploadStatus;
+ void Function(TaskProgressUpdate)? onTaskProgress;
+
+ UploadService(
+ this._uploadRepository,
+ ) {
+ _uploadRepository.onUploadStatus = _onUploadCallback;
+ _uploadRepository.onTaskProgress = _onTaskProgressCallback;
+ }
+
+ void _onTaskProgressCallback(TaskProgressUpdate update) {
+ onTaskProgress?.call(update);
+ }
+
+ void _onUploadCallback(TaskStatusUpdate update) {
+ onUploadStatus?.call(update);
+ }
+
+ Future cancelUpload(String id) async {
+ return await FileDownloader().cancelTaskWithId(id);
+ }
+
+ Future upload(File file) async {
+ final task = await _buildUploadTask(
+ hash(file.path).toString(),
+ file,
+ );
+
+ await _uploadRepository.upload(task);
+ }
+
+ Future _buildUploadTask(
+ String id,
+ File file, {
+ Map? fields,
+ }) async {
+ final serverEndpoint = Store.get(StoreKey.serverEndpoint);
+ final url = Uri.parse('$serverEndpoint/assets').toString();
+ final headers = ApiService.getRequestHeaders();
+ final deviceId = Store.get(StoreKey.deviceId);
+
+ final (baseDirectory, directory, filename) =
+ await Task.split(filePath: file.path);
+ final stats = await file.stat();
+ final fileCreatedAt = stats.changed;
+ final fileModifiedAt = stats.modified;
+
+ final fieldsMap = {
+ 'filename': filename,
+ 'deviceAssetId': id,
+ 'deviceId': deviceId,
+ 'fileCreatedAt': fileCreatedAt.toUtc().toIso8601String(),
+ 'fileModifiedAt': fileModifiedAt.toUtc().toIso8601String(),
+ 'isFavorite': 'false',
+ 'duration': '0',
+ if (fields != null) ...fields,
+ };
+
+ return UploadTask(
+ taskId: id,
+ httpRequestMethod: 'POST',
+ url: url,
+ headers: headers,
+ filename: filename,
+ fields: fieldsMap,
+ baseDirectory: baseDirectory,
+ directory: directory,
+ fileField: 'assetData',
+ group: uploadGroup,
+ updates: Updates.statusAndProgress,
+ );
+ }
+}
diff --git a/mobile/lib/utils/bytes_units.dart b/mobile/lib/utils/bytes_units.dart
index ea9d0f5cf5eb4..3a73e5b3200e6 100644
--- a/mobile/lib/utils/bytes_units.dart
+++ b/mobile/lib/utils/bytes_units.dart
@@ -1,3 +1,5 @@
+import 'dart:math';
+
String formatBytes(int bytes) {
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB'];
@@ -14,3 +16,10 @@ String formatBytes(int bytes) {
return "${remainder.toStringAsFixed(magnitude == 0 ? 0 : 1)} ${units[magnitude]}";
}
+
+String formatHumanReadableBytes(int bytes, int decimals) {
+ if (bytes <= 0) return "0 B";
+ const suffixes = ["B", "KB", "MB", "GB", "TB"];
+ var i = (log(bytes) / log(1024)).floor();
+ return '${(bytes / pow(1024, i)).toStringAsFixed(decimals)} ${suffixes[i]}';
+}
diff --git a/mobile/lib/utils/upload.dart b/mobile/lib/utils/upload.dart
new file mode 100644
index 0000000000000..a0b77f1d93b45
--- /dev/null
+++ b/mobile/lib/utils/upload.dart
@@ -0,0 +1 @@
+const uploadGroup = 'upload_group';
diff --git a/mobile/lib/widgets/album/album_viewer_appbar.dart b/mobile/lib/widgets/album/album_viewer_appbar.dart
index 7c36ebc21d4e4..b058f29e7d286 100644
--- a/mobile/lib/widgets/album/album_viewer_appbar.dart
+++ b/mobile/lib/widgets/album/album_viewer_appbar.dart
@@ -206,7 +206,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
),
ListTile(
leading: const Icon(Icons.settings_rounded),
- onTap: () => context.navigateTo(AlbumOptionsRoute()),
+ onTap: () => context.navigateTo(const AlbumOptionsRoute()),
title: const Text(
"translated_text_options",
style: TextStyle(fontWeight: FontWeight.w500),
diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock
index 34eb217828102..91c068ec2669d 100644
--- a/mobile/pubspec.lock
+++ b/mobile/pubspec.lock
@@ -1328,6 +1328,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.3.8"
+ share_handler:
+ dependency: "direct main"
+ description:
+ name: share_handler
+ sha256: "76575533be04df3fecbebd3c5b5325a8271b5973131f8b8b0ab8490c395a5d37"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.0.22"
+ share_handler_android:
+ dependency: transitive
+ description:
+ name: share_handler_android
+ sha256: "124dcc914fb7ecd89076d3dc28435b98fe2129a988bf7742f7a01dcb66a95667"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.0.9"
+ share_handler_ios:
+ dependency: transitive
+ description:
+ name: share_handler_ios
+ sha256: cdc21f88f336a944157a8e9ceb191525cee3b082d6eb6c2082488e4f09dc3ece
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.0.15"
+ share_handler_platform_interface:
+ dependency: transitive
+ description:
+ name: share_handler_platform_interface
+ sha256: "7a4df95a87b326b2f07458d937f2281874567c364b7b7ebe4e7d50efaae5f106"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.0.6"
share_plus:
dependency: "direct main"
description:
diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml
index c69290b29913b..6d9779b3e66a4 100644
--- a/mobile/pubspec.yaml
+++ b/mobile/pubspec.yaml
@@ -77,6 +77,7 @@ dependencies:
image_picker: ^1.0.7 # only used to select user profile image from system gallery -> we can simply select an image from within immich?
logging: ^1.2.0
file_picker: ^8.0.0+1
+ share_handler: ^0.0.22
# This is uncommented in F-Droid build script
# Taken from https://github.com/Myzel394/locus/blob/445013d22ec1d759027d4303bd65b30c5c8588c8/pubspec.yaml#L105