diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 25334de..842ea55 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -46,6 +46,9 @@ PODS: - Flutter - flutter_native_splash (0.0.1): - Flutter + - fluttertoast (0.0.2): + - Flutter + - Toast - FMDB (2.7.5): - FMDB/standard (= 2.7.5) - FMDB/standard (2.7.5) @@ -62,15 +65,20 @@ PODS: - permission_handler_apple (9.0.4): - Flutter - ReachabilitySwift (5.0.0) + - receive_sharing_intent (0.0.1): + - Flutter - SDWebImage (5.13.5): - SDWebImage/Core (= 5.13.5) - SDWebImage/Core (5.13.5) + - share_plus (0.0.1): + - Flutter - shared_preferences_ios (0.0.1): - Flutter - sqflite (0.0.2): - Flutter - FMDB (>= 2.7.5) - SwiftyGif (5.4.3) + - Toast (4.0.0) - url_launcher_ios (0.0.1): - Flutter - WeScan (1.7.0) @@ -83,12 +91,15 @@ DEPENDENCIES: - Flutter (from `Flutter`) - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) + - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) - pdfx (from `.symlinks/plugins/pdfx/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`) + - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -101,6 +112,7 @@ SPEC REPOS: - ReachabilitySwift - SDWebImage - SwiftyGif + - Toast - WeScan EXTERNAL SOURCES: @@ -118,6 +130,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" flutter_native_splash: :path: ".symlinks/plugins/flutter_native_splash/ios" + fluttertoast: + :path: ".symlinks/plugins/fluttertoast/ios" integration_test: :path: ".symlinks/plugins/integration_test/ios" local_auth_ios: @@ -130,6 +144,10 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/pdfx/ios" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" + receive_sharing_intent: + :path: ".symlinks/plugins/receive_sharing_intent/ios" + share_plus: + :path: ".symlinks/plugins/share_plus/ios" shared_preferences_ios: :path: ".symlinks/plugins/shared_preferences_ios/ios" sqflite: @@ -147,6 +165,7 @@ SPEC CHECKSUMS: Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef + fluttertoast: 74526702fea2c060ea55dde75895b7e1bde1c86b FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5 local_auth_ios: 0d333dde7780f669e66f19d2ff6005f3ea84008d @@ -155,10 +174,13 @@ SPEC CHECKSUMS: pdfx: 7b876b09de8b7a0bf444a4f82b439ffcff4ee1ec permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 + receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1 SDWebImage: 23d714cd599354ee7906dbae26dff89b421c4370 + share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 + Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de WeScan: fed582f6c38014d529afb5aa9ffd1bad38fc72b7 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index cef9439..664648c 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,12 +3,15 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 51; objects = { /* 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 */; }; + 4D0C7954292C15240064EE78 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D0C7953292C15240064EE78 /* ShareViewController.swift */; }; + 4D0C7957292C15240064EE78 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4D0C7955292C15240064EE78 /* MainInterface.storyboard */; }; + 4D0C795B292C15240064EE78 /* receive_sharing_intent.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 4D0C7951292C15240064EE78 /* receive_sharing_intent.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -16,7 +19,28 @@ B8F579F7EE511C92B2614EE2 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 40BF6B81A87C86D22CDB775A /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 4D0C7959292C15240064EE78 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 4D0C7950292C15240064EE78; + remoteInfo = receive_sharing_intent; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ + 4D0C795C292C15250064EE78 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 4D0C795B292C15240064EE78 /* receive_sharing_intent.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -34,6 +58,12 @@ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 40BF6B81A87C86D22CDB775A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4D0C7951292C15240064EE78 /* receive_sharing_intent.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = receive_sharing_intent.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 4D0C7953292C15240064EE78 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; + 4D0C7956292C15240064EE78 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; + 4D0C7958292C15240064EE78 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 4D0C7961292C196E0064EE78 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + 4D0C7962292C19960064EE78 /* receive_sharing_intent.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = receive_sharing_intent.entitlements; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -50,6 +80,13 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 4D0C794E292C15240064EE78 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -61,6 +98,17 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 4D0C7952292C15240064EE78 /* receive_sharing_intent */ = { + isa = PBXGroup; + children = ( + 4D0C7962292C19960064EE78 /* receive_sharing_intent.entitlements */, + 4D0C7953292C15240064EE78 /* ShareViewController.swift */, + 4D0C7955292C15240064EE78 /* MainInterface.storyboard */, + 4D0C7958292C15240064EE78 /* Info.plist */, + ); + path = receive_sharing_intent; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -77,6 +125,7 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, + 4D0C7952292C15240064EE78 /* receive_sharing_intent */, 97C146EF1CF9000F007C117D /* Products */, E525DE4999AE527627D97DCA /* Pods */, FB6F7F4A953DAAA3FFE794E6 /* Frameworks */, @@ -87,6 +136,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + 4D0C7951292C15240064EE78 /* receive_sharing_intent.appex */, ); name = Products; sourceTree = ""; @@ -94,6 +144,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 4D0C7961292C196E0064EE78 /* Runner.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, @@ -113,7 +164,6 @@ 9F0882A26646B3A4713EAA3B /* Pods-Runner.release.xcconfig */, 895AB075C3F1E3E87F6C3D1A /* Pods-Runner.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -128,6 +178,23 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 4D0C7950292C15240064EE78 /* receive_sharing_intent */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4D0C7960292C15250064EE78 /* Build configuration list for PBXNativeTarget "receive_sharing_intent" */; + buildPhases = ( + 4D0C794D292C15240064EE78 /* Sources */, + 4D0C794E292C15240064EE78 /* Frameworks */, + 4D0C794F292C15240064EE78 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = receive_sharing_intent; + productName = receive_sharing_intent; + productReference = 4D0C7951292C15240064EE78 /* receive_sharing_intent.appex */; + productType = "com.apple.product-type.app-extension"; + }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; @@ -140,10 +207,12 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 13B6B31A3992BA65B73408A6 /* [CP] Embed Pods Frameworks */, + 4D0C795C292C15250064EE78 /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( + 4D0C795A292C15240064EE78 /* PBXTargetDependency */, ); name = Runner; productName = Runner; @@ -156,9 +225,13 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { + LastSwiftUpdateCheck = 1400; LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { + 4D0C7950292C15240064EE78 = { + CreatedOnToolsVersion = 14.0.1; + }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; @@ -179,11 +252,20 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + 4D0C7950292C15240064EE78 /* receive_sharing_intent */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 4D0C794F292C15240064EE78 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4D0C7957292C15240064EE78 /* MainInterface.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -268,6 +350,14 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 4D0C794D292C15240064EE78 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4D0C7954292C15240064EE78 /* ShareViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -279,7 +369,23 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 4D0C795A292C15240064EE78 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 4D0C7950292C15240064EE78 /* receive_sharing_intent */; + targetProxy = 4D0C7959292C15240064EE78 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ + 4D0C7955292C15240064EE78 /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 4D0C7956292C15240064EE78 /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -353,16 +459,19 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterPaperlessNgScanAndShare; + PRODUCT_BUNDLE_IDENTIFIER = "de.astubenbord.paperless-mobile"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -370,6 +479,114 @@ }; name = Profile; }; + 4D0C795D292C15250064EE78 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = receive_sharing_intent/receive_sharing_intent.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = receive_sharing_intent/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = receive_sharing_intent; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "de.astubenbord.paperless-mobile.receive-sharing-intent"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 4D0C795E292C15250064EE78 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = receive_sharing_intent/receive_sharing_intent.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = receive_sharing_intent/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = receive_sharing_intent; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "de.astubenbord.paperless-mobile.receive-sharing-intent"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 4D0C795F292C15250064EE78 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = receive_sharing_intent/receive_sharing_intent.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = receive_sharing_intent/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = receive_sharing_intent; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "de.astubenbord.paperless-mobile.receive-sharing-intent"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Profile; + }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -481,16 +698,19 @@ isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterPaperlessNgScanAndShare; + PRODUCT_BUNDLE_IDENTIFIER = "de.astubenbord.paperless-mobile"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -503,16 +723,19 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterPaperlessNgScanAndShare; + PRODUCT_BUNDLE_IDENTIFIER = "de.astubenbord.paperless-mobile"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -523,6 +746,16 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 4D0C7960292C15250064EE78 /* Build configuration list for PBXNativeTarget "receive_sharing_intent" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4D0C795D292C15250064EE78 /* Debug */, + 4D0C795E292C15250064EE78 /* Release */, + 4D0C795F292C15250064EE78 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index f20953e..6cb665f 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,6 +2,22 @@ + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + ShareMedia + + + + + + NSPhotoLibraryUsageDescription + To upload photos, please allow permission to access your photo library. + NSFaceIDUsageDescription Why is my app authenticating using face id? CFBundleDevelopmentRegion @@ -48,6 +64,6 @@ UIStatusBarHidden CADisableMinimumFrameDurationOnPhone - - + + diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements new file mode 100644 index 0000000..eca2248 --- /dev/null +++ b/ios/Runner/Runner.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.de.astubenbord.paperless-mobile + + + diff --git a/ios/receive_sharing_intent/Base.lproj/MainInterface.storyboard b/ios/receive_sharing_intent/Base.lproj/MainInterface.storyboard new file mode 100644 index 0000000..286a508 --- /dev/null +++ b/ios/receive_sharing_intent/Base.lproj/MainInterface.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/receive_sharing_intent/Info.plist b/ios/receive_sharing_intent/Info.plist new file mode 100644 index 0000000..5729859 --- /dev/null +++ b/ios/receive_sharing_intent/Info.plist @@ -0,0 +1,28 @@ + + + + + NSExtension + + NSExtensionAttributes + + PHSupportedMediaTypes + + Image + + NSExtensionActivationRule + + NSExtensionActivationSupportsImageWithMaxCount + 1 + NSExtensionActivationSupportsFileWithMaxCount + 1 + + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.share-services + + + + diff --git a/ios/receive_sharing_intent/ShareViewController.swift b/ios/receive_sharing_intent/ShareViewController.swift new file mode 100644 index 0000000..444133a --- /dev/null +++ b/ios/receive_sharing_intent/ShareViewController.swift @@ -0,0 +1,337 @@ +import UIKit +import Social +import MobileCoreServices +import Photos + +class ShareViewController: SLComposeServiceViewController { + let hostAppBundleIdentifier = "de.astubenbord.paperless-mobile" + let sharedKey = "ShareKey" + var sharedMedia: [SharedMediaFile] = [] + var sharedText: [String] = [] + let imageContentType = kUTTypeImage as String + let videoContentType = kUTTypeMovie as String + let textContentType = kUTTypeText as String + let urlContentType = kUTTypeURL as String + let fileURLType = kUTTypeFileURL as String; + + override func isContentValid() -> Bool { + return true + } + + override func viewDidLoad() { + super.viewDidLoad(); + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments. + if let content = extensionContext!.inputItems[0] as? NSExtensionItem { + if let contents = content.attachments { + for (index, attachment) in (contents).enumerated() { + if attachment.hasItemConformingToTypeIdentifier(imageContentType) { + handleImages(content: content, attachment: attachment, index: index) + } else if attachment.hasItemConformingToTypeIdentifier(textContentType) { + handleText(content: content, attachment: attachment, index: index) + } else if attachment.hasItemConformingToTypeIdentifier(fileURLType) { + handleFiles(content: content, attachment: attachment, index: index) + } else if attachment.hasItemConformingToTypeIdentifier(urlContentType) { + handleUrl(content: content, attachment: attachment, index: index) + } else if attachment.hasItemConformingToTypeIdentifier(videoContentType) { + handleVideos(content: content, attachment: attachment, index: index) + } + } + } + } + } + + override func didSelectPost() { + // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments. + + // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context. + self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + } + + override func configurationItems() -> [Any]! { + // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here. + return [] + } + + private func handleText (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { + attachment.loadItem(forTypeIdentifier: textContentType, options: nil) { [weak self] data, error in + + if error == nil, let item = data as? String, let this = self { + + this.sharedText.append(item) + + // If this is the last item, save imagesData in userDefaults and redirect to host app + if index == (content.attachments?.count)! - 1 { + let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") + userDefaults?.set(this.sharedText, forKey: this.sharedKey) + userDefaults?.synchronize() + this.redirectToHostApp(type: .text) + } + + } else { + self?.dismissWithError() + } + } + } + + private func handleUrl (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { + attachment.loadItem(forTypeIdentifier: urlContentType, options: nil) { [weak self] data, error in + + if error == nil, let item = data as? URL, let this = self { + + this.sharedText.append(item.absoluteString) + + // If this is the last item, save imagesData in userDefaults and redirect to host app + if index == (content.attachments?.count)! - 1 { + let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") + userDefaults?.set(this.sharedText, forKey: this.sharedKey) + userDefaults?.synchronize() + this.redirectToHostApp(type: .text) + } + + } else { + self?.dismissWithError() + } + } + } + + private func handleImages (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { + attachment.loadItem(forTypeIdentifier: imageContentType, options: nil) { [weak self] data, error in + + if error == nil, let url = data as? URL, let this = self { + + // Always copy + let fileName = this.getFileName(from: url, type: .image) + let newPath = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")! + .appendingPathComponent(fileName) + let copied = this.copyFile(at: url, to: newPath) + if(copied) { + this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .image)) + } + + // If this is the last item, save imagesData in userDefaults and redirect to host app + if index == (content.attachments?.count)! - 1 { + let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") + userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey) + userDefaults?.synchronize() + this.redirectToHostApp(type: .media) + } + + } else { + self?.dismissWithError() + } + } + } + + private func handleVideos (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { + attachment.loadItem(forTypeIdentifier: videoContentType, options: nil) { [weak self] data, error in + + if error == nil, let url = data as? URL, let this = self { + + // Always copy + let fileName = this.getFileName(from: url, type: .video) + let newPath = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")! + .appendingPathComponent(fileName) + let copied = this.copyFile(at: url, to: newPath) + if(copied) { + guard let sharedFile = this.getSharedMediaFile(forVideo: newPath) else { + return + } + this.sharedMedia.append(sharedFile) + } + + // If this is the last item, save imagesData in userDefaults and redirect to host app + if index == (content.attachments?.count)! - 1 { + let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") + userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey) + userDefaults?.synchronize() + this.redirectToHostApp(type: .media) + } + + } else { + self?.dismissWithError() + } + } + } + + private func handleFiles (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { + attachment.loadItem(forTypeIdentifier: fileURLType, options: nil) { [weak self] data, error in + + if error == nil, let url = data as? URL, let this = self { + + // Always copy + let fileName = this.getFileName(from :url, type: .file) + let newPath = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")! + .appendingPathComponent(fileName) + let copied = this.copyFile(at: url, to: newPath) + if (copied) { + this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .file)) + } + + if index == (content.attachments?.count)! - 1 { + let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") + userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey) + userDefaults?.synchronize() + this.redirectToHostApp(type: .file) + } + + } else { + self?.dismissWithError() + } + } + } + + private func dismissWithError() { + print("[ERROR] Error loading data!") + let alert = UIAlertController(title: "Error", message: "Error loading data", preferredStyle: .alert) + + let action = UIAlertAction(title: "Error", style: .cancel) { _ in + self.dismiss(animated: true, completion: nil) + } + + alert.addAction(action) + present(alert, animated: true, completion: nil) + extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + } + + private func redirectToHostApp(type: RedirectType) { + let url = URL(string: "ShareMedia://dataUrl=\(sharedKey)#\(type)") + var responder = self as UIResponder? + let selectorOpenURL = sel_registerName("openURL:") + + while (responder != nil) { + if (responder?.responds(to: selectorOpenURL))! { + let _ = responder?.perform(selectorOpenURL, with: url) + } + responder = responder!.next + } + extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + } + + enum RedirectType { + case media + case text + case file + } + + func getExtension(from url: URL, type: SharedMediaType) -> String { + let parts = url.lastPathComponent.components(separatedBy: ".") + var ex: String? = nil + if (parts.count > 1) { + ex = parts.last + } + + if (ex == nil) { + switch type { + case .image: + ex = "PNG" + case .video: + ex = "MP4" + case .file: + ex = "TXT" + } + } + return ex ?? "Unknown" + } + + func getFileName(from url: URL, type: SharedMediaType) -> String { + var name = url.lastPathComponent + + if (name.isEmpty) { + name = UUID().uuidString + "." + getExtension(from: url, type: type) + } + + return name + } + + func copyFile(at srcURL: URL, to dstURL: URL) -> Bool { + do { + if FileManager.default.fileExists(atPath: dstURL.path) { + try FileManager.default.removeItem(at: dstURL) + } + try FileManager.default.copyItem(at: srcURL, to: dstURL) + } catch (let error) { + print("Cannot copy item at \(srcURL) to \(dstURL): \(error)") + return false + } + return true + } + + private func getSharedMediaFile(forVideo: URL) -> SharedMediaFile? { + let asset = AVAsset(url: forVideo) + let duration = (CMTimeGetSeconds(asset.duration) * 1000).rounded() + let thumbnailPath = getThumbnailPath(for: forVideo) + + if FileManager.default.fileExists(atPath: thumbnailPath.path) { + return SharedMediaFile(path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, duration: duration, type: .video) + } + + var saved = false + let assetImgGenerate = AVAssetImageGenerator(asset: asset) + assetImgGenerate.appliesPreferredTrackTransform = true + // let scale = UIScreen.main.scale + assetImgGenerate.maximumSize = CGSize(width: 360, height: 360) + do { + let img = try assetImgGenerate.copyCGImage(at: CMTimeMakeWithSeconds(600, preferredTimescale: Int32(1.0)), actualTime: nil) + try UIImage.pngData(UIImage(cgImage: img))()?.write(to: thumbnailPath) + saved = true + } catch { + saved = false + } + + return saved ? SharedMediaFile(path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, duration: duration, type: .video) : nil + + } + + private func getThumbnailPath(for url: URL) -> URL { + let fileName = Data(url.lastPathComponent.utf8).base64EncodedString().replacingOccurrences(of: "==", with: "") + let path = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: "group.\(hostAppBundleIdentifier)")! + .appendingPathComponent("\(fileName).jpg") + return path + } + + class SharedMediaFile: Codable { + var path: String; // can be image, video or url path. It can also be text content + var thumbnail: String?; // video thumbnail + var duration: Double?; // video duration in milliseconds + var type: SharedMediaType; + + + init(path: String, thumbnail: String?, duration: Double?, type: SharedMediaType) { + self.path = path + self.thumbnail = thumbnail + self.duration = duration + self.type = type + } + + // Debug method to print out SharedMediaFile details in the console + func toString() { + print("[SharedMediaFile] \n\tpath: \(self.path)\n\tthumbnail: \(self.thumbnail)\n\tduration: \(self.duration)\n\ttype: \(self.type)") + } + } + + enum SharedMediaType: Int, Codable { + case image + case video + case file + } + + func toData(data: [SharedMediaFile]) -> Data { + let encodedData = try? JSONEncoder().encode(data) + return encodedData! + } +} + +extension Array { + subscript (safe index: UInt) -> Element? { + return Int(index) < count ? self[Int(index)] : nil + } +} diff --git a/ios/receive_sharing_intent/receive_sharing_intent.entitlements b/ios/receive_sharing_intent/receive_sharing_intent.entitlements new file mode 100644 index 0000000..eca2248 --- /dev/null +++ b/ios/receive_sharing_intent/receive_sharing_intent.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.de.astubenbord.paperless-mobile + + + diff --git a/lib/features/documents/view/pages/document_details_page.dart b/lib/features/documents/view/pages/document_details_page.dart index 9775b5c..e95bb66 100644 --- a/lib/features/documents/view/pages/document_details_page.dart +++ b/lib/features/documents/view/pages/document_details_page.dart @@ -235,7 +235,7 @@ class _DocumentDetailsPageState extends State { try { await BlocProvider.of(context).assignAsn(document); } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); } } @@ -410,7 +410,7 @@ class _DocumentDetailsPageState extends State { await BlocProvider.of(context).removeDocument(document); showSnackBar(context, S.of(context).documentDeleteSuccessMessage); } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); } finally { Navigator.pop(context); } diff --git a/lib/features/documents/view/pages/document_edit_page.dart b/lib/features/documents/view/pages/document_edit_page.dart index 141afa5..5796567 100644 --- a/lib/features/documents/view/pages/document_edit_page.dart +++ b/lib/features/documents/view/pages/document_edit_page.dart @@ -84,7 +84,7 @@ class _DocumentEditPageState extends State { await getIt().updateDocument(updatedDocument); showSnackBar(context, S.of(context).documentUpdateErrorMessage); } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); } finally { Navigator.pop(context); } diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index b88836c..72f0948 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -53,7 +53,7 @@ class _DocumentsPageState extends State { try { BlocProvider.of(context).loadDocuments(); } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); } } @@ -73,7 +73,7 @@ class _DocumentsPageState extends State { try { await documentsCubit.loadMore(); } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); } } @@ -87,7 +87,7 @@ class _DocumentsPageState extends State { (filter) => filter.copyWith(page: 1), ); } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); } } diff --git a/lib/features/documents/view/widgets/search/document_filter_panel.dart b/lib/features/documents/view/widgets/search/document_filter_panel.dart index 91f038e..2498fa1 100644 --- a/lib/features/documents/view/widgets/search/document_filter_panel.dart +++ b/lib/features/documents/view/widgets/search/document_filter_panel.dart @@ -476,7 +476,7 @@ class _DocumentFilterPanelState extends State { FocusScope.of(context).unfocus(); widget.panelController.close(); } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); } } } diff --git a/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart b/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart index 6da5534..53e2e29 100644 --- a/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart +++ b/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart @@ -105,7 +105,7 @@ class _DocumentsPageAppBarState extends State { S.of(context).documentsPageBulkDeleteSuccessfulText, ); } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); } } } diff --git a/lib/features/documents/view/widgets/selection/saved_view_selection_widget.dart b/lib/features/documents/view/widgets/selection/saved_view_selection_widget.dart index 2522c44..6dd3816 100644 --- a/lib/features/documents/view/widgets/selection/saved_view_selection_widget.dart +++ b/lib/features/documents/view/widgets/selection/saved_view_selection_widget.dart @@ -88,7 +88,7 @@ class SavedViewSelectionWidget extends StatelessWidget { try { await BlocProvider.of(context).add(newView); } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); } } } @@ -105,7 +105,7 @@ class SavedViewSelectionWidget extends StatelessWidget { BlocProvider.of(context).selectView(null); } } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); } } @@ -120,7 +120,7 @@ class SavedViewSelectionWidget extends StatelessWidget { try { BlocProvider.of(context).remove(view); } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); } } } diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index 90cbc91..3d4782a 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -83,7 +83,7 @@ class _HomePageState extends State { BlocProvider.of(context).initialize(); BlocProvider.of(context).initialize(); } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); } } } diff --git a/lib/features/home/view/widget/bottom_navigation_bar.dart b/lib/features/home/view/widget/bottom_navigation_bar.dart index 64699c0..d7ed2a8 100644 --- a/lib/features/home/view/widget/bottom_navigation_bar.dart +++ b/lib/features/home/view/widget/bottom_navigation_bar.dart @@ -19,7 +19,7 @@ class BottomNavBar extends StatelessWidget { selectedIndex: selectedIndex, destinations: [ NavigationDestination( - icon: const Icon(Icons.description), + icon: const Icon(Icons.description_outlined), selectedIcon: Icon( Icons.description, color: Theme.of(context).colorScheme.primary, @@ -27,7 +27,7 @@ class BottomNavBar extends StatelessWidget { label: S.of(context).bottomNavDocumentsPageLabel, ), NavigationDestination( - icon: const Icon(Icons.document_scanner), + icon: const Icon(Icons.document_scanner_outlined), selectedIcon: Icon( Icons.document_scanner, color: Theme.of(context).colorScheme.primary, @@ -35,9 +35,7 @@ class BottomNavBar extends StatelessWidget { label: S.of(context).bottomNavScannerPageLabel, ), NavigationDestination( - icon: const Icon( - Icons.sell, - ), + icon: const Icon(Icons.sell_outlined), selectedIcon: Icon( Icons.sell, color: Theme.of(context).colorScheme.primary, diff --git a/lib/features/home/view/widget/info_drawer.dart b/lib/features/home/view/widget/info_drawer.dart index d43361d..4965e41 100644 --- a/lib/features/home/view/widget/info_drawer.dart +++ b/lib/features/home/view/widget/info_drawer.dart @@ -191,7 +191,7 @@ class InfoDrawer extends StatelessWidget { getIt().reset(); getIt().reset(); } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); } }, ), diff --git a/lib/features/labels/correspondent/view/pages/edit_correspondent_page.dart b/lib/features/labels/correspondent/view/pages/edit_correspondent_page.dart index e6a00be..b6b32db 100644 --- a/lib/features/labels/correspondent/view/pages/edit_correspondent_page.dart +++ b/lib/features/labels/correspondent/view/pages/edit_correspondent_page.dart @@ -39,7 +39,7 @@ class EditCorrespondentPage extends StatelessWidget { } Navigator.pop(context); } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); } } } diff --git a/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart b/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart index 5cf7594..5e5a0e0 100644 --- a/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart +++ b/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart @@ -60,7 +60,7 @@ class CorrespondentWidget extends StatelessWidget { } afterSelected?.call(); } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); } } } diff --git a/lib/features/labels/document_type/view/widgets/document_type_widget.dart b/lib/features/labels/document_type/view/widgets/document_type_widget.dart index 9bc35d7..8500adc 100644 --- a/lib/features/labels/document_type/view/widgets/document_type_widget.dart +++ b/lib/features/labels/document_type/view/widgets/document_type_widget.dart @@ -55,7 +55,7 @@ class DocumentTypeWidget extends StatelessWidget { } afterSelected?.call(); } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); } } } diff --git a/lib/features/labels/storage_path/view/pages/edit_storage_path_page.dart b/lib/features/labels/storage_path/view/pages/edit_storage_path_page.dart index be1a93a..cce1bab 100644 --- a/lib/features/labels/storage_path/view/pages/edit_storage_path_page.dart +++ b/lib/features/labels/storage_path/view/pages/edit_storage_path_page.dart @@ -44,7 +44,7 @@ class EditStoragePathPage extends StatelessWidget { } Navigator.pop(context); } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); } } } diff --git a/lib/features/labels/storage_path/view/widgets/storage_path_widget.dart b/lib/features/labels/storage_path/view/widgets/storage_path_widget.dart index 858097c..d713585 100644 --- a/lib/features/labels/storage_path/view/widgets/storage_path_widget.dart +++ b/lib/features/labels/storage_path/view/widgets/storage_path_widget.dart @@ -59,7 +59,7 @@ class StoragePathWidget extends StatelessWidget { } afterSelected?.call(); } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); } } } diff --git a/lib/features/labels/tags/view/pages/edit_tag_page.dart b/lib/features/labels/tags/view/pages/edit_tag_page.dart index ff2c02b..39bcf33 100644 --- a/lib/features/labels/tags/view/pages/edit_tag_page.dart +++ b/lib/features/labels/tags/view/pages/edit_tag_page.dart @@ -60,7 +60,7 @@ class EditTagPage extends StatelessWidget { cubit.updateFilter(filter: updatedFilter); Navigator.pop(context); } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); } } } diff --git a/lib/features/labels/tags/view/widgets/tag_widget.dart b/lib/features/labels/tags/view/widgets/tag_widget.dart index ed8c58d..8c7e98b 100644 --- a/lib/features/labels/tags/view/widgets/tag_widget.dart +++ b/lib/features/labels/tags/view/widgets/tag_widget.dart @@ -65,7 +65,7 @@ class TagWidget extends StatelessWidget { afterTagTapped!(); } } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); } } } diff --git a/lib/features/labels/view/pages/edit_label_page.dart b/lib/features/labels/view/pages/edit_label_page.dart index b6bf86e..7fac4f1 100644 --- a/lib/features/labels/view/pages/edit_label_page.dart +++ b/lib/features/labels/view/pages/edit_label_page.dart @@ -147,7 +147,7 @@ class _EditLabelPageState extends State> { } on PaperlessValidationErrors catch (errorMessages) { setState(() => _errors = errorMessages); } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); } } } diff --git a/lib/features/login/view/login_page.dart b/lib/features/login/view/login_page.dart index b86c1e7..393972e 100644 --- a/lib/features/login/view/login_page.dart +++ b/lib/features/login/view/login_page.dart @@ -98,9 +98,11 @@ class _LoginPageState extends State { form[ClientCertificateFormField.fkClientCertificate], ); } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); + } on Map catch (error, stackTrace) { + showGenericError(context, error.values.first, stackTrace); } catch (unknownError, stackTrace) { - showError(context, ErrorMessage.unknown(), stackTrace); + showErrorMessage(context, ErrorMessage.unknown(), stackTrace); } finally { setState(() => _isLoginLoading = false); } diff --git a/lib/features/scan/view/document_upload_page.dart b/lib/features/scan/view/document_upload_page.dart index 9ddd09f..0b62cc1 100644 --- a/lib/features/scan/view/document_upload_page.dart +++ b/lib/features/scan/view/document_upload_page.dart @@ -30,11 +30,16 @@ import 'package:intl/intl.dart'; class DocumentUploadPage extends StatefulWidget { final Uint8List fileBytes; + final String? title; + final String? filename; final void Function()? afterUpload; + const DocumentUploadPage({ Key? key, required this.fileBytes, this.afterUpload, + this.title, + this.filename, }) : super(key: key); @override @@ -42,18 +47,21 @@ class DocumentUploadPage extends StatefulWidget { } class _DocumentUploadPageState extends State { - static const fkFileName = "fileName"; - + static const fkFileName = "filename"; static final fileNameDateFormat = DateFormat("yyyy_MM_ddTHH_mm_ss"); + final GlobalKey _formKey = GlobalKey(); PaperlessValidationErrors _errors = {}; bool _isUploadLoading = false; + late bool _syncTitleAndFilename; + final _now = DateTime.now(); @override void initState() { super.initState(); - initializeDateFormatting(); //TODO: INTL (has to do with intl below) + _syncTitleAndFilename = widget.filename == null && widget.title == null; + initializeDateFormatting(); } @override @@ -83,7 +91,8 @@ class _DocumentUploadPageState extends State { FormBuilderTextField( autovalidateMode: AutovalidateMode.always, name: DocumentModel.titleKey, - initialValue: "scan_${fileNameDateFormat.format(DateTime.now())}", + initialValue: + widget.title ?? "scan_${fileNameDateFormat.format(_now)}", validator: FormBuilderValidators.required(), decoration: InputDecoration( labelText: S.of(context).documentTitlePropertyLabel, @@ -92,29 +101,58 @@ class _DocumentUploadPageState extends State { onPressed: () { _formKey.currentState?.fields[DocumentModel.titleKey] ?.didChange(""); - _formKey.currentState?.fields[fkFileName] - ?.didChange(".pdf"); + if (_syncTitleAndFilename) { + _formKey.currentState?.fields[fkFileName]?.didChange(""); + } }, ), errorText: _errors[DocumentModel.titleKey], ), onChanged: (value) { - final String? transformedValue = - value?.replaceAll(RegExp(r"[\W_]"), "_"); - _formKey.currentState?.fields[fkFileName] - ?.didChange("${transformedValue ?? ''}.pdf"); + final String transformedValue = _formatFilename(value ?? ''); + if (_syncTitleAndFilename) { + _formKey.currentState?.fields[fkFileName] + ?.didChange(transformedValue); + } }, ), FormBuilderTextField( autovalidateMode: AutovalidateMode.always, - readOnly: true, - enabled: false, + readOnly: _syncTitleAndFilename, + enabled: !_syncTitleAndFilename, name: fkFileName, decoration: InputDecoration( labelText: S.of(context).documentUploadFileNameLabel, + suffixText: ".pdf", + suffixIcon: IconButton( + icon: const Icon(Icons.clear), + onPressed: () => + _formKey.currentState?.fields[fkFileName]?.didChange(''), + ), ), initialValue: - "scan_${fileNameDateFormat.format(DateTime.now())}.pdf", + widget.filename ?? "scan_${fileNameDateFormat.format(_now)}", + ), + SwitchListTile( + value: _syncTitleAndFilename, + onChanged: (value) { + setState( + () => _syncTitleAndFilename = value, + ); + if (_syncTitleAndFilename) { + final String transformedValue = _formatFilename(_formKey + .currentState + ?.fields[DocumentModel.titleKey] + ?.value as String); + if (_syncTitleAndFilename) { + _formKey.currentState?.fields[fkFileName] + ?.didChange(transformedValue); + } + } + }, + title: Text(S + .of(context) + .documentUploadPageSynchronizeTitleAndFilenameLabel), //TODO: INTL ), FormBuilderDateTimePicker( autovalidateMode: AutovalidateMode.always, @@ -202,7 +240,7 @@ class _DocumentUploadPageState extends State { fv[DocumentModel.correspondentKey] as IdQueryParameter; await BlocProvider.of(context).addDocument( widget.fileBytes, - _formKey.currentState?.value[fkFileName], + _padWithPdfExtension(_formKey.currentState?.value[fkFileName]), onConsumptionFinished: _onConsumptionFinished, title: title, documentType: docType.id, @@ -215,11 +253,11 @@ class _DocumentUploadPageState extends State { Navigator.pop(context); widget.afterUpload?.call(); } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); } on PaperlessValidationErrors catch (errorMessages) { setState(() => _errors = errorMessages); } catch (unknownError, stackTrace) { - showError(context, ErrorMessage.unknown(), stackTrace); + showErrorMessage(context, ErrorMessage.unknown(), stackTrace); } finally { setState(() { _isUploadLoading = false; @@ -228,6 +266,14 @@ class _DocumentUploadPageState extends State { } } + String _padWithPdfExtension(String source) { + return source.endsWith(".pdf") ? source : '$source.pdf'; + } + + String _formatFilename(String source) { + return source.replaceAll(RegExp(r"[\W_]"), "_"); + } + void _onConsumptionFinished(document) { ScaffoldMessenger.of(rootScaffoldKey.currentContext!).showSnackBar( SnackBar( @@ -236,7 +282,7 @@ class _DocumentUploadPageState extends State { try { getIt().reloadDocuments(); } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); } }, label: diff --git a/lib/features/scan/view/scanner_page.dart b/lib/features/scan/view/scanner_page.dart index f301fdc..c51fe4d 100644 --- a/lib/features/scan/view/scanner_page.dart +++ b/lib/features/scan/view/scanner_page.dart @@ -192,7 +192,7 @@ class _ScannerPageState extends State BlocProvider.of(context) .removeScan(index); } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); } }, index: index, @@ -205,7 +205,7 @@ class _ScannerPageState extends State try { BlocProvider.of(context).reset(); } on ErrorMessage catch (error, stackTrace) { - showError(context, error, stackTrace); + showErrorMessage(context, error, stackTrace); } } @@ -224,10 +224,16 @@ class _ScannerPageState extends State ); if (result?.files.single.path != null) { File file = File(result!.files.single.path!); - if (!supportedFileExtensions.contains(file.path.split('.').last)) { - //TODO: Show error message; + if (!supportedFileExtensions.contains( + file.path.split('.').last.toLowerCase(), + )) { + showErrorMessage( + context, + const ErrorMessage(ErrorCode.unsupportedFileFormat), + ); return; } + final filename = extractFilenameFromPath(file.path); final mimeType = lookupMimeType(file.path) ?? ''; late Uint8List fileBytes; if (mimeType.startsWith('image')) { @@ -244,6 +250,7 @@ class _ScannerPageState extends State value: getIt(), child: LabelBlocProvider( child: DocumentUploadPage( + filename: filename, fileBytes: fileBytes, ), ), diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 9808aa5..9a8ba4b 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1,9 +1,9 @@ { "@@locale": "de", "documentTitlePropertyLabel": "Titel", - "documentCreatedPropertyLabel": "Erstellt Am", - "documentAddedPropertyLabel": "Hinzugefügt Am", - "documentModifiedPropertyLabel": "Geändert Am", + "documentCreatedPropertyLabel": "Ausgestellt am", + "documentAddedPropertyLabel": "Hinzugefügt am", + "documentModifiedPropertyLabel": "Geändert am", "documentDocumentTypePropertyLabel": "Dokumenttyp", "documentCorrespondentPropertyLabel": "Korrespondent", "documentStoragePathPropertyLabel": "Speicherpfad", @@ -107,7 +107,7 @@ "documentScannerPageUploadFromThisDeviceButtonLabel": "Lade ein Dokument von diesem Gerät hoch", "addTagPageTitle": "Neuer Tag", "addCorrespondentPageTitle": "Neuer Korrespondent", - "addDocumentTypePageTitle": "Neuer Dokumententyp", + "addDocumentTypePageTitle": "Neuer Dokumenttyp", "labelNamePropertyLabel": "Name", "labelMatchPropertyLabel": "Zuweisungsmuster", "labelMatchingAlgorithmPropertyLabel": "Zuweisungsalgorithmus", @@ -195,5 +195,6 @@ "appDrawerHeaderLoggedInAsText": "Eingeloggt als ", "labelAnyAssignedText": "Beliebig zugewiesen", "deleteViewDialogContentText": "Möchtest Du diese Ansicht wirklich löschen?", - "deleteViewDialogTitleText": "Lösche Ansicht " + "deleteViewDialogTitleText": "Lösche Ansicht ", + "documentUploadPageSynchronizeTitleAndFilenameLabel": "Synchronisiere Titel und Dateiname" } \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index fe46a00..caef897 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -196,5 +196,6 @@ "appDrawerHeaderLoggedInAsText": "Logged in as ", "labelAnyAssignedText": "Any assigned", "deleteViewDialogContentText": "Do you really want to delete this view?", - "deleteViewDialogTitleText": "Delete view " + "deleteViewDialogTitleText": "Delete view ", + "documentUploadPageSynchronizeTitleAndFilenameLabel": "Synchronize title and filename" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 7b2a0dd..b55319e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; +import 'package:fluttertoast/fluttertoast.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:intl/intl.dart'; import 'package:intl/intl_standalone.dart'; @@ -15,6 +16,7 @@ import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.da import 'package:paperless_mobile/core/global/asset_images.dart'; import 'package:paperless_mobile/core/global/constants.dart'; import 'package:paperless_mobile/core/global/http_self_signed_certificate_override.dart'; +import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/di_initializer.dart'; @@ -132,21 +134,40 @@ class AuthenticationWrapper extends StatefulWidget { class _AuthenticationWrapperState extends State { bool isFileTypeSupported(SharedMediaFile file) { - return supportedFileExtensions.contains(file.path.split('.').last); + return supportedFileExtensions.contains( + file.path.split('.').last.toLowerCase(), + ); } void handleReceivedFiles(List files) async { if (files.isEmpty) { return; } - if (!isFileTypeSupported(files.first)) { - showError(context, const ErrorMessage(ErrorCode.unsupportedFileFormat)); - await Future.delayed( - const Duration(seconds: 2), - () => SystemNavigator.pop(), + late final SharedMediaFile file; + if (Platform.isIOS) { + // Workaround: https://stackoverflow.com/a/72813212 + file = SharedMediaFile( + files.first.path.replaceAll('file://', ''), + files.first.thumbnail, + files.first.duration, + files.first.type, ); + } else { + file = files.first; } - final bytes = File(files.first.path).readAsBytesSync(); + + if (!isFileTypeSupported(file)) { + Fluttertoast.showToast( + msg: translateError(context, ErrorCode.unsupportedFileFormat), + ); + if (Platform.isAndroid) { + // As stated in the docs, SystemNavigator.pop() is ignored on IOS to comply with HCI guidelines. + await SystemNavigator.pop(); + } + return; + } + final filename = extractFilenameFromPath(file.path); + final bytes = File(file.path).readAsBytesSync(); Navigator.push( context, MaterialPageRoute( @@ -156,6 +177,7 @@ class _AuthenticationWrapperState extends State { child: DocumentUploadPage( fileBytes: bytes, afterUpload: () => SystemNavigator.pop(), + filename: filename, ), ), ), @@ -184,7 +206,7 @@ class _AuthenticationWrapperState extends State { @override Widget build(BuildContext context) { return SafeArea( - top: true, + top: false, left: false, right: false, bottom: false, diff --git a/lib/util.dart b/lib/util.dart index cb1115c..bbcb86b 100644 --- a/lib/util.dart +++ b/lib/util.dart @@ -34,7 +34,32 @@ void showSnackBar( ); } -void showError( +void showGenericError( + BuildContext context, + dynamic error, [ + StackTrace? stackTrace, +]) { + showSnackBar( + context, + error.toString(), + action: SnackBarAction( + label: S.of(context).errorReportLabel, + textColor: Colors.amber, + onPressed: () => GithubIssueService.createIssueFromError( + context, + stackTrace: stackTrace, + ), + ), + ); + log( + "An error has occurred.", + error: error, + stackTrace: stackTrace, + time: DateTime.now(), + ); +} + +void showErrorMessage( BuildContext context, ErrorMessage error, [ StackTrace? stackTrace, @@ -91,3 +116,7 @@ String formatLocalDate(BuildContext context, DateTime dateTime) { final tag = Localizations.maybeLocaleOf(context)?.toLanguageTag(); return DateFormat.yMMMd(tag).format(dateTime); } + +String extractFilenameFromPath(String path) { + return path.split(RegExp('[./]')).reversed.skip(1).first; +} diff --git a/pubspec.lock b/pubspec.lock index 7dc7da5..aeb2c31 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -588,6 +588,13 @@ packages: description: flutter source: sdk version: "0.0.0" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + url: "https://pub.dartlang.org" + source: hosted + version: "8.1.1" font_awesome_flutter: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 51c787e..f225b15 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -80,6 +80,7 @@ dependencies: receive_sharing_intent: ^1.4.5 uuid: ^3.0.6 flutter_typeahead: ^4.1.1 + fluttertoast: ^8.1.1 dev_dependencies: integration_test: