diff --git a/.github/workflows/xcode.yml b/.github/workflows/xcode.yml
index 44ca3963b1..bf3a7df058 100644
--- a/.github/workflows/xcode.yml
+++ b/.github/workflows/xcode.yml
@@ -20,9 +20,9 @@ on:
env:
PROJECT: Nextcloud.xcodeproj
- DESTINATION: platform=iOS Simulator,name=iPhone 17,OS=27.0
+ DESTINATION: platform=iOS Simulator,name=iPhone 17,OS=27.2
SCHEME: Nextcloud
- XCODE_VERSION: '27.1'
+ XCODE_VERSION: '27.2'
SERVER_BRANCH: stable30
PHP_VERSION: 8.3
@@ -60,7 +60,7 @@ jobs:
retention-days: 4
test:
- name: Run Tests
+ name: Run Unit Tests
runs-on: xcode-27
timeout-minutes: 40
needs: [build]
@@ -69,42 +69,6 @@ jobs:
steps:
- uses: actions/checkout@v7
- with:
- fetch-depth: 0
-
- - name: Detect passcode changes
- id: passcode_changes
- shell: bash
- env:
- PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
- PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
- PUSH_BEFORE_SHA: ${{ github.event.before }}
- run: |
- run_passcode_tests=false
- if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
- compare_base=$(git merge-base "$PR_BASE_SHA" "$PR_HEAD_SHA")
- compare_head="$PR_HEAD_SHA"
- else
- compare_base="$PUSH_BEFORE_SHA"
- compare_head="$GITHUB_SHA"
- fi
-
- if [ -z "$compare_base" ] || [ "$compare_base" = "0000000000000000000000000000000000000000" ]; then
- run_passcode_tests=true
- else
- git diff --name-only -z "$compare_base" "$compare_head" > "$RUNNER_TEMP/passcode-changed-files"
- while IFS= read -r -d '' changed_file; do
- case "$changed_file" in
- iOSClient/Utility/Passcode/* | \
- Tests/NextcloudUITests/PasscodeUITests.swift)
- run_passcode_tests=true
- break
- ;;
- esac
- done < "$RUNNER_TEMP/passcode-changed-files"
- fi
- echo "run=$run_passcode_tests" >> "$GITHUB_OUTPUT"
- echo "Run passcode UI tests: $run_passcode_tests"
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
@@ -166,33 +130,17 @@ jobs:
xcrun simctl boot "iPhone 17"
xcrun simctl bootstatus "iPhone 17" -b
- - name: Check if Safari is installed
- run: |
- if xcrun simctl listapps booted | grep -q com.apple.mobilesafari; then
- echo "Safari is installed in the simulator."
- else
- echo "Safari is NOT installed in the simulator."
- exit 1
- fi
-
- - name: Run Xcode Test
+ - name: Run Xcode Unit Tests
timeout-minutes: 25
shell: bash
- env:
- RUN_PASSCODE_TESTS: ${{ steps.passcode_changes.outputs.run }}
run: |
set -o pipefail
- test_selection=()
- if [ "$RUN_PASSCODE_TESTS" != "true" ]; then
- test_selection+=(-skip-testing:NextcloudUITests/PasscodeUITests)
- fi
+ # UI tests remain available in the shared scheme for local execution.
xcodebuild test-without-building \
-xctestrun $(find . -type f -name "*.xctestrun") \
-destination "${{ env.DESTINATION }}" \
-derivedDataPath "DerivedData" \
- "${test_selection[@]}" \
- -test-iterations 3 \
- -retry-tests-on-failure \
+ -only-testing:NextcloudUnitTests \
-test-timeouts-enabled YES \
-default-test-execution-time-allowance 600 \
-maximum-test-execution-time-allowance 600 \
diff --git a/AGENTS.md b/AGENTS.md
index 18c62c6bca..7fe8ec2350 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -31,7 +31,10 @@ Other platforms like Android are irrelevant for this project.
| `Brand` | Branding layer: `NCBrand.swift` (app constants, colors), `Database.swift` (Realm database name and schema version), Info.plists and entitlements for all targets, launch screen and intro screens. | Bump `databaseSchemaVersion` in `Brand/Database.swift` when changing Realm models. |
| `File Provider Extension` | Contains business logic for the iOS File Provider extension. | — |
| `File Provider Extension UI` | Contains UI logic for the iOS File Provider extension. | — |
-| `Share` | Contains code for the iOS Share extension. | — |
+| `Share` | Contains the active Share extension entry point, launched through `NSExtensionPrincipalClass`, plus input preparation, session storage and recent upload destinations. | — |
+| `Share/View` | Contains the SwiftUI Share extension interface. | Select the `Nextcloud` scheme for Xcode previews. Shared Share sources are hosted by the app only in Debug (`SHARE_PREVIEWS`) and excluded from app Release builds. |
+| `Share/Model` | Contains Share extension state, account handling and upload logic. | — |
+| `Share/UIKit` | Contains the previous UIKit Share extension sources and storyboard for reference. | Keep these reference files out of the Share target build. |
| `Widget` | Contains code for iOS Widgets. | — |
| `WidgetDashboardIntentHandler` | Contains the intent handler for the dashboard widget. | — |
| `Notification Service Extension` | Contains the push notification service extension. | — |
diff --git a/BackgroundUploadExtension/NCBackgroundUploadExtensionManager.swift b/BackgroundUploadExtension/NCBackgroundUploadExtensionManager.swift
index b69c70d046..7ccff9e408 100644
--- a/BackgroundUploadExtension/NCBackgroundUploadExtensionManager.swift
+++ b/BackgroundUploadExtension/NCBackgroundUploadExtensionManager.swift
@@ -393,6 +393,11 @@ final class NCBackgroundUploadExtensionManager {
/// Disables the PhotoKit extension after the feature or auto upload is turned off and no jobs remain.
/// Active metadata defers disabling so cancellations and terminal results can still be reconciled.
func disableIfIdle() async -> Bool {
+ // Foreground cleanup must not trigger the initial Photos permission prompt.
+ guard PHPhotoLibrary.authorizationStatus(for: .readWrite) != .notDetermined else {
+ return false
+ }
+
let featureEnabled = NCPreferences().shouldUseBackgroundUploadExtension
let account = await database.getTableAccountAsync(predicate: NSPredicate(format: "autoUploadStart == true"))
diff --git a/Brand/Share.plist b/Brand/Share.plist
index ec2daea2b1..3bb515ffbe 100755
--- a/Brand/Share.plist
+++ b/Brand/Share.plist
@@ -25,6 +25,8 @@
NSAllowsArbitraryLoads
+ NSFaceIDUsageDescription
+ Face ID is required to authenticate using face recognition.
NSExtension
NSExtensionAttributes
@@ -32,10 +34,10 @@
NSExtensionActivationRule
SUBQUERY (extensionItems, $extensionItem, SUBQUERY ($extensionItem.attachments, $attachment, (ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.data" OR ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.text")).@count == $extensionItem.attachments.@count).@count > 0
- NSExtensionMainStoryboard
- MainInterface
+ NSExtensionPrincipalClass
+ $(PRODUCT_MODULE_NAME).NCShareEntryController
NSExtensionPointIdentifier
com.apple.share-services
-
\ No newline at end of file
+
diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj
index 819de00a3f..4095bdc561 100644
--- a/Nextcloud.xcodeproj/project.pbxproj
+++ b/Nextcloud.xcodeproj/project.pbxproj
@@ -37,6 +37,8 @@
A1E2EE020000000000000008 /* NCEndToEndKeySetResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1E2EE020000000000000007 /* NCEndToEndKeySetResolverTests.swift */; };
A358B569BF5FDABA6D9012D1 /* NCErrorContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0CD9A00D74B01055F238E9 /* NCErrorContext.swift */; };
A5A87F9E4B0E4441A6A4BC20 /* NCContextMenuProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB7697C94BA14450A0867940 /* NCContextMenuProfile.swift */; };
+ A71107260000000000000101 /* NCShareFilenameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A71107260000000000000100 /* NCShareFilenameView.swift */; };
+ A71107260000000000000102 /* NCShareFilenameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A71107260000000000000100 /* NCShareFilenameView.swift */; };
AA3C85E82D36B08C00F74F12 /* UITestBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3C85E72D36B08C00F74F12 /* UITestBackend.swift */; };
AA3C85EB2D36BBFB00F74F12 /* OCSResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3C85EA2D36BBF400F74F12 /* OCSResponse.swift */; };
AA3C85EE2D36BCCE00F74F12 /* SharesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3C85ED2D36BCCB00F74F12 /* SharesResponse.swift */; };
@@ -73,6 +75,35 @@
AAFC0D062F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFC0D032F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift */; };
AAFC0D092F9AA10000F0A001 /* NCFocusedAutoUploadCloudAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFC0D082F9AA10000F0A001 /* NCFocusedAutoUploadCloudAnimation.swift */; };
AAFC0D0B2F9AA10000F0A001 /* NCAutoUploadCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFC0D0A2F9AA10000F0A001 /* NCAutoUploadCounter.swift */; };
+ AB1028260000000000030000 /* NCShareEntryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1028260000000000020000 /* NCShareEntryController.swift */; };
+ AB1028260000000000030002 /* NCShareViewModel+Accounts.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1028260000000000020002 /* NCShareViewModel+Accounts.swift */; };
+ AB1028260000000000030003 /* NCShareViewModel+Folders.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1028260000000000020003 /* NCShareViewModel+Folders.swift */; };
+ AB1028260000000000030004 /* NCShareViewModel+Upload.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1028260000000000020004 /* NCShareViewModel+Upload.swift */; };
+ AB1028260000000000030006 /* NCShareLoaderItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1028260000000000020006 /* NCShareLoaderItemProvider.swift */; };
+ AB1028260000000000030008 /* NCShareSessionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1028260000000000020008 /* NCShareSessionStore.swift */; };
+ AB1028260000000000030009 /* NCShareView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1028260000000000020009 /* NCShareView.swift */; };
+ AB102826000000000003000A /* NCShareViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB102826000000000002000A /* NCShareViewModel.swift */; };
+ AB1028260000000000040001 /* NCShareLoaderItemProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1028260000000000040000 /* NCShareLoaderItemProviderTests.swift */; };
+ AB1028260000000000040002 /* NCShareLoaderItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1028260000000000020006 /* NCShareLoaderItemProvider.swift */; };
+ AB1028260000000000050101 /* NCShareUploadConflictView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1028260000000000050001 /* NCShareUploadConflictView.swift */; };
+ AB1028260000000000050200 /* NCShareUploadNamePlannerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1028260000000000050300 /* NCShareUploadNamePlannerTests.swift */; };
+ AB1028260000000000050201 /* NCShareViewModel+Upload.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1028260000000000020004 /* NCShareViewModel+Upload.swift */; };
+ AB1028260000000000060001 /* NCShareAccountMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1028260000000000060000 /* NCShareAccountMenu.swift */; };
+ AB1028260000000000080000 /* NCShareViewModel+Accounts.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1028260000000000020002 /* NCShareViewModel+Accounts.swift */; };
+ AB1028260000000000080001 /* NCShareViewModel+Folders.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1028260000000000020003 /* NCShareViewModel+Folders.swift */; };
+ AB1028260000000000080002 /* NCShareViewModel+Upload.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1028260000000000020004 /* NCShareViewModel+Upload.swift */; };
+ AB1028260000000000080003 /* NCShareViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB102826000000000002000A /* NCShareViewModel.swift */; };
+ AB1028260000000000080004 /* NCShareLoaderItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1028260000000000020006 /* NCShareLoaderItemProvider.swift */; };
+ AB1028260000000000080005 /* NCShareSessionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1028260000000000020008 /* NCShareSessionStore.swift */; };
+ AB1028260000000000080006 /* NCShareAccountMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1028260000000000060000 /* NCShareAccountMenu.swift */; };
+ AB1028260000000000080007 /* NCShareUploadConflictView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1028260000000000050001 /* NCShareUploadConflictView.swift */; };
+ AB1028260000000000080008 /* NCShareView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1028260000000000020009 /* NCShareView.swift */; };
+ AB1028260000000000090100 /* NCShareRecentFoldersStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1028260000000000090000 /* NCShareRecentFoldersStore.swift */; };
+ AB1028260000000000090110 /* NCShareRecentFoldersStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1028260000000000090000 /* NCShareRecentFoldersStore.swift */; };
+ AB1028260000000000090120 /* NCShareRecentFoldersStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1028260000000000090000 /* NCShareRecentFoldersStore.swift */; };
+ AB1028260000000000090121 /* NCShareRecentFoldersStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1028260000000000090001 /* NCShareRecentFoldersStoreTests.swift */; };
+ AB1109260000000000010101 /* NCShareUploadButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1109260000000000010001 /* NCShareUploadButton.swift */; };
+ AB1109260000000000010102 /* NCShareUploadButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1109260000000000010001 /* NCShareUploadButton.swift */; };
AB6000012F60000100FE2775 /* NCTagEditorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6000002F60000100FE2775 /* NCTagEditorModel.swift */; };
AB6000032F60000200FE2775 /* NCTagEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6000022F60000200FE2775 /* NCTagEditorView.swift */; };
AB7000022F70000100FE2776 /* NCShareDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7000012F70000100FE2776 /* NCShareDetailsView.swift */; };
@@ -83,13 +114,6 @@
AC14ADAF2026101400000012 /* NCMediaViewerPreviewLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC14ADAF2026101400000011 /* NCMediaViewerPreviewLoader.swift */; };
AF1A9B6427D0CA1E00F17A9E /* UIAlertController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */; };
AF1A9B6527D0CC0500F17A9E /* UIAlertController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */; };
- AF22B206277B4E4C00DAB0CC /* NCCreateFormUploadConflict.swift in Sources */ = {isa = PBXBuildFile; fileRef = F704B5E42430AA8000632F5F /* NCCreateFormUploadConflict.swift */; };
- AF22B207277B4E4C00DAB0CC /* NCCreateFormUploadConflict.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F704B5E22430AA6F00632F5F /* NCCreateFormUploadConflict.storyboard */; };
- AF22B208277B4E4C00DAB0CC /* NCCreateFormUploadConflictCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F704B5E82430C0B800632F5F /* NCCreateFormUploadConflictCell.swift */; };
- AF22B209277B4E4C00DAB0CC /* NCCreateFormUploadConflictCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F704B5E62430C06700632F5F /* NCCreateFormUploadConflictCell.xib */; };
- AF22B20C277C6F4D00DAB0CC /* NCShareCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF22B20B277C6F4D00DAB0CC /* NCShareCell.swift */; };
- AF22B217277D196700DAB0CC /* NCShareExtension+DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF22B215277D196700DAB0CC /* NCShareExtension+DataSource.swift */; };
- AF22B218277D196700DAB0CC /* NCShareExtension+Files.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF22B216277D196700DAB0CC /* NCShareExtension+Files.swift */; };
AF2D7C7C2742556F00ADF566 /* NCShareLinkCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF2D7C7B2742556F00ADF566 /* NCShareLinkCell.swift */; };
AF2D7C7E2742559100ADF566 /* NCShareUserCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF2D7C7D2742559100ADF566 /* NCShareUserCell.swift */; };
AF3FDCC22796ECC300710F60 /* NCTrash+CollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF3FDCC12796ECC300710F60 /* NCTrash+CollectionView.swift */; };
@@ -104,7 +128,6 @@
AF4BF62127562B3F0081CEEF /* NCManageDatabase+Activity.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4BF61D27562B3F0081CEEF /* NCManageDatabase+Activity.swift */; };
AF56C1DC2784856200D8BAE2 /* NCActivityCommentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = AF56C1DB2784856200D8BAE2 /* NCActivityCommentView.xib */; };
AF730AF827834B1400B7520E /* NCShare+NCCellDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF730AF727834B1400B7520E /* NCShare+NCCellDelegate.swift */; };
- AF730AFA27843E4C00B7520E /* NCShareExtension+NCAccountRequestDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF730AF927843E4C00B7520E /* NCShareExtension+NCAccountRequestDelegate.swift */; };
AF7E504E27A2D8FF00B5E4AF /* UIBarButton+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF7E504D27A2D8FF00B5E4AF /* UIBarButton+Extension.swift */; };
AF93471227E2341B002537EE /* NCContextMenuShare.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF93471127E2341B002537EE /* NCContextMenuShare.swift */; };
AF93471927E2361E002537EE /* NCShareAdvancePermissionFooter.xib in Resources */ = {isa = PBXBuildFile; fileRef = AF93471427E2361E002537EE /* NCShareAdvancePermissionFooter.xib */; };
@@ -153,12 +176,10 @@
F33EE6E12BF4BDA500CA1A51 /* NIOSSL in Frameworks */ = {isa = PBXBuildFile; productRef = F33EE6E02BF4BDA500CA1A51 /* NIOSSL */; };
F33EE6E32BF4C00700CA1A51 /* NIOSSL in Frameworks */ = {isa = PBXBuildFile; productRef = F33EE6E22BF4C00700CA1A51 /* NIOSSL */; };
F33EE6E52BF4C02000CA1A51 /* NIOSSL in Frameworks */ = {isa = PBXBuildFile; productRef = F33EE6E42BF4C02000CA1A51 /* NIOSSL */; };
- F33EE6E72BF4C02600CA1A51 /* NIOSSL in Frameworks */ = {isa = PBXBuildFile; productRef = F33EE6E62BF4C02600CA1A51 /* NIOSSL */; };
F33EE6ED2BF4C03800CA1A51 /* NIOSSL in Frameworks */ = {isa = PBXBuildFile; productRef = F33EE6EC2BF4C03800CA1A51 /* NIOSSL */; };
F33EE6F02BF4C0FF00CA1A51 /* NIO in Frameworks */ = {isa = PBXBuildFile; productRef = F33EE6EF2BF4C0FF00CA1A51 /* NIO */; };
F33EE6F22BF4C9B200CA1A51 /* PKCS12.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33EE6F12BF4C9B200CA1A51 /* PKCS12.swift */; };
F343A4B32A1E01FF00DDA874 /* PHAsset+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F343A4B22A1E01FF00DDA874 /* PHAsset+Extension.swift */; };
- F343A4B62A1E084200DDA874 /* PHAsset+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F343A4B22A1E01FF00DDA874 /* PHAsset+Extension.swift */; };
F343A4BB2A1E734600DDA874 /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F343A4BA2A1E734600DDA874 /* Optional+Extension.swift */; };
F34BDB3A2F5744EC007A222C /* UINavigationItem+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F34BDB392F5744E5007A222C /* UINavigationItem+Extension.swift */; };
F34BDB3C2F574A58007A222C /* BidiSafeFilenameTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F34BDB3B2F574A58007A222C /* BidiSafeFilenameTests.swift */; };
@@ -246,7 +267,6 @@
F70716E62987F81500E72C1D /* DocumentActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70716E52987F81500E72C1D /* DocumentActionViewController.swift */; };
F70716ED2987F81500E72C1D /* File Provider Extension UI.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = F70716E32987F81500E72C1D /* File Provider Extension UI.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
F707C26521A2DC5200F6181E /* NCStoreReview.swift in Sources */ = {isa = PBXBuildFile; fileRef = F707C26421A2DC5200F6181E /* NCStoreReview.swift */; };
- F70821D829E59E6D001CA2D7 /* TagListView in Frameworks */ = {isa = PBXBuildFile; productRef = F70821D729E59E6D001CA2D7 /* TagListView */; };
F70898672EDDB39B00EF85BD /* NCNetworking+TransferDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70898662EDDB39300EF85BD /* NCNetworking+TransferDelegate.swift */; };
F70898692EDDB51700EF85BD /* NCSelectOpen+SelectDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70898682EDDB51200EF85BD /* NCSelectOpen+SelectDelegate.swift */; };
F70B86752642CE3B00ED5349 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = F70B86742642CE3B00ED5349 /* FirebaseCrashlytics */; };
@@ -272,11 +292,9 @@
F711A4DC2AF92CAE00095DD8 /* NCUtility+Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = F711A4DB2AF92CAD00095DD8 /* NCUtility+Date.swift */; };
F711A4DD2AF92CAE00095DD8 /* NCUtility+Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = F711A4DB2AF92CAD00095DD8 /* NCUtility+Date.swift */; };
F711A4DE2AF92CAE00095DD8 /* NCUtility+Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = F711A4DB2AF92CAD00095DD8 /* NCUtility+Date.swift */; };
- F711A4DF2AF92CAE00095DD8 /* NCUtility+Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = F711A4DB2AF92CAD00095DD8 /* NCUtility+Date.swift */; };
F711A4E32AF9310400095DD8 /* NCUtility+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF93474B27E34120002537EE /* NCUtility+Image.swift */; };
F711A4E52AF9310500095DD8 /* NCUtility+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF93474B27E34120002537EE /* NCUtility+Image.swift */; };
F711A4E92AF9327600095DD8 /* UIImage+animatedGIF.m in Sources */ = {isa = PBXBuildFile; fileRef = F713FEFF2472764100214AF6 /* UIImage+animatedGIF.m */; };
- F711A4EB2AF9327D00095DD8 /* UIImage+animatedGIF.m in Sources */ = {isa = PBXBuildFile; fileRef = F713FEFF2472764100214AF6 /* UIImage+animatedGIF.m */; };
F711D63128F44801003F43C8 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C9739428F17131002C43E2 /* IntentHandler.swift */; };
F712217E2F51AF9500ACEB08 /* LucidBanner in Frameworks */ = {isa = PBXBuildFile; productRef = F712217D2F51AF9500ACEB08 /* LucidBanner */; };
F71221802F51AF9F00ACEB08 /* LucidBanner in Frameworks */ = {isa = PBXBuildFile; productRef = F712217F2F51AF9F00ACEB08 /* LucidBanner */; };
@@ -285,10 +303,6 @@
F713FF002472764100214AF6 /* UIImage+animatedGIF.m in Sources */ = {isa = PBXBuildFile; fileRef = F713FEFF2472764100214AF6 /* UIImage+animatedGIF.m */; };
F7145610296433C80038D028 /* NCDocumentCamera.swift in Sources */ = {isa = PBXBuildFile; fileRef = F714560F296433C80038D028 /* NCDocumentCamera.swift */; };
F7145A231D12E3B700CAFEEC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F7E70DE91A24DE4100E1B66A /* Localizable.strings */; };
- F714803B262EBE3900693E51 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F714803A262EBE3900693E51 /* MainInterface.storyboard */; };
- F7148041262EBE4000693E51 /* NCShareExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7148040262EBE4000693E51 /* NCShareExtension.swift */; };
- F7148054262ED51000693E51 /* NCListCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F78ACD4321903CF20088454D /* NCListCell.xib */; };
- F714805E262ED52900693E51 /* NCSectionFooter.xib in Resources */ = {isa = PBXBuildFile; fileRef = F78ACD53219047D40088454D /* NCSectionFooter.xib */; };
F714A1472ED84AF90050A43B /* HudBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F714A1462ED84AF00050A43B /* HudBannerView.swift */; };
F7160A7D2BE931DE0034DCB3 /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = F7160A7C2BE931DE0034DCB3 /* RealmSwift */; };
F7160A822BE933390034DCB3 /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = F7160A812BE933390034DCB3 /* RealmSwift */; };
@@ -348,7 +362,6 @@
F72CD63A25C19EBF00F46F9A /* NCAutoUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72CD63925C19EBF00F46F9A /* NCAutoUpload.swift */; };
F72D1007210B6882009C96B7 /* NCPushNotificationEncryption.m in Sources */ = {isa = PBXBuildFile; fileRef = F72D1005210B6882009C96B7 /* NCPushNotificationEncryption.m */; };
F72D404923D2082500A97FD0 /* NCViewerDirectEditing.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72D404823D2082500A97FD0 /* NCViewerDirectEditing.swift */; };
- F72D7EB7263B1207000B3DFC /* MarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = F72D7EB6263B1207000B3DFC /* MarkdownKit */; };
F72DA9B425F53E4E00B87DB1 /* SwiftRichString in Frameworks */ = {isa = PBXBuildFile; productRef = F72DA9B325F53E4E00B87DB1 /* SwiftRichString */; };
F72EA95228B7BA2A00C88F0C /* DashboardWidgetProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72EA95128B7BA2A00C88F0C /* DashboardWidgetProvider.swift */; };
F72EA95428B7BABA00C88F0C /* FilesWidgetProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72EA95328B7BABA00C88F0C /* FilesWidgetProvider.swift */; };
@@ -378,7 +391,6 @@
F7381EE1218218C9000B1560 /* NCOffline.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7381EDA218218C9000B1560 /* NCOffline.swift */; };
F7381EE5218218C9000B1560 /* NCOffline.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7381EDE218218C9000B1560 /* NCOffline.storyboard */; };
F7386E482DA90E0F009A00F6 /* NCAppVersionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7386E452DA90E02009A00F6 /* NCAppVersionManager.swift */; };
- F73BC74F2F23811E003170C2 /* WarningBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7DF7B3E2F1A2EE400514020 /* WarningBannerView.swift */; };
F73CB3B222E072A000AD728E /* NCShareHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = F73CB3B122E072A000AD728E /* NCShareHeaderView.xib */; };
F73D11FA253C5F4800DF9BEC /* NCViewerDirectEditing.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F73D11F9253C5F4800DF9BEC /* NCViewerDirectEditing.storyboard */; };
F73EF7A72B0223900087E6E9 /* NCManageDatabase+Comments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73EF7A62B0223900087E6E9 /* NCManageDatabase+Comments.swift */; };
@@ -407,14 +419,12 @@
F73EF7E22B02266D0087E6E9 /* NCManageDatabase+Trash.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73EF7DE2B02266C0087E6E9 /* NCManageDatabase+Trash.swift */; };
F73EF7E52B02266D0087E6E9 /* NCManageDatabase+Trash.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73EF7DE2B02266C0087E6E9 /* NCManageDatabase+Trash.swift */; };
F73EFF9B2DB11EC900FD434C /* NCFiles+UIScrollViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73EFF9A2DB11EB900FD434C /* NCFiles+UIScrollViewDelegate.swift */; };
- F740BEF02A35C2AD00E9B6D5 /* UILabel+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7EE66AC2A20B226009AE765 /* UILabel+Extension.swift */; };
F7411C552D7B26D700F57358 /* NCNetworking+ServerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7411C532D7B26C600F57358 /* NCNetworking+ServerError.swift */; };
F741C2242B6B9FD600E849BB /* NCMediaSelectTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F741C2232B6B9FD600E849BB /* NCMediaSelectTabBar.swift */; };
F74230F32C79B57200CA1ACA /* NCNetworking+Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74230F22C79B57200CA1ACA /* NCNetworking+Task.swift */; };
F7434B3820E2400600417916 /* NCBrand.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76B3CCD1EAE01BD00921AC9 /* NCBrand.swift */; };
F743C89E2E5B25A1000173A9 /* UIScene+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F743C89D2E5B2595000173A9 /* UIScene+Extension.swift */; };
F745B253222D88AE00346520 /* NCLoginQRCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F745B252222D88AE00346520 /* NCLoginQRCode.swift */; };
- F746EC51273906C40052598D /* NCViewCertificateDetails.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7BC287D26663F6C004D46C5 /* NCViewCertificateDetails.storyboard */; };
F746EC52273906C40052598D /* NCViewCertificateDetails.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7BC287D26663F6C004D46C5 /* NCViewCertificateDetails.storyboard */; };
F746EC53273906C50052598D /* NCViewCertificateDetails.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7BC287D26663F6C004D46C5 /* NCViewCertificateDetails.storyboard */; };
F747EB0D2C4AC1FF00F959A8 /* NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F747EB0C2C4AC1FF00F959A8 /* NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift */; };
@@ -444,13 +454,10 @@
F74B6D982A7E239A00F03C5F /* NCManageDatabase+Chunk.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74B6D942A7E239A00F03C5F /* NCManageDatabase+Chunk.swift */; };
F74B6D9B2A7E239A00F03C5F /* NCManageDatabase+Chunk.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74B6D942A7E239A00F03C5F /* NCManageDatabase+Chunk.swift */; };
F74B91E52F51D4170050813D /* InfoBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74B91E42F51D4100050813D /* InfoBannerView.swift */; };
- F74B91E62F51D4170050813D /* InfoBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74B91E42F51D4100050813D /* InfoBannerView.swift */; };
- F74B91E82F51D45A0050813D /* ErrorBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74B91E72F51D4510050813D /* ErrorBannerView.swift */; };
F74B91E92F51D45A0050813D /* ErrorBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74B91E72F51D4510050813D /* ErrorBannerView.swift */; };
F74BAE172C7E2F4E0028D4FA /* FileProviderDomain.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76673EC22C901F5007ED366 /* FileProviderDomain.swift */; };
F74C0436253F1CDC009762AB /* NCShares.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74C0434253F1CDC009762AB /* NCShares.swift */; };
F74C0437253F1CDC009762AB /* NCShares.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F74C0435253F1CDC009762AB /* NCShares.storyboard */; };
- F74D50362C9856D300BBBF4C /* NCCollectionViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C1EEA425053A9C00866ACC /* NCCollectionViewDataSource.swift */; };
F7501C322212E57500FB1415 /* NCMedia.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7501C302212E57400FB1415 /* NCMedia.storyboard */; };
F7501C332212E57500FB1415 /* NCMedia.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7501C312212E57400FB1415 /* NCMedia.swift */; };
F751247C2C42919C00E63DB8 /* NCPhotoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F751247A2C42919C00E63DB8 /* NCPhotoCell.swift */; };
@@ -497,7 +504,6 @@
F7517608307F52E60057C9B0 /* NCMediaViewerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75175CD307F52E60057C9B0 /* NCMediaViewerAppearance.swift */; };
F7517609307F52E60057C9B0 /* NCMediaAdaptiveLayoutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75175D4307F52E60057C9B0 /* NCMediaAdaptiveLayoutView.swift */; };
F751760A307F52E60057C9B0 /* NCImageViewerContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75175AE307F52E60057C9B0 /* NCImageViewerContentView.swift */; };
- F751760B307F53680057C9B0 /* NCMediaViewerTransitionSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75175CE307F52E60057C9B0 /* NCMediaViewerTransitionSource.swift */; };
F752BA052E58C05200616A26 /* Maintenance.swift in Sources */ = {isa = PBXBuildFile; fileRef = F752BA042E58C05200616A26 /* Maintenance.swift */; };
F753BA93281FD8020015BFB6 /* EasyTipView in Frameworks */ = {isa = PBXBuildFile; productRef = F753BA92281FD8020015BFB6 /* EasyTipView */; };
F755BD9B20594AC7008C5FBB /* NCService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F755BD9A20594AC7008C5FBB /* NCService.swift */; };
@@ -672,7 +678,6 @@
F76BED83306D13DC002E1442 /* NCLocalDatabase+PhotoLibraryAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76BED79306D0000002E1442 /* NCLocalDatabase+PhotoLibraryAsset.swift */; };
F76C26A62850D3A500E42BDF /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F7F67BB81A24D27800EE80DA /* Images.xcassets */; };
F76D364628A4F8BF00214537 /* NCActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76D364528A4F8BF00214537 /* NCActivityIndicator.swift */; };
- F76D364728A4F8BF00214537 /* NCActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76D364528A4F8BF00214537 /* NCActivityIndicator.swift */; };
F76D3CF12428B40E005DFA87 /* NCViewerPDFSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76D3CF02428B40E005DFA87 /* NCViewerPDFSearch.swift */; };
F76D3CF32428B94E005DFA87 /* NCViewerPDFSearchCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F76D3CF22428B94E005DFA87 /* NCViewerPDFSearchCell.xib */; };
F76D3CF52428D0C1005DFA87 /* NCViewerPDF.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F76D3CF42428D0C0005DFA87 /* NCViewerPDF.storyboard */; };
@@ -715,7 +720,6 @@
F77BC3ED293E528A005F2B08 /* NCConfigServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77BC3EC293E528A005F2B08 /* NCConfigServer.swift */; };
F77C3F5B2D9BF8CD00F3C471 /* UITabBar+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77C3F5A2D9BF8B500F3C471 /* UITabBar+Extension.swift */; };
F77C97392953131000FDDD09 /* NCCameraRoll.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77C97382953131000FDDD09 /* NCCameraRoll.swift */; };
- F77C973A2953143A00FDDD09 /* NCCameraRoll.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77C97382953131000FDDD09 /* NCCameraRoll.swift */; };
F77CB6A92AA08053000C3CA4 /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = F77CB6A82AA08053000C3CA4 /* OpenSSL */; };
F77DD6A82C5CC093009448FB /* NCSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77DD6A72C5CC093009448FB /* NCSession.swift */; };
F77DD6A92C5CC093009448FB /* NCSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77DD6A72C5CC093009448FB /* NCSession.swift */; };
@@ -738,7 +742,6 @@
F7814E972F3B5F170074DA3A /* NCSVGRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7814E952F3B5F170074DA3A /* NCSVGRenderer.swift */; };
F7816EF22C2C3E1F00A52517 /* NCPushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7816EF12C2C3E1F00A52517 /* NCPushNotification.swift */; };
F7817CF829801A3500FFBC65 /* Data+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7817CF729801A3500FFBC65 /* Data+Extension.swift */; };
- F7817CFB29801A3500FFBC65 /* Data+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7817CF729801A3500FFBC65 /* Data+Extension.swift */; };
F7817CFF29802D1A00FFBC65 /* NCPushNotificationEncryption.m in Sources */ = {isa = PBXBuildFile; fileRef = F72D1005210B6882009C96B7 /* NCPushNotificationEncryption.m */; };
F78295311F962EFA00A572F5 /* NCEndToEndEncryption.m in Sources */ = {isa = PBXBuildFile; fileRef = F70CAE391F8CF31A008125FD /* NCEndToEndEncryption.m */; };
F782FDC424E6933900666099 /* NCUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70BFC7320E0FA7C00C67599 /* NCUtility.swift */; };
@@ -832,18 +835,12 @@
F798F0E225880608000DAFFD /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70CEF5523E9C7E50007035B /* UIColor+Extension.swift */; };
F798F0EC2588060A000DAFFD /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70CEF5523E9C7E50007035B /* UIColor+Extension.swift */; };
F799DF822C4B7DCC003410B5 /* NCSectionFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F799DF812C4B7DCC003410B5 /* NCSectionFooter.swift */; };
- F799DF832C4B7DCC003410B5 /* NCSectionFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F799DF812C4B7DCC003410B5 /* NCSectionFooter.swift */; };
F799DF852C4B7E56003410B5 /* NCSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F799DF842C4B7E56003410B5 /* NCSectionHeader.swift */; };
- F799DF862C4B7E56003410B5 /* NCSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F799DF842C4B7E56003410B5 /* NCSectionHeader.swift */; };
F799DF882C4B83CC003410B5 /* NCCollectionViewCommon+EasyTipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F799DF872C4B83CC003410B5 /* NCCollectionViewCommon+EasyTipView.swift */; };
F79A65C32191D90F00FF6DCC /* NCSelect.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F79A65C22191D90F00FF6DCC /* NCSelect.storyboard */; };
F79A65C62191D95E00FF6DCC /* NCSelect.swift in Sources */ = {isa = PBXBuildFile; fileRef = F79A65C52191D95E00FF6DCC /* NCSelect.swift */; };
F79B646026CA661600838ACA /* UIControl+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F79B645F26CA661600838ACA /* UIControl+Extension.swift */; };
- F79B646126CA661600838ACA /* UIControl+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F79B645F26CA661600838ACA /* UIControl+Extension.swift */; };
F79B869B265E19D40085C0E0 /* NSMutableAttributedString+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F79B869A265E19D40085C0E0 /* NSMutableAttributedString+Extension.swift */; };
- F79EC78926316AC4004E59D6 /* NCPopupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F702F30725EE5D47008F8E80 /* NCPopupViewController.swift */; };
- F79ED0F12D2FCA5B00A389D9 /* NCSectionFirstHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78ACD51219046DC0088454D /* NCSectionFirstHeader.swift */; };
- F79ED0F22D2FCA6A00A389D9 /* NCRecommendationsCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F75D901E2D2BE12E003E740B /* NCRecommendationsCell.xib */; };
F79FFB262A97C24A0055EEA4 /* NCNetworkingE2EEMarkFolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F79FFB252A97C24A0055EEA4 /* NCNetworkingE2EEMarkFolder.swift */; };
F79FFB272A97C24A0055EEA4 /* NCNetworkingE2EEMarkFolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F79FFB252A97C24A0055EEA4 /* NCNetworkingE2EEMarkFolder.swift */; };
F7A03E2F2D425A14007AA677 /* NCFavoriteNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A03E2E2D425A14007AA677 /* NCFavoriteNavigationController.swift */; };
@@ -859,7 +856,6 @@
F7A509252C26BD5D00326106 /* NCCreate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A509242C26BD5D00326106 /* NCCreate.swift */; };
F7A509262C26D95D00326106 /* RealmSwift in Embed Frameworks */ = {isa = PBXBuildFile; productRef = F7160A812BE933390034DCB3 /* RealmSwift */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
F7A560422AE1593700BE8FD6 /* NCSaveLivePhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A560412AE1593700BE8FD6 /* NCSaveLivePhoto.swift */; };
- F7A573692E190387009C9257 /* NCShareExtensionData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A573682E190377009C9257 /* NCShareExtensionData.swift */; };
F7A76DC8256A71CD00119AB3 /* UIImage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B7504A2397D38E004E13EC /* UIImage+Extension.swift */; };
F7A7FDDD2C2DBD6200E9A93A /* NCDeepLinkHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A7FDDB2C2DBD6200E9A93A /* NCDeepLinkHandler.swift */; };
F7A846DE2BB01ACB0024816F /* NCTrashCellProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A846DD2BB01ACB0024816F /* NCTrashCellProtocol.swift */; };
@@ -884,7 +880,6 @@
F7AE00F8230E81CB007ACF8A /* NCBrowserWeb.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7AE00F7230E81CB007ACF8A /* NCBrowserWeb.swift */; };
F7AE00FA230E81EB007ACF8A /* NCBrowserWeb.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7AE00F9230E81EB007ACF8A /* NCBrowserWeb.storyboard */; };
F7B398422A6A91D5007538D6 /* NCSectionFirstHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = F7B398412A6A91D5007538D6 /* NCSectionFirstHeader.xib */; };
- F7B398432A6A91D5007538D6 /* NCSectionFirstHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = F7B398412A6A91D5007538D6 /* NCSectionFirstHeader.xib */; };
F7B6B70427C4E7FA00A7F6EB /* NCScan+CollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B6B70327C4E7FA00A7F6EB /* NCScan+CollectionView.swift */; };
F7B7504B2397D38F004E13EC /* UIImage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B7504A2397D38E004E13EC /* UIImage+Extension.swift */; };
F7B769A82B7A0B2000C1AAEB /* NCManageDatabase+Metadata+Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B769A72B7A0B2000C1AAEB /* NCManageDatabase+Metadata+Session.swift */; };
@@ -923,7 +918,6 @@
F7C30DFA291BCF790017149B /* NCNetworkingE2EECreateFolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C30DF9291BCF790017149B /* NCNetworkingE2EECreateFolder.swift */; };
F7C30DFB291BCF790017149B /* NCNetworkingE2EECreateFolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C30DF9291BCF790017149B /* NCNetworkingE2EECreateFolder.swift */; };
F7C30DFD291BD0B80017149B /* NCNetworkingE2EEDelete.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C30DFC291BD0B80017149B /* NCNetworkingE2EEDelete.swift */; };
- F7C30DFE291BD0B80017149B /* NCNetworkingE2EEDelete.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C30DFC291BD0B80017149B /* NCNetworkingE2EEDelete.swift */; };
F7C30E00291BD2610017149B /* NCNetworkingE2EERename.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C30DFF291BD2610017149B /* NCNetworkingE2EERename.swift */; };
F7C30E01291BD2610017149B /* NCNetworkingE2EERename.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C30DFF291BD2610017149B /* NCNetworkingE2EERename.swift */; };
F7C55C512FB4A658004A974F /* NCAssistantInputModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C55C502FB4A651004A974F /* NCAssistantInputModel.swift */; };
@@ -961,7 +955,6 @@
F7C9B9232B582F550064EA91 /* NCManageDatabase+SecurityGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C9B91C2B582F550064EA91 /* NCManageDatabase+SecurityGuard.swift */; };
F7CADEFD2EA159210057849E /* NCMetadataUploadTranfersSuccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CADEFA2EA1591D0057849E /* NCMetadataUploadTranfersSuccess.swift */; };
F7CAFE182F164B9500DB35A5 /* NCCollectionViewCommon+CellDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CAFE172F164B9200DB35A5 /* NCCollectionViewCommon+CellDelegate.swift */; };
- F7CAFE192F168F6000DB35A5 /* NCDebouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A3DB8F2DDE238C008F7EC8 /* NCDebouncer.swift */; };
F7CAFE1B2F16AA8D00DB35A5 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CAFE1A2F16AA8600DB35A5 /* main.swift */; };
F7CAFE1D2F17A35F00DB35A5 /* ProgressQuantizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CAFE1C2F17A34F00DB35A5 /* ProgressQuantizer.swift */; };
F7CAFE1E2F17A37C00DB35A5 /* ProgressQuantizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CAFE1C2F17A34F00DB35A5 /* ProgressQuantizer.swift */; };
@@ -973,9 +966,7 @@
F7CB77642F5843E500DE649A /* UIFont+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CB77632F5843D700DE649A /* UIFont+Extension.swift */; };
F7CB77652F58463E00DE649A /* UIFont+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CB77632F5843D700DE649A /* UIFont+Extension.swift */; };
F7CBC1232BAC8B0000EC1D55 /* NCSectionFirstHeaderEmptyData.xib in Resources */ = {isa = PBXBuildFile; fileRef = F7CBC1212BAC8B0000EC1D55 /* NCSectionFirstHeaderEmptyData.xib */; };
- F7CBC1242BAC8B0000EC1D55 /* NCSectionFirstHeaderEmptyData.xib in Resources */ = {isa = PBXBuildFile; fileRef = F7CBC1212BAC8B0000EC1D55 /* NCSectionFirstHeaderEmptyData.xib */; };
F7CBC1252BAC8B0000EC1D55 /* NCSectionFirstHeaderEmptyData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CBC1222BAC8B0000EC1D55 /* NCSectionFirstHeaderEmptyData.swift */; };
- F7CBC1262BAC8B0000EC1D55 /* NCSectionFirstHeaderEmptyData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CBC1222BAC8B0000EC1D55 /* NCSectionFirstHeaderEmptyData.swift */; };
F7CCAB512ECF316700F8E68B /* NCCollectionViewCommon+SyncMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CCAB502ECF315F00F8E68B /* NCCollectionViewCommon+SyncMetadata.swift */; };
F7CEE6002BA9A5C9003EFD89 /* NCTrashGridCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F7CEE5FE2BA9A5C9003EFD89 /* NCTrashGridCell.xib */; };
F7CEE6012BA9A5C9003EFD89 /* NCTrashGridCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CEE5FF2BA9A5C9003EFD89 /* NCTrashGridCell.swift */; };
@@ -986,7 +977,6 @@
F7CF06872E1127460063AD04 /* NCManageDatabase+CreateMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CF06822E11273F0063AD04 /* NCManageDatabase+CreateMetadata.swift */; };
F7CF06882E1127460063AD04 /* NCManageDatabase+CreateMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CF06822E11273F0063AD04 /* NCManageDatabase+CreateMetadata.swift */; };
F7D1612023CF19E30039EBBF /* NCViewerRichWorkspace.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7D1611F23CF19E30039EBBF /* NCViewerRichWorkspace.storyboard */; };
- F7D496FC2EBFA541004F9823 /* NCRecommendationsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75D90202D2BE26C003E740B /* NCRecommendationsCell.swift */; };
F7D496FD2EBFA6D9004F9823 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A0D1342591FBC5008F8A13 /* String+Extension.swift */; };
F7D497082EBFAFD6004F9823 /* NCImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76B649B2ADFFAED00014640 /* NCImageCache.swift */; };
F7D4BF012CA1831900A5E746 /* NCCollectionViewCommonPinchGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF002CA1831600A5E746 /* NCCollectionViewCommonPinchGesture.swift */; };
@@ -1016,7 +1006,6 @@
F7D7A7712DCDD437003D2007 /* NCManageDatabase+AutoUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D7A76B2DCDD437003D2007 /* NCManageDatabase+AutoUpload.swift */; };
F7D7A7722DCDD437003D2007 /* NCManageDatabase+AutoUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D7A76B2DCDD437003D2007 /* NCManageDatabase+AutoUpload.swift */; };
F7D890752BD25C570050B8A6 /* NCCollectionViewCommon+DragDrop.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D890742BD25C570050B8A6 /* NCCollectionViewCommon+DragDrop.swift */; };
- F7DA0F662F66AA0D0033020C /* ShowBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7DA0F652F66AA0A0033020C /* ShowBanner.swift */; };
F7DA0F672F66AA0D0033020C /* ShowBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7DA0F652F66AA0A0033020C /* ShowBanner.swift */; };
F7DF7B3F2F1A2EF900514020 /* WarningBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7DF7B3E2F1A2EE400514020 /* WarningBannerView.swift */; };
F7DF7B422F1A36C100514020 /* HelperBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7DF7B412F1A36B600514020 /* HelperBanner.swift */; };
@@ -1054,8 +1043,6 @@
F7E98C1727E0D0FC001F9F19 /* NCManageDatabase+Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E98C1527E0D0FC001F9F19 /* NCManageDatabase+Video.swift */; };
F7E98C1927E0D0FC001F9F19 /* NCManageDatabase+Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E98C1527E0D0FC001F9F19 /* NCManageDatabase+Video.swift */; };
F7ED547C25EEA65400956C55 /* QRCodeReader in Frameworks */ = {isa = PBXBuildFile; productRef = F7ED547B25EEA65400956C55 /* QRCodeReader */; };
- F7EDE4D6262D7B9600414FE6 /* NCListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78ACD4121903CE00088454D /* NCListCell.swift */; };
- F7EDE4DB262D7BA200414FE6 /* NCCellMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370D26AE248A3D7A00121797 /* NCCellMain.swift */; };
F7EDE509262DA9D600414FE6 /* NCSelectCommandViewSelect.xib in Resources */ = {isa = PBXBuildFile; fileRef = F7EDE508262DA9D600414FE6 /* NCSelectCommandViewSelect.xib */; };
F7EDE514262DC2CD00414FE6 /* NCSelectCommandViewSelect+CreateFolder.xib in Resources */ = {isa = PBXBuildFile; fileRef = F7EDE513262DC2CD00414FE6 /* NCSelectCommandViewSelect+CreateFolder.xib */; };
F7EDE51B262DD0C400414FE6 /* NCSelectCommandViewCopyMove.xib in Resources */ = {isa = PBXBuildFile; fileRef = F7EDE51A262DD0C400414FE6 /* NCSelectCommandViewCopyMove.xib */; };
@@ -1083,10 +1070,7 @@
F7FDFF6D2E437E55000D7688 /* NCAccountSettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7FDFF562E437E55000D7688 /* NCAccountSettingsModel.swift */; };
F7FDFF6E2E437E55000D7688 /* NCAccountSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7FDFF572E437E55000D7688 /* NCAccountSettingsView.swift */; };
F7FDFF6F2E437E55000D7688 /* NCAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7FDFF592E437E55000D7688 /* NCAccount.swift */; };
- F7FDFF702E437E55000D7688 /* NCAccountRequest.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7FDFF512E437E55000D7688 /* NCAccountRequest.storyboard */; };
- F7FDFF722E437E55000D7688 /* NCAccountRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7FDFF522E437E55000D7688 /* NCAccountRequest.swift */; };
F7FF2CB12842159500EBB7A1 /* NCSectionHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = F7FF2CB02842159500EBB7A1 /* NCSectionHeader.xib */; };
- F7FFFCA02FB300440015441E /* NCAssistantSharedTextStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7FFFC9D2FB300440015441E /* NCAssistantSharedTextStore.swift */; };
F7FFFCA22FB300600015441E /* NCAssistantSharedTextStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7FFFC9D2FB300440015441E /* NCAssistantSharedTextStore.swift */; };
/* End PBXBuildFile section */
@@ -1321,6 +1305,7 @@
A1E2EE020000000000000001 /* NCEndToEndKeySetAccess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCEndToEndKeySetAccess.swift; sourceTree = ""; };
A1E2EE020000000000000002 /* NCEndToEndKeySetResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCEndToEndKeySetResolver.swift; sourceTree = ""; };
A1E2EE020000000000000007 /* NCEndToEndKeySetResolverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCEndToEndKeySetResolverTests.swift; sourceTree = ""; };
+ A71107260000000000000100 /* NCShareFilenameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareFilenameView.swift; sourceTree = ""; };
AA3C85E72D36B08C00F74F12 /* UITestBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestBackend.swift; sourceTree = ""; };
AA3C85EA2D36BBF400F74F12 /* OCSResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OCSResponse.swift; sourceTree = ""; };
AA3C85ED2D36BCCB00F74F12 /* SharesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharesResponse.swift; sourceTree = ""; };
@@ -1414,6 +1399,21 @@
AAFC0D032F9AA10000F0A001 /* NCFocusedAutoUploadScreenDimmer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFocusedAutoUploadScreenDimmer.swift; sourceTree = ""; };
AAFC0D082F9AA10000F0A001 /* NCFocusedAutoUploadCloudAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFocusedAutoUploadCloudAnimation.swift; sourceTree = ""; };
AAFC0D0A2F9AA10000F0A001 /* NCAutoUploadCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAutoUploadCounter.swift; sourceTree = ""; };
+ AB1028260000000000020000 /* NCShareEntryController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareEntryController.swift; sourceTree = ""; };
+ AB1028260000000000020002 /* NCShareViewModel+Accounts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCShareViewModel+Accounts.swift"; sourceTree = ""; };
+ AB1028260000000000020003 /* NCShareViewModel+Folders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCShareViewModel+Folders.swift"; sourceTree = ""; };
+ AB1028260000000000020004 /* NCShareViewModel+Upload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCShareViewModel+Upload.swift"; sourceTree = ""; };
+ AB1028260000000000020006 /* NCShareLoaderItemProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareLoaderItemProvider.swift; sourceTree = ""; };
+ AB1028260000000000020008 /* NCShareSessionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareSessionStore.swift; sourceTree = ""; };
+ AB1028260000000000020009 /* NCShareView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareView.swift; sourceTree = ""; };
+ AB102826000000000002000A /* NCShareViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareViewModel.swift; sourceTree = ""; };
+ AB1028260000000000040000 /* NCShareLoaderItemProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareLoaderItemProviderTests.swift; sourceTree = ""; };
+ AB1028260000000000050001 /* NCShareUploadConflictView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareUploadConflictView.swift; sourceTree = ""; };
+ AB1028260000000000050300 /* NCShareUploadNamePlannerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareUploadNamePlannerTests.swift; sourceTree = ""; };
+ AB1028260000000000060000 /* NCShareAccountMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareAccountMenu.swift; sourceTree = ""; };
+ AB1028260000000000090000 /* NCShareRecentFoldersStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareRecentFoldersStore.swift; sourceTree = ""; };
+ AB1028260000000000090001 /* NCShareRecentFoldersStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareRecentFoldersStoreTests.swift; sourceTree = ""; };
+ AB1109260000000000010001 /* NCShareUploadButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareUploadButton.swift; sourceTree = ""; };
AB6000002F60000100FE2775 /* NCTagEditorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTagEditorModel.swift; sourceTree = ""; };
AB6000022F60000200FE2775 /* NCTagEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTagEditorView.swift; sourceTree = ""; };
AB7000012F70000100FE2776 /* NCShareDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCShareDetailsView.swift; sourceTree = ""; };
@@ -1423,9 +1423,6 @@
AC14ADAF2026101400000001 /* NCMediaAdaptiveControlsLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMediaAdaptiveControlsLayout.swift; sourceTree = ""; };
AC14ADAF2026101400000011 /* NCMediaViewerPreviewLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMediaViewerPreviewLoader.swift; sourceTree = ""; };
AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Extension.swift"; sourceTree = ""; };
- AF22B20B277C6F4D00DAB0CC /* NCShareCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareCell.swift; sourceTree = ""; };
- AF22B215277D196700DAB0CC /* NCShareExtension+DataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCShareExtension+DataSource.swift"; sourceTree = ""; };
- AF22B216277D196700DAB0CC /* NCShareExtension+Files.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCShareExtension+Files.swift"; sourceTree = ""; };
AF2D7C7B2742556F00ADF566 /* NCShareLinkCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareLinkCell.swift; sourceTree = ""; };
AF2D7C7D2742559100ADF566 /* NCShareUserCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareUserCell.swift; sourceTree = ""; };
AF3FDCC12796ECC300710F60 /* NCTrash+CollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCTrash+CollectionView.swift"; sourceTree = ""; };
@@ -1434,7 +1431,6 @@
AF4BF61D27562B3F0081CEEF /* NCManageDatabase+Activity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+Activity.swift"; sourceTree = ""; };
AF56C1DB2784856200D8BAE2 /* NCActivityCommentView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NCActivityCommentView.xib; sourceTree = ""; };
AF730AF727834B1400B7520E /* NCShare+NCCellDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCShare+NCCellDelegate.swift"; sourceTree = ""; };
- AF730AF927843E4C00B7520E /* NCShareExtension+NCAccountRequestDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCShareExtension+NCAccountRequestDelegate.swift"; sourceTree = ""; };
AF7E504D27A2D8FF00B5E4AF /* UIBarButton+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButton+Extension.swift"; sourceTree = ""; };
AF8ED1F92757821000B8DBC4 /* NextcloudUnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NextcloudUnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
AF93471127E2341B002537EE /* NCContextMenuShare.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCContextMenuShare.swift; sourceTree = ""; };
@@ -1548,8 +1544,6 @@
F713FEFE2472764000214AF6 /* UIImage+animatedGIF.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+animatedGIF.h"; sourceTree = ""; };
F713FEFF2472764100214AF6 /* UIImage+animatedGIF.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImage+animatedGIF.m"; sourceTree = ""; };
F714560F296433C80038D028 /* NCDocumentCamera.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCDocumentCamera.swift; sourceTree = ""; };
- F714803A262EBE3900693E51 /* MainInterface.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = MainInterface.storyboard; sourceTree = ""; };
- F7148040262EBE4000693E51 /* NCShareExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCShareExtension.swift; sourceTree = ""; };
F7148046262EBE4B00693E51 /* Share-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Share-Bridging-Header.h"; sourceTree = ""; };
F714A1462ED84AF00050A43B /* HudBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HudBannerView.swift; sourceTree = ""; };
F7151A811D477A4B00E6AF45 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; };
@@ -1901,7 +1895,6 @@
F7A48414297028FC00BD1B49 /* Nextcloud Hub.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Nextcloud Hub.png"; sourceTree = SOURCE_ROOT; };
F7A509242C26BD5D00326106 /* NCCreate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCCreate.swift; sourceTree = ""; };
F7A560412AE1593700BE8FD6 /* NCSaveLivePhoto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCSaveLivePhoto.swift; sourceTree = ""; };
- F7A573682E190377009C9257 /* NCShareExtensionData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareExtensionData.swift; sourceTree = ""; };
F7A7FDDB2C2DBD6200E9A93A /* NCDeepLinkHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCDeepLinkHandler.swift; sourceTree = ""; };
F7A846DD2BB01ACB0024816F /* NCTrashCellProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTrashCellProtocol.swift; sourceTree = ""; };
F7AA41B827C7CF4600494705 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/InfoPlist.strings; sourceTree = ""; };
@@ -2180,12 +2173,9 @@
F710FC80277B7D2700AA9FBF /* RealmSwift in Frameworks */,
F70557B92ED44E4700135623 /* LucidBanner in Frameworks */,
F72AD70F28C24BA1006CB92D /* NextcloudKit in Frameworks */,
- F33EE6E72BF4C02600CA1A51 /* NIOSSL in Frameworks */,
F77CB6A92AA08053000C3CA4 /* OpenSSL in Frameworks */,
F760DE092AE66ED00027D78A /* KeychainAccess in Frameworks */,
- F70821D829E59E6D001CA2D7 /* TagListView in Frameworks */,
F7F623B72A5EFA0C0022D3D4 /* Gzip in Frameworks */,
- F72D7EB7263B1207000B3DFC /* MarkdownKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -2345,6 +2335,9 @@
F34BDB3B2F574A58007A222C /* BidiSafeFilenameTests.swift */,
439616AAA50F4360A761E5EF /* NCAlbumsPersistenceTests.swift */,
C0DECA012F65000100C0D001 /* NCCameraRollTests.swift */,
+ AB1028260000000000090001 /* NCShareRecentFoldersStoreTests.swift */,
+ AB1028260000000000050300 /* NCShareUploadNamePlannerTests.swift */,
+ AB1028260000000000040000 /* NCShareLoaderItemProviderTests.swift */,
4B74B947E07444CC9CFE0A6C /* NCDocumentEditorSupportTests.swift */,
);
path = NextcloudUnitTests;
@@ -2369,6 +2362,29 @@
path = Extensions;
sourceTree = "";
};
+ AB1028260000000000070000 /* View */ = {
+ isa = PBXGroup;
+ children = (
+ AB1028260000000000020009 /* NCShareView.swift */,
+ AB1028260000000000060000 /* NCShareAccountMenu.swift */,
+ AB1028260000000000050001 /* NCShareUploadConflictView.swift */,
+ AB1109260000000000010001 /* NCShareUploadButton.swift */,
+ A71107260000000000000100 /* NCShareFilenameView.swift */,
+ );
+ path = View;
+ sourceTree = "";
+ };
+ AB1028260000000000070001 /* Model */ = {
+ isa = PBXGroup;
+ children = (
+ AB102826000000000002000A /* NCShareViewModel.swift */,
+ AB1028260000000000020002 /* NCShareViewModel+Accounts.swift */,
+ AB1028260000000000020003 /* NCShareViewModel+Folders.swift */,
+ AB1028260000000000020004 /* NCShareViewModel+Upload.swift */,
+ );
+ path = Model;
+ sourceTree = "";
+ };
AF93471327E235EB002537EE /* Advanced */ = {
isa = PBXGroup;
children = (
@@ -3604,13 +3620,12 @@
F7C0F46D1C8880540059EC54 /* Share */ = {
isa = PBXGroup;
children = (
- F714803A262EBE3900693E51 /* MainInterface.storyboard */,
- F7148040262EBE4000693E51 /* NCShareExtension.swift */,
- F7A573682E190377009C9257 /* NCShareExtensionData.swift */,
- AF730AF927843E4C00B7520E /* NCShareExtension+NCAccountRequestDelegate.swift */,
- AF22B215277D196700DAB0CC /* NCShareExtension+DataSource.swift */,
- AF22B216277D196700DAB0CC /* NCShareExtension+Files.swift */,
- AF22B20B277C6F4D00DAB0CC /* NCShareCell.swift */,
+ AB1028260000000000090000 /* NCShareRecentFoldersStore.swift */,
+ AB1028260000000000020000 /* NCShareEntryController.swift */,
+ AB1028260000000000020006 /* NCShareLoaderItemProvider.swift */,
+ AB1028260000000000020008 /* NCShareSessionStore.swift */,
+ AB1028260000000000070000 /* View */,
+ AB1028260000000000070001 /* Model */,
F7148046262EBE4B00693E51 /* Share-Bridging-Header.h */,
);
path = Share;
@@ -4058,14 +4073,11 @@
);
name = Share;
packageProductDependencies = (
- F72D7EB6263B1207000B3DFC /* MarkdownKit */,
F710FC7F277B7D2700AA9FBF /* RealmSwift */,
F72AD70E28C24BA1006CB92D /* NextcloudKit */,
- F70821D729E59E6D001CA2D7 /* TagListView */,
F7F623B62A5EFA0C0022D3D4 /* Gzip */,
F77CB6A82AA08053000C3CA4 /* OpenSSL */,
F760DE082AE66ED00027D78A /* KeychainAccess */,
- F33EE6E62BF4C02600CA1A51 /* NIOSSL */,
F70557B82ED44E4700135623 /* LucidBanner */,
);
productName = "Share Ext";
@@ -4467,21 +4479,11 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- F714803B262EBE3900693E51 /* MainInterface.storyboard in Resources */,
- F7148054262ED51000693E51 /* NCListCell.xib in Resources */,
F7E4022C2BA85D1D007E5609 /* PrivacyInfo.xcprivacy in Resources */,
- AF22B209277B4E4C00DAB0CC /* NCCreateFormUploadConflictCell.xib in Resources */,
- F7CBC1242BAC8B0000EC1D55 /* NCSectionFirstHeaderEmptyData.xib in Resources */,
- F7B398432A6A91D5007538D6 /* NCSectionFirstHeader.xib in Resources */,
- AF22B207277B4E4C00DAB0CC /* NCCreateFormUploadConflict.storyboard in Resources */,
AA517B822D660EFE00F8D37C /* Localizable.stringsdict in Resources */,
F7145A231D12E3B700CAFEEC /* Localizable.strings in Resources */,
- F746EC51273906C40052598D /* NCViewCertificateDetails.storyboard in Resources */,
F76C26A62850D3A500E42BDF /* Images.xcassets in Resources */,
- F714805E262ED52900693E51 /* NCSectionFooter.xib in Resources */,
F700222D1EC479840080073F /* Custom.xcassets in Resources */,
- F7FDFF702E437E55000D7688 /* NCAccountRequest.storyboard in Resources */,
- F79ED0F22D2FCA6A00A389D9 /* NCRecommendationsCell.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -4717,6 +4719,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ AB1028260000000000090120 /* NCShareRecentFoldersStore.swift in Sources */,
+ AB1028260000000000090121 /* NCShareRecentFoldersStoreTests.swift in Sources */,
BAF64949B487A080A98F83F2 /* NKErrorUserFriendlyMessageTests.swift in Sources */,
AC13ADAF2026101300000002 /* NCMediaAdaptiveLayoutViewTests.swift in Sources */,
C0DEDA7E30C1000100A00002 /* NCAutoUploadDateTests.swift in Sources */,
@@ -4732,6 +4736,10 @@
F34BDB3C2F574A58007A222C /* BidiSafeFilenameTests.swift in Sources */,
63563FE53573412998852DD3 /* NCAlbumsPersistenceTests.swift in Sources */,
C0DECA022F65000100C0D001 /* NCCameraRollTests.swift in Sources */,
+ AB1028260000000000050200 /* NCShareUploadNamePlannerTests.swift in Sources */,
+ AB1028260000000000050201 /* NCShareViewModel+Upload.swift in Sources */,
+ AB1028260000000000040001 /* NCShareLoaderItemProviderTests.swift in Sources */,
+ AB1028260000000000040002 /* NCShareLoaderItemProvider.swift in Sources */,
22BA14432CF44BDBA2E6C291 /* NCDocumentEditorSupportTests.swift in Sources */,
F372087D2BAB4C0F006B5430 /* TestConstants.swift in Sources */,
F78E2D6C29AF02DB0024D4F3 /* Database.swift in Sources */,
@@ -4812,6 +4820,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ AB1028260000000000090100 /* NCShareRecentFoldersStore.swift in Sources */,
2EF7D3FD0885CE182717578E /* NKError+UserFriendlyMessage.swift in Sources */,
EB511D22564B7B9A605E70AE /* NCErrorContext.swift in Sources */,
F7864ACF2A78FE73004870E0 /* NCManageDatabase+LocalFile.swift in Sources */,
@@ -4819,36 +4828,24 @@
F7245925289BB59100474787 /* ThreadSafeDictionary.swift in Sources */,
F72EC7292F4617F600A2135C /* NotificationCenter+Extension.swift in Sources */,
F7BF9D852934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift in Sources */,
- F79EC78926316AC4004E59D6 /* NCPopupViewController.swift in Sources */,
F7C30DFB291BCF790017149B /* NCNetworkingE2EECreateFolder.swift in Sources */,
F73EF7DA2B0226080087E6E9 /* NCManageDatabase+Tip.swift in Sources */,
F70557BF2ED44F1800135623 /* UploadBannerView.swift in Sources */,
- F7817CFB29801A3500FFBC65 /* Data+Extension.swift in Sources */,
- F74B91E82F51D45A0050813D /* ErrorBannerView.swift in Sources */,
AF4BF61F27562B3F0081CEEF /* NCManageDatabase+Activity.swift in Sources */,
F760A48B2FE95D06001B212E /* NCTransferDelegateDispatcher.swift in Sources */,
- F7CBC1262BAC8B0000EC1D55 /* NCSectionFirstHeaderEmptyData.swift in Sources */,
F7A0D1362591FBC5008F8A13 /* String+Extension.swift in Sources */,
F7236DDE2F7C00E600F9D722 /* HudIndeterminateBannerView.swift in Sources */,
- F7EDE4D6262D7B9600414FE6 /* NCListCell.swift in Sources */,
F764C3E12FFB7DFA00029FD5 /* NCManageDatabase+MediaMetadataBackfill.swift in Sources */,
F74B6D982A7E239A00F03C5F /* NCManageDatabase+Chunk.swift in Sources */,
F7CF06872E1127460063AD04 /* NCManageDatabase+CreateMetadata.swift in Sources */,
- F7FDFF722E437E55000D7688 /* NCAccountRequest.swift in Sources */,
F7CAFE202F17A37C00DB35A5 /* ProgressQuantizer.swift in Sources */,
- F343A4B62A1E084200DDA874 /* PHAsset+Extension.swift in Sources */,
F70BFC7520E0FA7D00C67599 /* NCUtility.swift in Sources */,
F7E250002FE1000000000003 /* NCDocumentEditorSupport.swift in Sources */,
F71DFA222F59FB9A003A93C5 /* View+Extension.swift in Sources */,
- AF22B20C277C6F4D00DAB0CC /* NCShareCell.swift in Sources */,
F7B769AB2B7A0B2000C1AAEB /* NCManageDatabase+Metadata+Session.swift in Sources */,
F7E98C1727E0D0FC001F9F19 /* NCManageDatabase+Video.swift in Sources */,
F76D84B63063C2170081F798 /* NCManageDatabase+Albums.swift in Sources */,
F76340F82EBDE9760056F538 /* NCManageDatabaseCore.swift in Sources */,
- F79ED0F12D2FCA5B00A389D9 /* NCSectionFirstHeader.swift in Sources */,
- F79B646126CA661600838ACA /* UIControl+Extension.swift in Sources */,
- F77C973A2953143A00FDDD09 /* NCCameraRoll.swift in Sources */,
- F740BEF02A35C2AD00E9B6D5 /* UILabel+Extension.swift in Sources */,
F7C30E01291BD2610017149B /* NCNetworkingE2EERename.swift in Sources */,
F75F4BC22FD008D7009E55ED /* Optional+Extension.swift in Sources */,
AF4BF61A27562A4B0081CEEF /* NCManageDatabase+Metadata.swift in Sources */,
@@ -4858,23 +4855,15 @@
F78E2D6829AF02DB0024D4F3 /* Database.swift in Sources */,
F7DF7B432F1A373E00514020 /* HelperBanner.swift in Sources */,
AA8D31562D41052300FE2775 /* NCManageDatabase+DownloadLimit.swift in Sources */,
- F711A4DF2AF92CAE00095DD8 /* NCUtility+Date.swift in Sources */,
F78295311F962EFA00A572F5 /* NCEndToEndEncryption.m in Sources */,
- F7C30DFE291BD0B80017149B /* NCNetworkingE2EEDelete.swift in Sources */,
F7C630802FFE4F8000257EEB /* UIWindowScene+Extension.swift in Sources */,
F7D61EA92EBF1694007F865B /* NCManageDatabase+TableCapabilities.swift in Sources */,
F71916122E2901FB00E13E96 /* NCNetworking+Upload.swift in Sources */,
- F73BC74F2F23811E003170C2 /* WarningBannerView.swift in Sources */,
- F7CAFE192F168F6000DB35A5 /* NCDebouncer.swift in Sources */,
F77E8C252E79717D00EAE68F /* NCManageDatabase+LivePhoto.swift in Sources */,
F79792482F5EECE100FE9544 /* Font+Extension.swift in Sources */,
- F751760B307F53680057C9B0 /* NCMediaViewerTransitionSource.swift in Sources */,
F7D61EBF2EBF1878007F865B /* NCManageDatabase+Tag.swift in Sources */,
F74AF3A5247FB6AE00AC767B /* NCUtilityFileSystem.swift in Sources */,
AF1A9B6527D0CC0500F17A9E /* UIAlertController+Extension.swift in Sources */,
- AF22B206277B4E4C00DAB0CC /* NCCreateFormUploadConflict.swift in Sources */,
- F74D50362C9856D300BBBF4C /* NCCollectionViewDataSource.swift in Sources */,
- F7A573692E190387009C9257 /* NCShareExtensionData.swift in Sources */,
F7BDC1D4300F440A00C5D9FA /* NCManageDatabase+MediaPreviewBackfill.swift in Sources */,
F7BD71E62636EAFC00643C34 /* NCNetworkingE2EE.swift in Sources */,
A1E2EE020000000000000003 /* NCEndToEndKeySetAccess.swift in Sources */,
@@ -4882,21 +4871,13 @@
F7F878AF1FB9E3B900599E4F /* NCEndToEndMetadata.swift in Sources */,
F7327E3B2B73B8D600A462C7 /* Array+Extension.swift in Sources */,
F7D7A76C2DCDD437003D2007 /* NCManageDatabase+AutoUpload.swift in Sources */,
- F7FFFCA02FB300440015441E /* NCAssistantSharedTextStore.swift in Sources */,
F39170AF2CB82024006127BC /* FileAutoRenamer+Extensions.swift in Sources */,
- F7D496FC2EBFA541004F9823 /* NCRecommendationsCell.swift in Sources */,
- F799DF832C4B7DCC003410B5 /* NCSectionFooter.swift in Sources */,
- AF22B218277D196700DAB0CC /* NCShareExtension+Files.swift in Sources */,
- F799DF862C4B7E56003410B5 /* NCSectionHeader.swift in Sources */,
F702F2D025EE5B5C008F8E80 /* NCGlobal.swift in Sources */,
F72437802C10B92400C7C68D /* NCSharePermissions.swift in Sources */,
- F7EDE4DB262D7BA200414FE6 /* NCCellMain.swift in Sources */,
F72944F62A8424F800246839 /* NCEndToEndMetadataV1.swift in Sources */,
F73EF7BA2B0224AB0087E6E9 /* NCManageDatabase+ExternalSites.swift in Sources */,
- F711A4EB2AF9327D00095DD8 /* UIImage+animatedGIF.m in Sources */,
F36E64FA2B96236C0085ABB5 /* UIView+Extension.swift in Sources */,
F75A9EE723796C6F0044CFCE /* NCNetworking.swift in Sources */,
- AF730AFA27843E4C00B7520E /* NCShareExtension+NCAccountRequestDelegate.swift in Sources */,
F7CB77652F58463E00DE649A /* UIFont+Extension.swift in Sources */,
F7327E232B73A42F00A462C7 /* NCNetworking+Download.swift in Sources */,
F7D497082EBFAFD6004F9823 /* NCImageCache.swift in Sources */,
@@ -4920,16 +4901,22 @@
F79FFB272A97C24A0055EEA4 /* NCNetworkingE2EEMarkFolder.swift in Sources */,
F7D68FCE28CB9051009139F3 /* NCManageDatabase+DashboardWidget.swift in Sources */,
F3F442F12DDE2A7700FD701F /* NCMetadataPermissions.swift in Sources */,
- AF22B217277D196700DAB0CC /* NCShareExtension+DataSource.swift in Sources */,
F73EF7E22B02266D0087E6E9 /* NCManageDatabase+Trash.swift in Sources */,
F77DD6AB2C5CC093009448FB /* NCSession.swift in Sources */,
- F74B91E62F51D4170050813D /* InfoBannerView.swift in Sources */,
- F76D364728A4F8BF00214537 /* NCActivityIndicator.swift in Sources */,
F749B654297B0F2400087535 /* NCManageDatabase+Avatar.swift in Sources */,
- AF22B208277B4E4C00DAB0CC /* NCCreateFormUploadConflictCell.swift in Sources */,
- F7DA0F662F66AA0D0033020C /* ShowBanner.swift in Sources */,
F73EF7C22B02250B0087E6E9 /* NCManageDatabase+GPS.swift in Sources */,
- F7148041262EBE4000693E51 /* NCShareExtension.swift in Sources */,
+ AB1028260000000000030000 /* NCShareEntryController.swift in Sources */,
+ AB1028260000000000030002 /* NCShareViewModel+Accounts.swift in Sources */,
+ AB1028260000000000030003 /* NCShareViewModel+Folders.swift in Sources */,
+ AB1028260000000000030004 /* NCShareViewModel+Upload.swift in Sources */,
+ AB1028260000000000030006 /* NCShareLoaderItemProvider.swift in Sources */,
+ AB1028260000000000030008 /* NCShareSessionStore.swift in Sources */,
+ AB1028260000000000030009 /* NCShareView.swift in Sources */,
+ AB1028260000000000050101 /* NCShareUploadConflictView.swift in Sources */,
+ AB1109260000000000010101 /* NCShareUploadButton.swift in Sources */,
+ A71107260000000000000101 /* NCShareFilenameView.swift in Sources */,
+ AB1028260000000000060001 /* NCShareAccountMenu.swift in Sources */,
+ AB102826000000000003000A /* NCShareViewModel.swift in Sources */,
F71FA7992F3508C600E86192 /* NCNetworking+WebDAV.swift in Sources */,
F76B3CCF1EAE01BD00921AC9 /* NCBrand.swift in Sources */,
F72944F32A84246400246839 /* NCEndToEndMetadataV2.swift in Sources */,
@@ -5060,6 +5047,18 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ AB1028260000000000090110 /* NCShareRecentFoldersStore.swift in Sources */,
+ AB1028260000000000080000 /* NCShareViewModel+Accounts.swift in Sources */,
+ AB1028260000000000080001 /* NCShareViewModel+Folders.swift in Sources */,
+ AB1028260000000000080002 /* NCShareViewModel+Upload.swift in Sources */,
+ AB1028260000000000080003 /* NCShareViewModel.swift in Sources */,
+ AB1028260000000000080004 /* NCShareLoaderItemProvider.swift in Sources */,
+ AB1028260000000000080005 /* NCShareSessionStore.swift in Sources */,
+ AB1028260000000000080006 /* NCShareAccountMenu.swift in Sources */,
+ AB1028260000000000080007 /* NCShareUploadConflictView.swift in Sources */,
+ AB1109260000000000010102 /* NCShareUploadButton.swift in Sources */,
+ A71107260000000000000102 /* NCShareFilenameView.swift in Sources */,
+ AB1028260000000000080008 /* NCShareView.swift in Sources */,
822D68B06B101563A759AD59 /* NKError+UserFriendlyMessage.swift in Sources */,
A358B569BF5FDABA6D9012D1 /* NCErrorContext.swift in Sources */,
F76BED77306D0000002E1442 /* NCLocalDatabase.swift in Sources */,
@@ -6439,6 +6438,7 @@
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SHARE_PREVIEWS";
SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/iOSClient/Nextcloud-Bridging-Header.h";
_EXPERIMENTAL_SWIFT_EXPLICIT_MODULES = NO;
};
@@ -6452,6 +6452,21 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Brand/iOSClient.entitlements";
DEAD_CODE_STRIPPING = YES;
+ EXCLUDED_SOURCE_FILE_NAMES = (
+ NCShareRecentFoldersStore.swift,
+ "$(inherited)",
+ "NCShareViewModel+Accounts.swift",
+ "NCShareViewModel+Folders.swift",
+ "NCShareViewModel+Upload.swift",
+ NCShareViewModel.swift,
+ NCShareLoaderItemProvider.swift,
+ NCShareSessionStore.swift,
+ NCShareAccountMenu.swift,
+ NCShareUploadConflictView.swift,
+ NCShareUploadButton.swift,
+ NCShareFilenameView.swift,
+ NCShareView.swift,
+ );
GCC_SYMBOLS_PRIVATE_EXTERN = YES;
INFOPLIST_FILE = "$(SRCROOT)/Brand/iOSClient.plist";
OTHER_LDFLAGS = "";
@@ -6679,7 +6694,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 35.2.2;
+ MARKETING_VERSION = 35.3.0;
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = "-v";
OTHER_LDFLAGS = "";
@@ -6745,7 +6760,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 35.2.2;
+ MARKETING_VERSION = 35.3.0;
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = "-v";
OTHER_LDFLAGS = "";
@@ -7119,8 +7134,8 @@
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/nextcloud/NextcloudKit";
requirement = {
- kind = exactVersion;
- version = 7.6.4;
+ branch = main;
+ kind = branch;
};
};
F788ECC5263AAAF900ADC67F /* XCRemoteSwiftPackageReference "MarkdownKit" */ = {
@@ -7224,11 +7239,6 @@
package = F33EE6DF2BF4BDA500CA1A51 /* XCRemoteSwiftPackageReference "swift-nio-ssl" */;
productName = NIOSSL;
};
- F33EE6E62BF4C02600CA1A51 /* NIOSSL */ = {
- isa = XCSwiftPackageProductDependency;
- package = F33EE6DF2BF4BDA500CA1A51 /* XCRemoteSwiftPackageReference "swift-nio-ssl" */;
- productName = NIOSSL;
- };
F33EE6EC2BF4C03800CA1A51 /* NIOSSL */ = {
isa = XCSwiftPackageProductDependency;
package = F33EE6DF2BF4BDA500CA1A51 /* XCRemoteSwiftPackageReference "swift-nio-ssl" */;
@@ -7334,11 +7344,6 @@
package = F70557B52ED44E2700135623 /* XCRemoteSwiftPackageReference "LucidBanner" */;
productName = LucidBanner;
};
- F70821D729E59E6D001CA2D7 /* TagListView */ = {
- isa = XCSwiftPackageProductDependency;
- package = F7A1050C29E587AF00FFD92B /* XCRemoteSwiftPackageReference "TagListView" */;
- productName = TagListView;
- };
F70B86742642CE3B00ED5349 /* FirebaseCrashlytics */ = {
isa = XCSwiftPackageProductDependency;
package = F70B86732642CE3B00ED5349 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
@@ -7419,11 +7424,6 @@
package = F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */;
productName = NextcloudKit;
};
- F72D7EB6263B1207000B3DFC /* MarkdownKit */ = {
- isa = XCSwiftPackageProductDependency;
- package = F788ECC5263AAAF900ADC67F /* XCRemoteSwiftPackageReference "MarkdownKit" */;
- productName = MarkdownKit;
- };
F72DA9B325F53E4E00B87DB1 /* SwiftRichString */ = {
isa = XCSwiftPackageProductDependency;
package = F72DA9B225F53E4E00B87DB1 /* XCRemoteSwiftPackageReference "SwiftRichString" */;
diff --git a/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 89e544897d..4e862cb430 100644
--- a/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -204,8 +204,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/nextcloud/NextcloudKit",
"state" : {
- "revision" : "49eee789ab676d6aba0020ccbe0d850f62e1d449",
- "version" : "7.6.4"
+ "branch" : "main",
+ "revision" : "62d52ad3ac203eebb80c4999ae304d4fc6da6f3b"
}
},
{
diff --git a/Nextcloud.xcodeproj/xcshareddata/xcschemes/Nextcloud.xcscheme b/Nextcloud.xcodeproj/xcshareddata/xcschemes/Nextcloud.xcscheme
index 2d901b5ea6..3b6b034618 100755
--- a/Nextcloud.xcodeproj/xcshareddata/xcschemes/Nextcloud.xcscheme
+++ b/Nextcloud.xcodeproj/xcshareddata/xcschemes/Nextcloud.xcscheme
@@ -1,7 +1,7 @@
+ version = "1.7">
@@ -65,13 +65,13 @@
+ RemotePath = "/var/containers/Bundle/Application/690609F7-C7AD-4102-9416-5C89CC5CD598/Nextcloud.app">
, ()
+// SPDX-FileCopyrightText: Nextcloud GmbH
+// SPDX-FileCopyrightText:
+// SPDX-License-Identifier: GPL-3.0-or-later
```
Please read the [Code of Conduct](https://nextcloud.com/code-of-conduct/). This document offers some guidance to ensure Nextcloud participants can cooperate effectively in a positive and inspiring atmosphere, and to explain how together we can strengthen and support each other.
@@ -38,7 +40,9 @@ branch. Maybe start working on [starter issues](https://github.com/nextcloud/ios
Easy starting points are also reviewing [pull requests](https://github.com/nextcloud/ios/pulls)
-### Xcode 26.1 Project Setup
+### Xcode 27.2 Project Setup
+
+Open `Nextcloud.xcodeproj`; package dependencies are managed by Swift Package Manager. The CI toolchain and simulator destination are defined in [.github/workflows/xcode.yml](.github/workflows/xcode.yml) (currently Xcode 27.1 and iPhone 17 with iOS 27.0).
#### Dependencies
@@ -83,9 +87,8 @@ Mock everything which is not needed.
### Integration Tests
-In `TestConstants.swift` you must specify your credentials for the server you want to test against.
-For locally running tests and in the GitHub actions we use [this shallow CI server image](https://github.com/nextcloud/docker-ci/pkgs/container/continuous-integration-shallow-server).
-Due to technical constraints the GitHub actions rely on a bare metal installation of Nextcloud on the runner in difference to local tests which can leverage much cleaner and reliable Docker containers.
+Configure the server URL and credentials in [Tests/TestConstants.swift](Tests/TestConstants.swift). The defaults are `http://localhost:8080` and `admin` / `admin`, matching CI.
+Local tests use the [shallow CI server image](https://github.com/nextcloud/docker-ci/pkgs/container/continuous-integration-shallow-server) through the Docker script below. GitHub Actions instead installs Nextcloud directly on the runner and serves it on port 8080 using PHP. Both currently use the `stable30` server branch.
Currently, you still need to launch the server manually once before running any tests locally.
This project comes with a convenience script to do so, see or run [Tests/Server.sh](Tests/Server.sh).
diff --git a/Share/MainInterface.storyboard b/Share/MainInterface.storyboard
deleted file mode 100644
index 221ff91c0c..0000000000
--- a/Share/MainInterface.storyboard
+++ /dev/null
@@ -1,267 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Share/Model/NCShareViewModel+Accounts.swift b/Share/Model/NCShareViewModel+Accounts.swift
new file mode 100644
index 0000000000..0096366a7f
--- /dev/null
+++ b/Share/Model/NCShareViewModel+Accounts.swift
@@ -0,0 +1,150 @@
+// SPDX-FileCopyrightText: Nextcloud GmbH
+// SPDX-FileCopyrightText: 2026 Marino Faggiana
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import NextcloudKit
+import UIKit
+
+extension NCShareViewModel {
+ func loadAccounts() {
+ guard isAuthenticated, !isClosing, !isLoading, !isPreparingUpload,
+ !isLoadingAccounts else { return }
+ accountsTask = Task { @MainActor [self] in
+ defer { accountsTask = nil }
+ let selected = sessionStore.getSession().account
+ let storedAccounts = await NCManageDatabase.shared.getAllAccountOrderAliasAsync()
+ guard !Task.isCancelled, !isClosing, isAuthenticated,
+ sessionStore.getSession().account == selected else { return }
+ guard !storedAccounts.isEmpty else {
+ clearAccountView()
+ showAlert(description: "_no_active_account_") { [weak self] in self?.cancel(with: .noAccount) }
+ return
+ }
+ accounts = storedAccounts.map { account in
+ let name = account.displayName.isEmpty ? account.user : account.displayName
+ return NCShareAccountItem(
+ id: account.account,
+ name: account.alias.isEmpty ? name : name + " - " + account.alias,
+ user: account.user,
+ serverUrl: account.urlBase,
+ isSelected: account.account == selected,
+ avatar: utility.loadUserImage(for: account.user, displayName: account.displayName, urlBase: account.urlBase))
+ }.sorted { $0.isSelected && !$1.isSelected }
+ }
+ }
+
+ func selectAccount(_ account: String) {
+ guard isAuthenticated, !isClosing, !isPreparingUpload, !isSwitchingAccount,
+ accounts.contains(where: { $0.id == account }) else { return }
+ guard account != sessionStore.getSession().account else { return }
+ changeAccount(account)
+ }
+
+ /// Uses a request-local account without changing the main app selection.
+ /// Shows cached folders first, then resolves capabilities and refreshes the root.
+ func changeAccount(_ account: String) {
+ guard !isClosing, !isPreparingUpload else { return }
+ accountChangeTask?.cancel()
+ // Cancellation alone cannot prevent a late callback from publishing old data.
+ // Check this generation again after awaits before updating the interface.
+ let changeID = UUID()
+ accountChangeID = changeID
+ isSwitchingAccount = true
+ clearAccountView(preservingContent: true)
+ accountChangeTask = Task { @MainActor [self] in
+ defer { if accountChangeID == changeID { isSwitchingAccount = false } }
+ let selectedSession = await sessionStore.setSessionAccount(account)
+ guard !Task.isCancelled, !isClosing, accountChangeID == changeID else { return }
+ guard let session = selectedSession,
+ let tblAccount = sessionStore.account, tblAccount.account == account else {
+ showAlert(description: "_no_active_account_") { [weak self] in self?.cancel(with: .noAccount) }
+ return
+ }
+ NextcloudKit.shared.setup(groupIdentifier: NCBrandOptions.shared.capabilitiesGroup, delegate: NCNetworking.shared)
+ NextcloudKit.shared.appendSession(account: tblAccount.account,
+ urlBase: tblAccount.urlBase,
+ user: tblAccount.user,
+ userId: tblAccount.userId,
+ password: NCPreferences().getPassword(account: tblAccount.account),
+ userAgent: userAgent,
+ httpMaximumConnectionsPerHost: NCBrandOptions.shared.httpMaximumConnectionsPerHost,
+ httpMaximumConnectionsPerHostInDownload: NCBrandOptions.shared.httpMaximumConnectionsPerHostInDownload,
+ httpMaximumConnectionsPerHostInUpload: NCBrandOptions.shared.httpMaximumConnectionsPerHostInUpload,
+ groupIdentifier: NCBrandOptions.shared.capabilitiesGroup)
+ autoUploadFileName = NCManageDatabase.shared.getAccountAutoUploadFileName(account: account)
+ autoUploadDirectory = NCManageDatabase.shared.getAccountAutoUploadDirectory(
+ account: account, urlBase: session.urlBase, userId: session.userId)
+ // Publish the local browser before capabilities can trigger a network request.
+ await initializeFolderBrowser(refreshFromServer: false)
+ guard !Task.isCancelled, !isClosing, accountChangeID == changeID else { return }
+ isSwitchingAccount = false
+ loadAccounts()
+ await loadServerCapabilities(account: account, changeID: changeID)
+ guard !Task.isCancelled, !isClosing, accountChangeID == changeID else { return }
+ if serverCapabilities != nil, let root = rootFolder {
+ requestFolderChildren(root, refresh: true)
+ }
+ }
+ }
+
+ func retryCapabilities() {
+ guard !isOffline, isAuthenticated, !isClosing, !isLoadingCapabilities, !isSwitchingAccount, !isPreparingUpload else { return }
+ let account = sessionStore.getSession().account
+ guard !account.isEmpty else { return }
+ let changeID = accountChangeID
+ capabilitiesTask = Task { @MainActor in
+ await loadServerCapabilities(account: account, changeID: changeID)
+ guard isCurrentCapabilitiesRequest(account: account, changeID: changeID), serverCapabilities != nil else { return }
+ if let rootFolder {
+ requestFolderChildren(rootFolder, refresh: true)
+ } else {
+ await initializeFolderBrowser()
+ }
+ }
+ }
+
+ /// Resolves one capabilities snapshot for this account: database first, network
+ /// only when absent. All Share actions use the resulting serverCapabilities.
+ private func loadServerCapabilities(account: String, changeID: UUID) async {
+ isLoadingCapabilities = true
+ serverCapabilities = nil
+ capabilitiesErrorMessage = ""
+ defer { if accountChangeID == changeID { isLoadingCapabilities = false } }
+ // Prefer the actual cached response, rather than default capabilities.
+ var capabilities = await NCManageDatabase.shared.getCapabilities(account: account)
+ guard isCurrentCapabilitiesRequest(account: account, changeID: changeID) else { return }
+ if capabilities == nil {
+ guard !isOffline else { return }
+ let result = await NextcloudKit.shared.getCapabilitiesAsync(account: account) { [weak self] task in
+ Task { @MainActor [weak self] in
+ guard let self, !self.isClosing, self.accountChangeID == changeID else {
+ task.cancel()
+ return
+ }
+ self.capabilitiesRequest = task
+ }
+ }
+ guard isCurrentCapabilitiesRequest(account: account, changeID: changeID) else { return }
+ guard result.error == .success, let loaded = result.capabilities else {
+ capabilitiesErrorMessage = NSLocalizedString("_share_capabilities_unavailable_", comment: "")
+ return
+ }
+ if let data = result.responseData?.data {
+ await NCManageDatabase.shared.setDataCapabilities(data: data, account: account)
+ }
+ guard isCurrentCapabilitiesRequest(account: account, changeID: changeID) else { return }
+ capabilities = loaded
+ }
+ guard let capabilities else { return }
+ // Keep legacy networking services and NextcloudKit in sync with this request.
+ await NKCapabilities.shared.setCapabilities(for: account, capabilities: capabilities)
+ guard isCurrentCapabilitiesRequest(account: account, changeID: changeID) else { return }
+ NCNetworking.shared.capabilities[account] = capabilities
+ serverCapabilities = capabilities
+ NCBrandColor.shared.settingThemingColor(account: account, capabilities: capabilities)
+ }
+
+ private func isCurrentCapabilitiesRequest(account: String, changeID: UUID) -> Bool {
+ !Task.isCancelled && !isClosing && accountChangeID == changeID && sessionStore.getSession().account == account
+ }
+}
diff --git a/Share/Model/NCShareViewModel+Folders.swift b/Share/Model/NCShareViewModel+Folders.swift
new file mode 100644
index 0000000000..bd826dd9a3
--- /dev/null
+++ b/Share/Model/NCShareViewModel+Folders.swift
@@ -0,0 +1,264 @@
+// SPDX-FileCopyrightText: Nextcloud GmbH
+// SPDX-FileCopyrightText: 2026 Marino Faggiana
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import UIKit
+import NextcloudKit
+
+extension NCShareViewModel {
+ /// Flattens expanded branches into rows; collapsed children stay cached.
+ var visibleFolderRows: [NCShareFolderRow] {
+ var rows: [NCShareFolderRow] = []
+ var visited: Set = []
+ func append(_ folder: NCShareFolderItem, depth: Int) {
+ guard visited.insert(folder.serverUrl).inserted else { return }
+ rows.append(NCShareFolderRow(folder: folder, depth: depth))
+ if expandedFolderURLs.contains(folder.serverUrl) {
+ for child in folderChildren[folder.serverUrl] ?? [] { append(child, depth: depth + 1) }
+ }
+ }
+ if let rootFolder { append(rootFolder, depth: 0) }
+ return rows
+ }
+
+ func resetFolderBrowser(preservingContent: Bool = false) {
+ folderBrowserID = UUID()
+ folderSelectionTask?.cancel()
+ for task in folderTasks.values { task.cancel() }
+ for request in folderRequests.values { request.cancel() }
+ folderTasks.removeAll()
+ folderRequests.removeAll()
+ // Keep the previous snapshot visible but disabled while the account changes.
+ if !preservingContent {
+ rootFolder = nil
+ recentFolders = []
+ selectedFolder = nil
+ isRecentFolderSelected = false
+ folderChildren = [:]
+ expandedFolderURLs = []
+ }
+ folderScrollTarget = nil
+ loadedFolderURLs = []
+ folderLoadErrors = [:]
+ isLoading = false
+ }
+
+ func initializeFolderBrowser(refreshFromServer: Bool = true) async {
+ let session = sessionStore.getSession()
+ let browserID = folderBrowserID
+ let home = utilityFileSystem.getHomeServer(session: session)
+ let root = NCShareFolderItem(
+ ocId: "", name: NSLocalizedString("_share_home_folder_", comment: ""), serverUrl: home,
+ path: "/", image: NCImageCache.shared.getFolder(account: session.account), isEncrypted: false)
+ let recent = recentFoldersStore.folders(account: session.account).filter {
+ $0.serverUrl == home || $0.serverUrl.hasPrefix(home + "/")
+ }.map { item in
+ NCShareFolderItem(
+ ocId: item.ocId, name: item.name, serverUrl: item.serverUrl,
+ path: folderPath(item.serverUrl, home: home),
+ image: item.isEncrypted ? NCImageCache.shared.getFolderEncrypted(account: session.account) : NCImageCache.shared.getFolder(account: session.account),
+ isEncrypted: item.isEncrypted)
+ }
+ let cached = await cachedFolderChildren(at: home, session: session)
+ guard isCurrentBrowser(browserID, account: session.account) else { return }
+ // Publish the replacement snapshot together, without an empty intermediate list.
+ rootFolder = root
+ selectedFolder = root
+ isRecentFolderSelected = false
+ expandedFolderURLs = [home]
+ recentFolders = recent
+ folderChildren = [home: cached]
+ // Make cached destinations usable without waiting for the server refresh.
+ if refreshFromServer { requestFolderChildren(root) }
+ }
+
+ func toggleFolder(_ folder: NCShareFolderItem) {
+ guard canUseFolderBrowser, canAccessFolder(folder) else { return }
+ if expandedFolderURLs.contains(folder.serverUrl) {
+ expandedFolderURLs.remove(folder.serverUrl)
+ } else {
+ expandedFolderURLs.insert(folder.serverUrl)
+ if !loadedFolderURLs.contains(folder.serverUrl) { requestFolderChildren(folder) }
+ }
+ }
+
+ func retryFolder(_ folder: NCShareFolderItem) {
+ guard canUseFolderBrowser, canAccessFolder(folder) else { return }
+ expandedFolderURLs.insert(folder.serverUrl)
+ requestFolderChildren(folder, refresh: true)
+ }
+
+ /// Keeps at most one listing request per folder, while allowing independent
+ /// branches to load concurrently. Browser reset cancels all registered requests.
+ func requestFolderChildren(_ folder: NCShareFolderItem, refresh: Bool = false) {
+ guard !isOffline, !isClosing, folderTasks[folder.serverUrl] == nil,
+ refresh || !loadedFolderURLs.contains(folder.serverUrl) else { return }
+ let browserID = folderBrowserID
+ // Mark synchronously, before scheduling, to prevent duplicate requests.
+ folderTasks[folder.serverUrl] = Task { @MainActor in
+ await loadFolderChildren(folder, browserID: browserID)
+ }
+ }
+
+ /// Validates the destination on the server, then selects and expands it.
+ /// fromRecents controls which of the two occurrences displays the checkmark.
+ func selectFolder(_ folder: NCShareFolderItem, fromRecents: Bool = false) {
+ guard canUseFolderBrowser, let capabilities = serverCapabilities, canAccessFolder(folder) else { return }
+ let session = sessionStore.getSession()
+ let browserID = folderBrowserID
+ isLoading = true
+ folderSelectionTask = Task { @MainActor in
+ defer {
+ if folderBrowserID == browserID { isLoading = false }
+ }
+ // Recent destinations may have been deleted or moved since last use.
+ let result = await NCNetworking.shared.readFileAsync(serverUrlFileName: folder.serverUrl, account: session.account)
+ guard isCurrentBrowser(browserID, account: session.account) else { return }
+ guard result.error == .success, let metadata = result.metadata, metadata.directory else {
+ if result.error.errorCode == NCGlobal.shared.errorResourceNotFound || result.error == .success {
+ recentFoldersStore.remove(serverUrl: folder.serverUrl, account: session.account)
+ recentFolders.removeAll { $0.serverUrl == folder.serverUrl }
+ if fromRecents { return }
+ }
+ showAlert(description: result.error == .success ? "_share_folder_unavailable_" : result.error.errorDescription)
+ return
+ }
+ let selected = folderSnapshot(metadata, url: folder.serverUrl, parent: nil, session: session)
+ guard canAccessFolder(selected) else { return }
+ guard isCurrentBrowser(browserID, account: session.account), isAuthenticated else { return }
+ let home = utilityFileSystem.getHomeServer(session: session)
+ if folder.serverUrl != home,
+ let error = FileNameValidator.checkFileName(metadata.fileNameView, account: session.account, capabilities: capabilities) {
+ showAlert(description: error.errorDescription)
+ return
+ }
+ selectedFolder = selected
+ isRecentFolderSelected = fromRecents
+ expandedFolderURLs.insert(selected.serverUrl)
+ requestFolderChildren(selected)
+ }
+ }
+
+ var canUseFolderBrowser: Bool {
+ isAuthenticated && !isClosing && isAccountReady && !isLoading && !isPreparingUpload && !isLoadingAccounts
+ }
+
+ private func canAccessFolder(_ folder: NCShareFolderItem) -> Bool {
+ let session = sessionStore.getSession()
+ let home = utilityFileSystem.getHomeServer(session: session)
+ guard folder.serverUrl == home || folder.serverUrl.hasPrefix(home + "/") else { return false }
+ if folder.isEncrypted && !NCPreferences().isEndToEndEnabled(account: session.account) {
+ showAlert(description: "_e2e_goto_settings_for_enable_")
+ return false
+ }
+ return true
+ }
+
+ func isCurrentBrowser(_ browserID: UUID, account: String) -> Bool {
+ !Task.isCancelled && !isClosing && folderBrowserID == browserID && sessionStore.getSession().account == account
+ }
+
+ private func loadFolderChildren(_ folder: NCShareFolderItem, browserID: UUID) async {
+ let session = sessionStore.getSession()
+ guard isCurrentBrowser(browserID, account: session.account) else { return }
+ folderLoadErrors[folder.serverUrl] = nil
+ defer {
+ if folderBrowserID == browserID {
+ folderRequests[folder.serverUrl] = nil
+ folderTasks[folder.serverUrl] = nil
+ }
+ }
+ let result = await NextcloudKit.shared.readFileOrFolderAsync(
+ serverUrlFileName: folder.serverUrl, depth: "1",
+ showHiddenFiles: NCPreferences().getShowHiddenFiles(account: session.account), account: session.account
+ ) { [weak self] task in
+ Task { @MainActor [weak self] in
+ guard let self, self.folderBrowserID == browserID, !self.isClosing else {
+ task.cancel()
+ return
+ }
+ self.folderRequests[folder.serverUrl] = task
+ }
+ }
+
+ guard isCurrentBrowser(browserID, account: session.account) else { return }
+ guard result.error == .success else {
+ folderLoadErrors[folder.serverUrl] = result.error.userFriendlyMessage()
+ return
+ }
+ guard let files = result.files else {
+ folderLoadErrors[folder.serverUrl] = NSLocalizedString("_share_folder_unavailable_", comment: "")
+ return
+ }
+ // The destination browser needs only directories. Do not convert file
+ // types or replace the app's complete metadata listing in the database.
+ let directories = files.filter(\.directory)
+ let listing = await NCManageDatabaseCreateMetadata().convertFilesToMetadatasAsync(
+ directories, serverUrlMetadataFolder: folder.serverUrl)
+ guard isCurrentBrowser(browserID, account: session.account) else { return }
+ guard !listing.metadataFolder.ocId.isEmpty else {
+ folderLoadErrors[folder.serverUrl] = NSLocalizedString("_share_folder_unavailable_", comment: "")
+ return
+ }
+ let layout = NCManageDatabase.shared.getLayoutForView(account: session.account, key: NCGlobal.shared.layoutViewShareExtension, serverUrl: folder.serverUrl)
+ let sorted = await NCManageDatabase.shared.sortedMetadata(layoutForView: layout, account: session.account, metadatas: listing.metadatas)
+ guard isCurrentBrowser(browserID, account: session.account) else { return }
+ folderChildren[folder.serverUrl] = folderSnapshots(sorted, parent: listing.metadataFolder, session: session)
+ loadedFolderURLs.insert(folder.serverUrl)
+ let metadata = listing.metadataFolder
+ let updated = folderSnapshot(metadata, url: folder.serverUrl, parent: nil, session: session)
+ if rootFolder?.serverUrl == folder.serverUrl { rootFolder = updated }
+ if selectedFolder?.serverUrl == folder.serverUrl {
+ selectedFolder = updated
+ }
+ }
+
+ private func cachedFolderChildren(at url: String, session: NCSession.Session) async -> [NCShareFolderItem] {
+ let layout = NCManageDatabase.shared.getLayoutForView(account: session.account, key: NCGlobal.shared.layoutViewShareExtension, serverUrl: url)
+ let hidden = NCPreferences().getShowHiddenFiles(account: session.account)
+ let predicate = hidden
+ ? NSPredicate(format: "account == %@ AND serverUrl == %@ AND directory == true AND fileName != %@", session.account, url, NextcloudKit.shared.nkCommonInstance.rootFileName)
+ : NSPredicate(format: "account == %@ AND serverUrl == %@ AND directory == true AND fileName != %@ AND NOT fileName BEGINSWITH[c] %@", session.account, url, NextcloudKit.shared.nkCommonInstance.rootFileName, ".")
+ let metadatas = await NCManageDatabase.shared.getMetadatasAsync(predicate: predicate, withLayout: layout, withAccount: session.account)
+ return folderSnapshots(metadatas, parent: nil, session: session)
+ }
+
+ private func folderSnapshots(_ metadatas: [tableMetadata], parent: tableMetadata?, session: NCSession.Session) -> [NCShareFolderItem] {
+ let ordered = metadatas.filter(\.favorite) + metadatas.filter { !$0.favorite }
+ return ordered.map { metadata in
+ let childUrl = utilityFileSystem.createServerUrl(serverUrl: metadata.serverUrl, fileName: metadata.fileName)
+ return folderSnapshot(metadata, url: childUrl, parent: parent, session: session)
+ }
+ }
+
+ private func folderSnapshot(_ metadata: tableMetadata, url: String, parent: tableMetadata?, session: NCSession.Session) -> NCShareFolderItem {
+ let home = utilityFileSystem.getHomeServer(session: session)
+ let image: UIImage
+ let shared = parent.map { metadata.permissions.contains(NCMetadataPermissions.permissionShared) && !$0.permissions.contains(NCMetadataPermissions.permissionShared) } ?? false
+ let mounted = parent.map { metadata.permissions.contains(NCMetadataPermissions.permissionMounted) && !$0.permissions.contains(NCMetadataPermissions.permissionMounted) } ?? false
+ if metadata.e2eEncrypted {
+ image = NCImageCache.shared.getFolderEncrypted(account: session.account)
+ } else if shared {
+ image = NCImageCache.shared.getFolderSharedWithMe(account: session.account)
+ } else if !metadata.shareType.isEmpty {
+ image = metadata.shareType.contains(3) ? NCImageCache.shared.getFolderPublic(account: session.account) : NCImageCache.shared.getFolderSharedWithMe(account: session.account)
+ } else if metadata.mountType == "group" {
+ image = NCImageCache.shared.getFolderGroup(account: session.account)
+ } else if mounted {
+ image = NCImageCache.shared.getFolderExternal(account: session.account)
+ } else if metadata.fileName == autoUploadFileName && metadata.serverUrl == autoUploadDirectory {
+ image = NCImageCache.shared.getFolderAutomaticUpload(account: session.account)
+ } else {
+ image = NCImageCache.shared.getFolder(account: session.account)
+ }
+ return NCShareFolderItem(
+ ocId: metadata.ocId, name: url == home ? NSLocalizedString("_share_home_folder_", comment: "") : metadata.fileNameView,
+ serverUrl: url, path: folderPath(url, home: home), image: image,
+ isEncrypted: metadata.e2eEncrypted, isFavorite: metadata.favorite)
+ }
+
+ private func folderPath(_ url: String, home: String) -> String {
+ let path = url.hasPrefix(home) ? String(url.dropFirst(home.count)) : url
+ return path.isEmpty ? "/" : (path.removingPercentEncoding ?? path)
+ }
+}
diff --git a/Share/Model/NCShareViewModel+Upload.swift b/Share/Model/NCShareViewModel+Upload.swift
new file mode 100644
index 0000000000..77d4528467
--- /dev/null
+++ b/Share/Model/NCShareViewModel+Upload.swift
@@ -0,0 +1,368 @@
+// SPDX-FileCopyrightText: Nextcloud GmbH
+// SPDX-FileCopyrightText: 2026 Marino Faggiana
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import Foundation
+
+// The unit-test target compiles only the pure name planner below.
+#if EXTENSION_SHARE || SHARE_PREVIEWS
+import UIKit
+import NextcloudKit
+import LucidBanner
+
+// MARK: - Upload
+extension NCShareViewModel {
+ var canUpload: Bool { canUseUploadDestination && !fileNames.isEmpty }
+
+ /// Upload flow: inspect the destination, resolve name conflicts if needed,
+ /// stage local copies, then transfer. Keep editing disabled throughout this flow.
+ func upload() {
+ guard canUpload, let capabilities = serverCapabilities else { return }
+ runUpload { [self] in
+ let plan = try await prepareUploadPlan(capabilities: capabilities)
+ try Task.checkCancellation()
+ guard !isClosing, isAuthenticated else { return }
+ if plan.requiresConfirmation {
+ pendingUploadConfirmation = (plan, nil)
+ isShowingUploadConfirmation = true
+ } else {
+ try await stageAndUpload(plan: plan, replacing: false, capabilities: capabilities)
+ }
+ }
+ }
+
+ // Store the choice and dismiss the sheet first. Starting the transfer here
+ // would overlap sheet dismissal with the upload banner presentation.
+ func resolveUploadConfirmation(_ choice: NCShareUploadChoice?) {
+ pendingUploadConfirmation?.choice = choice
+ isShowingUploadConfirmation = false
+ }
+
+ func uploadConfirmationDismissed() {
+ guard let (plan, choice) = pendingUploadConfirmation else { return }
+ pendingUploadConfirmation = nil
+ guard let choice, let capabilities = serverCapabilities, isAuthenticated, !isClosing else {
+ isPreparingUpload = false
+ if !isAuthenticated && !isClosing {
+ validateAccountAndRequestPasscode()
+ }
+ return
+ }
+ runUpload { [self] in
+ try await stageAndUpload(plan: plan, replacing: choice == .replace, capabilities: capabilities)
+ }
+ }
+
+ private func runUpload(_ operation: @escaping @MainActor () async throws -> Void) {
+ isPreparingUpload = true
+ uploadTask = Task { @MainActor in
+ defer { if pendingUploadConfirmation == nil { isPreparingUpload = false } }
+ do {
+ try await operation()
+ } catch is CancellationError {
+ // No upload after cancellation or a destination change.
+ } catch {
+ if !Task.isCancelled, !isClosing { showAlert(description: error.localizedDescription) }
+ }
+ }
+ }
+
+ /// Builds a plan from a fresh server listing without renaming the input files.
+ /// Multiple files receive unique copy names; a single file may also be replaced
+ /// when the existing resource is a file with a usable ETag.
+ private func prepareUploadPlan(capabilities: NKCapabilities.Capabilities) async throws -> NCShareUploadConfirmation {
+ let session = sessionStore.getSession()
+ guard let destination = selectedFolder?.serverUrl else { throw CancellationError() }
+ let sourceNames = fileNames
+ guard !sourceNames.isEmpty else { throw NCShareRequestError.noFiles }
+ let renamer = FileAutoRenamer(capabilities: capabilities)
+ let requestedNames = try sourceNames.map { name in
+ let candidate = renamer.rename(filename: name).trimmingCharacters(in: .whitespacesAndNewlines)
+ if let error = FileNameValidator.checkFileName(candidate, account: session.account, capabilities: capabilities) {
+ throw NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: name + ": " + error.errorDescription)
+ }
+ return candidate
+ }
+
+ // Read the server, not the browser cache. Hidden files also reserve names.
+ let result = await NextcloudKit.shared.readFileOrFolderAsync(
+ serverUrlFileName: destination, depth: "1", showHiddenFiles: true, account: session.account)
+ try Task.checkCancellation()
+ guard result.error == .success else { throw result.error }
+ guard let files = result.files else {
+ throw NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: NSLocalizedString("_share_conflict_read_failed_", comment: ""))
+ }
+ guard !isClosing, sessionStore.getSession().account == session.account, selectedFolder?.serverUrl == destination else { throw CancellationError() }
+ let converter = NCManageDatabaseCreateMetadata()
+ var listing = await converter.convertFilesToMetadatasAsync(files, serverUrlMetadataFolder: destination)
+ guard !listing.metadataFolder.ocId.isEmpty else {
+ throw NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: NSLocalizedString("_share_conflict_read_failed_", comment: ""))
+ }
+ // Encrypted server names cannot be compared with the user-visible names.
+ // Decode the mapping and verify write access before allocating upload names.
+ if listing.metadataFolder.e2eEncrypted {
+ let networking = NCNetworkingE2EE()
+ await NCNetworkingE2EE.beginNewServerKeyValidationCycle()
+ let keyError = await networking.validateCurrentServerKey(account: session.account)
+ guard keyError == .success else { throw keyError }
+ let encrypted = await networking.getMetadata(fileId: listing.metadataFolder.ocId, e2eToken: nil, account: session.account)
+ if encrypted.error == .success, let metadata = encrypted.e2eMetadata {
+ let decoded = await NCEndToEndMetadata().decodeMetadata(metadata, signature: encrypted.signature, serverUrl: destination, session: session)
+ guard decoded.error == .success else { throw decoded.error }
+ guard decoded.access.writeAccessError == .success else { throw decoded.access.writeAccessError }
+ listing = await converter.convertFilesToMetadatasAsync(files, serverUrlMetadataFolder: destination)
+ } else if encrypted.error == .success {
+ throw NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: NSLocalizedString("_share_conflict_read_failed_", comment: ""))
+ } else if encrypted.error.errorCode != NCGlobal.shared.errorResourceNotFound || !listing.metadatas.isEmpty {
+ throw encrypted.error
+ }
+ guard !listing.metadatas.contains(where: { $0.fileNameView == NSLocalizedString("_e2e_file_encrypted_", comment: "") }) else {
+ throw NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: NSLocalizedString("_share_conflict_read_failed_", comment: ""))
+ }
+ }
+ try Task.checkCancellation()
+ // Retain the current encrypted-name mapping for the shared E2EE service.
+ await NCManageDatabase.shared.createDirectory(metadata: listing.metadataFolder)
+ await NCManageDatabase.shared.updateMetadatasFilesAsync(listing.metadatas, serverUrl: destination, account: session.account)
+ let names = Set(listing.metadatas.map(\.fileNameView))
+ let allocated = NCShareUploadNamePlanner.fileNames(for: requestedNames, existingNames: names)
+ let items = zip(sourceNames, allocated).map { NCShareUploadItem(sourceName: $0.0, fileName: $0.1) }
+ let existing = requestedNames.count == 1 ? listing.metadatas.first(where: { $0.fileNameView == requestedNames[0] }) : nil
+ let canReplace = existing.map { !$0.directory && !$0.etag.isEmpty && !$0.etag.hasPrefix("W/") } ?? false
+ return NCShareUploadConfirmation(
+ account: session.account, serverUrl: destination, items: items,
+ replacementFileName: canReplace ? existing?.fileNameView : nil,
+ replacementETag: canReplace ? existing?.etag : nil,
+ requiresConfirmation: allocated != requestedNames)
+ }
+
+ /// Copies inputs into the storage expected by the upload services. If staging
+ /// fails, remove those copies; retain the originals so the user can retry.
+ private func stageAndUpload(plan: NCShareUploadConfirmation, replacing: Bool, capabilities: NKCapabilities.Capabilities) async throws {
+ try Task.checkCancellation()
+ guard !isOffline else { throw URLError(.notConnectedToInternet) }
+ let session = sessionStore.getSession()
+ guard !isClosing, isAuthenticated, plan.account == session.account, plan.serverUrl == selectedFolder?.serverUrl else { throw CancellationError() }
+ var uploadMetadata: [tableMetadata] = []
+ let replacementETag: String?
+ if replacing, let etag = plan.replacementETag {
+ replacementETag = etag.hasPrefix("\"") || etag.hasPrefix("W/") ? etag : "\"" + etag + "\""
+ } else {
+ replacementETag = nil
+ }
+ var stagedDirectories: [String] = []
+ var staged = false
+ defer {
+ if !staged {
+ for directory in stagedDirectories { try? FileManager.default.removeItem(atPath: directory) }
+ }
+ }
+ for item in plan.items {
+ try Task.checkCancellation()
+ let fileName = replacing ? (plan.replacementFileName ?? item.fileName) : item.fileName
+ if let error = FileNameValidator.checkFileName(fileName, account: session.account, capabilities: capabilities) {
+ throw NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: fileName + ": " + error.errorDescription)
+ }
+ let ocId = UUID().uuidString
+ let directory = utilityFileSystem.getDirectoryProviderStorageOcId(ocId, userId: session.userId, urlBase: session.urlBase)
+ stagedDirectories.append(directory)
+ let path = URL(fileURLWithPath: directory).appendingPathComponent(fileName).path
+ try FileManager.default.copyItem(at: inputFileURL(named: item.sourceName), to: URL(fileURLWithPath: path))
+ let metadata = await NCManageDatabaseCreateMetadata().createMetadataAsync(
+ fileName: fileName, ocId: ocId, serverUrl: plan.serverUrl, session: session, sceneIdentifier: nil)
+ metadata.nativeFormat = true
+ metadata.session = NCNetworking.shared.sessionUpload
+ metadata.sessionSelector = NCGlobal.shared.selectorUploadFileShareExtension
+ metadata.size = utilityFileSystem.getFileSize(filePath: path)
+ metadata.status = NCGlobal.shared.metadataStatusWaitUpload
+ metadata.sessionDate = Date()
+ uploadMetadata.append(metadata)
+ }
+ try Task.checkCancellation()
+ guard !isClosing, isAuthenticated, sessionStore.getSession().account == plan.account, selectedFolder?.serverUrl == plan.serverUrl else { throw CancellationError() }
+ staged = true
+ await uploadAndExit(uploadMetadata, replacementETag: replacementETag)
+ }
+
+ private func uploadAndExit(_ uploadMetadata: [tableMetadata], replacementETag: String?) async {
+ // A Share extension process can be reused by the system. Treat each
+ // explicit share operation as a new server-key validation cycle.
+ await NCNetworkingE2EE.beginNewServerKeyValidationCycle()
+
+ var error: NKError?
+ var errorContext: NCErrorContext = .generic
+ guard !Task.isCancelled, !isClosing, let window = presenter?.view.window else {
+ return
+ }
+ let payload = LucidBannerPayload(stage: .button,
+ vPosition: .center,
+ blocksTouches: true)
+ let (uploadBanner, token) = showUploadBanner(windowScene: window.windowScene,
+ payload: payload,
+ allowMinimizeOnTap: false,
+ onButtonTap: { self.cancel() })
+ banner = uploadBanner
+
+ for (index, metadata) in uploadMetadata.enumerated() {
+ guard !Task.isCancelled, !isClosing else { return }
+ // BANNER
+ let payloadUpdate = LucidBannerPayload.Update(
+ title: NSLocalizedString("_upload_file_", comment: "") + " \(index + 1) " + NSLocalizedString("_of_", comment: "") + " \(uploadMetadata.count)",
+ systemImage: "arrowshape.up.circle",
+ imageAnimation: .breathe,
+ progress: 0)
+ banner?.update(payload: payloadUpdate)
+
+ error = await self.upload(metadata: metadata, replacementETag: replacementETag, token: token)
+ guard !Task.isCancelled, !isClosing else { return }
+ if error != .success {
+ errorContext = .upload(fileName: NCShareFilenameView.displayName(metadata.fileNameView))
+ break
+ }
+ }
+
+ if error == .success {
+ if let uploaded = uploadMetadata.first, let folder = selectedFolder,
+ uploaded.serverUrl == folder.serverUrl {
+ recentFoldersStore.record(.init(
+ serverUrl: folder.serverUrl, name: folder.name, ocId: folder.ocId, isEncrypted: folder.isEncrypted),
+ account: uploaded.account)
+ }
+ banner?.update(payload: LucidBannerPayload.Update(stage: .success, horizontalLayout: .centered(width: 100)), for: token)
+ } else {
+ let payload = LucidBannerPayload.Update(
+ title: errorContext.failureTitle ?? NSLocalizedString("_error_", comment: ""),
+ subtitle: error?.userFriendlyMessage(for: errorContext) ?? "", stage: .error)
+ banner?.update(payload: payload, for: token)
+ }
+
+ guard !Task.isCancelled, !isClosing else { return }
+ do {
+ try await Task.sleep(for: .seconds(2))
+ } catch {
+ return
+ }
+ guard !isClosing else { return }
+ banner?.dismiss()
+ cancel()
+ }
+
+ private func upload(metadata: tableMetadata, replacementETag: String?, token: Int?) async -> NKError {
+ let session = sessionStore.getSession()
+
+ let results = await NKTypeIdentifiers.shared.getInternalType(fileName: metadata.fileNameView, mimeType: metadata.contentType, directory: false, account: session.account)
+ guard !Task.isCancelled, !isClosing else {
+ return NKError(errorCode: NSURLErrorCancelled, errorDescription: "Transfer was cancelled.")
+ }
+ metadata.contentType = results.mimeType
+ metadata.iconName = results.iconName
+ metadata.classFile = results.classFile
+ metadata.typeIdentifier = results.typeIdentifier
+ metadata.serverUrlFileName = utilityFileSystem.createServerUrl(serverUrl: metadata.serverUrl, fileName: metadata.fileName)
+
+ let chunkSize = NCGlobal.shared.chunkSize
+ metadata.chunk = metadata.size > chunkSize ? chunkSize : 0
+ // E2EE
+ metadata.e2eEncrypted = metadata.isDirectoryE2EE
+
+ if metadata.isDirectoryE2EE {
+ return await NCNetworkingE2EEUpload().upload(metadata: metadata,
+ session: session,
+ controller: presenter,
+ banner: banner,
+ stageBanner: nil,
+ tokenBanner: token)
+ } else if metadata.chunk > 0 {
+ banner?.update(payload: LucidBannerPayload.Update(systemImage: "gearshape.arrow.triangle.2.circlepath",
+ imageAnimation: .rotate),
+ for: token)
+ // Await directly so cancellation belongs to the main upload task.
+ // Final MOVE must not overwrite a file created after conflict checking,
+ // unless the user chose replacement and its ETag still matches.
+ let results = await NCNetworking.shared.uploadChunkFile(
+ metadata: metadata,
+ overwrite: replacementETag != nil,
+ ifMatch: replacementETag) { total, counter in
+ Task {@MainActor in
+ self.banner?.update(payload: LucidBannerPayload.Update(progress: Double(counter) / Double(total)), for: token)
+ }
+ } uploadStart: { _ in
+ Task {@MainActor in
+ let payload = LucidBannerPayload.Update(
+ systemImage: "arrowshape.up.circle",
+ imageAnimation: .breathe
+ )
+ self.banner?.update(payload: payload, for: token)
+ }
+ } uploadProgressHandler: { _, _, progress in
+ Task {@MainActor in
+ self.banner?.update(payload: LucidBannerPayload.Update(progress: progress), for: token)
+ }
+ } assembling: {
+ Task {@MainActor in
+ let payload = LucidBannerPayload.Update(
+ systemImage: "gearshape.arrow.triangle.2.circlepath",
+ imageAnimation: .rotate
+ )
+ self.banner?.update(payload: payload, for: token)
+ }
+ }
+ return results.error
+ } else {
+ let fileNameLocalPath = utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId,
+ fileName: metadata.fileName,
+ userId: metadata.userId,
+ urlBase: metadata.urlBase)
+
+ // The server enforces the final decision: a free name must still be
+ // free, and a replacement must still match the ETag we inspected.
+ let results = await NCNetworking.shared.uploadFile(account: metadata.account,
+ fileNameLocalPath: fileNameLocalPath,
+ serverUrlFileName: metadata.serverUrlFileName,
+ creationDate: metadata.creationDate as Date,
+ dateModificationFile: metadata.date as Date,
+ customHeaders: replacementETag.map { ["If-Match": $0] } ?? ["If-None-Match": "*"],
+ progressHandler: { _, _, fractionCompleted in
+ Task {@MainActor in
+ self.banner?.update(payload: LucidBannerPayload.Update(progress: fractionCompleted),
+ for: token)
+ }
+ })
+ return results.error
+ }
+
+ }
+}
+#endif
+
+/// Reserves the entire batch before allocating copies, so an earlier conflict
+/// cannot consume the unchanged name of a later attachment.
+struct NCShareUploadNamePlanner {
+ static func fileNames(for names: [String], existingNames: Set) -> [String] {
+ var occupied = existingNames
+ let reserved = Set(names)
+ return names.map { name in
+ if !occupied.contains(name) {
+ occupied.insert(name)
+ return name
+ }
+ let filename = name as NSString
+ let ext = filename.pathExtension
+ var stem = ext.isEmpty ? name : filename.deletingPathExtension
+ var number = 1
+ if let range = stem.range(of: #" \([0-9]+\)$"#, options: .regularExpression),
+ let value = Int(stem[range].dropFirst(2).dropLast()), value > 0, value < Int.max / 2 {
+ number = value + 1
+ stem = String(stem[.. = []
+ @Published var loadedFolderURLs: Set = []
+ @Published var folderLoadErrors: [String: String] = [:]
+ @Published var folderTasks: [String: Task] = [:]
+ var folderRequests: [String: URLSessionTask] = [:]
+ var folderSelectionTask: Task?
+ // Invalidates asynchronous folder results when the account/browser is reset.
+ var folderBrowserID = UUID()
+ lazy var recentFoldersStore = NCShareRecentFoldersStore(defaults: UserDefaults(suiteName: NCBrandOptions.shared.capabilitiesGroup) ?? .standard)
+ @Published var fileNames: [String] = []
+ @Published var fileThumbnails: [String: UIImage] = [:]
+ private var thumbnailRequests: [String: QLThumbnailGenerator.Request] = [:]
+ private var failedThumbnails: Set = []
+ @Published var isLoading = false
+ @Published private(set) var isOffline = false
+ private var connectionMonitor: NWPathMonitor?
+ @Published var isPreparingUpload = false
+ @Published var isShowingUploadConfirmation = false
+ // Keep the plan and choice until SwiftUI finishes dismissing the sheet.
+ var pendingUploadConfirmation: (plan: NCShareUploadConfirmation, choice: NCShareUploadChoice?)?
+ var uploadTask: Task?
+ private(set) var isClosing = false
+ @Published var isPreparingInput = false
+ @Published var inputErrorMessage = ""
+ @Published var alertMessage = ""
+ private var alertDismissAction: (() -> Void)?
+ @Published var accounts: [NCShareAccountItem] = []
+ @Published var isSwitchingAccount = false
+ @Published var serverCapabilities: NKCapabilities.Capabilities?
+ @Published var isLoadingCapabilities = false
+ @Published var capabilitiesErrorMessage = ""
+ var isAccountReady: Bool { serverCapabilities != nil && !isSwitchingAccount && !isOffline }
+
+ var canUseUploadDestination: Bool {
+ guard let selectedFolder else { return false }
+ return canUseFolderBrowser && !isLoadingCapabilities && folderTasks.isEmpty &&
+ folderLoadErrors[selectedFolder.serverUrl] == nil && alertMessage.isEmpty &&
+ !isPreparingInput && inputErrorMessage.isEmpty
+ }
+
+ var capabilitiesTask: Task?
+ var capabilitiesRequest: URLSessionTask?
+ @Published var accountsTask: Task?
+ var isLoadingAccounts: Bool { accountsTask != nil }
+ var accountChangeTask: Task?
+ var accountChangeID = UUID()
+ @Published private var access = Access.locked
+ var isAuthenticated: Bool { access == .unprotected || access == .unlocked }
+
+ weak var presenter: UIViewController?
+ private var requestContext: NSExtensionContext?
+ lazy var sessionStore = NCShareSessionStore()
+ let inputDirectory = FileManager.default.temporaryDirectory.appendingPathComponent("NextcloudShare-" + UUID().uuidString, isDirectory: true)
+ private var inputTask: Task?
+ private var foregroundObserver: NSObjectProtocol?
+ private var backgroundObserver: NSObjectProtocol?
+
+ var autoUploadFileName = ""
+ var autoUploadDirectory = ""
+ let utilityFileSystem = NCUtilityFileSystem()
+ let utility = NCUtility()
+ private var maintenanceMode = false
+ var banner: LucidBanner?
+
+ /// Starts account setup and attachment import independently, once per request.
+ /// Protected content remains hidden until passcode authentication succeeds.
+ func start(context: NSExtensionContext?, presenter: UIViewController) {
+ guard connectionMonitor == nil else { return }
+ let monitor = NWPathMonitor()
+ connectionMonitor = monitor
+ // A usable network path does not guarantee that the server is reachable.
+ // Request failures are still reported separately by the networking calls.
+ monitor.pathUpdateHandler = { [weak self] path in
+ let offline = path.status != .satisfied
+ Task { @MainActor [weak self] in
+ guard let self, !self.isClosing else { return }
+ let wasOffline = self.isOffline
+ self.isOffline = offline
+ if wasOffline && !offline { self.retryConnection() }
+ }
+ }
+ monitor.start(queue: DispatchQueue(label: "com.nextcloud.share.connection"))
+ self.requestContext = context
+ self.presenter = presenter
+ let versionNextcloudiOS = String(format: NCBrandOptions.shared.textCopyrightNextcloudiOS, utility.getVersionBuild())
+ NextcloudKit.configureLogger(
+ logLevel: NCBrandOptions.shared.disable_log ? .disabled : NCPreferences().log,
+ logDirectory: NCPreferences.sharedLogDirectory
+ )
+
+ nkLog(start: "Start Share session " + versionNextcloudiOS)
+
+ NCBrandColor.shared.createUserColors()
+
+ backgroundObserver = NotificationCenter.default.addObserver(
+ forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main
+ ) { [weak self] _ in
+ Task { @MainActor [weak self] in self?.accountsTask?.cancel() }
+ }
+ foregroundObserver = NotificationCenter.default.addObserver(
+ forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main
+ ) { [weak self] _ in
+ Task { @MainActor [weak self] in
+ guard let self, !self.isClosing else { return }
+ self.accountsTask?.cancel()
+ guard !self.maintenanceMode, self.validateAccount() else { return }
+ self.resolveUploadConfirmation(nil)
+ if self.access == .unlocked { self.access = .locked }
+ self.requestPasscodeIfNeeded()
+ }
+ }
+ // Verify version
+ let versionApp = utility.getVersionMaintenance()
+ if let groupDefaults = UserDefaults(suiteName: NCBrandOptions.shared.capabilitiesGroup) {
+ let lastVersion = groupDefaults.string(forKey: NCGlobal.shared.udLastVersion)
+ if lastVersion != versionApp {
+ maintenanceMode = true
+ return
+ }
+ }
+
+ if let account = sessionStore.account?.account {
+ access = NCPreferences().presentPasscode ? .locked : .unprotected
+ changeAccount(account)
+ }
+ guard let items = requestContext?.inputItems as? [NSExtensionItem] else {
+ cancel(with: .noFiles)
+ return
+ }
+ isPreparingInput = true
+ let directory = inputDirectory
+ inputTask = Task { [weak self] in
+ defer {
+ if Task.isCancelled { try? FileManager.default.removeItem(at: directory) }
+ }
+ do {
+ let output = try await NCShareLoaderItemProvider.load(items: items, directory: directory)
+ guard let self, !Task.isCancelled else { return }
+ self.fileNames = output.fileNames
+ self.isPreparingInput = false
+ if !output.failures.isEmpty || output.fileNames.isEmpty {
+ let key = output.fileNames.isEmpty ? "_share_import_no_files_" : "_share_import_failed_"
+ self.inputErrorMessage = ([NSLocalizedString(key, comment: "")] + output.failures).joined(separator: "\n\n")
+ }
+ } catch is CancellationError {
+ // The loader removes its own request directory on cancellation.
+ } catch {
+ guard let self, !Task.isCancelled else { return }
+ self.isPreparingInput = false
+ self.inputErrorMessage = NSLocalizedString("_share_import_no_files_", comment: "") + "\n\n" + error.localizedDescription
+ }
+ }
+ }
+
+ func validateAccountAndRequestPasscode() {
+ guard !isClosing else { return }
+ guard !maintenanceMode else {
+ return showAlert(description: "_version_mismatch_error_") { [weak self] in
+ self?.cancel(with: .versionMismatch)
+ }
+ }
+ guard validateAccount() else { return }
+ requestPasscodeIfNeeded()
+ }
+
+ func retryConnection() {
+ guard !isOffline, isAuthenticated, !isClosing, !isSwitchingAccount, !isPreparingUpload else { return }
+ if serverCapabilities == nil {
+ retryCapabilities()
+ } else {
+ for row in visibleFolderRows where expandedFolderURLs.contains(row.folder.serverUrl) {
+ requestFolderChildren(row.folder, refresh: true)
+ }
+ }
+ }
+
+ func loadThumbnail(named fileName: String) {
+ guard !isClosing, fileThumbnails[fileName] == nil, thumbnailRequests[fileName] == nil,
+ !failedThumbnails.contains(fileName), fileNames.contains(fileName) else { return }
+ let request = QLThumbnailGenerator.Request(
+ fileAt: inputFileURL(named: fileName), size: CGSize(width: 36, height: 36),
+ scale: presenter?.view.window?.screen.scale ?? 2, representationTypes: .all)
+ thumbnailRequests[fileName] = request
+ QLThumbnailGenerator.shared.generateBestRepresentation(for: request) { [weak self] representation, _ in
+ let image = representation?.uiImage
+ Task { @MainActor [weak self] in
+ guard let self, !self.isClosing, self.thumbnailRequests[fileName] === request else { return }
+ self.thumbnailRequests[fileName] = nil
+ guard self.fileNames.contains(fileName) else { return }
+ if let image {
+ // Keep extension memory bounded even when sharing many files.
+ if self.fileThumbnails.count >= 64, let oldest = self.fileThumbnails.keys.first {
+ self.fileThumbnails[oldest] = nil
+ }
+ self.fileThumbnails[fileName] = image
+ } else {
+ self.failedThumbnails.insert(fileName)
+ }
+ }
+ }
+ }
+
+ func fileIcon(named fileName: String) -> String {
+ guard let type = UTType(filenameExtension: (fileName as NSString).pathExtension) else { return "doc" }
+ if type.conforms(to: .image) { return "photo" }
+ if type.conforms(to: .movie) { return "film" }
+ if type.conforms(to: .audio) { return "music.note" }
+ if type.conforms(to: .pdf) { return "doc.richtext" }
+ if type.conforms(to: .archive) { return "doc.zipper" }
+ if type.conforms(to: .text) { return "doc.text" }
+ return "doc"
+ }
+
+ private func discardThumbnail(named fileName: String) {
+ if let request = thumbnailRequests.removeValue(forKey: fileName) {
+ QLThumbnailGenerator.shared.cancel(request)
+ }
+ fileThumbnails[fileName] = nil
+ failedThumbnails.remove(fileName)
+ }
+
+ func inputFileURL(named fileName: String) -> URL {
+ inputDirectory.appendingPathComponent(fileName)
+ }
+
+ func acknowledgeInputError() {
+ inputErrorMessage = ""
+ if fileNames.isEmpty { cancel(with: .noFiles) }
+ }
+
+ isolated deinit {
+ stopRequestWork()
+ if let foregroundObserver { NotificationCenter.default.removeObserver(foregroundObserver) }
+ if let backgroundObserver { NotificationCenter.default.removeObserver(backgroundObserver) }
+ }
+
+ private func stopRequestWork() {
+ connectionMonitor?.cancel()
+ for request in thumbnailRequests.values { QLThumbnailGenerator.shared.cancel(request) }
+ inputTask?.cancel()
+ uploadTask?.cancel()
+ accountsTask?.cancel()
+ accountChangeTask?.cancel()
+ capabilitiesTask?.cancel()
+ capabilitiesRequest?.cancel()
+ folderSelectionTask?.cancel()
+ for task in folderTasks.values { task.cancel() }
+ for request in folderRequests.values { request.cancel() }
+ if !isPreparingInput { try? FileManager.default.removeItem(at: inputDirectory) }
+ }
+
+ private func validateAccount() -> Bool {
+ guard access != .accountUnavailable else { return false }
+ guard !NCPasscodeModel.isPasscodeReset else {
+ return showNoAccountAlert()
+ }
+
+ if let account = sessionStore.account?.account {
+ // Re-read the selected account after foregrounding. It may have been
+ // removed by the host app while this share request was suspended.
+ guard NCManageDatabase.shared.getTableAccount(account: account) != nil else {
+ return showNoAccountAlert()
+ }
+ return true
+ }
+
+ guard let activeAccount = NCManageDatabase.shared.getActiveTableAccount() else {
+ return showNoAccountAlert()
+ }
+
+ changeAccount(activeAccount.account)
+ return true
+ }
+
+ private func showNoAccountAlert() -> Bool {
+ let wasPresentingPasscode = access == .presentingPasscode
+ access = .accountUnavailable
+ accountChangeID = UUID()
+ accountChangeTask?.cancel()
+ accountsTask?.cancel()
+ uploadTask?.cancel()
+ pendingUploadConfirmation = nil
+ isShowingUploadConfirmation = false
+ clearAccountView()
+ banner?.dismiss()
+ if wasPresentingPasscode {
+ cancel(with: .noAccount)
+ } else {
+ showAlert(description: "_no_active_account_") { [weak self] in self?.cancel(with: .noAccount) }
+ }
+ return false
+ }
+
+ func clearAccountView(preservingContent: Bool = false) {
+ capabilitiesTask?.cancel()
+ capabilitiesRequest?.cancel()
+ serverCapabilities = nil
+ isLoadingCapabilities = false
+ capabilitiesErrorMessage = ""
+ resetFolderBrowser(preservingContent: preservingContent)
+ if !preservingContent {
+ accounts = []
+ }
+ }
+
+ /// Completes or cancels the extension request once and stops its pending work.
+ /// An in-flight importer cleans up its directory after observing cancellation.
+ func cancel(with error: NCShareRequestError? = nil) {
+ guard !isClosing else { return }
+ isClosing = true
+ alertDismissAction = nil
+ stopRequestWork()
+ thumbnailRequests.removeAll()
+ fileThumbnails.removeAll()
+ isShowingUploadConfirmation = false
+ pendingUploadConfirmation = nil
+ if let error {
+ requestContext?.cancelRequest(withError: error)
+ } else {
+ requestContext?.completeRequest(returningItems: requestContext?.inputItems, completionHandler: nil)
+ }
+ }
+
+ func showAlert(description: String, onDismiss: (() -> Void)? = nil) {
+ alertMessage = NSLocalizedString(description.replacingOccurrences(of: "\t", with: "\n"), comment: "")
+ alertDismissAction = onDismiss
+ }
+
+ func acknowledgeAlert() {
+ let action = alertDismissAction
+ alertMessage = ""
+ alertDismissAction = nil
+ action?()
+ }
+
+ func createFolder() {
+ guard canUseUploadDestination, let folder = selectedFolder, let capabilities = serverCapabilities,
+ let presenter, presenter.viewIfLoaded?.window != nil,
+ presenter.presentedViewController == nil,
+ !presenter.isBeingDismissed else { return }
+ let session = sessionStore.getSession()
+ let browserID = folderBrowserID
+ let alert = UIAlertController.createFolderWith(
+ serverUrl: folder.serverUrl, session: session, capabilities: capabilities,
+ completion: { [weak self] error, name in
+ guard let self, self.isCurrentBrowser(browserID, account: session.account) else { return }
+ guard error == .success else {
+ if let message = error.userFriendlyMessage(for: .createFolder(folderName: name)) {
+ self.showAlert(description: message)
+ }
+ return
+ }
+ self.isLoading = true
+ self.folderSelectionTask = Task { @MainActor [weak self] in
+ guard let self else { return }
+ defer { if self.folderBrowserID == browserID { self.isLoading = false } }
+ // Recents can point into a collapsed, not-yet-loaded branch.
+ // Load and expand its ancestors before scrolling to the new row.
+ guard var branch = self.rootFolder else { return }
+ while true {
+ guard self.isCurrentBrowser(browserID, account: session.account) else { return }
+ self.expandedFolderURLs.insert(branch.serverUrl)
+ if let task = self.folderTasks[branch.serverUrl] { await task.value }
+ guard self.isCurrentBrowser(browserID, account: session.account) else { return }
+ self.requestFolderChildren(branch, refresh: branch.serverUrl == folder.serverUrl)
+ if let task = self.folderTasks[branch.serverUrl] { await task.value }
+ guard self.isCurrentBrowser(browserID, account: session.account),
+ self.folderLoadErrors[branch.serverUrl] == nil else { return }
+ if branch.serverUrl == folder.serverUrl { break }
+ guard let child = self.folderChildren[branch.serverUrl]?.first(where: {
+ folder.serverUrl == $0.serverUrl || folder.serverUrl.hasPrefix($0.serverUrl + "/")
+ }) else { return }
+ branch = child
+ }
+ guard let created = self.folderChildren[folder.serverUrl]?.first(where: { $0.name == name }),
+ self.isAuthenticated else { return }
+ self.selectedFolder = created
+ self.isRecentFolderSelected = false
+ self.expandedFolderURLs.insert(created.serverUrl)
+ self.requestFolderChildren(created)
+ self.folderScrollTarget = created.serverUrl
+ }
+ })
+ presenter.present(alert, animated: true)
+ }
+
+ func showRenameFileDialog(named fileName: String) {
+ guard !isClosing, isAuthenticated, !isPreparingUpload, fileNames.contains(fileName),
+ let capabilities = serverCapabilities else { return }
+ let account = sessionStore.getSession().account
+ let alert = UIAlertController.renameFile(fileName: fileName, capabilities: capabilities, account: account) { [weak self] newFileName in
+ self?.renameFile(oldName: fileName, newName: newFileName)
+ }
+
+ presenter?.present(alert, animated: true)
+ }
+
+ private func renameFile(oldName: String, newName: String) {
+ guard !isClosing, isAuthenticated, !isPreparingUpload else { return }
+ guard newName != oldName else { return }
+ guard !newName.isEmpty, let fileIx = self.fileNames.firstIndex(of: oldName),
+ !self.fileNames.contains(newName),
+ newName == (newName as NSString).lastPathComponent,
+ newName != ".", newName != "..",
+ utilityFileSystem.moveFile(atPath: inputFileURL(named: oldName).path, toPath: inputFileURL(named: newName).path) else {
+ return showAlert(description: "'\(oldName)' -> '\(newName)'")
+ }
+
+ discardThumbnail(named: oldName)
+ fileNames[fileIx] = newName
+
+ }
+
+ func removeFile(named fileName: String) {
+ guard !isClosing, isAuthenticated, !isPreparingUpload else { return }
+ guard let index = self.fileNames.firstIndex(of: fileName) else {
+ return showAlert(description: fileName)
+ }
+ discardThumbnail(named: fileName)
+ try? FileManager.default.removeItem(at: inputFileURL(named: fileName))
+ self.fileNames.remove(at: index)
+ if self.fileNames.isEmpty {
+ cancel(with: .noFiles)
+ }
+ }
+
+ /// Require authentication once per foreground session without reopening after dismissal.
+ private func requestPasscodeIfNeeded() {
+ guard access != .presentingPasscode, access != .unlocked else { return }
+ if !NCPreferences().presentPasscode {
+ access = .unprotected
+ return
+ }
+ access = .locked
+ // The sheet's onDismiss resumes the passcode flow after its transition.
+ guard pendingUploadConfirmation == nil, let presenter else { return }
+ access = .presentingPasscode
+ NCPasscodeView.present(from: presenter, allowCancel: true) { [weak self] result in
+ guard let self, !self.isClosing else { return }
+ self.access = .locked
+ guard result == .authenticated else { return self.cancel(with: .noAccount) }
+ guard self.validateAccount() else { return }
+ self.access = .unlocked
+ if self.accounts.isEmpty { self.loadAccounts() }
+ }
+ }
+}
+
+/// A UI snapshot, so SwiftUI does not retain live Realm objects.
+struct NCShareFolderItem: Identifiable {
+ var id: String { serverUrl }
+ let ocId: String
+ let name: String
+ let serverUrl: String
+ let path: String
+ let image: UIImage?
+ let isEncrypted: Bool
+ var isFavorite = false
+}
+
+struct NCShareFolderRow: Identifiable {
+ var id: String { folder.id }
+ let folder: NCShareFolderItem
+ let depth: Int
+}
+
+enum NCShareRequestError: Error {
+ case cancel, fileUpload, noAccount, noFiles, versionMismatch
+}
+
+struct NCShareUploadItem: Sendable {
+ let sourceName: String
+ let fileName: String
+}
+
+struct NCShareUploadConfirmation: Sendable {
+ let account: String
+ let serverUrl: String
+ let items: [NCShareUploadItem]
+ let replacementFileName: String?
+ let replacementETag: String?
+ let requiresConfirmation: Bool
+
+ var isBatch: Bool { items.count > 1 }
+ var renamedItems: [NCShareUploadItem] { items.filter { $0.sourceName != $0.fileName } }
+}
+
+enum NCShareUploadChoice: Equatable, Sendable {
+ case replace, keepBoth
+}
+
+/// Account picker data contains no live Realm objects or mutable account flags.
+struct NCShareAccountItem: Identifiable {
+ let id: String
+ let name: String
+ let user: String
+ let serverUrl: String
+ let isSelected: Bool
+ var avatar: UIImage?
+}
+
+#if DEBUG && SHARE_PREVIEWS
+extension NCShareViewModel {
+ /// Static preview state. The entry controller never starts a share request.
+ /// Session storage stays lazy so constructing this model does not read accounts.
+ static func preview() -> NCShareViewModel {
+ let model = NCShareViewModel()
+ model.serverCapabilities = NKCapabilities.Capabilities()
+ model.access = .unprotected
+ let home = "https://cloud.example.com/remote.php/dav/files/marino"
+ let root = NCShareFolderItem(ocId: "root", name: "My files", serverUrl: home, path: "/", image: UIImage(systemName: "folder.fill"), isEncrypted: false)
+ let documents = NCShareFolderItem(ocId: "documents", name: "Documents", serverUrl: home + "/Documents", path: "/Documents", image: UIImage(systemName: "folder.fill"), isEncrypted: false, isFavorite: true)
+ let photos = NCShareFolderItem(ocId: "photos", name: "Photos", serverUrl: home + "/Photos", path: "/Photos", image: UIImage(systemName: "folder.fill"), isEncrypted: false)
+ let work = NCShareFolderItem(ocId: "work", name: "Work", serverUrl: home + "/Documents/Work", path: "/Documents/Work", image: UIImage(systemName: "folder.fill"), isEncrypted: false)
+ model.selectedFolder = documents
+ model.rootFolder = root
+ model.folderChildren = [home: [documents, photos], documents.serverUrl: [work]]
+ model.expandedFolderURLs = [home, documents.serverUrl]
+ model.loadedFolderURLs = [home, documents.serverUrl]
+ model.recentFolders = [documents, photos]
+ model.fileNames = ["Report.pdf", "Photo.jpg", "Notes.txt"]
+ model.accounts = [
+ NCShareAccountItem(id: "personal", name: "Personal", user: "marino", serverUrl: "https://cloud.example.com", isSelected: true),
+ NCShareAccountItem(id: "work", name: "Work", user: "marino.work", serverUrl: "https://team.example.com", isSelected: false)
+ ]
+ return model
+ }
+}
+#endif
diff --git a/Share/NCShareCell.swift b/Share/NCShareCell.swift
deleted file mode 100644
index 898f980850..0000000000
--- a/Share/NCShareCell.swift
+++ /dev/null
@@ -1,75 +0,0 @@
-// SPDX-FileCopyrightText: Nextcloud GmbH
-// SPDX-FileCopyrightText: 2021 Marino Faggiana
-// SPDX-FileCopyrightText: 2021 Henrik Storch
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-import UIKit
-import NextcloudKit
-
-protocol NCShareCellDelegate: AnyObject {
- func removeFile(named fileName: String)
- func showRenameFileDialog(named fileName: String, account: String)
- func renameFile(oldName: String, newName: String, account: String)
-}
-
-class NCShareCell: UITableViewCell {
- @IBOutlet weak var imageCell: UIImageView!
- @IBOutlet weak var fileNameCell: UILabel!
- @IBOutlet weak var fileNameExtensionCell: UILabel!
- @IBOutlet weak var moreButton: UIButton!
- @IBOutlet weak var sizeCell: UILabel!
- weak var delegate: (NCShareCellDelegate & UIViewController)?
- var fileName: String = ""
- var iconName: String = ""
- var account: String = ""
- let utilityFileSystem = NCUtilityFileSystem()
- let utility = NCUtility()
-
- func setup(fileName: String, iconName: String, account: String) {
- self.fileName = fileName
- self.account = account
- self.iconName = iconName
-
- backgroundColor = .systemBackground
- imageCell?.layer.cornerRadius = 6
- imageCell?.layer.masksToBounds = true
-
- if let image = UIImage.downsample(
- imageAt: URL(fileURLWithPath: NSTemporaryDirectory() + fileName),
- to: CGSize(width: 80, height: 80),
- scale: traitCollection.displayScale
- ) {
- imageCell.image = image
- imageCell.contentMode = .scaleAspectFill
- } else {
- imageCell.image = utility.loadImage(named: iconName, useTypeIconFile: true, account: account)
- imageCell.contentMode = .scaleAspectFit
- }
-
- setBidiSafeFilename(fileName, isDirectory: false, titleLabel: fileNameCell, extensionLabel: fileNameExtensionCell)
-
- let fileSize = utilityFileSystem.getFileSize(filePath: (NSTemporaryDirectory() + fileName))
- sizeCell?.text = utilityFileSystem.transformedSize(fileSize)
-
- moreButton?.setImage(NCImageCache.shared.getImageButtonMore(), for: .normal)
- }
-
- @IBAction func buttonTapped(_ sender: Any) {
- let nsName = fileName as NSString
- let ext = nsName.pathExtension
- let base = nsName.deletingPathExtension
- let displayName = ext.isEmpty || base.isEmpty ? fileName : base + "." + ext
- let alertController = UIAlertController(title: "", message: displayName, preferredStyle: .alert)
-
- alertController.addAction(UIAlertAction(title: NSLocalizedString("_rename_file_", comment: ""), style: .default) { _ in
- self.delegate?.showRenameFileDialog(named: self.fileName, account: self.account)
- })
-
- alertController.addAction(UIAlertAction(title: NSLocalizedString("_remove_file_", comment: ""), style: .default) { _ in
- self.delegate?.removeFile(named: self.fileName)
- })
-
- alertController.addAction(UIAlertAction(title: NSLocalizedString("_cancel_", comment: ""), style: .cancel) { _ in })
- delegate?.present(alertController, animated: true, completion: nil)
- }
-}
diff --git a/Share/NCShareEntryController.swift b/Share/NCShareEntryController.swift
new file mode 100644
index 0000000000..68072b6ec8
--- /dev/null
+++ b/Share/NCShareEntryController.swift
@@ -0,0 +1,43 @@
+// SPDX-FileCopyrightText: Nextcloud GmbH
+// SPDX-FileCopyrightText: 2026 Marino Faggiana
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import SwiftUI
+import LucidBanner
+
+/// Loads the SwiftUI interface directly as the extension's principal controller.
+@MainActor
+final class NCShareEntryController: UIHostingController {
+ private let model: NCShareViewModel
+ private var hasAppeared = false
+
+ // The extension system instantiates the principal class through Objective-C.
+ @objc init() {
+ let model = NCShareViewModel()
+ self.model = model
+ super.init(rootView: NCShareView(model: model))
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) {
+ fatalError("This extension is initialized through NSExtensionPrincipalClass.")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ // Prepare local state before the first appearance to avoid an empty initial screen.
+ model.start(context: extensionContext, presenter: self)
+ }
+
+ override func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+ // Scene-bound banners and passcode presentation require an attached window.
+ // Returning from a presented sheet must not restart the share request.
+ guard !hasAppeared, !model.isClosing else { return }
+ hasAppeared = true
+ if let windowScene = view.window?.windowScene {
+ model.banner = LucidBannerRegistry.shared.banner(for: windowScene)
+ }
+ model.validateAccountAndRequestPasscode()
+ }
+}
diff --git a/Share/NCShareExtension+DataSource.swift b/Share/NCShareExtension+DataSource.swift
deleted file mode 100644
index 03836ab45f..0000000000
--- a/Share/NCShareExtension+DataSource.swift
+++ /dev/null
@@ -1,192 +0,0 @@
-// SPDX-FileCopyrightText: Nextcloud GmbH
-// SPDX-FileCopyrightText: 2021 Marino Faggiana
-// SPDX-FileCopyrightText: 2021 Henrik Storch
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-import UIKit
-import NextcloudKit
-
-// MARK: - Collection View (target folder)
-
-extension NCShareExtension: UICollectionViewDelegate {
- func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
- Task {
- guard let tblAccount = NCShareExtensionData.shared.getTblAccoun(),
- let metadata = self.dataSource.getMetadata(indexPath: indexPath) else {
- return self.showAlert(description: "_invalid_url_")
- }
- let serverUrl = self.utilityFileSystem.createServerUrl(serverUrl: metadata.serverUrl, fileName: metadata.fileName)
-
- if metadata.e2eEncrypted && !NCPreferences().isEndToEndEnabled(account: tblAccount.account) {
- self.showAlert(title: "_info_", description: "_e2e_goto_settings_for_enable_")
- return
- }
- let capabilities = await NKCapabilities.shared.getCapabilities(for: tblAccount.account)
-
- if let fileNameError = FileNameValidator.checkFileName(metadata.fileNameView, account: tblAccount.account, capabilities: capabilities) {
- let message = "\(fileNameError.errorDescription) \(NSLocalizedString("_please_rename_file_", comment: ""))"
- await UIAlertController.warningAsync(message: message, presenter: self)
- return
- }
-
- self.serverUrl = serverUrl
- self.setNavigationBar(navigationTitle: metadata.fileNameView)
-
- await self.reloadData()
- await self.loadFolder()
- }
- }
-
- func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
- if kind == UICollectionView.elementKindSectionHeader {
- let session = NCShareExtensionData.shared.getSession()
- guard let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "sectionFirstHeaderEmptyData", for: indexPath) as? NCSectionFirstHeaderEmptyData else { return NCSectionFirstHeaderEmptyData() }
- if self.dataSourceTask?.state == .running {
- header.emptyImage.image = utility.loadImage(named: "wifi", colors: [NCBrandColor.shared.getElement(account: session.account)])
- header.emptyTitle.text = NSLocalizedString("_request_in_progress_", comment: "")
- header.emptyDescription.text = ""
- } else {
- header.emptyImage.image = NCImageCache.shared.getFolder(account: session.account)
- header.emptyTitle.text = NSLocalizedString("_files_no_folders_", comment: "")
- header.emptyDescription.text = ""
- }
- return header
- } else {
- return UICollectionReusableView()
- }
- }
-}
-
-extension NCShareExtension: UICollectionViewDelegateFlowLayout {
- func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
- var height: CGFloat = 0
- if self.dataSource.isEmpty() {
- height = NCUtility().getHeightHeaderEmptyData(view: view, portraitOffset: 0, landscapeOffset: -50)
- }
- return CGSize(width: collectionView.frame.width, height: height)
- }
-}
-
-extension NCShareExtension: UICollectionViewDataSource {
- func numberOfSections(in collectionView: UICollectionView) -> Int {
- return dataSource.numberOfSections()
- }
-
- func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
- return self.dataSource.numberOfItemsInSection(section)
- }
-
- func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
- let cell = (collectionView.dequeueReusableCell(withReuseIdentifier: "listCell", for: indexPath) as? NCListCell)!
- guard let metadata = self.dataSource.getMetadata(indexPath: indexPath) else {
- return cell
- }
-
- cell.metadata = metadata
- cell.setBidiSafeFilename(metadata.fileNameView, isDirectory: metadata.directory, titleLabel: cell.labelTitle, extensionLabel: cell.labelExtension)
- cell.labelTitle.textColor = NCBrandColor.shared.textColor
- cell.labelExtension?.textColor = NCBrandColor.shared.textColor
- cell.setButtonsHidden(true)
-
- if metadata.directory {
- setupDirectoryCell(cell, indexPath: indexPath, with: metadata)
- }
-
- if metadata.favorite {
- cell.imageFavorite.image = NCImageCache.shared.getImageFavorite()
- }
-
- cell.writeInfoDateSize(date: metadata.date, size: metadata.size)
-
- if metadata.isLivePhoto {
- cell.imageStatus.image = utility.loadImage(named: "livephoto", colors: [NCBrandColor.shared.iconImageColor2])
- }
-
- cell.setTags(tags: Array(metadata.tags))
-
- let accessibilityLabel = [metadata.fileNameView, cell.labelInfo.text, cell.labelSubinfo.text]
- .compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
- .filter { !$0.isEmpty }
- .joined(separator: ", ")
- cell.setAccessibility(label: accessibilityLabel, value: "")
- cell.accessibilityTraits = .button
-
- cell.separator.isHidden = collectionView.numberOfItems(inSection: indexPath.section) == indexPath.row + 1
-
- return cell
- }
-
- func setupDirectoryCell(_ cell: NCListCell, indexPath: IndexPath, with metadata: tableMetadata) {
- var isShare = false
- var isMounted = false
- let session = NCShareExtensionData.shared.getSession()
-
- if let metadataFolder = metadataFolder {
- isShare = metadata.permissions.contains(NCMetadataPermissions.permissionShared) && !metadataFolder.permissions.contains(NCMetadataPermissions.permissionShared)
- isMounted = metadata.permissions.contains(NCMetadataPermissions.permissionMounted) && !metadataFolder.permissions.contains(NCMetadataPermissions.permissionMounted)
- }
-
- if metadata.e2eEncrypted {
- cell.imageItem.image = NCImageCache.shared.getFolderEncrypted(account: metadata.account)
- } else if isShare {
- cell.imageItem.image = NCImageCache.shared.getFolderSharedWithMe(account: metadata.account)
- } else if !metadata.shareType.isEmpty {
- metadata.shareType.contains(3) ?
- (cell.imageItem.image = NCImageCache.shared.getFolderPublic(account: metadata.account)) :
- (cell.imageItem.image = NCImageCache.shared.getFolderSharedWithMe(account: metadata.account))
- } else if metadata.mountType == "group" {
- cell.imageItem.image = NCImageCache.shared.getFolderGroup(account: metadata.account)
- } else if isMounted {
- cell.imageItem.image = NCImageCache.shared.getFolderExternal(account: metadata.account)
- } else if metadata.fileName == autoUploadFileName && metadata.serverUrl == autoUploadDirectory {
- cell.imageItem.image = NCImageCache.shared.getFolderAutomaticUpload(account: metadata.account)
- } else {
- cell.imageItem.image = NCImageCache.shared.getFolder(account: metadata.account)
- }
-
- cell.labelInfo.text = utility.getRelativeDateTitle(metadata.date as Date)
-
- let lockServerUrl = utilityFileSystem.createServerUrl(serverUrl: metadata.serverUrl, fileName: metadata.fileName)
- let tableDirectory = NCManageDatabase.shared.getTableDirectory(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", session.account, lockServerUrl))
-
- // Local image: offline
- if tableDirectory != nil && tableDirectory!.offline {
- cell.imageLocal?.image = NCImageCache.shared.getImageOfflineFlag()
- }
- }
-}
-
-// MARK: - Table View (uploading files)
-
-extension NCShareExtension: UITableViewDelegate {
- func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
- return heightRowTableView
- }
-
- func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
- let fileName = filesName[indexPath.row]
- let session = NCShareExtensionData.shared.getSession()
-
- showRenameFileDialog(named: fileName, account: session.account)
- }
-}
-
-extension NCShareExtension: UITableViewDataSource {
- func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
- filesName.count
- }
-
- func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as? NCShareCell else {
- return UITableViewCell()
- }
-
- let fileName = filesName[indexPath.row]
- let session = NCShareExtensionData.shared.getSession()
-
- cell.setup(fileName: fileName, iconName: "", account: session.account)
- cell.delegate = self
-
- return cell
- }
-}
diff --git a/Share/NCShareExtension+Files.swift b/Share/NCShareExtension+Files.swift
deleted file mode 100644
index ab7db35aea..0000000000
--- a/Share/NCShareExtension+Files.swift
+++ /dev/null
@@ -1,203 +0,0 @@
-// SPDX-FileCopyrightText: Nextcloud GmbH
-// SPDX-FileCopyrightText: 2021 Marino Faggiana
-// SPDX-FileCopyrightText: 2021 Henrik Storch
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-import Foundation
-import UIKit
-import UniformTypeIdentifiers
-import NextcloudKit
-
-extension NCShareExtension {
- func reloadData() async {
- let session = NCShareExtensionData.shared.getSession()
- let layoutForView = NCManageDatabase.shared.getLayoutForView(account: session.account, key: keyLayout, serverUrl: serverUrl)
- let showHiddenFiles = NCPreferences().getShowHiddenFiles(account: session.account)
- let predicate = showHiddenFiles
- ? NSPredicate(
- format: "account == %@ AND serverUrl == %@ AND fileName != %@ AND directory == true",
- session.account,
- serverUrl,
- NextcloudKit.shared.nkCommonInstance.rootFileName
- )
- : NSPredicate(
- format: "account == %@ AND serverUrl == %@ AND fileName != %@ AND directory == true AND NOT fileName BEGINSWITH[c] %@",
- session.account,
- serverUrl,
- NextcloudKit.shared.nkCommonInstance.rootFileName,
- "."
- )
-
- let metadatas = await NCManageDatabase.shared.getMetadatasAsync(predicate: predicate,
- withLayout: layoutForView,
- withAccount: session.account)
- self.dataSource = NCCollectionViewDataSource(metadatas: metadatas,
- layoutForView: layoutForView,
- account: session.account)
- self.collectionView.reloadData()
- }
-
- @objc func didCreateFolder(_ notification: NSNotification) {
- Task {
- guard let userInfo = notification.userInfo as NSDictionary?,
- let ocId = userInfo["ocId"] as? String,
- let metadata = await NCManageDatabase.shared.getMetadataFromOcIdAsync(ocId)
- else { return }
-
- self.serverUrl += "/" + metadata.fileName
- await self.loadFolder()
- self.setNavigationBar(navigationTitle: metadata.fileNameView)
- }
- }
-
- func loadFolder() async {
- let session = NCShareExtensionData.shared.getSession()
- let resultsReadFolder = await NCNetworking.shared.readFolderAsync(serverUrl: serverUrl, account: session.account) { task in
- self.dataSourceTask = task
- self.collectionView.reloadData()
- }
-
- if resultsReadFolder.error == .success {
- self.metadataFolder = resultsReadFolder.metadataFolder
- await self.reloadData()
- } else if let message = resultsReadFolder.error.userFriendlyMessage() {
- self.showAlert(description: message)
- }
- }
-}
-
-class NCFilesExtensionHandler {
- var itemsProvider: [NSItemProvider] = []
- lazy var fileNames: [String] = []
- let dateFormatter: DateFormatter = {
- let formatter = DateFormatter()
- formatter.dateFormat = "yyyy-MM-dd HH-mm-ss-"
- return formatter
- }()
-
- @discardableResult
- init(items: [NSExtensionItem], completion: @escaping ([String]) -> Void) {
- NCUtilityFileSystem().emptyTemporaryDirectory()
- var counter = 0
-
- self.itemsProvider = items.compactMap({ $0.attachments }).flatMap { $0.filter({
- $0.hasItemConformingToTypeIdentifier(UTType.item.identifier as String) || $0.hasItemConformingToTypeIdentifier("public.url")
- }) }
-
- for (ix, provider) in itemsProvider.enumerated() {
- provider.loadItem(forTypeIdentifier: provider.typeIdentifier) { [self] item, error in
- defer {
- counter += 1
- if counter == itemsProvider.count { completion(self.fileNames) }
- }
- guard error == nil else { return }
- var originalName = (dateFormatter.string(from: Date())) + String(ix)
-
- if let url = item as? URL, url.isFileURL, !url.lastPathComponent.isEmpty {
- originalName = url.lastPathComponent
-
- if fileNames.contains(originalName) {
- let incrementalNumber = NCPreferences().incrementalNumber
- originalName = "\(url.deletingPathExtension().lastPathComponent) \(incrementalNumber).\(url.pathExtension)"
- }
- }
-
- var fileName: String?
- switch item {
- case let image as UIImage:
- fileName = getItem(image: image, fileName: originalName)
- case let url as URL:
- fileName = getItem(url: url, fileName: originalName)
- case let data as Data:
- fileName = getItem(data: data, fileName: originalName, provider: provider)
- case let text as String:
- fileName = getItem(string: text, fileName: originalName)
- default: return
- }
-
- if let fileName, !fileNames.contains(fileName) {
- fileNames.append(fileName)
- }
- }
- }
- }
-
- // Image
- func getItem(image: UIImage, fileName: String) -> String? {
- var fileUrl = URL(fileURLWithPath: NSTemporaryDirectory() + fileName)
- if fileUrl.pathExtension.isEmpty { fileUrl.appendPathExtension("png") }
- guard let pngImageData = image.pngData(), (try? pngImageData.write(to: fileUrl, options: [.atomic])) != nil
- else { return nil }
- return fileUrl.lastPathComponent
- }
-
- // URL
- // Does not work for directories
- func getItem(url: URL, fileName: String) -> String? {
- var fileName = fileName
- guard url.isFileURL else {
- guard !fileNames.contains(url.lastPathComponent) else { return nil }
- if !url.deletingPathExtension().lastPathComponent.isEmpty { fileName = url.deletingPathExtension().lastPathComponent }
- fileName += "." + (url.pathExtension.isEmpty ? "html" : url.pathExtension)
- let filenamePath = NSTemporaryDirectory() + fileName
-
- do {
- let downloadedContent = try Data(contentsOf: url)
- guard !FileManager.default.fileExists(atPath: filenamePath) else { return nil }
- try downloadedContent.write(to: URL(fileURLWithPath: filenamePath))
- } catch { print(error); return nil }
- return fileName
- }
-
- let filenamePath = NSTemporaryDirectory() + fileName
-
- try? FileManager.default.removeItem(atPath: filenamePath)
-
- do {
- try FileManager.default.copyItem(atPath: url.path, toPath: filenamePath)
-
- let attr = try FileManager.default.attributesOfItem(atPath: filenamePath)
- guard !attr.isEmpty else { return nil }
- return fileName
- } catch { return nil }
- }
-
- // Data
- func getItem(data: Data, fileName: String, provider: NSItemProvider) -> String? {
- guard !data.isEmpty else { return nil }
- var fileName = fileName
-
- if let url = URL(string: fileName), !url.pathExtension.isEmpty {
- fileName = url.lastPathComponent
- } else if let name = provider.suggestedName {
- fileName = name
- } else if let ext = provider.registeredTypeIdentifiers.last?.split(separator: ".").last {
- fileName += "." + ext
- } // else: no file information, use default name without ext
-
- // when sharing images in safari only data is retuned.
- // also, when sharing option "Automatic" is slected extension will return both raw data and a url, which will be downloaded, causing the image to appear twice with different names
- if let image = UIImage(data: data) {
- return getItem(image: image, fileName: fileName)
- }
-
- let filenamePath = NSTemporaryDirectory() + fileName
- FileManager.default.createFile(atPath: filenamePath, contents: data, attributes: nil)
- return fileName
- }
-
- // String
- func getItem(string: String, fileName: String) -> String? {
- guard !string.isEmpty else { return nil }
- let filenamePath = NSTemporaryDirectory() + fileName + ".txt"
- FileManager.default.createFile(atPath: filenamePath, contents: string.data(using: String.Encoding.utf8), attributes: nil)
- return fileName
- }
-}
-
-extension NSItemProvider {
- var typeIdentifier: String {
- if hasItemConformingToTypeIdentifier("public.url") { return "public.url" } else
- if hasItemConformingToTypeIdentifier(UTType.item.identifier as String) { return UTType.item.identifier as String } else { return "" }
- }
-}
diff --git a/Share/NCShareExtension+NCAccountRequestDelegate.swift b/Share/NCShareExtension+NCAccountRequestDelegate.swift
deleted file mode 100644
index b1de031237..0000000000
--- a/Share/NCShareExtension+NCAccountRequestDelegate.swift
+++ /dev/null
@@ -1,132 +0,0 @@
-// SPDX-FileCopyrightText: Nextcloud GmbH
-// SPDX-FileCopyrightText: 2021 Marino Faggiana
-// SPDX-FileCopyrightText: 2021 Henrik Storch
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-import NextcloudKit
-import UIKit
-
-extension NCShareExtension: NCAccountRequestDelegate {
-
- // MARK: - Account
-
- func showAccountPicker() {
- Task {
- let accounts = await NCManageDatabase.shared.getAllAccountOrderAliasAsync()
- let session = NCShareExtensionData.shared.getSession()
-
- guard accounts.count > 1,
- let vcAccountRequest = UIStoryboard(name: "NCAccountRequest", bundle: nil).instantiateInitialViewController() as? NCAccountRequest else {
- return
- }
-
- // Only here change the active account
- for account in accounts {
- account.active = account.account == session.account
- }
-
- vcAccountRequest.activeAccount = session.account
- vcAccountRequest.accounts = accounts.sorted { sorg, dest -> Bool in
- return sorg.active && !dest.active
- }
- vcAccountRequest.enableTimerProgress = false
- vcAccountRequest.enableAddAccount = false
- vcAccountRequest.delegate = self
- vcAccountRequest.dismissDidEnterBackground = true
-
- let maximumHeight = view.safeAreaLayoutGuide.layoutFrame.height * 0.8
- let height = min(CGFloat(accounts.count * Int(vcAccountRequest.heightCell) + 45), maximumHeight)
-
- let popup = NCPopupViewController(contentController: vcAccountRequest, popupWidth: 300, popupHeight: height + 20)
-
- self.present(popup, animated: true)
-
- }
- }
-
- func accountRequestAddAccount() { }
-
- func accountRequestChangeAccount(account: String, controller: UIViewController?) {
- Task {
- let session = await NCShareExtensionData.shared.setSessionAccount(account)
- guard let tblAccount = NCShareExtensionData.shared.getTblAccoun() else {
- cancel(with: NCShareExtensionError.noAccount)
- return
- }
- showAccountView()
-
- NextcloudKit.shared.setup(groupIdentifier: NCBrandOptions.shared.capabilitiesGroup, delegate: NCNetworking.shared)
- NextcloudKit.shared.appendSession(account: tblAccount.account,
- urlBase: tblAccount.urlBase,
- user: tblAccount.user,
- userId: tblAccount.userId,
- password: NCPreferences().getPassword(account: tblAccount.account),
- userAgent: userAgent,
- httpMaximumConnectionsPerHost: NCBrandOptions.shared.httpMaximumConnectionsPerHost,
- httpMaximumConnectionsPerHostInDownload: NCBrandOptions.shared.httpMaximumConnectionsPerHostInDownload,
- httpMaximumConnectionsPerHostInUpload: NCBrandOptions.shared.httpMaximumConnectionsPerHostInUpload,
- groupIdentifier: NCBrandOptions.shared.capabilitiesGroup)
-
- autoUploadFileName = NCManageDatabase.shared.getAccountAutoUploadFileName(account: account)
- autoUploadDirectory = NCManageDatabase.shared.getAccountAutoUploadDirectory(account: session.account,
- urlBase: session.urlBase,
- userId: session.userId)
- serverUrl = utilityFileSystem.getHomeServer(session: session)
-
- setNavigationBar(navigationTitle: NCBrandOptions.shared.brand)
-
- await reloadData()
- await loadFolder()
- }
- }
-}
-
-extension NCShareExtension: NCCreateFormUploadConflictDelegate {
- func dismissCreateFormUploadConflict(metadatas: [tableMetadata]?) {
- guard let metadatas else {
- uploadMetadata.removeAll()
- return
- }
-
- self.uploadMetadata.append(contentsOf: metadatas)
- Task {
- await uploadAndExit()
- }
- }
-}
-
-extension NCShareExtension: NCShareCellDelegate {
- func showRenameFileDialog(named fileName: String, account: String) {
- guard let capabilities = NCNetworking.shared.capabilities[account] else {
- return
- }
- let alert = UIAlertController.renameFile(fileName: fileName, capabilities: capabilities, account: account) { [self] newFileName in
- renameFile(oldName: fileName, newName: newFileName, account: account)
- }
-
- present(alert, animated: true)
- }
-
- func renameFile(oldName: String, newName: String, account: String) {
- guard let fileIx = self.filesName.firstIndex(of: oldName),
- !self.filesName.contains(newName),
- utilityFileSystem.moveFile(atPath: (NSTemporaryDirectory() + oldName), toPath: (NSTemporaryDirectory() + newName)) else {
- return showAlert(title: "_single_file_conflict_title_", description: "'\(oldName)' -> '\(newName)'")
- }
-
- filesName[fileIx] = newName
- tableView.reloadData()
- }
-
- func removeFile(named fileName: String) {
- guard let index = self.filesName.firstIndex(of: fileName) else {
- return showAlert(title: "_file_not_found_", description: fileName)
- }
- self.filesName.remove(at: index)
- if self.filesName.isEmpty {
- cancel(with: NCShareExtensionError.noFiles)
- } else {
- self.setCommandView()
- }
- }
-}
diff --git a/Share/NCShareExtension.swift b/Share/NCShareExtension.swift
deleted file mode 100644
index a7b2f68c4f..0000000000
--- a/Share/NCShareExtension.swift
+++ /dev/null
@@ -1,601 +0,0 @@
-// SPDX-FileCopyrightText: Nextcloud GmbH
-// SPDX-FileCopyrightText: 2021 Marino Faggiana
-// SPDX-FileCopyrightText: 2021 Henrik Storch
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-import UIKit
-import UniformTypeIdentifiers
-import NextcloudKit
-import LucidBanner
-import SwiftUI
-
-enum NCShareExtensionError: Error {
- case cancel, fileUpload, noAccount, noFiles, versionMismatch
-}
-
-class NCShareExtension: UIViewController {
- @IBOutlet weak var collectionView: UICollectionView!
- @IBOutlet weak var tableView: UITableView!
- @IBOutlet weak var cancelButton: UIBarButtonItem!
- @IBOutlet weak var separatorView: UIView!
- @IBOutlet weak var commandView: UIView!
- @IBOutlet weak var separatorHeightConstraint: NSLayoutConstraint!
- @IBOutlet weak var commandViewHeightConstraint: NSLayoutConstraint!
-
- @IBOutlet weak var createFolderView: UIView!
- @IBOutlet weak var createFolderImage: UIImageView!
- @IBOutlet weak var createFolderLabel: UILabel!
- @IBOutlet weak var uploadView: UIView!
- @IBOutlet weak var uploadImage: UIImageView!
- @IBOutlet weak var uploadLabel: UILabel!
-
- // -------------------------------------------------------------
- var serverUrl = ""
- var filesName: [String] = []
- // -------------------------------------------------------------
-
- let keyLayout = NCGlobal.shared.layoutViewShareExtension
- var metadataFolder: tableMetadata?
- var dataSourceTask: URLSessionTask?
- var dataSource = NCCollectionViewDataSource()
- let heightRowTableView: CGFloat = 50
- let heightCommandView: CGFloat = 170
- var autoUploadFileName = ""
- var autoUploadDirectory = ""
- var progress: CGFloat = 0
- var counterUploaded: Int = 0
- var uploadMetadata: [tableMetadata] = []
- let utilityFileSystem = NCUtilityFileSystem()
- let utility = NCUtility()
- let global = NCGlobal.shared
- var maintenanceMode: Bool = false
- var token: Int?
- var banner: LucidBanner?
- var sceneIdentifier: String = UUID().uuidString
- private var isPresentingNoAccountAlert = false
- private var isPresentingPasscode = false
- private var isPasscodeAuthenticated = false
-
- // MARK: - View Life Cycle
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- collectionView.register(UINib(nibName: "NCSectionFirstHeaderEmptyData", bundle: nil), forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "sectionFirstHeaderEmptyData")
- collectionView.register(UINib(nibName: "NCListCell", bundle: nil), forCellWithReuseIdentifier: "listCell")
- collectionView.collectionViewLayout = NCListLayout()
-
- commandView.backgroundColor = .secondarySystemBackground
- separatorView.backgroundColor = .separator
- separatorHeightConstraint.constant = 0.5
-
- tableView.separatorColor = .separator
- tableView.layer.cornerRadius = 10
- tableView.tableFooterView = UIView(frame: CGRect(origin: .zero, size: CGSize(width: 0, height: 1)))
- commandViewHeightConstraint.constant = heightCommandView
-
- cancelButton.title = NSLocalizedString("_cancel_", comment: "")
-
- createFolderView.layer.cornerRadius = 10
- createFolderImage.image = utility.loadImage(named: "folder.badge.plus", colors: [NCBrandColor.shared.iconImageColor])
- createFolderLabel.text = NSLocalizedString("_create_folder_", comment: "")
- let createFolderGesture = UITapGestureRecognizer(target: self, action: #selector(actionCreateFolder(_:)))
- createFolderView.addGestureRecognizer(createFolderGesture)
-
- uploadView.layer.cornerRadius = 10
-
- uploadLabel.text = NSLocalizedString("_upload_", comment: "")
- uploadLabel.textColor = .systemBlue
- let uploadGesture = UITapGestureRecognizer(target: self, action: #selector(actionUpload(_:)))
- uploadView.addGestureRecognizer(uploadGesture)
-
- let versionNextcloudiOS = String(format: NCBrandOptions.shared.textCopyrightNextcloudiOS, utility.getVersionBuild())
- NextcloudKit.configureLogger(
- logLevel: NCBrandOptions.shared.disable_log ? .disabled : NCPreferences().log,
- logDirectory: NCPreferences.sharedLogDirectory
- )
-
- nkLog(start: "Start Share session " + versionNextcloudiOS)
-
- NCBrandColor.shared.createUserColors()
-
- registerForTraitChanges([UITraitUserInterfaceStyle.self]) { (self: Self, _) in
- guard !self.maintenanceMode else {
- return
- }
- self.updateAppearance()
- }
-
- NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: nil) { _ in
- guard !self.maintenanceMode,
- self.validateAccount() else {
- return
- }
-
- self.isPasscodeAuthenticated = false
- self.requestPasscodeIfNeeded()
- }
-
- // Verify version
- let versionApp = NCUtility().getVersionMaintenance()
- if let groupDefaults = UserDefaults(suiteName: NCBrandOptions.shared.capabilitiesGroup) {
- let lastVersion = groupDefaults.string(forKey: NCGlobal.shared.udLastVersion)
- if lastVersion != versionApp {
- maintenanceMode = true
- return
- }
- }
-
- if let account = NCShareExtensionData.shared.getTblAccoun()?.account {
- accountRequestChangeAccount(account: account, controller: nil)
- }
-
- NCNetworking.shared.setupScene(sceneIdentifier: sceneIdentifier, controller: self)
-
- if let windowScene = view.window?.windowScene {
- banner = LucidBannerRegistry.shared.banner(for: windowScene)
- }
- }
-
- override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
-
- // Verify version
- guard !maintenanceMode else {
- return showAlert(description: "_version_mismatch_error_") {
- self.cancel(with: .versionMismatch)
- }
- }
-
- guard validateAccount() else {
- return
- }
-
- guard let inputItems = extensionContext?.inputItems as? [NSExtensionItem] else {
- cancel(with: .noFiles)
- return
- }
-
- Task { @MainActor in
- NCFilesExtensionHandler(items: inputItems) { fileNames in
- self.filesName = fileNames
- DispatchQueue.main.async {
- self.setCommandView()
- }
- }
-
- self.requestPasscodeIfNeeded()
-
- self.collectionView.reloadData()
- }
- }
-
- override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
- super.viewWillTransition(to: size, with: coordinator)
-
- coordinator.animate(alongsideTransition: nil) { _ in
- self.collectionView?.collectionViewLayout.invalidateLayout()
- }
- }
-
- private func updateAppearance() {
- collectionView.visibleCells.forEach { $0.setNeedsLayout() }
- tableView.visibleCells.forEach { $0.setNeedsLayout() }
- }
-
- private func validateAccount() -> Bool {
- guard !NCPasscodeModel.isPasscodeReset else {
- return showNoAccountAlert()
- }
-
- if let account = NCShareExtensionData.shared.getTblAccoun()?.account,
- NCManageDatabase.shared.getTableAccount(account: account) != nil {
- return true
- }
-
- guard let activeAccount = NCManageDatabase.shared.getActiveTableAccount() else {
- return showNoAccountAlert()
- }
-
- clearAccountView()
- accountRequestChangeAccount(account: activeAccount.account, controller: nil)
- return true
- }
-
- private func showNoAccountAlert() -> Bool {
- clearAccountView()
- if !isPresentingNoAccountAlert {
- isPresentingNoAccountAlert = true
- showAlert(description: "_no_active_account_") {
- self.cancel(with: .noAccount)
- }
- }
- return false
- }
-
- private func clearAccountView() {
- dataSourceTask?.cancel()
- dataSourceTask = nil
- dataSource = NCCollectionViewDataSource()
- metadataFolder = nil
-
- collectionView.isHidden = true
- tableView.isHidden = true
- commandView.isHidden = true
- separatorView.isHidden = true
- navigationItem.leftBarButtonItems = nil
- navigationItem.title = NCBrandOptions.shared.brand
- view.layoutIfNeeded()
- }
-
- func showAccountView() {
- collectionView.isHidden = false
- tableView.isHidden = false
- commandView.isHidden = false
- separatorView.isHidden = false
- }
-
- // MARK: -
-
- func cancel(with error: NCShareExtensionError? = nil) {
- if let error {
- extensionContext?.cancelRequest(withError: error)
- } else {
- extensionContext?.completeRequest(returningItems: extensionContext?.inputItems, completionHandler: nil)
- }
- }
-
- func showAlert(title: String = "_error_", description: String, onDismiss: (() -> Void)? = nil) {
- let description = description.replacingOccurrences(of: "\t", with: "\n")
- let alertController = UIAlertController(title: NSLocalizedString(title, comment: ""), message: NSLocalizedString(description, comment: ""), preferredStyle: .alert)
- alertController.addAction(UIAlertAction(title: NSLocalizedString("_ok_", comment: ""), style: .default, handler: { _ in
- onDismiss?()
- }))
- self.present(alertController, animated: true)
- }
-
- func setNavigationBar(navigationTitle: String) {
- guard let tblAccount = NCShareExtensionData.shared.getTblAccoun() else {
- return
- }
- let session = NCShareExtensionData.shared.getSession()
-
- navigationItem.title = navigationTitle
- cancelButton.title = NSLocalizedString("_cancel_", comment: "")
-
- // BACK BUTTON
- let backButton = UIButton(type: .custom)
- backButton.setImage(UIImage(named: "back"), for: .normal)
- backButton.tintColor = .systemBlue
- backButton.semanticContentAttribute = .forceLeftToRight
- backButton.setTitle(" " + NSLocalizedString("_back_", comment: ""), for: .normal)
- backButton.setTitleColor(.systemBlue, for: .normal)
- backButton.action(for: .touchUpInside) { _ in
- while self.serverUrl.last != "/" { self.serverUrl.removeLast() }
- self.serverUrl.removeLast()
- Task {
- await self.reloadData()
- }
- var navigationTitle = (self.serverUrl as NSString).lastPathComponent
- if self.utilityFileSystem.getHomeServer(session: session) == self.serverUrl {
- navigationTitle = NCBrandOptions.shared.brand
- }
- self.setNavigationBar(navigationTitle: navigationTitle)
- }
-
- let image = utility.loadUserImage(for: tblAccount.user, displayName: tblAccount.displayName, urlBase: tblAccount.urlBase)
- let profileButton = UIButton(type: .custom)
- profileButton.setImage(image, for: .normal)
-
- if serverUrl == utilityFileSystem.getHomeServer(session: session) {
- var title = " "
- if !tblAccount.alias.isEmpty {
- title += tblAccount.alias
- } else {
- title += tblAccount.displayName
- }
-
- profileButton.setTitle(title, for: .normal)
- profileButton.setTitleColor(.systemBlue, for: .normal)
- }
-
- profileButton.semanticContentAttribute = .forceLeftToRight
- profileButton.sizeToFit()
- profileButton.action(for: .touchUpInside) { _ in
- self.showAccountPicker()
- }
- var navItems = [UIBarButtonItem(customView: profileButton)]
- if serverUrl != utilityFileSystem.getHomeServer(session: session) {
- let space = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
- space.width = 20
- navItems.append(contentsOf: [UIBarButtonItem(customView: backButton), space])
- }
- navigationItem.setLeftBarButtonItems(navItems, animated: true)
- }
-
- func setCommandView() {
- guard !filesName.isEmpty else {
- cancel(with: .noFiles)
- return
- }
- let counter = min(CGFloat(filesName.count), 3)
- self.commandViewHeightConstraint.constant = heightCommandView + (self.heightRowTableView * counter)
-
- if filesName.count <= 3 {
- self.tableView.isScrollEnabled = false
- }
- uploadLabel.text = NSLocalizedString("_upload_", comment: "") + " \(filesName.count) " + NSLocalizedString("_files_", comment: "")
- self.tableView.reloadData()
- }
-
- // MARK: ACTION
-
- @IBAction func actionCancel(_ sender: UIBarButtonItem) {
- cancel()
- }
-
- @objc func actionCreateFolder(_ sender: Any?) {
- let session = NCShareExtensionData.shared.getSession()
- guard let capabilities = NCNetworking.shared.capabilities[session.account] else {
- return
- }
- let alertController = UIAlertController.createFolderWith(serverUrl: serverUrl, session: session, capabilities: capabilities) { error, folderName in
- let context = NCErrorContext.createFolder(folderName: folderName)
-
- if error == .success {
- Task {
- await self.loadFolder()
- }
- } else if let message = error.userFriendlyMessage(for: context) {
- self.showAlert(title: context.failureTitle ?? "_error_", description: message)
- }
- }
- self.present(alertController, animated: true)
- }
-}
-
-// MARK: - Upload
-extension NCShareExtension {
- @objc func actionUpload(_ sender: Any?) {
- Task { @MainActor in
- guard let tblAccount = NCShareExtensionData.shared.getTblAccoun(),
- let capabilities = NCNetworking.shared.capabilities[tblAccount.account] else {
- return
- }
- guard !filesName.isEmpty else { return showAlert(description: "_files_no_files_") }
- let session = NCShareExtensionData.shared.getSession()
-
- var conflicts: [tableMetadata] = []
- var invalidNameIndexes: [Int] = []
-
- for (index, fileName) in filesName.enumerated() {
- let newFileName = FileAutoRenamer.rename(fileName, capabilities: capabilities)
-
- if let fileNameError = FileNameValidator.checkFileName(newFileName, account: tblAccount.account, capabilities: capabilities) {
- if filesName.count == 1 {
- showRenameFileDialog(named: fileName, account: tblAccount.account)
- return
- } else {
- let message = "\(fileNameError.errorDescription) \(NSLocalizedString("_please_rename_file_", comment: ""))"
- await UIAlertController.warningAsync(message: message, presenter: self)
-
- invalidNameIndexes.append(index)
- continue
- }
- }
- }
-
- for index in invalidNameIndexes.reversed() {
- filesName.remove(at: index)
- }
-
- for fileName in filesName {
- let ocId = NSUUID().uuidString
- let toPath = utilityFileSystem.getDirectoryProviderStorageOcId(ocId,
- fileName: fileName,
- userId: session.userId,
- urlBase: session.urlBase)
- guard utilityFileSystem.copyFile(atPath: (NSTemporaryDirectory() + fileName), toPath: toPath) else {
- continue
- }
- let metadataForUpload = await NCManageDatabaseCreateMetadata().createMetadataAsync(
- fileName: fileName,
- ocId: ocId,
- serverUrl: serverUrl,
- session: session,
- sceneIdentifier: self.sceneIdentifier)
-
- metadataForUpload.session = NCNetworking.shared.sessionUpload
- metadataForUpload.sessionSelector = NCGlobal.shared.selectorUploadFileShareExtension
- metadataForUpload.size = utilityFileSystem.getFileSize(filePath: toPath)
- metadataForUpload.status = NCGlobal.shared.metadataStatusWaitUpload
- metadataForUpload.sessionDate = Date()
- if NCManageDatabase.shared.getMetadataConflict(account: session.account, serverUrl: serverUrl, fileNameView: fileName, nativeFormat: metadataForUpload.nativeFormat) != nil {
- conflicts.append(metadataForUpload)
- } else {
- uploadMetadata.append(metadataForUpload)
- }
- }
-
- tableView.reloadData()
-
- if !conflicts.isEmpty {
- guard let conflict = UIStoryboard(name: "NCCreateFormUploadConflict", bundle: nil).instantiateInitialViewController() as? NCCreateFormUploadConflict
- else { return }
-
- conflict.account = session.account
- conflict.serverUrl = self.serverUrl
- conflict.metadatasUploadInConflict = conflicts
- conflict.delegate = self
- self.present(conflict, animated: true, completion: nil)
- } else {
- await uploadAndExit()
- }
- }
- }
-
- @MainActor
- func uploadAndExit() async {
- // A Share extension process can be reused by the system. Treat each
- // explicit share operation as a new server-key validation cycle.
- await NCNetworkingE2EE.beginNewServerKeyValidationCycle()
-
- var error: NKError?
- var errorContext: NCErrorContext = .generic
- guard let window = self.view.window else {
- return
- }
- let payload = LucidBannerPayload(stage: .button,
- vPosition: .center,
- blocksTouches: true)
- (banner, token) = showUploadBanner(windowScene: window.windowScene,
- payload: payload,
- allowMinimizeOnTap: false,
- onButtonTap: {
- self.cancel()
- })
-
- for metadata in self.uploadMetadata {
- // BANNER
- let payloadUpdate = LucidBannerPayload.Update(
- title: NSLocalizedString("_upload_file_", comment: "") + " \(self.counterUploaded + 1) " + NSLocalizedString("_of_", comment: "") + " \(self.filesName.count)",
- systemImage: "arrowshape.up.circle",
- imageAnimation: .breathe,
- progress: 0)
- banner?.update(payload: payloadUpdate)
-
- error = await self.upload(metadata: metadata)
- if error != .success {
- errorContext = .upload(fileName: metadata.fileNameView)
- break
- }
- }
-
- if error == .success {
- banner?.update(payload: LucidBannerPayload.Update(stage: .success, horizontalLayout: .centered(width: 100)), for: self.token)
- } else {
- let payload = LucidBannerPayload.Update(title: errorContext.failureTitle ?? NSLocalizedString("_error_", comment: ""),
- subtitle: error?.userFriendlyMessage(for: errorContext) ?? "",
- stage: .error)
-
- banner?.update(payload: payload, for: self.token)
- }
-
- Task { @MainActor in
- try? await Task.sleep(for: .seconds(2))
- banner?.dismiss()
- extensionContext?.completeRequest(returningItems: extensionContext?.inputItems, completionHandler: nil)
- }
- }
-
- @MainActor
- func upload(metadata: tableMetadata) async -> NKError? {
- let session = NCShareExtensionData.shared.getSession()
- var error: NKError = .success
-
- let results = await NKTypeIdentifiers.shared.getInternalType(fileName: metadata.fileNameView, mimeType: metadata.contentType, directory: false, account: session.account)
- metadata.contentType = results.mimeType
- metadata.iconName = results.iconName
- metadata.classFile = results.classFile
- metadata.typeIdentifier = results.typeIdentifier
- metadata.serverUrlFileName = utilityFileSystem.createServerUrl(serverUrl: metadata.serverUrl, fileName: metadata.fileName)
-
- // CHUNK
- if metadata.size > self.global.chunkSize {
- metadata.chunk = self.global.chunkSize
- } else {
- metadata.chunk = 0
- }
- // E2EE
- metadata.e2eEncrypted = metadata.isDirectoryE2EE
-
- self.counterUploaded += 1
-
- if metadata.isDirectoryE2EE {
- error = await NCNetworkingE2EEUpload().upload(metadata: metadata,
- session: session,
- controller: self,
- banner: banner,
- stageBanner: nil,
- tokenBanner: self.token)
- } else if metadata.chunk > 0 {
- banner?.update(payload: LucidBannerPayload.Update(systemImage: "gearshape.arrow.triangle.2.circlepath",
- imageAnimation: .rotate),
- for: self.token)
- let task = Task { () -> (account: String, file: NKFile?, error: NKError) in
- let results = await NCNetworking.shared.uploadChunkFile(metadata: metadata) { total, counter in
- Task {@MainActor in
- self.banner?.update(payload: LucidBannerPayload.Update(progress: Double(counter) / Double(total)), for: self.token)
- }
- } uploadStart: { _ in
- Task {@MainActor in
- let payload = LucidBannerPayload.Update(
- systemImage: "arrowshape.up.circle",
- imageAnimation: .breathe
- )
- self.banner?.update(payload: payload, for: self.token)
- }
- } uploadProgressHandler: { _, _, progress in
- Task {@MainActor in
- self.banner?.update(payload: LucidBannerPayload.Update(progress: progress), for: self.token)
- }
- } assembling: {
- Task {@MainActor in
- let payload = LucidBannerPayload.Update(
- systemImage: "gearshape.arrow.triangle.2.circlepath",
- imageAnimation: .rotate
- )
- self.banner?.update(payload: payload, for: self.token)
- }
- }
-
- return results
- }
-
- let results = await task.value
- error = results.error
- } else {
- let fileNameLocalPath = utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId,
- fileName: metadata.fileName,
- userId: metadata.userId,
- urlBase: metadata.urlBase)
-
- let results = await NCNetworking.shared.uploadFile(account: metadata.account,
- fileNameLocalPath: fileNameLocalPath,
- serverUrlFileName: metadata.serverUrlFileName,
- creationDate: metadata.creationDate as Date,
- dateModificationFile: metadata.date as Date) { _ in
- } progressHandler: { _, _, fractionCompleted in
- Task {@MainActor in
- self.banner?.update(payload: LucidBannerPayload.Update(progress: fractionCompleted),
- for: self.token)
- }
- }
- error = results.error
- }
-
- return error
- }
-}
-
-extension NCShareExtension {
- /// Require authentication once per foreground session without reopening after dismissal.
- private func requestPasscodeIfNeeded() {
- guard NCPreferences().presentPasscode,
- !isPasscodeAuthenticated,
- !isPresentingPasscode else { return }
- isPresentingPasscode = true
- // Embed in the content controller; the helper hides the outer navigation bar.
- NCPasscodeView.embed(in: self) { [weak self] result in
- guard let self else { return }
- self.isPresentingPasscode = false
- switch result {
- case .authenticated:
- self.isPasscodeAuthenticated = true
- _ = self.validateAccount()
- case .cancelled, .reset, .configured, .disabled:
- self.cancel(with: .noAccount)
- }
- }
- }
-}
diff --git a/Share/NCShareExtensionData.swift b/Share/NCShareExtensionData.swift
deleted file mode 100644
index 1861a4cfea..0000000000
--- a/Share/NCShareExtensionData.swift
+++ /dev/null
@@ -1,47 +0,0 @@
-// SPDX-FileCopyrightText: Nextcloud GmbH
-// SPDX-FileCopyrightText: 2025 Marino Faggiana
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-import UIKit
-import NextcloudKit
-
-class NCShareExtensionData: NSObject {
- static let shared = NCShareExtensionData()
-
- private var tblAccount: tableAccount?
-
- override init() {
- self.tblAccount = NCManageDatabase.shared.getActiveTableAccount()
- if let account = self.tblAccount?.account {
- Task { @MainActor in
- if let capabilities = await NCManageDatabase.shared.getCapabilities(account: account) {
- NCBrandColor.shared.settingThemingColor(account: account, capabilities: capabilities)
- }
- }
- }
- }
-
- func setSessionAccount(_ account: String) async -> NCSession.Session {
- if account != self.tblAccount?.account,
- let tblAccount = await NCManageDatabase.shared.getTableAccountAsync(account: account) {
- self.tblAccount = tblAccount
- if let capabilities = await NCManageDatabase.shared.getCapabilities(account: account) {
- NCBrandColor.shared.settingThemingColor(account: account, capabilities: capabilities)
- }
- }
-
- return getSession()
- }
-
- func getSession() -> NCSession.Session {
- if let tblAccount {
- return NCSession.Session(account: tblAccount.account, urlBase: tblAccount.urlBase, user: tblAccount.user, userId: tblAccount.userId)
- } else {
- return NCSession.Session(account: "", urlBase: "", user: "", userId: "")
- }
- }
-
- func getTblAccoun() -> tableAccount? {
- return self.tblAccount
- }
-}
diff --git a/Share/NCShareLoaderItemProvider.swift b/Share/NCShareLoaderItemProvider.swift
new file mode 100644
index 0000000000..0c9056efbf
--- /dev/null
+++ b/Share/NCShareLoaderItemProvider.swift
@@ -0,0 +1,384 @@
+// SPDX-FileCopyrightText: Nextcloud GmbH
+// SPDX-FileCopyrightText: 2026 Marino Faggiana
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import UIKit
+import UniformTypeIdentifiers
+
+/// Imports one representation per attachment, in source order. File copies are
+/// completed inside provider callbacks, before iOS releases its temporary URLs.
+@MainActor
+final class NCShareLoaderItemProvider {
+ struct Output: Sendable {
+ var fileNames: [String] = []
+ var failures: [String] = []
+ }
+
+ private enum LoadedURL: Sendable {
+ case file(String)
+ case remote(URL)
+ }
+
+ /// NSItemProvider does not guarantee a completion callback after cancellation.
+ /// This gate resumes exactly once and synchronizes only callback state.
+ private final class Pending: @unchecked Sendable {
+ private let lock = NSLock()
+ private var continuation: CheckedContinuation?
+ private var cancellation: (@Sendable () -> Void)?
+ private var finished = false
+ private var cancelled = false
+
+ private var isCancelled: Bool {
+ lock.lock()
+ defer { lock.unlock() }
+ return cancelled
+ }
+
+ func install(_ continuation: CheckedContinuation) -> Bool {
+ lock.lock()
+ let cancelled = cancelled
+ if !cancelled { self.continuation = continuation }
+ lock.unlock()
+ if cancelled { continuation.resume(throwing: CancellationError()) }
+ return !cancelled
+ }
+
+ func setCancellation(_ cancellation: @escaping @Sendable () -> Void) {
+ lock.lock()
+ let cancelled = cancelled
+ if !finished { self.cancellation = cancellation }
+ lock.unlock()
+ if cancelled { cancellation() }
+ }
+
+ func finish(_ operation: () throws -> Value) {
+ guard !isCancelled else { return }
+ let result = Result(catching: operation)
+ lock.lock()
+ guard !finished else { lock.unlock(); return }
+ finished = true
+ let continuation = continuation
+ self.continuation = nil
+ cancellation = nil
+ lock.unlock()
+ continuation?.resume(with: result)
+ }
+
+ func cancel() {
+ lock.lock()
+ guard !finished else { lock.unlock(); return }
+ finished = true
+ cancelled = true
+ let continuation = continuation
+ let cancellation = cancellation
+ self.continuation = nil
+ self.cancellation = nil
+ lock.unlock()
+ cancellation?()
+ continuation?.resume(throwing: CancellationError())
+ }
+ }
+
+ private static func request(_ start: (Pending) -> (@Sendable () -> Void)) async throws -> Value {
+ let pending = Pending()
+ return try await withTaskCancellationHandler {
+ try await withCheckedThrowingContinuation { continuation in
+ guard pending.install(continuation) else { return }
+ pending.setCancellation(start(pending))
+ }
+ } onCancel: {
+ pending.cancel()
+ }
+ }
+
+ static func load(items: [NSExtensionItem], directory: URL) async throws -> Output {
+ try Task.checkCancellation()
+ try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
+ do {
+ var output = Output()
+ // Sequential imports bound peak memory use and preserve attachment order.
+ for (index, provider) in items.flatMap({ $0.attachments ?? [] }).enumerated() {
+ try Task.checkCancellation()
+ do {
+ let name = try await load(provider: provider, index: index, directory: directory)
+ output.fileNames.append(name)
+ } catch {
+ let format = NSLocalizedString("_share_import_item_error_", comment: "")
+ output.failures.append(String(format: format, index + 1, error.localizedDescription))
+ }
+ try Task.checkCancellation()
+ }
+ return output
+ } catch {
+ try? FileManager.default.removeItem(at: directory)
+ throw error
+ }
+ }
+
+ private static func load(provider: NSItemProvider, index: Int, directory: URL) async throws -> String {
+ let suggestedName = provider.suggestedName
+ let fallbackName = "Shared item \(index + 1)"
+ var lastError: Error = CocoaError(.featureUnsupported)
+
+ if provider.hasItemConformingToTypeIdentifier(UTType.fileURL.identifier),
+ provider.canLoadObject(ofClass: NSURL.self) {
+ // A file URL is a reference to a file, not data to save as a .file-url.
+ do {
+ return try await loadLink(provider: provider, name: suggestedName, fallbackName: fallbackName, directory: directory)
+ } catch {
+ lastError = error
+ }
+ try Task.checkCancellation()
+ }
+
+ // These are alternative representations of the same attachment, not
+ // separate files. Stop at the first successful import and prefer a file
+ // representation over loading the entire payload into memory.
+ let types = provider.registeredTypeIdentifiers.compactMap { UTType($0) }.filter {
+ $0.conforms(to: .data) && !$0.conforms(to: .url) && !$0.conforms(to: .plainText)
+ }
+ for type in types {
+ try Task.checkCancellation()
+ do {
+ return try await loadFile(provider: provider, type: type, name: suggestedName, fallbackName: fallbackName, directory: directory)
+ } catch {
+ lastError = error
+ }
+ try Task.checkCancellation()
+ do {
+ return try await loadData(provider: provider, type: type, name: suggestedName, fallbackName: fallbackName, directory: directory)
+ } catch {
+ lastError = error
+ }
+ }
+
+ try Task.checkCancellation()
+ if provider.canLoadObject(ofClass: UIImage.self) {
+ do {
+ return try await loadImage(provider: provider, name: suggestedName, fallbackName: fallbackName, directory: directory)
+ } catch {
+ lastError = error
+ }
+ }
+ try Task.checkCancellation()
+ if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier),
+ provider.canLoadObject(ofClass: NSURL.self) {
+ do {
+ return try await loadLink(provider: provider, name: suggestedName, fallbackName: fallbackName, directory: directory)
+ } catch {
+ lastError = error
+ }
+ }
+ try Task.checkCancellation()
+ if provider.canLoadObject(ofClass: NSString.self) {
+ do {
+ return try await loadText(provider: provider, name: suggestedName, fallbackName: fallbackName, directory: directory)
+ } catch {
+ lastError = error
+ }
+ }
+ try Task.checkCancellation()
+ if provider.canLoadObject(ofClass: NSAttributedString.self) {
+ do {
+ return try await loadAttributedText(provider: provider, name: suggestedName, fallbackName: fallbackName, directory: directory)
+ } catch {
+ lastError = error
+ }
+ }
+ try Task.checkCancellation()
+ throw lastError
+ }
+
+ private static func loadFile(provider: NSItemProvider, type: UTType, name: String?, fallbackName: String, directory: URL) async throws -> String {
+ try await request { pending in
+ let progress = provider.loadFileRepresentation(forTypeIdentifier: type.identifier) { url, error in
+ pending.finish {
+ if let error { throw error }
+ guard let url else { throw CocoaError(.fileReadUnknown) }
+ return try copy(source: url, name: name, fallbackName: fallbackName, type: type, directory: directory)
+ }
+ }
+ return { progress.cancel() }
+ }
+ }
+
+ private static func loadData(provider: NSItemProvider, type: UTType, name: String?, fallbackName: String, directory: URL) async throws -> String {
+ try await request { pending in
+ let progress = provider.loadDataRepresentation(forTypeIdentifier: type.identifier) { data, error in
+ pending.finish {
+ if let error { throw error }
+ guard let data else { throw CocoaError(.fileReadUnknown) }
+ return try write(data, name: name ?? fallbackName, type: type, directory: directory)
+ }
+ }
+ return { progress.cancel() }
+ }
+ }
+
+ private static func loadImage(provider: NSItemProvider, name: String?, fallbackName: String, directory: URL) async throws -> String {
+ try await request { pending in
+ let progress = provider.loadObject(ofClass: UIImage.self) { object, error in
+ pending.finish {
+ if let error { throw error }
+ guard let image = object as? UIImage,
+ let data = autoreleasepool(invoking: { image.pngData() }) else { throw CocoaError(.fileReadCorruptFile) }
+ return try write(data, name: name ?? fallbackName, type: .png, directory: directory)
+ }
+ }
+ return { progress.cancel() }
+ }
+ }
+
+ private static func loadText(provider: NSItemProvider, name: String?, fallbackName: String, directory: URL) async throws -> String {
+ try await request { pending in
+ let progress = provider.loadObject(ofClass: NSString.self) { object, error in
+ pending.finish {
+ if let error { throw error }
+ guard let text = object as? String else { throw CocoaError(.fileReadCorruptFile) }
+ return try write(Data(text.utf8), name: name ?? fallbackName, type: .utf8PlainText, directory: directory)
+ }
+ }
+ return { progress.cancel() }
+ }
+ }
+
+ /// Last-resort text fallback for Notes-style providers. A file or data
+ /// representation (for example RTF) is preferred so formatting is preserved.
+ private static func loadAttributedText(provider: NSItemProvider, name: String?, fallbackName: String, directory: URL) async throws -> String {
+ try await request { pending in
+ let progress = provider.loadObject(ofClass: NSAttributedString.self) { object, error in
+ pending.finish {
+ if let error { throw error }
+ guard let text = object as? NSAttributedString else { throw CocoaError(.fileReadCorruptFile) }
+ return try write(Data(text.string.utf8), name: name ?? fallbackName, type: .utf8PlainText, directory: directory)
+ }
+ }
+ return { progress.cancel() }
+ }
+ }
+
+ private static func loadLink(provider: NSItemProvider, name: String?, fallbackName: String, directory: URL) async throws -> String {
+ let content: LoadedURL = try await request { pending in
+ let progress = provider.loadObject(ofClass: NSURL.self) { object, error in
+ pending.finish {
+ if let error { throw error }
+ guard let url = object as? URL else { throw URLError(.badURL) }
+ if url.isFileURL {
+ // Even a provider advertising only public.url may return
+ // a file reference. Copy it before this callback returns.
+ let name = try copyReferencedFile(source: url, name: name, fallbackName: fallbackName, directory: directory)
+ return .file(name)
+ }
+ return .remote(url)
+ }
+ }
+ return { progress.cancel() }
+ }
+ switch content {
+ case .file(let name):
+ return name
+ case .remote(let url):
+ guard ["https", "http"].contains(url.scheme?.lowercased() ?? "") else {
+ throw URLError(.unsupportedURL)
+ }
+ // Preserve the previous behavior: a shared web link uploads the
+ // downloaded resource. URLSession streams it to disk, not Data.
+ try Task.checkCancellation()
+ return try await download(url: url, name: name, fallbackName: fallbackName, directory: directory)
+ }
+ }
+
+ private static func download(url: URL, name: String?, fallbackName: String, directory: URL) async throws -> String {
+ try await request { pending in
+ let task = URLSession.shared.downloadTask(with: url) { temporaryURL, response, error in
+ pending.finish {
+ if let error { throw error }
+ guard let response = response as? HTTPURLResponse,
+ (200..<300).contains(response.statusCode),
+ let temporaryURL else { throw URLError(.badServerResponse) }
+ let type = response.mimeType.flatMap { UTType(mimeType: $0) }
+ let destination = destination(name: name ?? response.suggestedFilename ?? fallbackName, type: type, directory: directory)
+ try FileManager.default.copyItem(at: temporaryURL, to: destination)
+ return destination.lastPathComponent
+ }
+ }
+ task.resume()
+ return { task.cancel() }
+ }
+ }
+
+ nonisolated private static func write(_ data: Data, name: String, type: UTType, directory: URL) throws -> String {
+ let url = destination(name: name, type: type, directory: directory)
+ try data.write(to: url, options: .atomic)
+ return url.lastPathComponent
+ }
+
+ nonisolated private static func copyReferencedFile(source: URL, name: String?, fallbackName: String, directory: URL) throws -> String {
+ let access = source.startAccessingSecurityScopedResource()
+ defer { if access { source.stopAccessingSecurityScopedResource() } }
+ var coordinationError: NSError?
+ var result: Result?
+ // Coordinate references to external documents, including File Provider
+ // files that may need to be materialized before they can be read.
+ NSFileCoordinator().coordinate(readingItemAt: source, options: [], error: &coordinationError) { readableURL in
+ result = Result { try copy(source: readableURL, name: name, fallbackName: fallbackName, type: nil, directory: directory) }
+ }
+ if let coordinationError { throw coordinationError }
+ guard let result else { throw CocoaError(.fileReadUnknown) }
+ return try result.get()
+ }
+
+ nonisolated private static func copy(source: URL, name: String?, fallbackName: String, type: UTType?, directory: URL) throws -> String {
+ let access = source.startAccessingSecurityScopedResource()
+ defer { if access { source.stopAccessingSecurityScopedResource() } }
+ let values = try source.resourceValues(forKeys: [.isRegularFileKey])
+ guard values.isRegularFile == true else { throw CocoaError(.featureUnsupported) }
+ let sourceName = source.lastPathComponent.isEmpty ? fallbackName : source.lastPathComponent
+ let sourceType = UTType(filenameExtension: source.pathExtension)
+ let contentType: UTType?
+ if let sourceType, sourceType.conforms(to: type ?? .data) {
+ contentType = sourceType
+ } else {
+ contentType = type
+ }
+ let destination = destination(name: name ?? sourceName, type: contentType, directory: directory)
+ do {
+ try FileManager.default.copyItem(at: source, to: destination)
+ return destination.lastPathComponent
+ } catch {
+ // A failed copy may leave a partial file behind.
+ try? FileManager.default.removeItem(at: destination)
+ throw error
+ }
+ }
+
+ nonisolated private static func destination(name: String, type: UTType?, directory: URL) -> URL {
+ // Suggested names are untrusted paths. Keep only a single filename.
+ var name = (name as NSString).lastPathComponent
+ .components(separatedBy: .controlCharacters).joined()
+ .replacingOccurrences(of: "\\", with: "_")
+ .replacingOccurrences(of: "/", with: "_")
+ if name.isEmpty || name == "." || name == ".." { name = "Shared item" }
+ // The plain-text fallback writes UTF-8 bytes even if that UTType has no filename tag.
+ if let type, let preferredExtension = type == .utf8PlainText ? "txt" : type.preferredFilenameExtension {
+ let existingExtension = (name as NSString).pathExtension
+ if existingExtension.isEmpty {
+ name += "." + preferredExtension
+ } else if UTType(filenameExtension: existingExtension)?.conforms(to: type) != true {
+ name = (name as NSString).deletingPathExtension + "." + preferredExtension
+ }
+ }
+ let ext = (name as NSString).pathExtension
+ var stem = ext.isEmpty ? name : (name as NSString).deletingPathExtension
+ // Leave room for an extension and collision suffix on common file systems.
+ while stem.utf8.count > 180 { stem.removeLast() }
+ let suffix = ext.isEmpty ? "" : "." + String(ext.prefix(32))
+ var candidate = directory.appendingPathComponent(stem + suffix)
+ var counter = 1
+ while FileManager.default.fileExists(atPath: candidate.path) {
+ candidate = directory.appendingPathComponent(stem + " (\(counter))" + suffix)
+ counter += 1
+ }
+ return candidate
+ }
+}
diff --git a/Share/NCShareRecentFoldersStore.swift b/Share/NCShareRecentFoldersStore.swift
new file mode 100644
index 0000000000..fc3e074cd2
--- /dev/null
+++ b/Share/NCShareRecentFoldersStore.swift
@@ -0,0 +1,46 @@
+// SPDX-FileCopyrightText: Nextcloud GmbH
+// SPDX-FileCopyrightText: 2026 Marino Faggiana
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import Foundation
+
+@MainActor
+struct NCShareRecentFoldersStore {
+ struct Item: Codable, Equatable, Sendable {
+ let serverUrl: String
+ let name: String
+ let ocId: String
+ let isEncrypted: Bool
+ }
+
+ private let limit = 3
+ private let defaults: UserDefaults
+ private let keyPrefix = "share.recentFolders.v1."
+
+ init(defaults: UserDefaults) {
+ self.defaults = defaults
+ }
+
+ func folders(account: String) -> [Item] {
+ guard let data = defaults.data(forKey: keyPrefix + account),
+ let items = try? JSONDecoder().decode([Item].self, from: data) else { return [] }
+ var seen: Set = []
+ return Array(items.filter { !$0.serverUrl.isEmpty && seen.insert($0.serverUrl).inserted }.prefix(limit))
+ }
+
+ /// Call only after the complete upload operation has succeeded.
+ func record(_ item: Item, account: String) {
+ guard !account.isEmpty, !item.serverUrl.isEmpty else { return }
+ let updated = [item] + folders(account: account).filter { $0.serverUrl != item.serverUrl }
+ save(Array(updated.prefix(limit)), account: account)
+ }
+
+ func remove(serverUrl: String, account: String) {
+ save(folders(account: account).filter { $0.serverUrl != serverUrl }, account: account)
+ }
+
+ private func save(_ items: [Item], account: String) {
+ guard let data = try? JSONEncoder().encode(items) else { return }
+ defaults.set(data, forKey: keyPrefix + account)
+ }
+}
diff --git a/Share/NCShareSessionStore.swift b/Share/NCShareSessionStore.swift
new file mode 100644
index 0000000000..90547c2014
--- /dev/null
+++ b/Share/NCShareSessionStore.swift
@@ -0,0 +1,28 @@
+// SPDX-FileCopyrightText: Nextcloud GmbH
+// SPDX-FileCopyrightText: 2026 Marino Faggiana
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import Foundation
+import NextcloudKit
+
+@MainActor
+final class NCShareSessionStore {
+ private(set) var account: tableAccount? = NCManageDatabase.shared.getActiveTableAccount()
+
+ /// Selection belongs to this share request; do not change the app's active account.
+ /// Re-read even the current account because it may have been removed by the app.
+ func setSessionAccount(_ account: String) async -> NCSession.Session? {
+ guard let selected = await NCManageDatabase.shared.getTableAccountAsync(account: account) else { return nil }
+ guard !Task.isCancelled else { return nil }
+ self.account = selected
+ return getSession()
+ }
+
+ func getSession() -> NCSession.Session {
+ if let account {
+ return NCSession.Session(account: account.account, urlBase: account.urlBase, user: account.user, userId: account.userId)
+ }
+ return NCSession.Session(account: "", urlBase: "", user: "", userId: "")
+ }
+
+}
diff --git a/Share/Share-Bridging-Header.h b/Share/Share-Bridging-Header.h
index 68997c812e..fce95c266c 100644
--- a/Share/Share-Bridging-Header.h
+++ b/Share/Share-Bridging-Header.h
@@ -5,4 +5,3 @@
// Use this file to import your target's public headers that you would like to expose to Swift.
#import "NCEndToEndEncryption.h"
-#import "UIImage+animatedGIF.h"
diff --git a/Share/View/NCShareAccountMenu.swift b/Share/View/NCShareAccountMenu.swift
new file mode 100644
index 0000000000..d701bb60fe
--- /dev/null
+++ b/Share/View/NCShareAccountMenu.swift
@@ -0,0 +1,67 @@
+// SPDX-FileCopyrightText: Nextcloud GmbH
+// SPDX-FileCopyrightText: 2026 Marino Faggiana
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import SwiftUI
+
+@MainActor
+struct NCShareAccountMenu: View {
+ @ObservedObject var model: NCShareViewModel
+
+ var body: some View {
+ let selected = model.accounts.first(where: \.isSelected)
+ Menu {
+ Picker(NSLocalizedString("_account_select_", comment: ""), selection: Binding(
+ get: { model.accounts.first(where: \.isSelected)?.id ?? "" },
+ set: { model.selectAccount($0) }
+ )) {
+ ForEach(model.accounts) { account in
+ Label {
+ Text(verbatim: account.name)
+ Text(verbatim: account.user + " · " + account.serverUrl)
+ } icon: {
+ avatar(account.avatar)
+ }
+ .tag(account.id)
+ }
+ }
+ .pickerStyle(.inline)
+ } label: {
+ avatar(selected?.avatar)
+ .frame(minWidth: 44, minHeight: 44)
+ }
+ .accessibilityLabel(NSLocalizedString("_account_select_", comment: ""))
+ .accessibilityValue(Text(verbatim: selected?.name ?? ""))
+ .buttonStyle(.plain)
+ .dynamicTypeSize(.xSmall ... .accessibility2)
+ .disabled(model.accounts.count < 2)
+
+ }
+
+ @ViewBuilder
+ private func avatar(_ image: UIImage?) -> some View {
+ if let image {
+ Image(uiImage: image)
+ .renderingMode(.original)
+ .resizable()
+ .scaledToFit()
+ .frame(width: 30, height: 30)
+ .clipShape(Circle())
+ } else {
+ Image(systemName: "person.crop.circle")
+ }
+ }
+}
+
+#if DEBUG && SHARE_PREVIEWS
+#Preview("Accounts") {
+ NavigationStack {
+ Text("Share")
+ .toolbar {
+ ToolbarItem(placement: .topBarLeading) {
+ NCShareAccountMenu(model: .preview())
+ }
+ }
+ }
+}
+#endif
diff --git a/Share/View/NCShareFilenameView.swift b/Share/View/NCShareFilenameView.swift
new file mode 100644
index 0000000000..781226fa82
--- /dev/null
+++ b/Share/View/NCShareFilenameView.swift
@@ -0,0 +1,60 @@
+// SPDX-FileCopyrightText: Nextcloud GmbH
+// SPDX-FileCopyrightText: 2026 Marino Faggiana
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import SwiftUI
+
+/// Keep the actual extension in its own text run, as in UIKit's setBidiSafeFilename.
+/// Only presentation changes; file operations always use the original name.
+struct NCShareFilenameView: View {
+ let name: String
+
+ var body: some View {
+ let filename = name as NSString
+ let ext = filename.pathExtension
+ let base = filename.deletingPathExtension
+ HStack(alignment: .firstTextBaseline, spacing: 0) {
+ if ext.isEmpty || base.isEmpty {
+ Text(verbatim: Self.displayName(name))
+ .lineLimit(1)
+ .truncationMode(.middle)
+ } else {
+ Text(verbatim: Self.displayName(base))
+ .lineLimit(1)
+ .truncationMode(.middle)
+ Text(verbatim: "." + Self.displayName(ext))
+ .lineLimit(1)
+ .fixedSize(horizontal: true, vertical: false)
+ }
+ }
+ .accessibilityElement(children: .ignore)
+ .accessibilityLabel(Text(verbatim: Self.displayName(name)))
+ }
+
+ /// Remove explicit direction controls from display text, including dialog titles.
+ /// Ordinary Arabic/Hebrew letters retain their natural text direction.
+ static func displayName(_ name: String) -> String {
+ String(String.UnicodeScalarView(name.unicodeScalars.filter {
+ switch $0.value {
+ case 0x061C, 0x200E...0x200F, 0x202A...0x202E, 0x2066...0x2069:
+ return false
+ default:
+ return true
+ }
+ }))
+ }
+}
+
+#if DEBUG && SHARE_PREVIEWS
+#Preview("File names and BIDI overrides") {
+ VStack(alignment: .leading, spacing: 16) {
+ NCShareFilenameView(name: "Report.pdf")
+ NCShareFilenameView(name: "photo\u{202E}gpj.exe")
+ NCShareFilenameView(name: "تقرير.pdf")
+ NCShareFilenameView(name: ".gitignore")
+ NCShareFilenameView(name: "A very long file name with a visible extension.pdf")
+ }
+ .frame(width: 240)
+ .padding()
+}
+#endif
diff --git a/Share/View/NCShareUploadButton.swift b/Share/View/NCShareUploadButton.swift
new file mode 100644
index 0000000000..ca82b2e1b0
--- /dev/null
+++ b/Share/View/NCShareUploadButton.swift
@@ -0,0 +1,34 @@
+// SPDX-FileCopyrightText: Nextcloud GmbH
+// SPDX-FileCopyrightText: 2026 Marino Faggiana
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import SwiftUI
+
+@MainActor
+struct NCShareUploadButton: View {
+ let fileCount: Int
+ var isPreparing = false
+ var title: String?
+ let action: @MainActor () -> Void
+
+ var body: some View {
+ Button(action: action) {
+ HStack(spacing: 8) {
+ if isPreparing {
+ ProgressView()
+ .tint(.white)
+ Text(NSLocalizedString("_share_conflict_checking_", comment: ""))
+ } else {
+ Text(title ?? String.localizedStringWithFormat(NSLocalizedString("_share_upload_to_", comment: ""), fileCount))
+ }
+ }
+ .lineLimit(2)
+ .truncationMode(.middle)
+ .multilineTextAlignment(.center)
+ .frame(maxWidth: .infinity)
+ }
+ .buttonStyle(.borderedProminent)
+ .buttonBorderShape(.roundedRectangle(radius: 12))
+ .padding(.horizontal, 16)
+ }
+}
diff --git a/Share/View/NCShareUploadConflictView.swift b/Share/View/NCShareUploadConflictView.swift
new file mode 100644
index 0000000000..118f3437d7
--- /dev/null
+++ b/Share/View/NCShareUploadConflictView.swift
@@ -0,0 +1,116 @@
+// SPDX-FileCopyrightText: Nextcloud GmbH
+// SPDX-FileCopyrightText: 2026 Marino Faggiana
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import SwiftUI
+
+@MainActor
+struct NCShareUploadConflictView: View {
+ let confirmation: NCShareUploadConfirmation
+ let resolve: @MainActor (NCShareUploadChoice?) -> Void
+
+ var body: some View {
+ let renamedItems = confirmation.renamedItems
+ NavigationStack {
+ ScrollView {
+ VStack(alignment: .leading, spacing: 8) {
+ Text(NSLocalizedString("_share_conflict_new_names_", comment: ""))
+ .font(.subheadline.weight(.semibold))
+ .foregroundStyle(.secondary)
+ .accessibilityAddTraits(.isHeader)
+ .padding(.horizontal, 12)
+ VStack(alignment: .leading, spacing: 0) {
+ ForEach(renamedItems, id: \.sourceName) { item in
+ VStack(alignment: .leading, spacing: 4) {
+ NCShareFilenameView(name: item.sourceName)
+ .cappedFont(.caption, maxDynamicType: .accessibility2)
+ .foregroundStyle(.secondary)
+ NCShareFilenameView(name: item.fileName)
+ }
+ .padding(.vertical, 12)
+ if item.sourceName != renamedItems.last?.sourceName {
+ Divider()
+ }
+ }
+ }
+ .padding(.horizontal, 12)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .background(Color(uiColor: .secondarySystemGroupedBackground), in: RoundedRectangle(cornerRadius: 12))
+ Text(confirmation.isBatch
+ ? NSLocalizedString("_share_conflict_batch_message_", comment: "")
+ : String(format: NSLocalizedString("_share_conflict_single_message_", comment: ""), NCShareFilenameView.displayName(confirmation.items[0].sourceName)))
+ .font(.footnote)
+ .foregroundStyle(.secondary)
+ .padding(.horizontal, 12)
+ }
+ .padding(.horizontal, 16)
+ .padding(.vertical, 12)
+ }
+ .safeAreaInset(edge: .bottom, spacing: 0) {
+ VStack(spacing: 16) {
+ if !confirmation.isBatch, confirmation.replacementFileName != nil {
+ Button(role: .destructive) {
+ resolve(.replace)
+ } label: {
+ Text(NSLocalizedString("_share_conflict_replace_", comment: ""))
+ .multilineTextAlignment(.center)
+ .frame(maxWidth: .infinity)
+ }
+ .buttonStyle(.bordered)
+ .buttonBorderShape(.roundedRectangle(radius: 12))
+ .tint(.red)
+ .padding(.horizontal, 16)
+ }
+
+ NCShareUploadButton(
+ fileCount: confirmation.items.count,
+ title: confirmation.isBatch ? nil : NSLocalizedString("_share_conflict_upload_with_new_name_", comment: "")
+ ) {
+ resolve(.keepBoth)
+ }
+ }
+ .padding(.top, 10)
+ .padding(.bottom, 16)
+ .background(Color(uiColor: .systemGroupedBackground))
+ }
+ .background(Color(uiColor: .systemGroupedBackground))
+ .navigationTitle(NSLocalizedString("_share_conflict_title_", comment: ""))
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .cancellationAction) {
+ Button(NSLocalizedString("_cancel_", comment: "")) { resolve(nil) }
+ }
+ }
+ }
+ }
+}
+
+#if DEBUG && SHARE_PREVIEWS
+#Preview("Single file conflict") {
+ NCShareUploadConflictView(
+ confirmation: NCShareUploadConfirmation(
+ account: "personal",
+ serverUrl: "https://cloud.example.com/Documents",
+ items: [NCShareUploadItem(sourceName: "Report.pdf", fileName: "Report (1).pdf")],
+ replacementFileName: "Report.pdf",
+ replacementETag: "preview-etag",
+ requiresConfirmation: true),
+ resolve: { _ in })
+}
+
+#Preview("Multiple file conflicts") {
+ NCShareUploadConflictView(
+ confirmation: NCShareUploadConfirmation(
+ account: "personal",
+ serverUrl: "https://cloud.example.com/Documents",
+ items: [
+ NCShareUploadItem(sourceName: "Report.pdf", fileName: "Report (1).pdf"),
+ NCShareUploadItem(sourceName: "Photo.jpg", fileName: "Photo (2).jpg"),
+ NCShareUploadItem(sourceName: "Notes.txt", fileName: "Notes.txt")
+ ],
+ replacementFileName: nil,
+ replacementETag: nil,
+ requiresConfirmation: true),
+ resolve: { _ in })
+}
+#endif
diff --git a/Share/View/NCShareView.swift b/Share/View/NCShareView.swift
new file mode 100644
index 0000000000..c53dea4305
--- /dev/null
+++ b/Share/View/NCShareView.swift
@@ -0,0 +1,428 @@
+// SPDX-FileCopyrightText: Nextcloud GmbH
+// SPDX-FileCopyrightText: 2026 Marino Faggiana
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import SwiftUI
+
+@MainActor
+struct NCShareView: View {
+ @ObservedObject var model: NCShareViewModel
+ @State private var fileActions: String?
+
+ private var canShowContent: Bool { model.rootFolder != nil && model.isAuthenticated }
+
+ private var isRefreshing: Bool {
+ model.isLoading || model.isSwitchingAccount || model.isLoadingAccounts ||
+ model.isLoadingCapabilities || !model.folderTasks.isEmpty
+ }
+
+ var body: some View {
+ NavigationStack {
+ VStack(spacing: 0) {
+ statusView
+ .frame(maxWidth: .infinity)
+ .background(Color(uiColor: .secondarySystemGroupedBackground), in: RoundedRectangle(cornerRadius: 12))
+ .padding(.horizontal, 16)
+ if canShowContent {
+ GeometryReader { geometry in
+ let sideBySide = UIDevice.current.userInterfaceIdiom == .phone && geometry.size.width > geometry.size.height
+ VStack(spacing: 0) {
+ if sideBySide {
+ createFolderHeader
+ .padding(.horizontal, 16)
+ HStack(spacing: 10) {
+ folderList(showHeader: false, horizontalInset: 0)
+ .frame(maxWidth: .infinity)
+ fileList(fillsHeight: true, horizontalInset: 0)
+ .frame(width: min(320, geometry.size.width * 0.38))
+ }
+ .padding(.horizontal, 16)
+ } else {
+ folderList()
+ }
+ VStack(spacing: 10) {
+ if !sideBySide {
+ fileList()
+ }
+ uploadButton
+ }
+ .padding(.top, 10)
+ .padding(.bottom, 16)
+ .background(Color(uiColor: .systemGroupedBackground))
+ }
+ }
+ } else {
+ Spacer(minLength: 0)
+ }
+ }
+ .background(Color(uiColor: .systemGroupedBackground))
+ .dynamicTypeSize(.xSmall ... .accessibility2)
+ .background(Color(uiColor: .systemBackground))
+ .sheet(isPresented: $model.isShowingUploadConfirmation, onDismiss: model.uploadConfirmationDismissed) {
+ if let confirmation = model.pendingUploadConfirmation?.plan {
+ NCShareUploadConflictView(confirmation: confirmation, resolve: model.resolveUploadConfirmation)
+ }
+ }
+ .navigationTitle(String(format: NSLocalizedString("_share_save_to_", comment: "Share title; %@ is the cloud brand name"), NCBrandOptions.shared.brand))
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar(.visible, for: .navigationBar)
+ .toolbarBackground(.hidden, for: .navigationBar)
+ .toolbar {
+ ToolbarItemGroup(placement: .topBarLeading) {
+ if canShowContent {
+ HStack(spacing: 8) {
+ NCShareAccountMenu(model: model)
+ .disabled(model.isLoading || model.isPreparingUpload || model.isSwitchingAccount || model.isLoadingAccounts)
+ if isRefreshing {
+ ProgressView()
+ .accessibilityLabel(NSLocalizedString("_request_in_progress_", comment: ""))
+ .padding(.trailing, 8)
+ }
+ }
+ }
+ }
+ ToolbarItem(placement: .topBarTrailing) {
+ Button(NSLocalizedString("_cancel_", comment: "")) { model.cancel() }
+ }
+ }
+ }
+ }
+
+ // Show one message at a time in the dedicated top area, in priority order.
+ // Folder and attachment rows remain content-only even when a request fails.
+ @ViewBuilder
+ private var statusView: some View {
+ if !model.alertMessage.isEmpty {
+ statusError(model.alertMessage) { model.acknowledgeAlert() }
+ } else if !canShowContent {
+ ProgressView(NSLocalizedString("_request_in_progress_", comment: ""))
+ .cappedFont(.subheadline, maxDynamicType: .accessibility2)
+ .padding()
+ } else if !model.inputErrorMessage.isEmpty && !model.isLoadingAccounts {
+ statusError(model.inputErrorMessage) { model.acknowledgeInputError() }
+ } else if model.isOffline {
+ VStack(spacing: 8) {
+ Label(NSLocalizedString("_share_online_required_", comment: ""), systemImage: "wifi.slash")
+ .cappedFont(.subheadline, maxDynamicType: .accessibility2)
+ .multilineTextAlignment(.center)
+ Button(NSLocalizedString("_share_capabilities_retry_", comment: ""), action: model.retryConnection)
+ }
+ .padding()
+ } else if !model.capabilitiesErrorMessage.isEmpty {
+ VStack(spacing: 8) {
+ Text(verbatim: model.capabilitiesErrorMessage)
+ .cappedFont(.subheadline, maxDynamicType: .accessibility2)
+ .multilineTextAlignment(.leading)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ Button(NSLocalizedString("_share_capabilities_retry_", comment: ""), action: model.retryCapabilities)
+ .disabled(model.isSwitchingAccount)
+ }
+ .padding()
+ } else if let row = model.visibleFolderRows.first(where: { model.folderLoadErrors[$0.folder.serverUrl] != nil }),
+ let error = model.folderLoadErrors[row.folder.serverUrl] {
+ VStack(spacing: 8) {
+ Text(verbatim: error)
+ .cappedFont(.subheadline, maxDynamicType: .accessibility2)
+ .multilineTextAlignment(.center)
+ Button(NSLocalizedString("_share_retry_folder_", comment: "")) { model.retryFolder(row.folder) }
+ }
+ .padding()
+
+ } else if model.isPreparingInput {
+ ProgressView(NSLocalizedString("_share_import_preparing_", comment: ""))
+ .padding()
+ }
+ }
+
+ private func statusError(_ message: String, acknowledge: @escaping () -> Void) -> some View {
+ VStack(spacing: 8) {
+ Text(verbatim: message)
+ .cappedFont(.subheadline, maxDynamicType: .accessibility2)
+ .multilineTextAlignment(.center)
+ Button(NSLocalizedString("_ok_", comment: ""), action: acknowledge)
+ }
+ .padding()
+ }
+
+ private func folderList(showHeader: Bool = true, horizontalInset: CGFloat = 16) -> some View {
+ VStack(spacing: 0) {
+ if showHeader {
+ createFolderHeader
+ .padding(.horizontal, horizontalInset)
+ }
+ ScrollViewReader { proxy in
+ List {
+ Section {
+ if !model.recentFolders.isEmpty {
+ folderListTitle("_share_recent_folders_")
+ ForEach(model.recentFolders) { folder in
+ folderRow(folder, depth: nil)
+ }
+ }
+ folderListTitle("_share_all_folders_")
+ ForEach(model.visibleFolderRows) { row in
+ folderRow(row.folder, depth: row.depth)
+ .id(row.folder.serverUrl)
+ }
+ }
+ }
+ .listStyle(.plain)
+ .contentMargins(.vertical, 0, for: .scrollContent)
+ .scrollContentBackground(.hidden)
+ .environment(\.defaultMinListRowHeight, 0)
+ .background(Color(uiColor: .secondarySystemGroupedBackground))
+ .clipShape(RoundedRectangle(cornerRadius: 12))
+ .padding(.horizontal, horizontalInset)
+ .onChange(of: model.folderScrollTarget) { _, target in
+ guard let target else { return }
+ withAnimation { proxy.scrollTo(target, anchor: .center) }
+ model.folderScrollTarget = nil
+ }
+ }
+ }
+ .frame(maxHeight: .infinity)
+ .clipped()
+ .background(Color(uiColor: .systemGroupedBackground))
+ .opacity(isRefreshing ? 0.45 : 1)
+ .disabled(isRefreshing || model.isPreparingUpload || !model.isAccountReady)
+ }
+
+ private func folderListTitle(_ key: String) -> some View {
+ Text(NSLocalizedString(key, comment: ""))
+ .cappedFont(.caption, maxDynamicType: .accessibility2)
+ .foregroundStyle(.secondary)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.top, 8)
+ .padding(.bottom, 2)
+ .accessibilityAddTraits(.isHeader)
+ .listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 12))
+ .listRowSeparator(.hidden)
+ .listRowBackground(Color(uiColor: .secondarySystemGroupedBackground))
+ }
+
+ private var createFolderHeader: some View {
+ HStack {
+ Spacer()
+ Button(action: model.createFolder) {
+ HStack(spacing: 8) {
+ Text(NSLocalizedString("_create_folder_", comment: ""))
+ Image(systemName: "folder.badge.plus")
+ }
+ .cappedFont(.subheadline, maxDynamicType: .accessibility2)
+ .frame(minHeight: 50)
+ .contentShape(Rectangle())
+ }
+ .buttonStyle(.borderless)
+ .tint(.blue)
+ .disabled(!model.canUseUploadDestination)
+ }
+ .textCase(nil)
+ .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
+ }
+
+ private func folderRow(_ folder: NCShareFolderItem, depth: Int?) -> some View {
+ let isSelected = model.selectedFolder?.serverUrl == folder.serverUrl && model.isRecentFolderSelected == (depth == nil)
+ return HStack(spacing: 4) {
+ if let depth {
+ Button {
+ model.toggleFolder(folder)
+ } label: {
+ Image(systemName: model.expandedFolderURLs.contains(folder.serverUrl) ? "chevron.down" : "chevron.right")
+ .frame(width: 50, height: 50)
+ .contentShape(Rectangle())
+ }
+ .buttonStyle(.borderless)
+ .accessibilityLabel(String(format: NSLocalizedString(
+ model.expandedFolderURLs.contains(folder.serverUrl) ? "_share_collapse_folder_" : "_share_expand_folder_", comment: ""), folder.name))
+ .padding(.leading, CGFloat(min(depth, 8)) * 16)
+ } else {
+ Image(systemName: "clock")
+ .cappedFont(.subheadline, maxDynamicType: .accessibility2)
+ .foregroundStyle(.secondary)
+ .frame(width: 50, height: 50)
+ .accessibilityHidden(true)
+ }
+ Button {
+ model.selectFolder(folder, fromRecents: depth == nil)
+ } label: {
+ HStack(spacing: 8) {
+ if let image = folder.image {
+ Image(uiImage: image)
+ .renderingMode(.original)
+ .resizable()
+ .scaledToFit()
+ .frame(width: 26, height: 26)
+ .overlay(alignment: .topTrailing) {
+ if folder.isFavorite {
+ Image(systemName: "star.fill")
+ .resizable()
+ .scaledToFit()
+ .foregroundStyle(Color(uiColor: NCBrandColor.shared.yellowFavorite))
+ .frame(width: 10, height: 10)
+ .background {
+ Image(systemName: "star")
+ .resizable()
+ .scaledToFit()
+ .fontWeight(.thin)
+ .foregroundStyle(Color(uiColor: .systemBackground))
+ .frame(width: 12, height: 12)
+ }
+ .offset(x: 3, y: -3)
+ .accessibilityLabel(NSLocalizedString("_share_favorite_folder_", comment: ""))
+ }
+ }
+ }
+ VStack(alignment: .leading, spacing: 1) {
+ Text(verbatim: folder.name)
+ .cappedFont(.body, maxDynamicType: .accessibility2)
+ .foregroundStyle(Color(uiColor: .label))
+ .lineLimit(1)
+ if depth == nil {
+ Text(verbatim: folder.path)
+ .cappedFont(.caption2, maxDynamicType: .accessibility2)
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ .truncationMode(.middle)
+ }
+ }
+ Spacer(minLength: 4)
+ if isSelected {
+ Image(systemName: "checkmark")
+ .frame(width: 50, height: 50)
+ .accessibilityHidden(true)
+ }
+ }
+ .frame(minHeight: 50)
+ .contentShape(Rectangle())
+ }
+ .buttonStyle(.borderless)
+ .accessibilityAddTraits(isSelected ? .isSelected : [])
+ }
+ .background {
+ if isSelected {
+ RoundedRectangle(cornerRadius: 8)
+ .fill(Color.accentColor.opacity(0.1))
+ .padding(.horizontal, 5)
+ }
+ }
+ .listRowSeparator(.hidden)
+ .listRowInsets(EdgeInsets(top: 0, leading: 2, bottom: 0, trailing: 2))
+ .listRowBackground(Color(uiColor: .secondarySystemGroupedBackground))
+ }
+
+ private func fileList(fillsHeight: Bool = false, horizontalInset: CGFloat = 16) -> some View {
+ List(model.fileNames, id: \.self) { fileName in
+ Button {
+ fileActions = fileName
+ } label: {
+ HStack {
+ Label {
+ NCShareFilenameView(name: fileName)
+ } icon: {
+ Group {
+ if let thumbnail = model.fileThumbnails[fileName] {
+ Image(uiImage: thumbnail)
+ .resizable()
+ .scaledToFill()
+ } else {
+ Image(systemName: model.fileIcon(named: fileName))
+ }
+ }
+ .frame(width: 36, height: 36)
+ .clipShape(RoundedRectangle(cornerRadius: 4))
+ .accessibilityHidden(true)
+ .task(id: fileName) { model.loadThumbnail(named: fileName) }
+ }
+ .foregroundStyle(.primary)
+ Spacer(minLength: 0)
+ Image(systemName: "ellipsis")
+ .foregroundStyle(Color.accentColor)
+ .frame(width: 50, height: 50)
+ .accessibilityHidden(true)
+ }
+ .cappedFont(.body, maxDynamicType: .accessibility2)
+ .frame(maxWidth: .infinity, minHeight: 50)
+ .contentShape(Rectangle())
+ }
+ .buttonStyle(.plain)
+ .confirmationDialog(Text(verbatim: NCShareFilenameView.displayName(fileName)), isPresented: Binding(
+ get: { fileActions == fileName },
+ set: { if !$0 { fileActions = nil } }
+ ), titleVisibility: .visible) {
+ Button(NSLocalizedString("_rename_file_", comment: "")) {
+ model.showRenameFileDialog(named: fileName)
+ }
+ Button(NSLocalizedString("_remove_file_", comment: ""), role: .destructive) {
+ model.removeFile(named: fileName)
+ }
+ Button(NSLocalizedString("_cancel_", comment: ""), role: .cancel) { }
+ }
+ .listRowInsets(EdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 2))
+ .listRowSeparator(.hidden)
+ .listRowBackground(Color(uiColor: .secondarySystemGroupedBackground))
+ }
+ .listStyle(.plain)
+ .contentMargins(.vertical, 0, for: .scrollContent)
+ .scrollContentBackground(.hidden)
+ .environment(\.defaultMinListRowHeight, 50)
+ .frame(height: fillsHeight ? nil : CGFloat(min(max(model.fileNames.count, 1), 3)) * 50)
+ .frame(maxHeight: fillsHeight ? .infinity : nil)
+ .background(Color(uiColor: .secondarySystemGroupedBackground))
+ .clipShape(RoundedRectangle(cornerRadius: 12))
+ .padding(.horizontal, horizontalInset)
+ .opacity(isRefreshing ? 0.45 : 1)
+ .disabled(isRefreshing || model.isPreparingUpload || model.isPreparingInput || !model.isAccountReady || !model.inputErrorMessage.isEmpty)
+ }
+
+ private var uploadButton: some View {
+ NCShareUploadButton(fileCount: model.fileNames.count, isPreparing: model.isPreparingUpload, action: model.upload)
+ .disabled(!model.canUpload)
+ }
+
+}
+
+#if DEBUG && SHARE_PREVIEWS
+#Preview("Share") {
+ NCShareView(model: .preview())
+ .environment(\.locale, Locale(identifier: "en_US"))
+ .allowsHitTesting(false)
+}
+
+#Preview("No folders") {
+ let model = NCShareViewModel.preview()
+ model.folderChildren = [:]
+ model.recentFolders = []
+ if let rootFolder = model.rootFolder {
+ model.loadedFolderURLs = [rootFolder.serverUrl]
+ }
+ return NCShareView(model: model)
+ .environment(\.locale, Locale(identifier: "en_US"))
+ .allowsHitTesting(false)
+}
+
+#Preview("Preparing files") {
+ let model = NCShareViewModel.preview()
+ model.fileNames = []
+ model.isPreparingInput = true
+ return NCShareView(model: model)
+ .environment(\.locale, Locale(identifier: "en_US"))
+ .allowsHitTesting(false)
+}
+#Preview("Capabilities unavailable") {
+ let model = NCShareViewModel.preview()
+ model.serverCapabilities = nil
+ model.capabilitiesErrorMessage = NSLocalizedString("_share_capabilities_unavailable_", comment: "")
+ return NCShareView(model: model)
+ .environment(\.locale, Locale(identifier: "en_US"))
+ .allowsHitTesting(false)
+}
+
+#Preview("Loading capabilities") {
+ let model = NCShareViewModel.preview()
+ model.serverCapabilities = nil
+ model.isLoadingCapabilities = true
+ return NCShareView(model: model)
+ .environment(\.locale, Locale(identifier: "en_US"))
+ .allowsHitTesting(false)
+}
+#endif
diff --git a/Tests/NextcloudUITests/BaseUIXCTestCase.swift b/Tests/NextcloudUITests/BaseUIXCTestCase.swift
index 734d3f0b82..d718cb5d71 100644
--- a/Tests/NextcloudUITests/BaseUIXCTestCase.swift
+++ b/Tests/NextcloudUITests/BaseUIXCTestCase.swift
@@ -59,8 +59,8 @@ class BaseUIXCTestCase: XCTestCase {
try await Task.sleep(for: .seconds(TestConstants.controlExistenceTimeout))
}
- /// Sign in through the app's authentication flow, supporting system authentication and the embedded fallback.
- func logIn() async throws {
+ /// Sign in through the browser by default, or let the app provision a test account on the login screen.
+ func logIn(usingAppConfig: Bool = false) async throws {
let login = app.buttons["login"]
let deadline = Date().addingTimeInterval(30)
// Startup can restore the main interface after the first accessibility snapshot.
@@ -79,7 +79,19 @@ class BaseUIXCTestCase: XCTestCase {
throw UITestError.waitForExistence(login)
}
login.tap()
+ if !usingAppConfig {
+ try await authenticateInBrowser()
+ }
+
+ let sidebar = app.buttons["openSidebar"].firstMatch
+ let loginTimeout: TimeInterval = usingAppConfig ? 90 : 30
+ guard sidebar.await(timeout: loginTimeout) || app.buttons["accountSwitcher"].exists else {
+ attachAuthenticationDiagnostics()
+ throw UITestError.waitForExistence(sidebar)
+ }
+ }
+ private func authenticateInBrowser() async throws {
let serverAddress = app.textFields["serverAddress"].firstMatch
guard serverAddress.await() else { throw UITestError.waitForExistence(serverAddress) }
serverAddress.tap()
@@ -108,12 +120,6 @@ class BaseUIXCTestCase: XCTestCase {
attachAuthenticationDiagnostics()
throw UITestError.waitForExistence(grant)
}
-
- let sidebar = app.buttons["openSidebar"].firstMatch
- guard sidebar.await(timeout: 30) || app.buttons["accountSwitcher"].exists else {
- attachAuthenticationDiagnostics()
- throw UITestError.waitForExistence(sidebar)
- }
}
/// Waiting alone does not trigger interruption monitors; dismiss password prompts in their owning process.
diff --git a/Tests/NextcloudUITests/PasscodeUITests.swift b/Tests/NextcloudUITests/PasscodeUITests.swift
index 1e29c63d4b..f752c9a4af 100644
--- a/Tests/NextcloudUITests/PasscodeUITests.swift
+++ b/Tests/NextcloudUITests/PasscodeUITests.swift
@@ -27,11 +27,14 @@ final class PasscodeUITests: BaseUIXCTestCase {
addUIInterruptionMonitor(withDescription: "Allow Notifications", for: "Allow")
app = XCUIApplication()
app.launchArguments = ["UI_TESTING", "UI_TESTING_PASSCODE", "UI_TESTING_NO_EXIT"] + languageArguments
+ app.launchEnvironment["UI_TESTING_PASSCODE_SERVER"] = TestConstants.server
+ app.launchEnvironment["UI_TESTING_PASSCODE_USERNAME"] = TestConstants.username
+ app.launchEnvironment["UI_TESTING_PASSCODE_PASSWORD"] = TestConstants.password
if name.contains("testAppResetAfterTenFailuresAcrossLockouts") {
app.launchEnvironment["UI_TESTING_PASSCODE_LOCKOUT_SECONDS"] = String(resetTestLockoutSeconds)
}
app.launch()
- try await logIn()
+ try await logIn(usingAppConfig: true)
openSettings()
XCTAssertFalse(changeSetting.exists, "Each test must start with app lock disabled.")
}
diff --git a/Tests/NextcloudUnitTests/NCShareLoaderItemProviderTests.swift b/Tests/NextcloudUnitTests/NCShareLoaderItemProviderTests.swift
new file mode 100644
index 0000000000..aea5dfcd33
--- /dev/null
+++ b/Tests/NextcloudUnitTests/NCShareLoaderItemProviderTests.swift
@@ -0,0 +1,242 @@
+// SPDX-FileCopyrightText: Nextcloud GmbH
+// SPDX-FileCopyrightText: 2026 Marino Faggiana
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import Foundation
+import UIKit
+import Testing
+import UniformTypeIdentifiers
+@testable import Nextcloud
+
+// The Share importer source is included directly in this test target because
+// an application extension cannot be imported as a framework by the app tests.
+@MainActor
+@Suite("Share extension input preparation")
+struct NCShareLoaderItemProviderTests {
+ @Test("Repeated names retain every attachment and its source order")
+ func duplicateNames() async throws {
+ let directory = temporaryDirectory()
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let payloads = [Data("first".utf8), Data("second".utf8), Data("third".utf8)]
+ let providers = payloads.map { provider(data: $0, type: .pdf, name: "report.pdf") }
+
+ let output = try await load(providers, directory: directory)
+
+ #expect(output.failures.isEmpty)
+ #expect(output.fileNames.count == 3)
+ #expect(Set(output.fileNames).count == 3)
+ let imported = try output.fileNames.map { try Data(contentsOf: directory.appendingPathComponent($0)) }
+ #expect(imported == payloads)
+ }
+
+ @Test("JPEG bytes and extension are preserved without PNG conversion")
+ func originalImageFormat() async throws {
+ let directory = temporaryDirectory()
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let jpeg = Data([0xff, 0xd8, 0xff, 0xd9])
+
+ let output = try await load([provider(data: jpeg, type: .jpeg, name: "photo.jpg")], directory: directory)
+
+ let name = try #require(output.fileNames.first)
+ #expect(output.failures.isEmpty)
+ #expect(UTType(filenameExtension: (name as NSString).pathExtension) == .jpeg)
+ #expect(try Data(contentsOf: directory.appendingPathComponent(name)) == jpeg)
+ }
+
+ @Test("A provider with multiple formats imports only its preferred representation")
+ func multipleRepresentations() async throws {
+ let directory = temporaryDirectory()
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let item = provider(data: Data("preferred".utf8), type: .png, name: "image.png")
+ item.registerDataRepresentation(forTypeIdentifier: UTType.jpeg.identifier, visibility: .all) { completion in
+ completion(Data("alternative".utf8), nil)
+ return nil
+ }
+
+ let output = try await load([item], directory: directory)
+
+ #expect(output.fileNames.count == 1)
+ let name = try #require(output.fileNames.first)
+ #expect(try Data(contentsOf: directory.appendingPathComponent(name)) == Data("preferred".utf8))
+ }
+
+ @Test("Shared text is saved as UTF-8 without altering its content")
+ func unicodeText() async throws {
+ let directory = temporaryDirectory()
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let text = "Caffè ☕️\nSeconda riga"
+ let item = NSItemProvider(object: text as NSString)
+
+ let output = try await load([item], directory: directory)
+
+ #expect(output.failures.isEmpty)
+ let name = try #require(output.fileNames.first)
+ #expect((name as NSString).pathExtension == "txt")
+ #expect(try String(contentsOf: directory.appendingPathComponent(name), encoding: .utf8) == text)
+ }
+
+ @Test("Failed image and URL representations do not discard usable text")
+ func failedRepresentationsFallBackToText() async throws {
+ let directory = temporaryDirectory()
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let text = "A note that must not be discarded"
+ let item = NSItemProvider(object: text as NSString)
+ for type in [UTType.png, UTType.url] {
+ item.registerDataRepresentation(forTypeIdentifier: type.identifier, visibility: .all) { completion in
+ completion(nil, CocoaError(.fileReadCorruptFile))
+ return nil
+ }
+ }
+
+ let output = try await load([item], directory: directory)
+
+ #expect(output.failures.isEmpty)
+ #expect(output.fileNames.count == 1)
+ let name = try #require(output.fileNames.first)
+ #expect(try String(contentsOf: directory.appendingPathComponent(name), encoding: .utf8) == text)
+ }
+
+ @Test("Attributed text is imported once, retaining a rich representation when available")
+ func attributedText() async throws {
+ let directory = temporaryDirectory()
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let text = "A note with formatting\nCaffè ☕️"
+ let attributed = NSAttributedString(string: text, attributes: [.font: UIFont.boldSystemFont(ofSize: 17)])
+ let rtf = try attributed.data(from: NSRange(location: 0, length: attributed.length),
+ documentAttributes: [.documentType: NSAttributedString.DocumentType.rtf])
+ let item = provider(data: rtf, type: .rtf, name: "Note")
+ item.registerObject(text as NSString, visibility: .all)
+
+ let output = try await load([item], directory: directory)
+
+ #expect(output.failures.isEmpty)
+ #expect(output.fileNames.count == 1)
+ let name = try #require(output.fileNames.first)
+ let file = directory.appendingPathComponent(name)
+ #expect(file.pathExtension == "rtf")
+ #expect(try Data(contentsOf: file) == rtf)
+ }
+
+ @Test("An empty file is valid input")
+ func emptyFile() async throws {
+ let directory = temporaryDirectory()
+ defer { try? FileManager.default.removeItem(at: directory) }
+
+ let output = try await load([provider(data: Data(), type: .pdf, name: "empty.pdf")], directory: directory)
+
+ let name = try #require(output.fileNames.first)
+ #expect(output.failures.isEmpty)
+ #expect(try Data(contentsOf: directory.appendingPathComponent(name)).isEmpty)
+ }
+
+ @Test("Failures are reported alongside successfully imported attachments")
+ func partialFailure() async throws {
+ let directory = temporaryDirectory()
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let broken = NSItemProvider()
+ broken.registerDataRepresentation(forTypeIdentifier: UTType.pdf.identifier, visibility: .all) { completion in
+ completion(nil, CocoaError(.fileReadNoPermission))
+ return nil
+ }
+
+ let output = try await load([broken, provider(data: Data("ok".utf8), type: .pdf, name: "ok.pdf")], directory: directory)
+
+ #expect(output.failures.count == 1)
+ #expect(output.fileNames.count == 1)
+ let name = try #require(output.fileNames.first)
+ #expect(try Data(contentsOf: directory.appendingPathComponent(name)) == Data("ok".utf8))
+ }
+
+ @Test("Suggested paths cannot escape the request directory or replace unrelated files")
+ func pathConfinement() async throws {
+ let root = temporaryDirectory()
+ let directory = root.appendingPathComponent("request", isDirectory: true)
+ try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
+ defer { try? FileManager.default.removeItem(at: root) }
+ let outside = root.appendingPathComponent("outside.pdf")
+ let original = Data("keep me".utf8)
+ try original.write(to: outside)
+
+ let output = try await load([provider(data: Data("new".utf8), type: .pdf, name: "../outside.pdf")], directory: directory)
+
+ let name = try #require(output.fileNames.first)
+ #expect(!name.contains("/"))
+ #expect(directory.appendingPathComponent(name).deletingLastPathComponent().standardizedFileURL == directory.standardizedFileURL)
+ #expect(try Data(contentsOf: outside) == original)
+ }
+
+ @Test("A file URL is copied while the original remains untouched")
+ func fileURL() async throws {
+ let root = temporaryDirectory()
+ try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
+ defer { try? FileManager.default.removeItem(at: root) }
+ let source = root.appendingPathComponent("original.pdf")
+ let bytes = Data("original contents".utf8)
+ try bytes.write(to: source)
+ let directory = root.appendingPathComponent("request", isDirectory: true)
+ let item = NSItemProvider(object: source as NSURL)
+
+ let output = try await load([item], directory: directory)
+
+ let name = try #require(output.fileNames.first)
+ #expect(output.failures.isEmpty)
+ #expect(try Data(contentsOf: source) == bytes)
+ #expect(try Data(contentsOf: directory.appendingPathComponent(name)) == bytes)
+ }
+
+ @Test("Unsupported and missing attachments complete with an explicit result")
+ func unsupportedInput() async throws {
+ let directory = temporaryDirectory()
+ defer { try? FileManager.default.removeItem(at: directory) }
+
+ let unsupported = try await load([NSItemProvider()], directory: directory)
+ #expect(unsupported.fileNames.isEmpty)
+ #expect(unsupported.failures.count == 1)
+ let empty = try await load([], directory: directory)
+ #expect(empty.fileNames.isEmpty)
+ #expect(empty.failures.isEmpty)
+ }
+
+ @Test("Cancellation stops a provider even when it never calls its completion", .timeLimit(.minutes(1)))
+ func cancelledProvider() async throws {
+ let directory = temporaryDirectory()
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let stalled = NSItemProvider()
+ let (stream, started) = AsyncStream.makeStream()
+ stalled.registerDataRepresentation(forTypeIdentifier: UTType.pdf.identifier, visibility: .all) { _ in
+ started.yield(())
+ started.finish()
+ return Progress(totalUnitCount: 1)
+ }
+ let task = Task { try await load([stalled], directory: directory) }
+ for await _ in stream { break }
+ task.cancel()
+
+ do {
+ _ = try await task.value
+ Issue.record("Expected cancellation")
+ } catch is CancellationError {
+ #expect(!FileManager.default.fileExists(atPath: directory.path))
+ }
+ }
+
+ private func temporaryDirectory() -> URL {
+ FileManager.default.temporaryDirectory.appendingPathComponent("ShareInputTests-" + UUID().uuidString, isDirectory: true)
+ }
+
+ private func provider(data: Data, type: UTType, name: String) -> NSItemProvider {
+ let item = NSItemProvider()
+ item.suggestedName = name
+ item.registerDataRepresentation(forTypeIdentifier: type.identifier, visibility: .all) { completion in
+ completion(data, nil)
+ return nil
+ }
+ return item
+ }
+
+ private func load(_ providers: [NSItemProvider], directory: URL) async throws -> NCShareLoaderItemProvider.Output {
+ let item = NSExtensionItem()
+ item.attachments = providers
+ return try await NCShareLoaderItemProvider.load(items: [item], directory: directory)
+ }
+}
diff --git a/Tests/NextcloudUnitTests/NCShareRecentFoldersStoreTests.swift b/Tests/NextcloudUnitTests/NCShareRecentFoldersStoreTests.swift
new file mode 100644
index 0000000000..956302d04e
--- /dev/null
+++ b/Tests/NextcloudUnitTests/NCShareRecentFoldersStoreTests.swift
@@ -0,0 +1,90 @@
+// SPDX-FileCopyrightText: Nextcloud GmbH
+// SPDX-FileCopyrightText: 2026 Marino Faggiana
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import Foundation
+import Testing
+@testable import Nextcloud
+
+@MainActor
+@Suite("Share recent destinations")
+struct NCShareRecentFoldersStoreTests {
+ @Test("Only the last three destinations are retained, newest first")
+ func threeRecentDestinations() throws {
+ let suite = "ShareRecentFoldersTests-" + UUID().uuidString
+ let defaults = try #require(UserDefaults(suiteName: suite))
+ defer { defaults.removePersistentDomain(forName: suite) }
+ let store = NCShareRecentFoldersStore(defaults: defaults)
+ for number in 1...7 { store.record(item(number), account: "personal") }
+
+ #expect(store.folders(account: "personal") == [7, 6, 5].map(item))
+ }
+
+ @Test("Reusing a destination moves it to the top without duplicating it")
+ func movesExistingDestination() throws {
+ let suite = "ShareRecentFoldersTests-" + UUID().uuidString
+ let defaults = try #require(UserDefaults(suiteName: suite))
+ defer { defaults.removePersistentDomain(forName: suite) }
+ let store = NCShareRecentFoldersStore(defaults: defaults)
+ for number in 1...3 { store.record(item(number), account: "personal") }
+ store.record(item(2), account: "personal")
+
+ #expect(store.folders(account: "personal") == [2, 3, 1].map(item))
+ }
+
+ @Test("Accounts have independent histories")
+ func separatesAccounts() throws {
+ let suite = "ShareRecentFoldersTests-" + UUID().uuidString
+ let defaults = try #require(UserDefaults(suiteName: suite))
+ defer { defaults.removePersistentDomain(forName: suite) }
+ let store = NCShareRecentFoldersStore(defaults: defaults)
+ store.record(item(1), account: "personal")
+ store.record(item(2), account: "work")
+
+ #expect(store.folders(account: "personal") == [item(1)])
+ #expect(store.folders(account: "work") == [item(2)])
+ #expect(store.folders(account: "missing").isEmpty)
+ }
+
+ @Test("The history survives recreating the store")
+ func persistsDestinations() throws {
+ let suite = "ShareRecentFoldersTests-" + UUID().uuidString
+ let defaults = try #require(UserDefaults(suiteName: suite))
+ defer { defaults.removePersistentDomain(forName: suite) }
+ NCShareRecentFoldersStore(defaults: defaults).record(item(1), account: "personal")
+
+ let restored = NCShareRecentFoldersStore(defaults: defaults)
+ #expect(restored.folders(account: "personal") == [item(1)])
+ }
+
+ @Test("A stale folder is removed only from the requested account")
+ func removesUnavailableDestination() throws {
+ let suite = "ShareRecentFoldersTests-" + UUID().uuidString
+ let defaults = try #require(UserDefaults(suiteName: suite))
+ defer { defaults.removePersistentDomain(forName: suite) }
+ let store = NCShareRecentFoldersStore(defaults: defaults)
+ store.record(item(1), account: "personal")
+ store.record(item(2), account: "personal")
+ store.record(item(1), account: "work")
+ store.remove(serverUrl: item(1).serverUrl, account: "personal")
+
+ #expect(store.folders(account: "personal") == [item(2)])
+ #expect(store.folders(account: "work") == [item(1)])
+ }
+
+ @Test("Unreadable saved data does not prevent a new destination being recorded")
+ func corruptHistory() throws {
+ let suite = "ShareRecentFoldersTests-" + UUID().uuidString
+ let defaults = try #require(UserDefaults(suiteName: suite))
+ defer { defaults.removePersistentDomain(forName: suite) }
+ defaults.set(Data("invalid".utf8), forKey: "share.recentFolders.v1.personal")
+ let store = NCShareRecentFoldersStore(defaults: defaults)
+ #expect(store.folders(account: "personal").isEmpty)
+ store.record(item(1), account: "personal")
+ #expect(store.folders(account: "personal") == [item(1)])
+ }
+
+ private func item(_ number: Int) -> NCShareRecentFoldersStore.Item {
+ .init(serverUrl: "https://cloud.example.com/files/folder\(number)", name: "Folder \(number)", ocId: "folder\(number)", isEncrypted: false)
+ }
+}
diff --git a/Tests/NextcloudUnitTests/NCShareUploadNamePlannerTests.swift b/Tests/NextcloudUnitTests/NCShareUploadNamePlannerTests.swift
new file mode 100644
index 0000000000..ed42a58f63
--- /dev/null
+++ b/Tests/NextcloudUnitTests/NCShareUploadNamePlannerTests.swift
@@ -0,0 +1,69 @@
+// SPDX-FileCopyrightText: Nextcloud GmbH
+// SPDX-FileCopyrightText: 2026 Marino Faggiana
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import Foundation
+import Testing
+@testable import Nextcloud
+
+// The extension's pure name planner is included directly in this test target.
+@Suite("Share extension conflict names")
+struct NCShareUploadNamePlannerTests {
+ @Test("Unused names remain unchanged")
+ func unchangedNames() {
+ let names = ["photo.jpg", "report.pdf"]
+ #expect(NCShareUploadNamePlanner.fileNames(for: names, existingNames: []) == names)
+ }
+
+ @Test("Existing files keep their names and copies receive the first free suffix")
+ func existingCopies() {
+ #expect(NCShareUploadNamePlanner.fileNames(
+ for: ["photo.jpg"], existingNames: ["photo.jpg", "photo (1).jpg", "photo (2).jpg"]
+ ) == ["photo (3).jpg"])
+ }
+
+ @Test("An earlier conflict cannot consume the original name of a later attachment")
+ func reservesBatchNames() {
+ #expect(NCShareUploadNamePlanner.fileNames(
+ for: ["photo.jpg", "photo (1).jpg"], existingNames: ["photo.jpg"]
+ ) == ["photo (2).jpg", "photo (1).jpg"])
+ }
+
+ @Test("Different source names that become equal after validation remain distinct")
+ func duplicateValidatedNames() {
+ let names = NCShareUploadNamePlanner.fileNames(for: ["report.pdf", "report.pdf", "report.pdf"], existingNames: [])
+ #expect(names == ["report.pdf", "report (1).pdf", "report (2).pdf"])
+ }
+
+ @Test("An existing numeric suffix advances without nesting another suffix")
+ func existingSuffix() {
+ #expect(NCShareUploadNamePlanner.fileNames(
+ for: ["photo (3).JPG"], existingNames: ["photo (3).JPG", "photo (4).JPG"]
+ ) == ["photo (5).JPG"])
+ }
+
+ @Test("Files without extensions and hidden names remain valid")
+ func extensionlessAndHidden() {
+ #expect(NCShareUploadNamePlanner.fileNames(
+ for: ["README", ".env"], existingNames: ["README", ".env"]
+ ) == ["README (1)", ".env (1)"])
+ }
+
+ @Test("Canonical Unicode spellings refer to the same occupied name")
+ func unicodeNames() {
+ let names = NCShareUploadNamePlanner.fileNames(for: ["café.txt"], existingNames: ["cafe\u{301}.txt"])
+ #expect(names == ["café (1).txt"])
+ }
+
+ @Test("A hundred conflicts produce a hundred distinct new names")
+ func largeBatch() {
+ let names = (1...100).map { "photo\($0).jpg" }
+ let existing = Set(names + names.map { ($0 as NSString).deletingPathExtension + " (1).jpg" })
+ let allocated = NCShareUploadNamePlanner.fileNames(for: names, existingNames: existing)
+
+ #expect(allocated.count == 100)
+ #expect(Set(allocated).count == 100)
+ #expect(Set(allocated).isDisjoint(with: existing))
+ #expect(allocated == (1...100).map { "photo\($0) (2).jpg" })
+ }
+}
diff --git a/iOSClient/Extensions/UIAlertController+Extension.swift b/iOSClient/Extensions/UIAlertController+Extension.swift
index f4a2773c6a..389b38f33b 100644
--- a/iOSClient/Extensions/UIAlertController+Extension.swift
+++ b/iOSClient/Extensions/UIAlertController+Extension.swift
@@ -22,8 +22,8 @@ extension UIAlertController {
let alertController = UIAlertController(title: NSLocalizedString("_create_folder_", comment: ""), message: nil, preferredStyle: .alert)
let isDirectoryEncrypted = NCUtilityFileSystem().isDirectoryE2EE(serverUrl: serverUrl, urlBase: session.urlBase, userId: session.userId, account: session.account)
- let okAction = UIAlertAction(title: NSLocalizedString("_save_", comment: ""), style: .default, handler: { _ in
- guard let fileNameFolder = alertController.textFields?.first?.text else { return }
+ let okAction = UIAlertAction(title: NSLocalizedString("_save_", comment: ""), style: .default, handler: { [weak alertController] _ in
+ guard let fileNameFolder = alertController?.textFields?.first?.text else { return }
if markE2ee {
if NCNetworking.shared.isOffline {
@@ -115,11 +115,11 @@ extension UIAlertController {
textField.autocapitalizationType = .sentences
}
- NotificationCenter.default.addObserver(
- forName: UITextField.textDidChangeNotification,
- object: alertController.textFields?.first,
- queue: .main) { _ in
- guard let text = alertController.textFields?.first?.text else {
+ // The field owns this handler, so validation ends with the dialog's lifetime.
+ // Weak captures avoid retaining the alert through its field and actions.
+ alertController.textFields?.first?.addAction(UIAction { [weak alertController, weak okAction] _ in
+ guard let alertController, let okAction,
+ let text = alertController.textFields?.first?.text else {
return
}
let folderName = text.trimmingCharacters(in: .whitespaces)
@@ -147,7 +147,7 @@ extension UIAlertController {
])
alertController.setValue(attributedString, forKey: "attributedMessage")
- }
+ }, for: .editingChanged)
alertController.addAction(cancelAction)
alertController.addAction(okAction)
diff --git a/iOSClient/Login/NCLogin.swift b/iOSClient/Login/NCLogin.swift
index bc03980e3c..86b2053048 100644
--- a/iOSClient/Login/NCLogin.swift
+++ b/iOSClient/Login/NCLogin.swift
@@ -214,6 +214,18 @@ class NCLogin: UIViewController, UITextFieldDelegate, NCLoginQRCodeDelegate {
}
private func handleLoginWithAppConfig() {
+#if DEBUG
+ // Passcode UI tests provision an account through the same API as AppConfig.
+ let process = ProcessInfo.processInfo
+ let environment = process.environment
+ if process.arguments.contains("UI_TESTING"), process.arguments.contains("UI_TESTING_PASSCODE"),
+ let server = environment["UI_TESTING_PASSCODE_SERVER"],
+ let username = environment["UI_TESTING_PASSCODE_USERNAME"],
+ let password = environment["UI_TESTING_PASSCODE_PASSWORD"] {
+ Task { await getAppPassword(urlBase: server, user: username, password: password) }
+ return
+ }
+#endif
let accountCount = NCManageDatabase.shared.getAccounts()?.count ?? 0
// load AppConfig
diff --git a/iOSClient/Networking/NCNetworking+Upload.swift b/iOSClient/Networking/NCNetworking+Upload.swift
index 7a2810aabd..49fa61b4c0 100644
--- a/iOSClient/Networking/NCNetworking+Upload.swift
+++ b/iOSClient/Networking/NCNetworking+Upload.swift
@@ -74,6 +74,8 @@ extension NCNetworking {
func uploadChunkFile(metadata: tableMetadata,
performPostProcessing: Bool = true,
customHeaders: [String: String]? = nil,
+ overwrite: Bool = true,
+ ifMatch: String? = nil,
backgroundContext: ChunkUploadBackgroundContext? = nil,
chunkProgressHandler: @escaping (_ total: Int, _ counter: Int) -> Void = { _, _ in },
uploadStart: @escaping (_ filesChunk: [(fileName: String, size: Int64)]) -> Void = { _ in },
@@ -101,7 +103,9 @@ extension NCNetworking {
filesChunk: filesChunk,
chunkSize: self.global.chunkSize,
account: metadata.account,
- options: options) { total, counter in
+ options: options,
+ ifMatch: ifMatch,
+ overwrite: overwrite) { total, counter in
chunkProgressHandler(total, counter)
} uploadStart: { filesChunk in
Task {
diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings
index 38914c5899..d25674f40b 100644
--- a/iOSClient/Supporting Files/en.lproj/Localizable.strings
+++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings
@@ -878,6 +878,44 @@
"_passcode_incorrect_" = "Incorrect passcode. Try again.";
"_passcode_digits_entered_" = "Passcode digits entered";
+/* Share extension input preparation */
+"_share_import_preparing_" = "Preparing shared files…";
+"_share_import_failed_" = "Some shared items could not be prepared. Review the files before uploading.";
+"_share_import_no_files_" = "The shared items could not be prepared.";
+"_share_import_item_error_" = "Item %d: %@";
+"_share_import_copy_failed_" = "A shared file could not be prepared for upload. Please try again.";
+
+/* Share extension upload conflicts */
+"_share_conflict_title_" = "Files already exist";
+"_share_conflict_single_message_" = "An item named \"%@\" already exists in this folder.";
+"_share_conflict_batch_message_" = "Some names are already in use. These files will be uploaded with new names, keeping the existing files.";
+"_share_conflict_new_names_" = "New file names";
+"_share_conflict_replace_" = "Replace existing file";
+"_share_conflict_upload_with_new_name_" = "Upload with a new name";
+"_share_conflict_keep_both_" = "Keep both";
+"_share_conflict_checking_" = "Preparing upload…";
+"_share_conflict_read_failed_" = "The destination folder could not be checked. Please try again.";
+
+/* Share extension destination browser */
+"_share_save_to_" = "Save to %@";
+"_share_home_folder_" = "My files";
+"_share_recent_folders_" = "Recent";
+"_share_recent_folders_empty_" = "Your last three upload destinations will appear here.";
+"_share_all_folders_" = "All folders";
+"_share_expand_folder_" = "Expand %@";
+"_share_collapse_folder_" = "Collapse %@";
+"_share_retry_folder_" = "Retry loading folders";
+"_share_folder_unavailable_" = "This folder is no longer available.";
+
+/* Share extension capabilities */
+"_share_capabilities_loading_" = "Loading server capabilities…";
+"_share_capabilities_unavailable_" = "Server capabilities could not be loaded. Please try again before uploading, renaming files, or creating folders.";
+"_share_capabilities_retry_" = "Try again";
+"_share_online_required_" = "An internet connection is required to upload files. Reconnect to continue.";
+"_share_favorite_folder_" = "Favorite folder";
+"_share_checking_folder_" = "Checking folder…";
+"_share_loading_folders_" = "Loading folders…";
+
"_background_upload_job_missing_" = "Upload failed. Please retry.";
// MARK: Error messages
diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.stringsdict b/iOSClient/Supporting Files/en.lproj/Localizable.stringsdict
index 034f6cd7aa..767f642f68 100644
--- a/iOSClient/Supporting Files/en.lproj/Localizable.stringsdict
+++ b/iOSClient/Supporting Files/en.lproj/Localizable.stringsdict
@@ -2,6 +2,22 @@
+ _share_upload_to_
+
+ NSStringLocalizedFormatKey
+ %#@files@
+ files
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ one
+ Upload %ld file
+ other
+ Upload %ld files
+
+
_remaining_share_downloads_
NSStringLocalizedFormatKey
diff --git a/iOSClient/Utility/Passcode/NCPasscodeModel.swift b/iOSClient/Utility/Passcode/NCPasscodeModel.swift
index 9c2a982fe7..883fa1fd72 100644
--- a/iOSClient/Utility/Passcode/NCPasscodeModel.swift
+++ b/iOSClient/Utility/Passcode/NCPasscodeModel.swift
@@ -85,12 +85,16 @@ final class NCPasscodeModel: ObservableObject {
var biometricSymbol: String? {
if isPreview { return step == .authenticate ? "faceid" : nil }
- guard step == .authenticate, mode != .change, preferences.touchFaceID, preferences.passcode != nil else { return nil }
+ guard isBiometricAuthenticationEnabled else { return nil }
let context = LAContext()
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) else { return nil }
return context.biometryType == .faceID ? "faceid" : "touchid"
}
+ private var isBiometricAuthenticationEnabled: Bool {
+ step == .authenticate && mode != .change && preferences.touchFaceID && preferences.passcode != nil
+ }
+
var canEnterCode: Bool {
result == nil && lockoutEnd == nil && !isAuthenticating
}
@@ -160,8 +164,9 @@ final class NCPasscodeModel: ObservableObject {
func authenticateWithBiometrics() async {
guard !isPreview else { return }
refreshProtection()
- guard canEnterCode, biometricSymbol != nil else { return }
+ guard canEnterCode, isBiometricAuthenticationEnabled else { return }
let context = LAContext()
+ guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) else { return }
let request = UUID()
biometricRequest = request
biometricContext = context
diff --git a/iOSClient/Utility/Passcode/NCPasscodeView.swift b/iOSClient/Utility/Passcode/NCPasscodeView.swift
index 5a7f8567fa..96816b4754 100644
--- a/iOSClient/Utility/Passcode/NCPasscodeView.swift
+++ b/iOSClient/Utility/Passcode/NCPasscodeView.swift
@@ -38,6 +38,29 @@ struct NCPasscodeView: View {
}
let model = NCPasscodeModel(mode: mode, allowCancel: allowCancel)
let controller = UIHostingController(rootView: NCPasscodeView(model: model, windowScene: top.view.window?.windowScene))
+#if EXTENSION_SHARE
+ let definedPresentationContext = top.definesPresentationContext
+ top.definesPresentationContext = true
+ controller.modalPresentationStyle = .overCurrentContext
+ controller.modalTransitionStyle = .crossDissolve
+ controller.view.accessibilityViewIsModal = true
+ controller.isModalInPresentation = true
+ model.onCompletion = { [weak controller, weak top] result in
+ // Keep the keypad visible while the extension closes on cancellation.
+ guard result == .authenticated, let controller else {
+ completion(result)
+ return
+ }
+ controller.dismiss(animated: false) {
+ top?.definesPresentationContext = definedPresentationContext
+ completion(result)
+ }
+ }
+ // Start once UIKit has finished presenting the keypad inside the Share panel.
+ top.present(controller, animated: true) {
+ Task { await model.start() }
+ }
+#else
controller.modalPresentationStyle = allowCancel ? .pageSheet : .fullScreen
controller.isModalInPresentation = !allowCancel
model.onCompletion = { [weak controller] result in
@@ -51,42 +74,7 @@ struct NCPasscodeView: View {
}
}
top.present(controller, animated: allowCancel)
- }
-
- /// Covers an extension's existing panel without presenting another modal or changing its size.
- static func embed(in container: UIViewController,
- completion: @escaping @MainActor (NCPasscodeModel.Result) -> Void) {
- let navigationController = (container as? UINavigationController) ?? container.navigationController
- let wasNavigationBarHidden = navigationController?.isNavigationBarHidden ?? true
- navigationController?.setNavigationBarHidden(true, animated: false)
- let model = NCPasscodeModel(mode: .authenticate, allowCancel: true)
- let controller = UIHostingController(rootView: NCPasscodeView(model: model,
- windowScene: container.view.window?.windowScene))
- model.onCompletion = { [weak controller, weak navigationController] result in
- guard let controller else { return }
- // Keep protected content covered while the caller closes the extension on cancellation.
- guard result == .authenticated else {
- completion(result)
- return
- }
- controller.willMove(toParent: nil)
- controller.view.removeFromSuperview()
- controller.removeFromParent()
- navigationController?.setNavigationBarHidden(wasNavigationBarHidden, animated: false)
- completion(result)
- }
- container.addChild(controller)
- controller.view.backgroundColor = .systemBackground
- controller.view.accessibilityViewIsModal = true
- controller.view.translatesAutoresizingMaskIntoConstraints = false
- container.view.addSubview(controller.view)
- NSLayoutConstraint.activate([
- controller.view.leadingAnchor.constraint(equalTo: container.view.leadingAnchor),
- controller.view.trailingAnchor.constraint(equalTo: container.view.trailingAnchor),
- controller.view.topAnchor.constraint(equalTo: container.view.topAnchor),
- controller.view.bottomAnchor.constraint(equalTo: container.view.bottomAnchor)
- ])
- controller.didMove(toParent: container)
+#endif
}
var body: some View {
@@ -161,11 +149,15 @@ struct NCPasscodeView: View {
.interactiveDismissDisabled(!model.allowCancel)
.sensoryFeedback(.error, trigger: model.errorFeedback)
.task {
+#if EXTENSION_SHARE
+ model.refreshProtection()
+#else
if let windowScene, windowScene.activationState != .foregroundActive {
model.refreshProtection()
} else {
await model.start()
}
+#endif
}
.task(id: model.lockoutEnd) {
guard let deadline = model.lockoutEnd else { return }
@@ -175,10 +167,12 @@ struct NCPasscodeView: View {
model.refreshProtection()
} catch { return }
}
+#if !EXTENSION_SHARE
.onReceive(NotificationCenter.default.publisher(for: UIScene.didActivateNotification)) { notification in
guard let windowScene, (notification.object as? UIScene) === windowScene else { return }
Task { await model.start() }
}
+#endif
.onReceive(NotificationCenter.default.publisher(for: UIScene.didEnterBackgroundNotification)) { notification in
guard let windowScene, (notification.object as? UIScene) === windowScene else { return }
model.cancelBiometrics()