From 36704347f4f0bd842885fd4b21d71f3d87f75144 Mon Sep 17 00:00:00 2001 From: Agustin Castanos <122444213+acastanos@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:27:29 +0100 Subject: [PATCH 1/5] WIP --- .../android/app/src/main/AndroidManifest.xml | 1 + example/ios/Flutter/AppFrameworkInfo.plist | 2 +- example/ios/Podfile | 2 +- example/ios/Podfile.lock | 28 ++++ example/ios/Runner.xcodeproj/project.pbxproj | 136 +++++++++++++++++- .../contents.xcworkspacedata | 3 + example/ios/Runner/Info.plist | 5 + .../view_models/pdf_combiner_view_model.dart | 6 + example/lib/views/pdf_combiner_screen.dart | 35 ++++- .../lib/views/widgets/file_type_dialog.dart | 130 ++++++++++++++--- example/lib/views/widgets/file_type_icon.dart | 3 + .../macos/Runner/DebugProfile.entitlements | 2 + example/macos/Runner/Release.entitlements | 2 + example/pubspec.lock | 8 ++ lib/models/merge_input.dart | 12 +- lib/pdf_combiner.dart | 47 +++--- lib/utils/document_utils_io.dart | 85 ++++++++++- lib/utils/document_utils_web.dart | 7 + pubspec.yaml | 1 + 19 files changed, 460 insertions(+), 55 deletions(-) create mode 100644 example/ios/Podfile.lock diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 2693c6ab..1a373b77 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ + CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/example/ios/Podfile b/example/ios/Podfile index d97f17e2..e51a31d9 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +# platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock new file mode 100644 index 00000000..f17b2203 --- /dev/null +++ b/example/ios/Podfile.lock @@ -0,0 +1,28 @@ +PODS: + - Flutter (1.0.0) + - open_file_ios (1.0.3): + - Flutter + - permission_handler_apple (9.3.0): + - Flutter + +DEPENDENCIES: + - Flutter (from `Flutter`) + - open_file_ios (from `.symlinks/plugins/open_file_ios/ios`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + open_file_ios: + :path: ".symlinks/plugins/open_file_ios/ios" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" + +SPEC CHECKSUMS: + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + open_file_ios: 46184d802ee7959203f6392abcfa0dd49fdb5be0 + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + +PODFILE CHECKSUM: 4f1c12611da7338d21589c0b2ecd6bd20b109694 + +COCOAPODS: 1.16.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 3338a5fe..63135109 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 60; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -15,6 +15,8 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + A021E3BCFA6FC4B73F44D982 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 852424BFF52109C6CB366881 /* Pods_RunnerTests.framework */; }; + D5B03BD0E847D1D12C06C41B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6867D887379682898D1644CE /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -41,14 +43,21 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 03514AA92ECFAAC67531D7E0 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 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 = ""; }; + 206C37A4720B206D6B1AC3C4 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 4F2A9E6E66066F49BFC21E74 /* 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 = ""; }; + 6867D887379682898D1644CE /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 852424BFF52109C6CB366881 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9412236D714078BCECD7D318 /* 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 = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -56,6 +65,8 @@ 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 = ""; }; + A0187530B0C85FA276EC72DB /* 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 = ""; }; + EEDC78A53309521E7A97BC63 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -63,6 +74,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A021E3BCFA6FC4B73F44D982 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -71,12 +83,22 @@ buildActionMask = 2147483647; files = ( 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + D5B03BD0E847D1D12C06C41B /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 1FBD1FA9799B105384E88DD0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 6867D887379682898D1644CE /* Pods_Runner.framework */, + 852424BFF52109C6CB366881 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 331C8082294A63A400263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( @@ -85,9 +107,24 @@ path = RunnerTests; sourceTree = ""; }; + 3A0F5EE6E8F5E1C43A5BADCC /* Pods */ = { + isa = PBXGroup; + children = ( + A0187530B0C85FA276EC72DB /* Pods-Runner.debug.xcconfig */, + 9412236D714078BCECD7D318 /* Pods-Runner.release.xcconfig */, + 4F2A9E6E66066F49BFC21E74 /* Pods-Runner.profile.xcconfig */, + EEDC78A53309521E7A97BC63 /* Pods-RunnerTests.debug.xcconfig */, + 03514AA92ECFAAC67531D7E0 /* Pods-RunnerTests.release.xcconfig */, + 206C37A4720B206D6B1AC3C4 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -103,6 +140,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + 3A0F5EE6E8F5E1C43A5BADCC /* Pods */, + 1FBD1FA9799B105384E88DD0 /* Frameworks */, ); sourceTree = ""; }; @@ -137,6 +176,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + CFA0AB402F277C276F0F1A80 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, 328562033B987F342D6D6D04 /* Frameworks */, @@ -155,12 +195,15 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + BC4D21716821DA8325256653 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 1229700873A4E86F4A2BD8F2 /* [CP] Embed Pods Frameworks */, + 825C32A37B02CA77CCB1D0F3 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -204,7 +247,7 @@ ); mainGroup = 97C146E51CF9000F007C117D; packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; @@ -238,6 +281,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 1229700873A4E86F4A2BD8F2 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -254,6 +314,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 825C32A37B02CA77CCB1D0F3 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -269,6 +346,50 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + BC4D21716821DA8325256653 /* [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-Runner-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; + }; + CFA0AB402F277C276F0F1A80 /* [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-RunnerTests-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; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -362,7 +483,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -398,6 +519,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = EEDC78A53309521E7A97BC63 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -416,6 +538,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 03514AA92ECFAAC67531D7E0 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -432,6 +555,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 206C37A4720B206D6B1AC3C4 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -495,7 +619,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -546,7 +670,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -644,7 +768,7 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16..21a3cc14 100644 --- a/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index c0f01d89..8091963a 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -47,5 +47,10 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + diff --git a/example/lib/view_models/pdf_combiner_view_model.dart b/example/lib/view_models/pdf_combiner_view_model.dart index c3cd95d9..0bf3b015 100644 --- a/example/lib/view_models/pdf_combiner_view_model.dart +++ b/example/lib/view_models/pdf_combiner_view_model.dart @@ -31,6 +31,9 @@ class PdfCombinerViewModel { selectedFiles += result.files.map((file) => MergeInput.bytes(file.bytes!)).toList(); break; + case MergeInputType.url: + // Currently not supported in file picker + break; } } @@ -51,6 +54,9 @@ class PdfCombinerViewModel { ), ); break; + case MergeInputType.url: + // Currently not supported in drag and drop + break; } outputFiles = []; diff --git a/example/lib/views/pdf_combiner_screen.dart b/example/lib/views/pdf_combiner_screen.dart index 3c08aa21..e831172e 100644 --- a/example/lib/views/pdf_combiner_screen.dart +++ b/example/lib/views/pdf_combiner_screen.dart @@ -18,6 +18,8 @@ extension on MergeInput { return p.basename(path ?? ''); case MergeInputType.bytes: return 'File in bytes $index'; + case MergeInputType.url: + return url ?? 'File from URL $index'; } } } @@ -55,11 +57,20 @@ class _PdfCombinerScreenState extends State { child: Stack( children: [ DropTarget( - onDragDone: (details) async { - final fileType = + onDragDone: (details) async { + final selection = await showFileTypeDialog(context); // Show dialog - if (fileType == null) return; - await _viewModel.addFilesDragAndDrop(fileType, details.files); + if (selection == null) return; + if (selection.type == MergeInputType.url) { + if (selection.url != null && selection.url!.isNotEmpty) { + _viewModel.selectedFiles += [ + MergeInput.url(selection.url!) + ]; + } + } else { + await _viewModel.addFilesDragAndDrop( + selection.type, details.files); + } setState(() {}); }, child: (_viewModel.isEmpty()) @@ -188,6 +199,7 @@ class _PdfCombinerScreenState extends State { .getBytesFromPathOrBlob(_viewModel .selectedFiles[index] .toString()), + MergeInputType.url => Future.value(null), }, builder: (context, snapshot) { if (snapshot.connectionState == @@ -270,9 +282,16 @@ class _PdfCombinerScreenState extends State { // Function to pick PDF files from the device Future _pickFiles() async { - final fileType = await showFileTypeDialog(context); - if (fileType == null) return; - await _viewModel.pickFiles(fileType); + final selection = await showFileTypeDialog(context); + if (selection == null) return; + if (selection.type == MergeInputType.url) { + if (selection.url != null && selection.url!.isNotEmpty) { + _viewModel.selectedFiles += [MergeInput.url(selection.url!)]; + } + setState(() {}); + } else { + await _viewModel.pickFiles(selection.type); + } setState(() {}); } @@ -353,6 +372,8 @@ class _PdfCombinerScreenState extends State { return; case MergeInputType.bytes: return; + case MergeInputType.url: + return; } } } diff --git a/example/lib/views/widgets/file_type_dialog.dart b/example/lib/views/widgets/file_type_dialog.dart index 2fdfe916..f18bf0a9 100644 --- a/example/lib/views/widgets/file_type_dialog.dart +++ b/example/lib/views/widgets/file_type_dialog.dart @@ -1,22 +1,118 @@ import 'package:flutter/material.dart'; import 'package:pdf_combiner/models/merge_input.dart'; -Future showFileTypeDialog(BuildContext context) async => - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => AlertDialog( - title: const Text('File Type'), - content: const Text('Select a file type'), - actions: [ - TextButton( - child: const Text('Path'), - onPressed: () => Navigator.of(context).pop(MergeInputType.path), - ), - TextButton( - child: Text('Bytes'), - onPressed: () => Navigator.of(context).pop(MergeInputType.bytes), +class FileTypeSelection { + final MergeInputType type; + final String? url; + + const FileTypeSelection(this.type, {this.url}); +} + +class RadioGroupItem { + final T value; + final String label; + const RadioGroupItem(this.value, this.label); +} + +class RadioGroup extends StatelessWidget { + final T groupValue; + final ValueChanged onChanged; + final List> items; + + const RadioGroup({ + Key? key, + required this.groupValue, + required this.onChanged, + required this.items, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: items.map((item) { + return InkWell( + onTap: () => onChanged(item.value), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + children: [ + Radio(value: item.value, groupValue: groupValue, onChanged: onChanged), + const SizedBox(width: 8), + Expanded(child: Text(item.label)), + ], + ), ), - ], - ), + ); + }).toList(), ); + } +} + +Future showFileTypeDialog(BuildContext context) async { + return showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + MergeInputType selected = MergeInputType.path; + final controller = TextEditingController(); + + return StatefulBuilder(builder: (context, setState) { + final isUrlSelected = selected == MergeInputType.url; + final canAccept = selected != null && + (!isUrlSelected || controller.text.trim().isNotEmpty); + + return AlertDialog( + title: const Text('File Type'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RadioGroup( + groupValue: selected, + onChanged: (v) => setState(() => selected = v!), + items: const [ + RadioGroupItem(MergeInputType.path, 'Path'), + RadioGroupItem(MergeInputType.bytes, 'Bytes'), + RadioGroupItem(MergeInputType.url, 'Url'), + ], + ), + if (isUrlSelected) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: TextField( + controller: controller, + autofocus: true, + decoration: + const InputDecoration(hintText: 'https://...'), + keyboardType: TextInputType.url, + onChanged: (_) => setState(() {}), + ), + ), + ], + ), + ), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () => Navigator.of(context).pop(null), + ), + TextButton( + child: const Text('Accept'), + onPressed: canAccept + ? () { + final url = controller.text.trim(); + if (selected == MergeInputType.url) { + Navigator.of(context) + .pop(FileTypeSelection(selected, url: url)); + } else { + Navigator.of(context).pop(FileTypeSelection(selected)); + } + } + : null, + ), + ], + ); + }); + }, + ); +} diff --git a/example/lib/views/widgets/file_type_icon.dart b/example/lib/views/widgets/file_type_icon.dart index 798d4b46..e3a426e4 100644 --- a/example/lib/views/widgets/file_type_icon.dart +++ b/example/lib/views/widgets/file_type_icon.dart @@ -16,6 +16,8 @@ class FileTypeIcon extends StatelessWidget { OpenFile.open(input.path!); case MergeInputType.bytes: null; + case MergeInputType.url: + null; } }, child: FutureBuilder( @@ -24,6 +26,7 @@ class FileTypeIcon extends StatelessWidget { FileMagicNumber.detectFileTypeFromBytes(input.bytes!)), MergeInputType.path => FileMagicNumber.detectFileTypeFromPathOrBlob(input.path!), + MergeInputType.url => Future.value(null), }, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { diff --git a/example/macos/Runner/DebugProfile.entitlements b/example/macos/Runner/DebugProfile.entitlements index afdd4995..494fdde7 100644 --- a/example/macos/Runner/DebugProfile.entitlements +++ b/example/macos/Runner/DebugProfile.entitlements @@ -10,5 +10,7 @@ com.apple.security.network.server + com.apple.security.network.client + diff --git a/example/macos/Runner/Release.entitlements b/example/macos/Runner/Release.entitlements index 976176a5..6dd5c931 100644 --- a/example/macos/Runner/Release.entitlements +++ b/example/macos/Runner/Release.entitlements @@ -6,5 +6,7 @@ com.apple.security.network.server + com.apple.security.network.client + diff --git a/example/pubspec.lock b/example/pubspec.lock index 7097c386..3e3be2d0 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -282,6 +282,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" http_multi_server: dependency: transitive description: diff --git a/lib/models/merge_input.dart b/lib/models/merge_input.dart index 946c7b1b..4611b6e0 100644 --- a/lib/models/merge_input.dart +++ b/lib/models/merge_input.dart @@ -4,6 +4,7 @@ import 'dart:typed_data' show Uint8List; enum MergeInputType { path, bytes, + url, } /// A class representing an input for merging PDFs. @@ -11,11 +12,12 @@ enum MergeInputType { /// It can be created from a file path or a byte array. class MergeInput { final String? path; + final String? url; final Uint8List? bytes; final MergeInputType type; - const MergeInput(this.type, {this.path, this.bytes}) - : assert((path != null) != (bytes != null)); + const MergeInput(this.type, {this.path, this.bytes, this.url}) + : assert(((path != null) != (bytes != null))!= (url != null)); /// Creates a [MergeInput] from a file path. factory MergeInput.path(String path) => @@ -24,6 +26,10 @@ class MergeInput { /// Creates a [MergeInput] from a byte array. factory MergeInput.bytes(Uint8List bytes) => MergeInput(MergeInputType.bytes, bytes: bytes); + + /// Creates a [MergeInput] from a url string. + factory MergeInput.url(String url) => + MergeInput(MergeInputType.url, url: url); @override String toString() { @@ -32,6 +38,8 @@ class MergeInput { return path!; case MergeInputType.bytes: return bytes!.toString(); + case MergeInputType.url: + return url!; } } } diff --git a/lib/pdf_combiner.dart b/lib/pdf_combiner.dart index 51259bb3..d27e3314 100644 --- a/lib/pdf_combiner.dart +++ b/lib/pdf_combiner.dart @@ -49,17 +49,18 @@ class PdfCombiner { required List inputs, required String outputPath, }) async { + final List newInputs = await DocumentUtils.conversionUrlInputsToPaths(inputs); List temporalPaths = []; - final List mutablePaths = List.from(inputs); - if (inputs.isEmpty) { + final List mutablePaths = List.from(newInputs); + if (newInputs.isEmpty) { throw (PdfCombinerException( PdfCombinerMessages.emptyParameterMessage("inputs"))); } else if (outputPath.trim().isEmpty) { throw (PdfCombinerException( PdfCombinerMessages.emptyParameterMessage("outputPath"))); } else { - for (int i = 0; i < inputs.length; i++) { - final input = inputs[i]; + for (int i = 0; i < newInputs.length; i++) { + final input = newInputs[i]; final isPDF = await DocumentUtils.isPDF(input); final isImage = await DocumentUtils.isImage(input); final outputPathIsPDF = DocumentUtils.hasPDFExtension(outputPath); @@ -112,8 +113,9 @@ class PdfCombiner { required List inputs, required String outputPath, }) async { + final List newInputs = await DocumentUtils.conversionUrlInputsToPaths(inputs); final temportalFilePaths = []; - if (inputs.isEmpty) { + if (newInputs.isEmpty) { throw PdfCombinerException( PdfCombinerMessages.emptyParameterMessage("inputPaths")); } else { @@ -121,7 +123,7 @@ class PdfCombiner { bool success = true; String? path; - for (MergeInput input in inputs) { + for (MergeInput input in newInputs) { success = await DocumentUtils.isPDF(input); path = input.path; } @@ -134,7 +136,7 @@ class PdfCombiner { throw PdfCombinerException(PdfCombinerMessages.errorMessagePDF(path)); } else { final inputPaths = await Future.wait( - inputs.map( + newInputs.map( (input) async { final result = await DocumentUtils.prepareInput(input); switch (input.type) { @@ -143,6 +145,8 @@ class PdfCombiner { break; case MergeInputType.path: break; + case MergeInputType.url: + break; } return result; }, @@ -190,12 +194,13 @@ class PdfCombiner { required String outputPath, PdfFromMultipleImageConfig config = const PdfFromMultipleImageConfig(), }) async { + final List newInputs = await DocumentUtils.conversionUrlInputsToPaths(inputs); final temportalFilePaths = []; final outputPathIsPDF = DocumentUtils.hasPDFExtension(outputPath); if (!outputPathIsPDF) { throw PdfCombinerException( PdfCombinerMessages.errorMessageInvalidOutputPath(outputPath)); - } else if (inputs.isEmpty) { + } else if (newInputs.isEmpty) { throw PdfCombinerException( PdfCombinerMessages.emptyParameterMessage("inputPaths")); } else { @@ -204,9 +209,9 @@ class PdfCombiner { String? path; int i = 0; - while (i < inputs.length && success) { - success = await DocumentUtils.isImage(inputs[i]); - path = inputs[i].path; + while (i < newInputs.length && success) { + success = await DocumentUtils.isImage(newInputs[i]); + path = newInputs[i].path; i++; } @@ -215,7 +220,7 @@ class PdfCombiner { PdfCombinerMessages.errorMessageImage(path ?? '')); } else { final inputPaths = await Future.wait( - inputs.map( + newInputs.map( (input) async { final result = await DocumentUtils.prepareInput(input); switch (input.type) { @@ -224,6 +229,8 @@ class PdfCombiner { break; case MergeInputType.path: break; + case MergeInputType.url: + break; } return result; }, @@ -279,20 +286,24 @@ class PdfCombiner { required String outputDirPath, ImageFromPdfConfig config = const ImageFromPdfConfig(), }) async { + final MergeInput newInput = await DocumentUtils.conversionUrlInputsToPaths([input]).then((value) => value.first); String? temportalFilePath; try { - bool success = await DocumentUtils.isPDF(input); + bool success = await DocumentUtils.isPDF(newInput); if (!success) { String inputTypeMessage; - switch (input.type) { + switch (newInput.type) { case MergeInputType.bytes: inputTypeMessage = "File in bytes"; break; case MergeInputType.path: - inputTypeMessage = input.path!; + inputTypeMessage = newInput.path!; + break; + case MergeInputType.url: + inputTypeMessage = newInput.url!; break; } @@ -300,13 +311,15 @@ class PdfCombiner { inputTypeMessage, )); } else { - final inputPath = await DocumentUtils.prepareInput(input); - switch (input.type) { + final inputPath = await DocumentUtils.prepareInput(newInput); + switch (newInput.type) { case MergeInputType.bytes: temportalFilePath = inputPath; break; case MergeInputType.path: break; + case MergeInputType.url: + break; } final response = await ImagesFromPdfIsolate.createImageFromPDF( inputPath: inputPath, diff --git a/lib/utils/document_utils_io.dart b/lib/utils/document_utils_io.dart index 415712be..a6150cfb 100644 --- a/lib/utils/document_utils_io.dart +++ b/lib/utils/document_utils_io.dart @@ -5,14 +5,18 @@ import 'package:file_magic_number/file_magic_number.dart'; import 'package:path/path.dart' as p; import 'package:pdf_combiner/models/merge_input.dart'; import 'package:pdf_combiner/pdf_combiner.dart'; +import 'package:http/http.dart' as http; extension on MergeInputType { - String extension() { + String extension(MergeInput input) { switch (this) { case MergeInputType.path: - return '.pdf'; + return p.extension(input.path!); case MergeInputType.bytes: - return '.png'; + final magicType = FileMagicNumber.detectFileTypeFromBytes(input.bytes); + return _magicTypeToString(magicType); + case MergeInputType.url: + return p.extension(input.url!); } } @@ -22,6 +26,23 @@ extension on MergeInputType { return 'pdf_input'; case MergeInputType.bytes: return 'image_input'; + case MergeInputType.url: + return 'url_input'; + } + } + + String _magicTypeToString(FileMagicNumberType type) { + switch (type) { + case FileMagicNumberType.pdf: + return 'pdf'; + case FileMagicNumberType.jpg: + return 'jpg'; + case FileMagicNumberType.png: + return 'png'; + case FileMagicNumberType.heic: + return 'heic'; + default: + return 'nonSupported'; } } } @@ -64,6 +85,56 @@ class DocumentUtils { } } + /// Converts any `MergeInput.url` entries into temporary `MergeInput.path` files. + /// + /// Downloads each URL to a temporary file inside the configured temporal + /// folder and returns a new list where URLs were replaced by `MergeInput.path` + /// referencing the downloaded file. `path` and `bytes` inputs are preserved. + + static Future> conversionUrlInputsToPaths( + List inputs) async { + final outputs = []; + final tempDirPath = getTemporalFolderPath(); + final tempDir = Directory(tempDirPath); + if (!await tempDir.exists()) { + await tempDir.create(recursive: true); + } + + for (final input in inputs) { + switch (input.type) { + case MergeInputType.path: + outputs.add(input); + break; + case MergeInputType.bytes: + outputs.add(input); + break; + case MergeInputType.url: + try { + final response = await http.get(Uri.parse(input.url!)); + if (response.statusCode == 200) { + final byteInput = MergeInput.bytes(response.bodyBytes); + + final fileName = + '${byteInput.type.filenamePrefix()}_${DateTime.now().millisecondsSinceEpoch}_${Random().nextInt(10000)}${byteInput.type.extension(byteInput)}'; + final tempPath = p.join(tempDirPath, fileName); + final file = File(tempPath); + + await file.writeAsBytes(byteInput.bytes!); + + outputs.add(MergeInput.path(file.path)); + } else { + throw Exception( + 'Fallo al descargar archivo. Código: ${response.statusCode}'); + } + } catch (e) { + throw Exception('Error procesando URL: $e'); + } + + } + } + return outputs; + } + /// Returns the absolute path to the system's temporary directory. /// /// By default, this returns the system's temporary directory path @@ -111,6 +182,8 @@ class DocumentUtils { case MergeInputType.bytes: return FileMagicNumber.detectFileTypeFromBytes(input.bytes!) == FileMagicNumberType.pdf; + case MergeInputType.url: + return false; } } @@ -151,6 +224,8 @@ class DocumentUtils { case MergeInputType.bytes: fileType = FileMagicNumber.detectFileTypeFromBytes(input.bytes!); break; + case MergeInputType.url: + break; } return fileType == FileMagicNumberType.png || fileType == FileMagicNumberType.jpg || @@ -177,11 +252,13 @@ class DocumentUtils { await tempDir.create(recursive: true); } final fileName = - '${input.type.filenamePrefix()}_${DateTime.now().millisecondsSinceEpoch}_${Random().nextInt(10000)}${input.type.extension()}'; + '${input.type.filenamePrefix()}_${DateTime.now().millisecondsSinceEpoch}_${Random().nextInt(10000)}${input.type.extension(input)}'; final tempPath = p.join(tempDirPath, fileName); final file = File(tempPath); await file.writeAsBytes(input.bytes!); return tempPath; + case MergeInputType.url: + return input.url!; } } } diff --git a/lib/utils/document_utils_web.dart b/lib/utils/document_utils_web.dart index b49fbdc9..fa97a5c4 100644 --- a/lib/utils/document_utils_web.dart +++ b/lib/utils/document_utils_web.dart @@ -45,6 +45,8 @@ class DocumentUtils { case MergeInputType.bytes: return FileMagicNumber.detectFileTypeFromBytes(input.bytes!) == FileMagicNumberType.pdf; + case MergeInputType.url: + return false; } } @@ -63,6 +65,9 @@ class DocumentUtils { case MergeInputType.bytes: fileType = FileMagicNumber.detectFileTypeFromBytes(input.bytes!); break; + case MergeInputType.url: + fileType = FileMagicNumberType.unknown; + break; } return fileType == FileMagicNumberType.png || fileType == FileMagicNumberType.jpg || @@ -105,6 +110,8 @@ class DocumentUtils { return input.path!; case MergeInputType.bytes: return createBlobUrl(input.bytes!); + case MergeInputType.url: + return input.url!; } } } diff --git a/pubspec.yaml b/pubspec.yaml index 6fe3d469..95c74cc1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: plugin_platform_interface: ^2.0.2 web: ^1.0.0 file_magic_number: ^1.4.1 + http: ^1.6.0 dev_dependencies: flutter_test: From 69a351041521822a78dfcbc9b913c36bc57273c3 Mon Sep 17 00:00:00 2001 From: Agustin Castanos <122444213+acastanos@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:29:43 +0100 Subject: [PATCH 2/5] update web function --- example/lib/views/pdf_combiner_screen.dart | 57 +++++++++++++++++-- .../lib/views/widgets/file_type_dialog.dart | 31 +++++----- example/lib/views/widgets/file_type_icon.dart | 9 +-- example/pubspec.lock | 2 +- example/pubspec.yaml | 1 + lib/utils/document_utils_io.dart | 23 ++------ lib/utils/document_utils_web.dart | 11 +++- lib/utils/string_extenxion.dart | 17 ++++++ 8 files changed, 105 insertions(+), 46 deletions(-) create mode 100644 lib/utils/string_extenxion.dart diff --git a/example/lib/views/pdf_combiner_screen.dart b/example/lib/views/pdf_combiner_screen.dart index e831172e..c516ad93 100644 --- a/example/lib/views/pdf_combiner_screen.dart +++ b/example/lib/views/pdf_combiner_screen.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:desktop_drop/desktop_drop.dart'; import 'package:file_magic_number/file_magic_number.dart'; import 'package:flutter/material.dart'; @@ -8,7 +10,7 @@ import 'package:pdf_combiner/models/merge_input.dart'; import 'package:pdf_combiner_example/utils/uint8list_extension.dart'; import 'package:pdf_combiner_example/views/widgets/file_type_dialog.dart'; import 'package:pdf_combiner_example/views/widgets/file_type_icon.dart'; - +import 'package:http/http.dart' as http; import '../view_models/pdf_combiner_view_model.dart'; extension on MergeInput { @@ -57,7 +59,7 @@ class _PdfCombinerScreenState extends State { child: Stack( children: [ DropTarget( - onDragDone: (details) async { + onDragDone: (details) async { final selection = await showFileTypeDialog(context); // Show dialog if (selection == null) return; @@ -199,7 +201,9 @@ class _PdfCombinerScreenState extends State { .getBytesFromPathOrBlob(_viewModel .selectedFiles[index] .toString()), - MergeInputType.url => Future.value(null), + MergeInputType.url => getUrlFileSize( + _viewModel.selectedFiles[index] + .toString()), }, builder: (context, snapshot) { if (snapshot.connectionState == @@ -209,7 +213,17 @@ class _PdfCombinerScreenState extends State { } else if (snapshot.hasError) { return const Icon(Icons.error); } else { - return Text(snapshot.data?.size() ?? + final input = + _viewModel.selectedFiles[index]; + if (input.type == + MergeInputType.url) { + final len = snapshot.data as int?; + return Text(len != null + ? formatBytes(len) + : "Unknown Size"); + } + final snapshotUint = snapshot.data as Uint8List?; + return Text(snapshotUint?.size() ?? "Unknown Size"); } }), @@ -282,7 +296,7 @@ class _PdfCombinerScreenState extends State { // Function to pick PDF files from the device Future _pickFiles() async { - final selection = await showFileTypeDialog(context); + final selection = await showFileTypeDialog(context); if (selection == null) return; if (selection.type == MergeInputType.url) { if (selection.url != null && selection.url!.isNotEmpty) { @@ -394,4 +408,37 @@ class _PdfCombinerScreenState extends State { .showSnackBar(SnackBar(content: Text(message))); } } + + String formatBytes(int bytes) { + if (bytes >= 1e9) return "${(bytes / 1e9).toStringAsFixed(2)} GB"; + if (bytes >= 1e6) return "${(bytes / 1e6).toStringAsFixed(2)} MB"; + if (bytes >= 1e3) return "${(bytes / 1e3).toStringAsFixed(2)} KB"; + return "$bytes B"; + } + + Future getUrlFileSize(String url) async { + final uri = Uri.parse(url); + final client = http.Client(); + try { + final head = await client.head(uri); + if (head.statusCode >= 200 && head.statusCode < 300) { + final cl = head.headers['content-length']; + if (cl != null) return int.tryParse(cl); + } + + final range = await client.get(uri, headers: {'Range': 'bytes=0-0'}); + if (range.statusCode == 206) { + final cr = range.headers['content-range']; + if (cr != null) { + final parts = cr.split('/'); + if (parts.length == 2) return int.tryParse(parts[1]); + } + } + } catch (_) { + return null; + } finally { + client.close(); + } + return null; + } } diff --git a/example/lib/views/widgets/file_type_dialog.dart b/example/lib/views/widgets/file_type_dialog.dart index f18bf0a9..21e8ad6f 100644 --- a/example/lib/views/widgets/file_type_dialog.dart +++ b/example/lib/views/widgets/file_type_dialog.dart @@ -14,17 +14,17 @@ class RadioGroupItem { const RadioGroupItem(this.value, this.label); } -class RadioGroup extends StatelessWidget { +class RadioGroupFileType extends StatelessWidget { final T groupValue; final ValueChanged onChanged; final List> items; - const RadioGroup({ - Key? key, + const RadioGroupFileType({ + super.key, required this.groupValue, required this.onChanged, required this.items, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -34,14 +34,14 @@ class RadioGroup extends StatelessWidget { onTap: () => onChanged(item.value), child: Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), - child: Row( - children: [ - Radio(value: item.value, groupValue: groupValue, onChanged: onChanged), - const SizedBox(width: 8), - Expanded(child: Text(item.label)), - ], - ), - ), + child: RadioGroup( + groupValue: groupValue, + onChanged: onChanged, + child: ListTile( + title: Text(item.label), + leading: Radio(toggleable: true, value: item.value), + ),), + ), ); }).toList(), ); @@ -58,8 +58,7 @@ Future showFileTypeDialog(BuildContext context) async { return StatefulBuilder(builder: (context, setState) { final isUrlSelected = selected == MergeInputType.url; - final canAccept = selected != null && - (!isUrlSelected || controller.text.trim().isNotEmpty); + final canAccept = (!isUrlSelected || controller.text.trim().isNotEmpty); return AlertDialog( title: const Text('File Type'), @@ -67,7 +66,7 @@ Future showFileTypeDialog(BuildContext context) async { child: Column( mainAxisSize: MainAxisSize.min, children: [ - RadioGroup( + RadioGroupFileType( groupValue: selected, onChanged: (v) => setState(() => selected = v!), items: const [ @@ -97,7 +96,6 @@ Future showFileTypeDialog(BuildContext context) async { onPressed: () => Navigator.of(context).pop(null), ), TextButton( - child: const Text('Accept'), onPressed: canAccept ? () { final url = controller.text.trim(); @@ -109,6 +107,7 @@ Future showFileTypeDialog(BuildContext context) async { } } : null, + child: const Text('Accept'), ), ], ); diff --git a/example/lib/views/widgets/file_type_icon.dart b/example/lib/views/widgets/file_type_icon.dart index e3a426e4..ec786b5c 100644 --- a/example/lib/views/widgets/file_type_icon.dart +++ b/example/lib/views/widgets/file_type_icon.dart @@ -2,14 +2,14 @@ import 'package:file_magic_number/file_magic_number.dart'; import 'package:flutter/material.dart'; import 'package:open_file/open_file.dart'; import 'package:pdf_combiner/models/merge_input.dart'; +import 'package:pdf_combiner/utils/string_extenxion.dart'; class FileTypeIcon extends StatelessWidget { final MergeInput input; const FileTypeIcon({super.key, required this.input}); @override - Widget build(BuildContext context) { - return TextButton( + Widget build(BuildContext context) => TextButton( onPressed: () { switch (input.type) { case MergeInputType.path: @@ -26,7 +26,7 @@ class FileTypeIcon extends StatelessWidget { FileMagicNumber.detectFileTypeFromBytes(input.bytes!)), MergeInputType.path => FileMagicNumber.detectFileTypeFromPathOrBlob(input.path!), - MergeInputType.url => Future.value(null), + MergeInputType.url => Future.value(input.url.stringToMagicType), }, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { @@ -49,5 +49,6 @@ class FileTypeIcon extends StatelessWidget { } }), ); - } + + } diff --git a/example/pubspec.lock b/example/pubspec.lock index 3e3be2d0..e237d73b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -283,7 +283,7 @@ packages: source: hosted version: "1.0.0" http: - dependency: transitive + dependency: "direct main" description: name: http sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index afc73407..2d6f3dbd 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: open_file: ^3.5.11 platform_detail: ^5.1.0 file_magic_number: ^1.4.2 + http: ^1.6.0 dev_dependencies: integration_test: diff --git a/lib/utils/document_utils_io.dart b/lib/utils/document_utils_io.dart index a6150cfb..ab59becf 100644 --- a/lib/utils/document_utils_io.dart +++ b/lib/utils/document_utils_io.dart @@ -2,10 +2,11 @@ import 'dart:io'; import 'dart:math'; import 'package:file_magic_number/file_magic_number.dart'; -import 'package:path/path.dart' as p; import 'package:pdf_combiner/models/merge_input.dart'; +import 'package:path/path.dart' as p; import 'package:pdf_combiner/pdf_combiner.dart'; import 'package:http/http.dart' as http; +import 'package:pdf_combiner/utils/string_extenxion.dart'; extension on MergeInputType { String extension(MergeInput input) { @@ -14,7 +15,7 @@ extension on MergeInputType { return p.extension(input.path!); case MergeInputType.bytes: final magicType = FileMagicNumber.detectFileTypeFromBytes(input.bytes); - return _magicTypeToString(magicType); + return magicType.name; case MergeInputType.url: return p.extension(input.url!); } @@ -30,21 +31,6 @@ extension on MergeInputType { return 'url_input'; } } - - String _magicTypeToString(FileMagicNumberType type) { - switch (type) { - case FileMagicNumberType.pdf: - return 'pdf'; - case FileMagicNumberType.jpg: - return 'jpg'; - case FileMagicNumberType.png: - return 'png'; - case FileMagicNumberType.heic: - return 'heic'; - default: - return 'nonSupported'; - } - } } /// Utility class for handling document-related checks in a file system environment. @@ -183,7 +169,7 @@ class DocumentUtils { return FileMagicNumber.detectFileTypeFromBytes(input.bytes!) == FileMagicNumberType.pdf; case MergeInputType.url: - return false; + return input.url.stringToMagicType == FileMagicNumberType.pdf; } } @@ -225,6 +211,7 @@ class DocumentUtils { fileType = FileMagicNumber.detectFileTypeFromBytes(input.bytes!); break; case MergeInputType.url: + fileType = input.url!.stringToMagicType; break; } return fileType == FileMagicNumberType.png || diff --git a/lib/utils/document_utils_web.dart b/lib/utils/document_utils_web.dart index fa97a5c4..7d14e518 100644 --- a/lib/utils/document_utils_web.dart +++ b/lib/utils/document_utils_web.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:file_magic_number/file_magic_number.dart'; import 'package:path/path.dart' as p; import 'package:pdf_combiner/models/merge_input.dart'; +import 'package:pdf_combiner/utils/string_extenxion.dart'; import 'package:web/web.dart' as web; /// Utility class for handling document-related checks in a web environment. @@ -46,7 +47,7 @@ class DocumentUtils { return FileMagicNumber.detectFileTypeFromBytes(input.bytes!) == FileMagicNumberType.pdf; case MergeInputType.url: - return false; + return input.url.stringToMagicType == FileMagicNumberType.pdf; } } @@ -66,7 +67,7 @@ class DocumentUtils { fileType = FileMagicNumber.detectFileTypeFromBytes(input.bytes!); break; case MergeInputType.url: - fileType = FileMagicNumberType.unknown; + fileType = input.url.stringToMagicType; break; } return fileType == FileMagicNumberType.png || @@ -104,6 +105,7 @@ class DocumentUtils { /// /// - [MergeInput.path]: Returns the path as-is. /// - [MergeInput.bytes]: Creates a blob URL and returns it. + /// - [MergeInput.url]: Returns the URL as-is. static Future prepareInput(MergeInput input) async { switch (input.type) { case MergeInputType.path: @@ -114,4 +116,9 @@ class DocumentUtils { return input.url!; } } + + static Future> conversionUrlInputsToPaths( + List inputs) async { + return inputs; + } } diff --git a/lib/utils/string_extenxion.dart b/lib/utils/string_extenxion.dart new file mode 100644 index 00000000..c335cebe --- /dev/null +++ b/lib/utils/string_extenxion.dart @@ -0,0 +1,17 @@ +import 'package:file_magic_number/file_magic_number.dart'; + +extension StringExtension on String? { + FileMagicNumberType get stringToMagicType { + final s = this ?? ''; + final segments = s.split('/').where((p) => p.isNotEmpty).toList(); + final lastSegment = segments.isEmpty ? '' : segments.last; + final cleaned = lastSegment.split(RegExp(r'[?#]')).first; + final extension = cleaned.contains('.') + ? cleaned.substring(cleaned.lastIndexOf('.') + 1) + : ''; + return FileMagicNumberType.values.firstWhere( + (e) => e.name.toLowerCase() == extension.toLowerCase(), + orElse: () => FileMagicNumberType.unknown, + ); + } +} From 22dbdf65870e8f740a30a7beecba02b57c4a1950 Mon Sep 17 00:00:00 2001 From: Agustin Castanos <122444213+acastanos@users.noreply.github.com> Date: Mon, 2 Feb 2026 09:18:23 +0100 Subject: [PATCH 3/5] Remove URL Input in drag-and-drop in file type dialog --- example/lib/views/pdf_combiner_screen.dart | 2 +- .../lib/views/widgets/file_type_dialog.dart | 33 +++++++++++-------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/example/lib/views/pdf_combiner_screen.dart b/example/lib/views/pdf_combiner_screen.dart index c516ad93..3b376327 100644 --- a/example/lib/views/pdf_combiner_screen.dart +++ b/example/lib/views/pdf_combiner_screen.dart @@ -61,7 +61,7 @@ class _PdfCombinerScreenState extends State { DropTarget( onDragDone: (details) async { final selection = - await showFileTypeDialog(context); // Show dialog + await showFileTypeDialog(context, isDrag: true); // Show dialog if (selection == null) return; if (selection.type == MergeInputType.url) { if (selection.url != null && selection.url!.isNotEmpty) { diff --git a/example/lib/views/widgets/file_type_dialog.dart b/example/lib/views/widgets/file_type_dialog.dart index 21e8ad6f..a9aa35b8 100644 --- a/example/lib/views/widgets/file_type_dialog.dart +++ b/example/lib/views/widgets/file_type_dialog.dart @@ -35,20 +35,22 @@ class RadioGroupFileType extends StatelessWidget { child: Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: RadioGroup( - groupValue: groupValue, - onChanged: onChanged, - child: ListTile( - title: Text(item.label), - leading: Radio(toggleable: true, value: item.value), - ),), - ), + groupValue: groupValue, + onChanged: onChanged, + child: ListTile( + title: Text(item.label), + leading: Radio(toggleable: true, value: item.value), + ), + ), + ), ); }).toList(), ); } } -Future showFileTypeDialog(BuildContext context) async { +Future showFileTypeDialog(BuildContext context, + {bool isDrag = false}) async { return showDialog( context: context, barrierDismissible: false, @@ -69,11 +71,16 @@ Future showFileTypeDialog(BuildContext context) async { RadioGroupFileType( groupValue: selected, onChanged: (v) => setState(() => selected = v!), - items: const [ - RadioGroupItem(MergeInputType.path, 'Path'), - RadioGroupItem(MergeInputType.bytes, 'Bytes'), - RadioGroupItem(MergeInputType.url, 'Url'), - ], + items: isDrag + ? const [ + RadioGroupItem(MergeInputType.path, 'Path'), + RadioGroupItem(MergeInputType.bytes, 'Bytes'), + ] + : const [ + RadioGroupItem(MergeInputType.path, 'Path'), + RadioGroupItem(MergeInputType.bytes, 'Bytes'), + RadioGroupItem(MergeInputType.url, 'Url'), + ], ), if (isUrlSelected) Padding( From 061cf91305a1727fc2c33aee9a0fb9c48536d462 Mon Sep 17 00:00:00 2001 From: Agustin Castanos <122444213+acastanos@users.noreply.github.com> Date: Tue, 3 Feb 2026 11:41:42 +0100 Subject: [PATCH 4/5] Add URL handling and file size retrieval functions --- example/ios/Flutter/AppFrameworkInfo.plist | 2 +- .../view_models/pdf_combiner_view_model.dart | 36 ++++++++++++++++++ example/lib/views/pdf_combiner_screen.dart | 37 +------------------ example/lib/views/widgets/file_type_icon.dart | 2 +- lib/pdf_combiner.dart | 9 +++-- lib/utils/document_utils_io.dart | 2 +- lib/utils/document_utils_web.dart | 6 ++- ...g_extenxion.dart => string_extension.dart} | 0 8 files changed, 51 insertions(+), 43 deletions(-) rename lib/utils/{string_extenxion.dart => string_extension.dart} (100%) diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf76..7c569640 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 13.0 + 12.0 diff --git a/example/lib/view_models/pdf_combiner_view_model.dart b/example/lib/view_models/pdf_combiner_view_model.dart index 0bf3b015..0b25b484 100644 --- a/example/lib/view_models/pdf_combiner_view_model.dart +++ b/example/lib/view_models/pdf_combiner_view_model.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/services.dart'; +import 'package:http/http.dart' as http; import 'package:path_provider/path_provider.dart'; import 'package:pdf_combiner/exception/pdf_combiner_exception.dart'; import 'package:pdf_combiner/models/merge_input.dart'; @@ -161,4 +162,39 @@ class PdfCombinerViewModel { void removeFileAt(int index) { selectedFiles.removeAt(index); } + +/// Function to format bytes into a human-readable string + String formatBytes(int bytes) { + if (bytes >= 1e9) return "${(bytes / 1e9).toStringAsFixed(2)} GB"; + if (bytes >= 1e6) return "${(bytes / 1e6).toStringAsFixed(2)} MB"; + if (bytes >= 1e3) return "${(bytes / 1e3).toStringAsFixed(2)} KB"; + return "$bytes B"; + } + +/// Function to get the file size from a URL + Future getUrlFileSize(String url) async { + final uri = Uri.parse(url); + final client = http.Client(); + try { + final head = await client.head(uri); + if (head.statusCode >= 200 && head.statusCode < 300) { + final cl = head.headers['content-length']; + if (cl != null) return int.tryParse(cl); + } + + final range = await client.get(uri, headers: {'Range': 'bytes=0-0'}); + if (range.statusCode == 206) { + final cr = range.headers['content-range']; + if (cr != null) { + final parts = cr.split('/'); + if (parts.length == 2) return int.tryParse(parts[1]); + } + } + } catch (_) { + return null; + } finally { + client.close(); + } + return null; + } } diff --git a/example/lib/views/pdf_combiner_screen.dart b/example/lib/views/pdf_combiner_screen.dart index 3b376327..1b02c3cd 100644 --- a/example/lib/views/pdf_combiner_screen.dart +++ b/example/lib/views/pdf_combiner_screen.dart @@ -201,7 +201,7 @@ class _PdfCombinerScreenState extends State { .getBytesFromPathOrBlob(_viewModel .selectedFiles[index] .toString()), - MergeInputType.url => getUrlFileSize( + MergeInputType.url => _viewModel.getUrlFileSize( _viewModel.selectedFiles[index] .toString()), }, @@ -219,7 +219,7 @@ class _PdfCombinerScreenState extends State { MergeInputType.url) { final len = snapshot.data as int?; return Text(len != null - ? formatBytes(len) + ? _viewModel.formatBytes(len) : "Unknown Size"); } final snapshotUint = snapshot.data as Uint8List?; @@ -408,37 +408,4 @@ class _PdfCombinerScreenState extends State { .showSnackBar(SnackBar(content: Text(message))); } } - - String formatBytes(int bytes) { - if (bytes >= 1e9) return "${(bytes / 1e9).toStringAsFixed(2)} GB"; - if (bytes >= 1e6) return "${(bytes / 1e6).toStringAsFixed(2)} MB"; - if (bytes >= 1e3) return "${(bytes / 1e3).toStringAsFixed(2)} KB"; - return "$bytes B"; - } - - Future getUrlFileSize(String url) async { - final uri = Uri.parse(url); - final client = http.Client(); - try { - final head = await client.head(uri); - if (head.statusCode >= 200 && head.statusCode < 300) { - final cl = head.headers['content-length']; - if (cl != null) return int.tryParse(cl); - } - - final range = await client.get(uri, headers: {'Range': 'bytes=0-0'}); - if (range.statusCode == 206) { - final cr = range.headers['content-range']; - if (cr != null) { - final parts = cr.split('/'); - if (parts.length == 2) return int.tryParse(parts[1]); - } - } - } catch (_) { - return null; - } finally { - client.close(); - } - return null; - } } diff --git a/example/lib/views/widgets/file_type_icon.dart b/example/lib/views/widgets/file_type_icon.dart index ec786b5c..3141eb63 100644 --- a/example/lib/views/widgets/file_type_icon.dart +++ b/example/lib/views/widgets/file_type_icon.dart @@ -2,7 +2,7 @@ import 'package:file_magic_number/file_magic_number.dart'; import 'package:flutter/material.dart'; import 'package:open_file/open_file.dart'; import 'package:pdf_combiner/models/merge_input.dart'; -import 'package:pdf_combiner/utils/string_extenxion.dart'; +import 'package:pdf_combiner/utils/string_extension.dart'; class FileTypeIcon extends StatelessWidget { final MergeInput input; diff --git a/lib/pdf_combiner.dart b/lib/pdf_combiner.dart index d27e3314..9983ed9c 100644 --- a/lib/pdf_combiner.dart +++ b/lib/pdf_combiner.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:pdf_combiner/exception/pdf_combiner_exception.dart'; import 'package:pdf_combiner/isolates/images_from_pdf_isolate.dart'; import 'package:pdf_combiner/models/merge_input.dart'; @@ -49,7 +50,7 @@ class PdfCombiner { required List inputs, required String outputPath, }) async { - final List newInputs = await DocumentUtils.conversionUrlInputsToPaths(inputs); + final List newInputs = kIsWeb ? inputs :await DocumentUtils.conversionUrlInputsToPaths(inputs); List temporalPaths = []; final List mutablePaths = List.from(newInputs); if (newInputs.isEmpty) { @@ -113,7 +114,7 @@ class PdfCombiner { required List inputs, required String outputPath, }) async { - final List newInputs = await DocumentUtils.conversionUrlInputsToPaths(inputs); + final List newInputs = kIsWeb ? inputs :await DocumentUtils.conversionUrlInputsToPaths(inputs); final temportalFilePaths = []; if (newInputs.isEmpty) { throw PdfCombinerException( @@ -194,7 +195,7 @@ class PdfCombiner { required String outputPath, PdfFromMultipleImageConfig config = const PdfFromMultipleImageConfig(), }) async { - final List newInputs = await DocumentUtils.conversionUrlInputsToPaths(inputs); + final List newInputs = kIsWeb ? inputs : await DocumentUtils.conversionUrlInputsToPaths(inputs); final temportalFilePaths = []; final outputPathIsPDF = DocumentUtils.hasPDFExtension(outputPath); if (!outputPathIsPDF) { @@ -286,7 +287,7 @@ class PdfCombiner { required String outputDirPath, ImageFromPdfConfig config = const ImageFromPdfConfig(), }) async { - final MergeInput newInput = await DocumentUtils.conversionUrlInputsToPaths([input]).then((value) => value.first); + final MergeInput newInput = kIsWeb ? input : await DocumentUtils.conversionUrlInputsToPaths([input]).then((value) => value.first); String? temportalFilePath; try { bool success = await DocumentUtils.isPDF(newInput); diff --git a/lib/utils/document_utils_io.dart b/lib/utils/document_utils_io.dart index ab59becf..6143fba8 100644 --- a/lib/utils/document_utils_io.dart +++ b/lib/utils/document_utils_io.dart @@ -6,7 +6,7 @@ import 'package:pdf_combiner/models/merge_input.dart'; import 'package:path/path.dart' as p; import 'package:pdf_combiner/pdf_combiner.dart'; import 'package:http/http.dart' as http; -import 'package:pdf_combiner/utils/string_extenxion.dart'; +import 'package:pdf_combiner/utils/string_extension.dart'; extension on MergeInputType { String extension(MergeInput input) { diff --git a/lib/utils/document_utils_web.dart b/lib/utils/document_utils_web.dart index 7d14e518..1f5fed28 100644 --- a/lib/utils/document_utils_web.dart +++ b/lib/utils/document_utils_web.dart @@ -4,7 +4,7 @@ import 'dart:typed_data'; import 'package:file_magic_number/file_magic_number.dart'; import 'package:path/path.dart' as p; import 'package:pdf_combiner/models/merge_input.dart'; -import 'package:pdf_combiner/utils/string_extenxion.dart'; +import 'package:pdf_combiner/utils/string_extension.dart'; import 'package:web/web.dart' as web; /// Utility class for handling document-related checks in a web environment. @@ -117,8 +117,12 @@ class DocumentUtils { } } + + static Future> conversionUrlInputsToPaths( List inputs) async { + + //Returns the same inputs in web environment is not necessary to convert urls to paths return inputs; } } diff --git a/lib/utils/string_extenxion.dart b/lib/utils/string_extension.dart similarity index 100% rename from lib/utils/string_extenxion.dart rename to lib/utils/string_extension.dart From ce49ce666b6107044d18a5808b53ecf95497266c Mon Sep 17 00:00:00 2001 From: Agustin Castanos <122444213+acastanos@users.noreply.github.com> Date: Wed, 4 Feb 2026 08:54:57 +0100 Subject: [PATCH 5/5] Remove iOS Podfile and related files --- example/ios/Podfile | 44 ------- example/ios/Podfile.lock | 28 ---- example/ios/Runner.xcodeproj/project.pbxproj | 122 ------------------ .../contents.xcworkspacedata | 3 - lib/pdf_combiner.dart | 6 +- lib/utils/document_utils_io.dart | 7 +- test/merge_input_test.dart | 25 ++++ test/string_extension_test.dart | 52 ++++++++ 8 files changed, 84 insertions(+), 203 deletions(-) delete mode 100644 example/ios/Podfile delete mode 100644 example/ios/Podfile.lock create mode 100644 test/string_extension_test.dart diff --git a/example/ios/Podfile b/example/ios/Podfile deleted file mode 100644 index e51a31d9..00000000 --- a/example/ios/Podfile +++ /dev/null @@ -1,44 +0,0 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '13.0' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_ios_podfile_setup - -target 'Runner' do - use_frameworks! - use_modular_headers! - - flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) - target 'RunnerTests' do - inherit! :search_paths - end -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_ios_build_settings(target) - end -end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock deleted file mode 100644 index f17b2203..00000000 --- a/example/ios/Podfile.lock +++ /dev/null @@ -1,28 +0,0 @@ -PODS: - - Flutter (1.0.0) - - open_file_ios (1.0.3): - - Flutter - - permission_handler_apple (9.3.0): - - Flutter - -DEPENDENCIES: - - Flutter (from `Flutter`) - - open_file_ios (from `.symlinks/plugins/open_file_ios/ios`) - - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - -EXTERNAL SOURCES: - Flutter: - :path: Flutter - open_file_ios: - :path: ".symlinks/plugins/open_file_ios/ios" - permission_handler_apple: - :path: ".symlinks/plugins/permission_handler_apple/ios" - -SPEC CHECKSUMS: - Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - open_file_ios: 46184d802ee7959203f6392abcfa0dd49fdb5be0 - permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d - -PODFILE CHECKSUM: 4f1c12611da7338d21589c0b2ecd6bd20b109694 - -COCOAPODS: 1.16.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 63135109..8ef023a4 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -15,8 +15,6 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - A021E3BCFA6FC4B73F44D982 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 852424BFF52109C6CB366881 /* Pods_RunnerTests.framework */; }; - D5B03BD0E847D1D12C06C41B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6867D887379682898D1644CE /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -43,21 +41,15 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 03514AA92ECFAAC67531D7E0 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 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 = ""; }; - 206C37A4720B206D6B1AC3C4 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 4F2A9E6E66066F49BFC21E74 /* 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 = ""; }; - 6867D887379682898D1644CE /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = ""; }; 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 852424BFF52109C6CB366881 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 9412236D714078BCECD7D318 /* 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 = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -65,8 +57,6 @@ 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 = ""; }; - A0187530B0C85FA276EC72DB /* 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 = ""; }; - EEDC78A53309521E7A97BC63 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -74,7 +64,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A021E3BCFA6FC4B73F44D982 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -83,22 +72,12 @@ buildActionMask = 2147483647; files = ( 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, - D5B03BD0E847D1D12C06C41B /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 1FBD1FA9799B105384E88DD0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 6867D887379682898D1644CE /* Pods_Runner.framework */, - 852424BFF52109C6CB366881 /* Pods_RunnerTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; 331C8082294A63A400263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( @@ -107,20 +86,6 @@ path = RunnerTests; sourceTree = ""; }; - 3A0F5EE6E8F5E1C43A5BADCC /* Pods */ = { - isa = PBXGroup; - children = ( - A0187530B0C85FA276EC72DB /* Pods-Runner.debug.xcconfig */, - 9412236D714078BCECD7D318 /* Pods-Runner.release.xcconfig */, - 4F2A9E6E66066F49BFC21E74 /* Pods-Runner.profile.xcconfig */, - EEDC78A53309521E7A97BC63 /* Pods-RunnerTests.debug.xcconfig */, - 03514AA92ECFAAC67531D7E0 /* Pods-RunnerTests.release.xcconfig */, - 206C37A4720B206D6B1AC3C4 /* Pods-RunnerTests.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -140,8 +105,6 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, - 3A0F5EE6E8F5E1C43A5BADCC /* Pods */, - 1FBD1FA9799B105384E88DD0 /* Frameworks */, ); sourceTree = ""; }; @@ -176,7 +139,6 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - CFA0AB402F277C276F0F1A80 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, 328562033B987F342D6D6D04 /* Frameworks */, @@ -195,15 +157,12 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - BC4D21716821DA8325256653 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 1229700873A4E86F4A2BD8F2 /* [CP] Embed Pods Frameworks */, - 825C32A37B02CA77CCB1D0F3 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -281,23 +240,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 1229700873A4E86F4A2BD8F2 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -314,23 +256,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 825C32A37B02CA77CCB1D0F3 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -346,50 +271,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - BC4D21716821DA8325256653 /* [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-Runner-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; - }; - CFA0AB402F277C276F0F1A80 /* [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-RunnerTests-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; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -519,7 +400,6 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = EEDC78A53309521E7A97BC63 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -538,7 +418,6 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 03514AA92ECFAAC67531D7E0 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -555,7 +434,6 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 206C37A4720B206D6B1AC3C4 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata index 21a3cc14..1d526a16 100644 --- a/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,7 +4,4 @@ - - diff --git a/lib/pdf_combiner.dart b/lib/pdf_combiner.dart index 9983ed9c..06e38b8c 100644 --- a/lib/pdf_combiner.dart +++ b/lib/pdf_combiner.dart @@ -50,7 +50,7 @@ class PdfCombiner { required List inputs, required String outputPath, }) async { - final List newInputs = kIsWeb ? inputs :await DocumentUtils.conversionUrlInputsToPaths(inputs); + final List newInputs = await DocumentUtils.conversionUrlInputsToPaths(inputs); List temporalPaths = []; final List mutablePaths = List.from(newInputs); if (newInputs.isEmpty) { @@ -114,7 +114,7 @@ class PdfCombiner { required List inputs, required String outputPath, }) async { - final List newInputs = kIsWeb ? inputs :await DocumentUtils.conversionUrlInputsToPaths(inputs); + final List newInputs = await DocumentUtils.conversionUrlInputsToPaths(inputs); final temportalFilePaths = []; if (newInputs.isEmpty) { throw PdfCombinerException( @@ -287,7 +287,7 @@ class PdfCombiner { required String outputDirPath, ImageFromPdfConfig config = const ImageFromPdfConfig(), }) async { - final MergeInput newInput = kIsWeb ? input : await DocumentUtils.conversionUrlInputsToPaths([input]).then((value) => value.first); + final MergeInput newInput = await DocumentUtils.conversionUrlInputsToPaths([input]).then((value) => value.first); String? temportalFilePath; try { bool success = await DocumentUtils.isPDF(newInput); diff --git a/lib/utils/document_utils_io.dart b/lib/utils/document_utils_io.dart index 6143fba8..dbfee90d 100644 --- a/lib/utils/document_utils_io.dart +++ b/lib/utils/document_utils_io.dart @@ -2,10 +2,12 @@ import 'dart:io'; import 'dart:math'; import 'package:file_magic_number/file_magic_number.dart'; +import 'package:pdf_combiner/exception/pdf_combiner_exception.dart'; import 'package:pdf_combiner/models/merge_input.dart'; import 'package:path/path.dart' as p; import 'package:pdf_combiner/pdf_combiner.dart'; import 'package:http/http.dart' as http; +import 'package:pdf_combiner/responses/pdf_combiner_messages.dart'; import 'package:pdf_combiner/utils/string_extension.dart'; extension on MergeInputType { @@ -109,11 +111,10 @@ class DocumentUtils { outputs.add(MergeInput.path(file.path)); } else { - throw Exception( - 'Fallo al descargar archivo. Código: ${response.statusCode}'); + throw PdfCombinerException(PdfCombinerMessages.errorMessagePDF(input.url!)); } } catch (e) { - throw Exception('Error procesando URL: $e'); + throw PdfCombinerException(e.toString()); } } diff --git a/test/merge_input_test.dart b/test/merge_input_test.dart index 88e2eaa2..be72593c 100644 --- a/test/merge_input_test.dart +++ b/test/merge_input_test.dart @@ -11,7 +11,10 @@ void main() { expect(input.type, MergeInputType.path); expect(input.path, '/path/to/file.pdf'); expect(input.bytes, isNull); + expect(input.url, isNull); expect(input.type == MergeInputType.bytes, isFalse); + expect(input.type == MergeInputType.url, isFalse); + expect(input.type == MergeInputType.path, isTrue); }); test('bytes constructor creates MergeInput with bytes type', () { @@ -21,9 +24,25 @@ void main() { expect(input.type, MergeInputType.bytes); expect(input.bytes, bytes); expect(input.path, isNull); + expect(input.url, isNull); expect(input.type == MergeInputType.bytes, isTrue); + expect(input.type == MergeInputType.path, isFalse); + expect(input.type == MergeInputType.url, isFalse); }); + test('url constructor creates MergeInput with url type', () { + final input = MergeInput.url('Https://example.com/file.pdf'); + + expect(input.type, MergeInputType.url); + expect(input.path, isNull); + expect(input.bytes, isNull); + expect(input.url, 'Https://example.com/file.pdf'); + expect(input.type == MergeInputType.bytes, isFalse); + expect(input.type == MergeInputType.path, isFalse); + expect(input.type == MergeInputType.url, isTrue); + }); + + test('toString returns path for path type', () { final input = MergeInput.path('/path/to/file.pdf'); @@ -36,5 +55,11 @@ void main() { expect(input.toString(), bytes.toString()); }); + + test('toString returns url for url type', () { + final input = MergeInput.url('Https://example.com/file.pdf'); + + expect(input.toString(), 'Https://example.com/file.pdf'); + }); }); } diff --git a/test/string_extension_test.dart b/test/string_extension_test.dart new file mode 100644 index 00000000..497e9bc6 --- /dev/null +++ b/test/string_extension_test.dart @@ -0,0 +1,52 @@ +import 'package:file_magic_number/file_magic_number.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pdf_combiner/utils/string_extension.dart'; + +void main() { + group('StringExtension', () { + test('stringToMagicType detects PDF extension', () { + expect('document.pdf'.stringToMagicType, FileMagicNumberType.pdf); + expect('path/to/document.pdf'.stringToMagicType, FileMagicNumberType.pdf); + }); + + test('stringToMagicType detects PDF extension from URL', () { + expect('https://example.com/document.pdf'.stringToMagicType, + FileMagicNumberType.pdf); + expect('https://example.com/document.pdf?query=1'.stringToMagicType, + FileMagicNumberType.pdf); + }); + + test('stringToMagicType detects PNG extension', () { + expect('image.png'.stringToMagicType, FileMagicNumberType.png); + }); + + test('stringToMagicType detects JPG extension', () { + expect('image.jpg'.stringToMagicType, FileMagicNumberType.jpg); + expect('image.jpeg'.stringToMagicType, + FileMagicNumberType.unknown); + }); + + test('stringToMagicType detects HEIC extension', () { + expect('image.heic'.stringToMagicType, FileMagicNumberType.heic); + }); + + test('stringToMagicType handles unknown extension', () { + expect('file.txt'.stringToMagicType, FileMagicNumberType.unknown); + expect('file'.stringToMagicType, FileMagicNumberType.unknown); + }); + + test('stringToMagicType handles null', () { + String? nullString; + expect(nullString.stringToMagicType, FileMagicNumberType.unknown); + }); + + test('stringToMagicType handles empty string', () { + expect(''.stringToMagicType, FileMagicNumberType.unknown); + }); + + test('stringToMagicType handles complex URLs', () { + expect('https://example.com/doc.pdf#fragment'.stringToMagicType, + FileMagicNumberType.pdf); + }); + }); +} \ No newline at end of file